Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 142 additions & 31 deletions src/components/ProjectSettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,13 @@ export function ProjectSettingsModal({
const [settings, setSettings] = useState<ProjectSettings>(() =>
defaultProjectSettings(),
);
const [settingsByRoot, setSettingsByRoot] = useState<
Record<string, ProjectSettings>
>({});
const [dirtyWorktreeRoots, setDirtyWorktreeRoots] = useState<Set<string>>(
() => new Set(),
);
const [worktreeRootPath, setWorktreeRootPath] = useState("");
const [identity, setIdentity] = useState<string | null>(null);
const [worktrees, setWorktrees] = useState<RootedWorktree[]>([]);
const [branches, setBranches] = useState<ProjectBranch[]>([]);
Expand Down Expand Up @@ -269,9 +276,25 @@ export function ProjectSettingsModal({
setName(projectName);
}, [project?.repoPath, projectName]);

useEffect(() => {
const requestedRoot = project?.repoPath;
setWorktreeRootPath(
requestedRoot && projectRoots.includes(requestedRoot)
? requestedRoot
: (projectRoots[0] ?? ""),
);
}, [project?.repoPath, projectRootsKey]);

const activeWorktreeRoot = projectRoots.includes(worktreeRootPath)
? worktreeRootPath
: (projectRoots[0] ?? "");

useEffect(() => {
if (!project) {
setSettings(defaultProjectSettings());
setSettingsByRoot({});
setDirtyWorktreeRoots(new Set());
setWorktreeRootPath("");
setIdentity(null);
setWorktrees([]);
setBranches([]);
Expand All @@ -290,24 +313,36 @@ export function ProjectSettingsModal({
let cancelled = false;
setLoading(true);
setWorktreesLoading(true);
setBranchesLoading(true);
setBranches([]);
setRemovingPath(null);
setConfirmRemove(null);
setError(null);
setWorktreeError(null);
setBranchError(null);

api
.getProjectSettings(project.repoPath)
.then((record) => {
Promise.all(
projectRoots.map(async (rootPath) => ({
rootPath,
record: await api.getProjectSettings(rootPath),
})),
)
.then((entries) => {
if (cancelled) return;
setSettings(record.settings);
setIdentity(record.key);
const nextByRoot = Object.fromEntries(
entries.map(({ rootPath, record }) => [rootPath, record.settings]),
);
const currentRecord = entries.find(
({ rootPath }) => rootPath === project.repoPath,
)?.record;
setSettings(currentRecord?.settings ?? defaultProjectSettings());
setSettingsByRoot(nextByRoot);
setDirtyWorktreeRoots(new Set());
setIdentity(currentRecord?.key ?? null);
})
.catch((e) => {
if (cancelled) return;
setSettings(defaultProjectSettings());
setSettingsByRoot({});
setDirtyWorktreeRoots(new Set());
setIdentity(null);
setError(String(e));
})
Expand All @@ -329,11 +364,28 @@ export function ProjectSettingsModal({
if (!cancelled) setWorktreesLoading(false);
});

return () => {
cancelled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [project, projectRootsKey]);

useEffect(() => {
if (!project || !activeWorktreeRoot) {
setBranches([]);
setBranchesLoading(false);
setBranchError(null);
return;
}

let cancelled = false;
setBranches([]);
setBranchesLoading(true);
setBranchError(null);
api
.listProjectBranches(project.repoPath)
.listProjectBranches(activeWorktreeRoot)
.then((items) => {
if (cancelled) return;
setBranches(items);
if (!cancelled) setBranches(items);
})
.catch((e) => {
if (cancelled) return;
Expand All @@ -347,11 +399,15 @@ export function ProjectSettingsModal({
return () => {
cancelled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [project, projectRootsKey]);
}, [project, activeWorktreeRoot]);

const prompt = settings.pull_requests.generation_prompt ?? "";
const configuredBaseBranch = settings.worktrees.base_branch;
const activeRootSettings =
settingsByRoot[activeWorktreeRoot] ??
(activeWorktreeRoot === project?.repoPath
? settings
: defaultProjectSettings());
const configuredBaseBranch = activeRootSettings.worktrees.base_branch;
const configuredProjectBranch = branches.find(
(branch) => projectBranchReference(branch) === configuredBaseBranch,
) ?? branches.find((branch) => branch.name === configuredBaseBranch);
Expand Down Expand Up @@ -401,6 +457,11 @@ export function ProjectSettingsModal({
},
];
}, [branches, t]);
const worktreeRootOptions: SelectItem[] = projectRoots.map((rootPath) => ({
value: rootPath,
label: rootPath,
searchText: `${basenamePath(rootPath)} ${rootPath}`,
}));

function updatePrompt(value: string) {
const next = Array.from(value).slice(0, PROMPT_MAX_CHARS).join("");
Expand All @@ -420,31 +481,44 @@ export function ProjectSettingsModal({
}));
}

function updateWorktreeBaseBranch(value: string) {
const next = Array.from(value).slice(0, BRANCH_MAX_CHARS).join("");
setSettings((current) => ({
...current,
worktrees: {
...current.worktrees,
base_branch: next,
},
}));
function updateWorktreeBaseBranch(value: string | null) {
if (!activeWorktreeRoot) return;
const next =
value === null
? null
: Array.from(value).slice(0, BRANCH_MAX_CHARS).join("");
setSettingsByRoot((current) => {
const currentSettings =
current[activeWorktreeRoot] ??
(activeWorktreeRoot === project?.repoPath
? settings
: defaultProjectSettings());
return {
...current,
[activeWorktreeRoot]: {
...currentSettings,
worktrees: {
...currentSettings.worktrees,
base_branch: next,
},
},
};
});
setDirtyWorktreeRoots((current) => {
const nextRoots = new Set(current);
nextRoots.add(activeWorktreeRoot);
return nextRoots;
});
}

function selectWorktreeBaseBranch(value: string) {
if (value === AUTOMATIC_BASE_BRANCH_VALUE) {
setSettings((current) => ({
...current,
worktrees: { ...current.worktrees, base_branch: null },
}));
updateWorktreeBaseBranch(null);
return;
}
if (value === CUSTOM_BASE_BRANCH_VALUE) {
if (selectedBaseBranch !== CUSTOM_BASE_BRANCH_VALUE) {
setSettings((current) => ({
...current,
worktrees: { ...current.worktrees, base_branch: "" },
}));
updateWorktreeBaseBranch("");
}
return;
}
Expand Down Expand Up @@ -472,9 +546,18 @@ export function ProjectSettingsModal({
return;
}
}
for (const rootPath of dirtyWorktreeRoots) {
if (rootPath === project.repoPath) continue;
const rootSettings = settingsByRoot[rootPath];
if (rootSettings) {
await api.updateProjectSettings(rootPath, rootSettings);
}
}
const currentRootWorktreeSettings =
settingsByRoot[project.repoPath]?.worktrees ?? settings.worktrees;
const record = await api.updateProjectSettings(
project.repoPath,
settings,
{ ...settings, worktrees: currentRootWorktreeSettings },
);
setSettings(record.settings);
setIdentity(record.key);
Expand Down Expand Up @@ -655,6 +738,34 @@ export function ProjectSettingsModal({
title={dt(t, "dialogs.projectSettings.worktrees")}
description={dt(t, "dialogs.projectSettings.worktreesHint")}
>
{projectRoots.length > 1 ? (
<Field
label={dt(
t,
"dialogs.projectSettings.worktreeRepository",
)}
hint={dt(
t,
"dialogs.projectSettings.worktreeRepositoryHint",
)}
>
<Select
value={activeWorktreeRoot}
onValueChange={setWorktreeRootPath}
options={worktreeRootOptions}
searchable
disabled={loading || saving}
aria-label={dt(
t,
"dialogs.projectSettings.worktreeRepository",
)}
searchPlaceholder={dt(
t,
"dialogs.projectSettings.searchRepositories",
)}
/>
</Field>
) : null}
<Field
label={dt(t, "dialogs.projectSettings.worktreeBaseBranch")}
hint={dt(
Expand Down
3 changes: 3 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2198,6 +2198,9 @@
"pullRequestsHint": "Project-specific defaults for pull request workflows.",
"worktrees": "Worktrees",
"worktreesHint": "Defaults and linked git worktrees for this project.",
"worktreeRepository": "Repository for new worktrees",
"worktreeRepositoryHint": "Each repository root keeps its own base branch setting.",
"searchRepositories": "Search repositories",
"worktreeBaseBranch": "Base branch for new worktrees",
"worktreeBaseBranchHint": "Choose a local or remote-tracking branch, or enter another branch name. Automatic chooses main, master, or the current HEAD.",
"worktreeBaseBranchAutomatic": "Automatic",
Expand Down
3 changes: 3 additions & 0 deletions src/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -2198,6 +2198,9 @@
"pullRequestsHint": "プル リクエスト ワークフローのプロジェクト固有のデフォルト。",
"worktrees": "ワークツリー",
"worktreesHint": "このプロジェクトの既定値とリンクされた Git ワークツリー。",
"worktreeRepository": "新しいワークツリーのリポジトリ",
"worktreeRepositoryHint": "リポジトリルートごとに独自のベースブランチ設定を保持します。",
"searchRepositories": "リポジトリを検索",
"worktreeBaseBranch": "新しいワークツリーのベースブランチ",
"worktreeBaseBranchHint": "ローカルまたはリモート追跡ブランチを選択するか、別のブランチ名を入力します。自動選択では main、master、または現在の HEAD を使用します。",
"worktreeBaseBranchAutomatic": "自動選択",
Expand Down
3 changes: 3 additions & 0 deletions src/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -2198,6 +2198,9 @@
"pullRequestsHint": "이 프로젝트의 pull request 작업에만 적용되는 기본값입니다.",
"worktrees": "워크트리",
"worktreesHint": "이 프로젝트의 기본값과 연결 Git 워크트리입니다.",
"worktreeRepository": "새 워크트리를 만들 저장소",
"worktreeRepositoryHint": "각 저장소 루트는 자체 기준 브랜치 설정을 사용합니다.",
"searchRepositories": "저장소 검색",
"worktreeBaseBranch": "새 워크트리의 기준 브랜치",
"worktreeBaseBranchHint": "로컬 또는 원격 추적 브랜치를 선택하거나 다른 브랜치 이름을 직접 입력하세요. 자동 선택은 main, master 또는 현재 HEAD를 사용합니다.",
"worktreeBaseBranchAutomatic": "자동 선택",
Expand Down
3 changes: 3 additions & 0 deletions src/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2198,6 +2198,9 @@
"pullRequestsHint": "项目特定的默认值为拉取请求工作流。",
"worktrees": "工作树",
"worktreesHint": "此项目的默认值和已注册的链接 Git 工作树。",
"worktreeRepository": "新工作树的仓库",
"worktreeRepositoryHint": "每个仓库根目录保留自己的基准分支设置。",
"searchRepositories": "搜索仓库",
"worktreeBaseBranch": "新工作树的基准分支",
"worktreeBaseBranchHint": "选择本地或远程跟踪分支,或输入其他分支名称。自动选择将使用 main、master 或当前 HEAD。",
"worktreeBaseBranchAutomatic": "自动选择",
Expand Down
Loading
Loading