Skip to content

Commit c382ab0

Browse files
authored
Merge pull request #1 from Thlnking/feature/custom-worktree-paths
feat(settings): Custom workspace worktree folder can be configured
2 parents ff552b7 + 52f6df3 commit c382ab0

6 files changed

Lines changed: 116 additions & 27 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Logs
22
logs
33
*.log
4+
**-lock.**
45
npm-debug.log*
56
yarn-debug.log*
67
yarn-error.log*

src-tauri/src/shared/workspaces_core/worktree.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,20 @@ where
134134
return Err("Cannot create a worktree from another worktree.".to_string());
135135
}
136136

137-
let worktree_root = data_dir.join("worktrees").join(&parent_entry.id);
137+
// Determine worktree root: per-workspace setting > global setting > default
138+
let worktree_root = if let Some(custom_folder) = &parent_entry.settings.worktrees_folder {
139+
PathBuf::from(custom_folder)
140+
} else {
141+
let global_folder = {
142+
let settings = app_settings.lock().await;
143+
settings.global_worktrees_folder.clone()
144+
};
145+
if let Some(global_folder) = global_folder {
146+
PathBuf::from(global_folder).join(&parent_entry.id)
147+
} else {
148+
data_dir.join("worktrees").join(&parent_entry.id)
149+
}
150+
};
138151
std::fs::create_dir_all(&worktree_root)
139152
.map_err(|err| format!("Failed to create worktree directory: {err}"))?;
140153

src-tauri/src/types.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,8 @@ pub(crate) struct WorkspaceSettings {
329329
pub(crate) launch_scripts: Option<Vec<LaunchScriptEntry>>,
330330
#[serde(default, rename = "worktreeSetupScript")]
331331
pub(crate) worktree_setup_script: Option<String>,
332+
#[serde(default, rename = "worktreesFolder")]
333+
pub(crate) worktrees_folder: Option<String>,
332334
}
333335

334336
#[derive(Debug, Serialize, Deserialize, Clone)]
@@ -635,6 +637,8 @@ pub(crate) struct AppSettings {
635637
pub(crate) composer_code_block_copy_use_modifier: bool,
636638
#[serde(default = "default_workspace_groups", rename = "workspaceGroups")]
637639
pub(crate) workspace_groups: Vec<WorkspaceGroup>,
640+
#[serde(default, rename = "globalWorktreesFolder")]
641+
pub(crate) global_worktrees_folder: Option<String>,
638642
#[serde(default = "default_open_app_targets", rename = "openAppTargets")]
639643
pub(crate) open_app_targets: Vec<OpenAppTarget>,
640644
#[serde(default = "default_selected_open_app_id", rename = "selectedOpenAppId")]
@@ -1182,6 +1186,7 @@ impl Default for AppSettings {
11821186
composer_list_continuation: default_composer_list_continuation(),
11831187
composer_code_block_copy_use_modifier: default_composer_code_block_copy_use_modifier(),
11841188
workspace_groups: default_workspace_groups(),
1189+
global_worktrees_folder: None,
11851190
open_app_targets: default_open_app_targets(),
11861191
selected_open_app_id: default_selected_open_app_id(),
11871192
}

src/features/settings/components/sections/SettingsEnvironmentsSection.tsx

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ type SettingsEnvironmentsSectionProps = {
1111
environmentDraftScript: string;
1212
environmentSavedScript: string | null;
1313
environmentDirty: boolean;
14+
worktreesFolderDraft: string;
15+
worktreesFolderSaved: string | null;
16+
worktreesFolderDirty: boolean;
1417
onSetEnvironmentWorkspaceId: Dispatch<SetStateAction<string | null>>;
1518
onSetEnvironmentDraftScript: Dispatch<SetStateAction<string>>;
19+
onSetWorktreesFolderDraft: Dispatch<SetStateAction<string>>;
1620
onSaveEnvironmentSetup: () => Promise<void>;
1721
};
1822

@@ -24,14 +28,20 @@ export function SettingsEnvironmentsSection({
2428
environmentDraftScript,
2529
environmentSavedScript,
2630
environmentDirty,
31+
worktreesFolderDraft,
32+
worktreesFolderSaved,
33+
worktreesFolderDirty,
2734
onSetEnvironmentWorkspaceId,
2835
onSetEnvironmentDraftScript,
36+
onSetWorktreesFolderDraft,
2937
onSaveEnvironmentSetup,
3038
}: SettingsEnvironmentsSectionProps) {
39+
const hasAnyChanges = environmentDirty || worktreesFolderDirty;
40+
3141
return (
3242
<SettingsSection
3343
title="Environments"
34-
subtitle="Configure per-project setup scripts that run after worktree creation."
44+
subtitle="Configure per-project setup scripts and worktree locations."
3545
>
3646
{mainWorkspaces.length === 0 ? (
3747
<div className="settings-empty">No projects yet.</div>
@@ -116,12 +126,57 @@ export function SettingsEnvironmentsSection({
116126
onClick={() => {
117127
void onSaveEnvironmentSetup();
118128
}}
119-
disabled={environmentSaving || !environmentDirty}
129+
disabled={environmentSaving || !hasAnyChanges}
120130
>
121131
{environmentSaving ? "Saving..." : "Save"}
122132
</button>
123133
</div>
124134
</div>
135+
136+
<div className="settings-field">
137+
<label className="settings-field-label" htmlFor="settings-worktrees-folder">
138+
Worktrees folder
139+
</label>
140+
<div className="settings-help">
141+
Custom location for worktrees. Leave empty to use the default location.
142+
</div>
143+
<div className="settings-field-row">
144+
<input
145+
id="settings-worktrees-folder"
146+
type="text"
147+
className="settings-input"
148+
value={worktreesFolderDraft}
149+
onChange={(event) => onSetWorktreesFolderDraft(event.target.value)}
150+
placeholder="/path/to/worktrees"
151+
disabled={environmentSaving}
152+
/>
153+
<button
154+
type="button"
155+
className="ghost settings-button-compact"
156+
onClick={async () => {
157+
try {
158+
const { open } = await import("@tauri-apps/plugin-dialog");
159+
const selected = await open({
160+
directory: true,
161+
multiple: false,
162+
title: "Select worktrees folder",
163+
});
164+
if (selected && typeof selected === "string") {
165+
onSetWorktreesFolderDraft(selected);
166+
}
167+
} catch (error) {
168+
pushErrorToast({
169+
title: "Failed to open folder picker",
170+
message: error instanceof Error ? error.message : String(error),
171+
});
172+
}
173+
}}
174+
disabled={environmentSaving}
175+
>
176+
Browse
177+
</button>
178+
</div>
179+
</div>
125180
</>
126181
)}
127182
</SettingsSection>

src/features/settings/hooks/useSettingsEnvironmentsSection.ts

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,37 +19,33 @@ export type SettingsEnvironmentsSectionProps = {
1919
environmentDraftScript: string;
2020
environmentSavedScript: string | null;
2121
environmentDirty: boolean;
22+
worktreesFolderDraft: string;
23+
worktreesFolderSaved: string | null;
24+
worktreesFolderDirty: boolean;
2225
onSetEnvironmentWorkspaceId: Dispatch<SetStateAction<string | null>>;
2326
onSetEnvironmentDraftScript: Dispatch<SetStateAction<string>>;
27+
onSetWorktreesFolderDraft: Dispatch<SetStateAction<string>>;
2428
onSaveEnvironmentSetup: () => Promise<void>;
2529
};
2630

2731
export const useSettingsEnvironmentsSection = ({
2832
mainWorkspaces,
2933
onUpdateWorkspaceSettings,
3034
}: UseSettingsEnvironmentsSectionArgs): SettingsEnvironmentsSectionProps => {
31-
const [environmentWorkspaceId, setEnvironmentWorkspaceId] = useState<string | null>(
32-
null,
33-
);
35+
const [environmentWorkspaceId, setEnvironmentWorkspaceId] = useState<string | null>(null);
3436
const [environmentDraftScript, setEnvironmentDraftScript] = useState("");
35-
const [environmentSavedScript, setEnvironmentSavedScript] = useState<string | null>(
36-
null,
37-
);
38-
const [environmentLoadedWorkspaceId, setEnvironmentLoadedWorkspaceId] = useState<
39-
string | null
40-
>(null);
37+
const [environmentSavedScript, setEnvironmentSavedScript] = useState<string | null>(null);
38+
const [environmentLoadedWorkspaceId, setEnvironmentLoadedWorkspaceId] = useState<string | null>(null);
4139
const [environmentError, setEnvironmentError] = useState<string | null>(null);
4240
const [environmentSaving, setEnvironmentSaving] = useState(false);
41+
const [worktreesFolderDraft, setWorktreesFolderDraft] = useState("");
42+
const [worktreesFolderSaved, setWorktreesFolderSaved] = useState<string | null>(null);
4343

4444
const environmentWorkspace = useMemo(() => {
45-
if (mainWorkspaces.length === 0) {
46-
return null;
47-
}
45+
if (mainWorkspaces.length === 0) return null;
4846
if (environmentWorkspaceId) {
4947
const found = mainWorkspaces.find((workspace) => workspace.id === environmentWorkspaceId);
50-
if (found) {
51-
return found;
52-
}
48+
if (found) return found;
5349
}
5450
return mainWorkspaces[0] ?? null;
5551
}, [environmentWorkspaceId, mainWorkspaces]);
@@ -58,11 +54,16 @@ export const useSettingsEnvironmentsSection = ({
5854
return normalizeWorktreeSetupScript(environmentWorkspace?.settings.worktreeSetupScript);
5955
}, [environmentWorkspace?.settings.worktreeSetupScript]);
6056

57+
const worktreesFolderFromWorkspace = useMemo(() => {
58+
return environmentWorkspace?.settings.worktreesFolder ?? null;
59+
}, [environmentWorkspace?.settings.worktreesFolder]);
60+
6161
const environmentDraftNormalized = useMemo(() => {
6262
return normalizeWorktreeSetupScript(environmentDraftScript);
6363
}, [environmentDraftScript]);
6464

6565
const environmentDirty = environmentDraftNormalized !== environmentSavedScript;
66+
const worktreesFolderDirty = (worktreesFolderDraft.trim() || null) !== worktreesFolderSaved;
6667

6768
useEffect(() => {
6869
if (!environmentWorkspace) {
@@ -72,53 +73,61 @@ export const useSettingsEnvironmentsSection = ({
7273
setEnvironmentDraftScript("");
7374
setEnvironmentError(null);
7475
setEnvironmentSaving(false);
76+
setWorktreesFolderDraft("");
77+
setWorktreesFolderSaved(null);
7578
return;
7679
}
77-
7880
if (environmentWorkspaceId !== environmentWorkspace.id) {
7981
setEnvironmentWorkspaceId(environmentWorkspace.id);
8082
}
8183
}, [environmentWorkspace, environmentWorkspaceId]);
8284

8385
useEffect(() => {
84-
if (!environmentWorkspace) {
85-
return;
86-
}
87-
86+
if (!environmentWorkspace) return;
8887
if (environmentLoadedWorkspaceId !== environmentWorkspace.id) {
8988
setEnvironmentLoadedWorkspaceId(environmentWorkspace.id);
9089
setEnvironmentSavedScript(environmentSavedScriptFromWorkspace);
9190
setEnvironmentDraftScript(environmentSavedScriptFromWorkspace ?? "");
91+
setWorktreesFolderSaved(worktreesFolderFromWorkspace);
92+
setWorktreesFolderDraft(worktreesFolderFromWorkspace ?? "");
9293
setEnvironmentError(null);
9394
return;
9495
}
95-
9696
if (!environmentDirty && environmentSavedScript !== environmentSavedScriptFromWorkspace) {
9797
setEnvironmentSavedScript(environmentSavedScriptFromWorkspace);
9898
setEnvironmentDraftScript(environmentSavedScriptFromWorkspace ?? "");
9999
setEnvironmentError(null);
100100
}
101+
if (!worktreesFolderDirty && worktreesFolderSaved !== worktreesFolderFromWorkspace) {
102+
setWorktreesFolderSaved(worktreesFolderFromWorkspace);
103+
setWorktreesFolderDraft(worktreesFolderFromWorkspace ?? "");
104+
}
101105
}, [
102106
environmentDirty,
103107
environmentLoadedWorkspaceId,
104108
environmentSavedScript,
105109
environmentSavedScriptFromWorkspace,
106110
environmentWorkspace,
111+
worktreesFolderDirty,
112+
worktreesFolderFromWorkspace,
113+
worktreesFolderSaved,
107114
]);
108115

109116
const handleSaveEnvironmentSetup = async () => {
110-
if (!environmentWorkspace || environmentSaving) {
111-
return;
112-
}
117+
if (!environmentWorkspace || environmentSaving) return;
113118
const nextScript = environmentDraftNormalized;
119+
const nextFolder = worktreesFolderDraft.trim() || null;
114120
setEnvironmentSaving(true);
115121
setEnvironmentError(null);
116122
try {
117123
await onUpdateWorkspaceSettings(environmentWorkspace.id, {
118124
worktreeSetupScript: nextScript,
125+
worktreesFolder: nextFolder,
119126
});
120127
setEnvironmentSavedScript(nextScript);
121128
setEnvironmentDraftScript(nextScript ?? "");
129+
setWorktreesFolderSaved(nextFolder);
130+
setWorktreesFolderDraft(nextFolder ?? "");
122131
} catch (error) {
123132
setEnvironmentError(error instanceof Error ? error.message : String(error));
124133
} finally {
@@ -134,8 +143,12 @@ export const useSettingsEnvironmentsSection = ({
134143
environmentDraftScript,
135144
environmentSavedScript,
136145
environmentDirty,
146+
worktreesFolderDraft,
147+
worktreesFolderSaved,
148+
worktreesFolderDirty,
137149
onSetEnvironmentWorkspaceId: setEnvironmentWorkspaceId,
138150
onSetEnvironmentDraftScript: setEnvironmentDraftScript,
151+
onSetWorktreesFolderDraft: setWorktreesFolderDraft,
139152
onSaveEnvironmentSetup: handleSaveEnvironmentSetup,
140153
};
141154
};

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type WorkspaceSettings = {
77
launchScript?: string | null;
88
launchScripts?: LaunchScriptEntry[] | null;
99
worktreeSetupScript?: string | null;
10+
worktreesFolder?: string | null;
1011
};
1112

1213
export type LaunchScriptIconId =
@@ -303,6 +304,7 @@ export type AppSettings = {
303304
composerListContinuation: boolean;
304305
composerCodeBlockCopyUseModifier: boolean;
305306
workspaceGroups: WorkspaceGroup[];
307+
globalWorktreesFolder: string | null;
306308
openAppTargets: OpenAppTarget[];
307309
selectedOpenAppId: string;
308310
};

0 commit comments

Comments
 (0)