aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-08-28 18:25:07 +0300
committerGitHub <noreply@github.com>2026-08-28 18:25:07 +0300
commit095f1337d6502081658973ae3487dee8c6b34e1a (patch)
tree4174cac82dd51dabcc1fcef57b45df2462ef1918 /src
parent13a9f6402fb77a31499bf758b56ca42b79936706 (diff)
fix: trust worktree repositories per request (#3344)
refs #3044
Diffstat (limited to 'src')
-rw-r--r--src/api/schema/tests.rs2
-rw-r--r--src/api/schema/worktrees.rs8
-rw-r--r--src/app/api/worktrees.rs101
-rw-r--r--src/app/api/worktrees/deferred.rs15
-rw-r--r--src/app/worktrees.rs35
-rw-r--r--src/cli/spec.rs12
-rw-r--r--src/cli/worktree.rs47
-rw-r--r--src/worktree.rs175
8 files changed, 276 insertions, 119 deletions
diff --git a/src/api/schema/tests.rs b/src/api/schema/tests.rs
index 5199038d..663f3fc4 100644
--- a/src/api/schema/tests.rs
+++ b/src/api/schema/tests.rs
@@ -720,10 +720,12 @@ fn worktree_request_and_response_round_trip() {
branch: Some("worktree/api".into()),
base: Some("HEAD".into()),
focus: true,
+ trust_repository: true,
..WorktreeCreateParams::default()
}),
};
let json = serde_json::to_string(&request).unwrap();
+ assert!(json.contains("\"trust_repository\":true"));
let restored: Request = serde_json::from_str(&json).unwrap();
assert_eq!(restored, request);
diff --git a/src/api/schema/worktrees.rs b/src/api/schema/worktrees.rs
index dc35128e..3b0d4b35 100644
--- a/src/api/schema/worktrees.rs
+++ b/src/api/schema/worktrees.rs
@@ -6,6 +6,8 @@ pub struct WorktreeListParams {
pub workspace_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
+ #[serde(default, skip_serializing_if = "super::is_false")]
+ pub trust_repository: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema, Default)]
@@ -24,6 +26,8 @@ pub struct WorktreeCreateParams {
pub label: Option<String>,
#[serde(default)]
pub focus: bool,
+ #[serde(default, skip_serializing_if = "super::is_false")]
+ pub trust_repository: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema, Default)]
@@ -40,6 +44,8 @@ pub struct WorktreeOpenParams {
pub label: Option<String>,
#[serde(default)]
pub focus: bool,
+ #[serde(default, skip_serializing_if = "super::is_false")]
+ pub trust_repository: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
@@ -47,6 +53,8 @@ pub struct WorktreeRemoveParams {
pub workspace_id: String,
#[serde(default)]
pub force: bool,
+ #[serde(default, skip_serializing_if = "super::is_false")]
+ pub trust_repository: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
diff --git a/src/app/api/worktrees.rs b/src/app/api/worktrees.rs
index 774b981a..4a3e23a8 100644
--- a/src/app/api/worktrees.rs
+++ b/src/app/api/worktrees.rs
@@ -50,11 +50,18 @@ impl App {
id: String,
params: WorktreeListParams,
) -> String {
- let source = match self.resolve_worktree_list_source(params.workspace_id, params.cwd) {
+ let source = match self.resolve_worktree_list_source(
+ params.workspace_id,
+ params.cwd,
+ params.trust_repository,
+ ) {
Ok(source) => source,
Err(err) => return encode_error(id, err.code, err.message),
};
- let entries = match crate::worktree::list_existing_worktrees(&source.source_repo_root) {
+ let entries = match crate::worktree::list_existing_worktrees(
+ &source.source_repo_root,
+ params.trust_repository,
+ ) {
Ok(entries) => entries,
Err(err) => return encode_error(id, "worktree_list_failed", err),
};
@@ -88,7 +95,12 @@ impl App {
Ok(source) => source,
Err(err) => return encode_error(id, err.code, err.message),
};
- let entry = match self.find_worktree_entry(&source, params.path, params.branch) {
+ let entry = match self.find_worktree_entry(
+ &source,
+ params.path,
+ params.branch,
+ params.trust_repository,
+ ) {
Ok(entry) => entry,
Err(err) => return encode_error(id, err.code, err.message),
};
@@ -235,6 +247,7 @@ impl App {
&mut self,
workspace_id: Option<String>,
cwd: Option<String>,
+ trust_repository: bool,
) -> Result<WorktreeSource, ApiFailure> {
if workspace_id.is_some() && cwd.is_some() {
return Err(ApiFailure::new(
@@ -250,7 +263,7 @@ impl App {
format!("workspace {workspace_id} not found"),
));
};
- return self.worktree_list_source_from_workspace(ws_idx);
+ return self.worktree_list_source_from_workspace(ws_idx, trust_repository);
}
if let Some(cwd) = cwd {
@@ -261,8 +274,13 @@ impl App {
"Herdr worktree actions require a path inside a Git work tree",
)
})?;
- let workspace_idx = self.list_source_workspace_idx_for_space(&space);
- return Ok(worktree_source_from_space(space, workspace_idx, true));
+ let workspace_idx = self.list_source_workspace_idx_for_space(&space, trust_repository);
+ return Ok(worktree_source_from_space(
+ space,
+ workspace_idx,
+ true,
+ trust_repository,
+ ));
}
let Some(ws_idx) = self.state.active.or_else(|| {
@@ -276,7 +294,7 @@ impl App {
"workspace_id or cwd is required when no workspace is active",
));
};
- self.worktree_list_source_from_workspace(ws_idx)
+ self.worktree_list_source_from_workspace(ws_idx, trust_repository)
}
fn worktree_source_from_workspace(&self, ws_idx: usize) -> Result<WorktreeSource, ApiFailure> {
@@ -331,6 +349,7 @@ impl App {
fn worktree_list_source_from_workspace(
&self,
ws_idx: usize,
+ trust_repository: bool,
) -> Result<WorktreeSource, ApiFailure> {
let Some(ws) = self.state.workspaces.get(ws_idx) else {
return Err(ApiFailure::new(
@@ -370,11 +389,16 @@ impl App {
));
};
let workspace_idx = if space.is_linked_worktree {
- self.list_source_workspace_idx_for_space(&space)
+ self.list_source_workspace_idx_for_space(&space, trust_repository)
} else {
Some(ws_idx)
};
- Ok(worktree_source_from_space(space, workspace_idx, true))
+ Ok(worktree_source_from_space(
+ space,
+ workspace_idx,
+ true,
+ trust_repository,
+ ))
}
fn ensure_source_parent_membership(
@@ -415,9 +439,10 @@ impl App {
fn list_source_workspace_idx_for_space(
&self,
space: &crate::workspace::GitSpaceMetadata,
+ trust_repository: bool,
) -> Option<usize> {
if space.is_linked_worktree {
- let parent_checkout = parent_checkout_path_for_space(space);
+ let parent_checkout = parent_checkout_path_for_space(space, trust_repository);
self.open_workspace_idx_for_checkout(&parent_checkout)
} else {
self.find_parent_workspace_for_space(space)
@@ -475,9 +500,11 @@ impl App {
source: &WorktreeSource,
path: Option<String>,
branch: Option<String>,
+ trust_repository: bool,
) -> Result<crate::worktree::ExistingWorktree, ApiFailure> {
- let entries = crate::worktree::list_existing_worktrees(&source.source_repo_root)
- .map_err(|err| ApiFailure::new("worktree_list_failed", err))?;
+ let entries =
+ crate::worktree::list_existing_worktrees(&source.source_repo_root, trust_repository)
+ .map_err(|err| ApiFailure::new("worktree_list_failed", err))?;
if let Some(path) = path {
let expected = absolute_user_path(&path)?;
let expected = crate::worktree::canonical_or_original(&expected);
@@ -667,9 +694,10 @@ fn worktree_source_from_space(
space: crate::workspace::GitSpaceMetadata,
workspace_idx: Option<usize>,
allow_linked: bool,
+ trust_repository: bool,
) -> WorktreeSource {
let source_checkout_path = if allow_linked {
- parent_checkout_path_for_space(&space)
+ parent_checkout_path_for_space(&space, trust_repository)
} else {
space.repo_root.clone()
};
@@ -682,12 +710,15 @@ fn worktree_source_from_space(
}
}
-fn parent_checkout_path_for_space(space: &crate::workspace::GitSpaceMetadata) -> PathBuf {
+fn parent_checkout_path_for_space(
+ space: &crate::workspace::GitSpaceMetadata,
+ trust_repository: bool,
+) -> PathBuf {
if !space.is_linked_worktree {
return space.repo_root.clone();
}
- crate::worktree::list_existing_worktrees(&space.repo_root)
+ crate::worktree::list_existing_worktrees(&space.repo_root, trust_repository)
.ok()
.and_then(|entries| {
entries.into_iter().find_map(|entry| {
@@ -957,8 +988,12 @@ mod tests {
for (_, runtime) in app.terminal_runtimes.drain() {
runtime.shutdown();
}
- let remove =
- crate::worktree::build_worktree_remove_command(&repo, Path::new(&worktree.path), false);
+ let remove = crate::worktree::build_worktree_remove_command(
+ &repo,
+ Path::new(&worktree.path),
+ false,
+ false,
+ );
crate::worktree::run_worktree_command(&remove).unwrap();
let _ = std::fs::remove_dir_all(worktree_root);
let _ = std::fs::remove_dir_all(repo);
@@ -1271,8 +1306,12 @@ mod tests {
for (_, runtime) in app.terminal_runtimes.drain() {
runtime.shutdown();
}
- let remove =
- crate::worktree::build_worktree_remove_command(&repo, Path::new(&worktree.path), false);
+ let remove = crate::worktree::build_worktree_remove_command(
+ &repo,
+ Path::new(&worktree.path),
+ false,
+ false,
+ );
crate::worktree::run_worktree_command(&remove).unwrap();
let _ = std::fs::remove_dir_all(worktree_root);
let _ = std::fs::remove_dir_all(repo);
@@ -1440,7 +1479,7 @@ mod tests {
)
}));
- let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false);
+ let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false, false);
crate::worktree::run_worktree_command(&remove).unwrap();
let _ = std::fs::remove_dir_all(repo);
}
@@ -1518,7 +1557,7 @@ mod tests {
)
}));
- let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false);
+ let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false, false);
crate::worktree::run_worktree_command(&remove).unwrap();
let _ = std::fs::remove_dir_all(repo);
}
@@ -1603,6 +1642,7 @@ mod tests {
method: crate::api::schema::Method::WorktreeList(WorktreeListParams {
workspace_id: Some(app.state.workspaces[0].id.clone()),
cwd: None,
+ trust_repository: false,
}),
});
@@ -1620,7 +1660,7 @@ mod tests {
);
assert!(entry.is_linked_worktree);
- let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false);
+ let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false, false);
crate::worktree::run_worktree_command(&remove).unwrap();
let _ = std::fs::remove_dir_all(repo);
}
@@ -1653,10 +1693,12 @@ mod tests {
crate::api::schema::Method::WorktreeList(WorktreeListParams {
workspace_id: Some(child_id),
cwd: None,
+ trust_repository: false,
}),
crate::api::schema::Method::WorktreeList(WorktreeListParams {
workspace_id: None,
cwd: Some(checkout.display().to_string()),
+ trust_repository: false,
}),
] {
let response = app.handle_api_request(Request {
@@ -1681,7 +1723,7 @@ mod tests {
}));
}
- let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false);
+ let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false, false);
crate::worktree::run_worktree_command(&remove).unwrap();
let _ = std::fs::remove_dir_all(repo);
}
@@ -1710,6 +1752,7 @@ mod tests {
method: crate::api::schema::Method::WorktreeList(WorktreeListParams {
workspace_id: Some(app.state.workspaces[0].id.clone()),
cwd: None,
+ trust_repository: false,
}),
});
@@ -1767,6 +1810,7 @@ mod tests {
method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams {
workspace_id: child_id.clone(),
force: false,
+ trust_repository: false,
}),
},
);
@@ -1782,6 +1826,7 @@ mod tests {
method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams {
workspace_id: child_id,
force: true,
+ trust_repository: false,
}),
},
);
@@ -1838,6 +1883,7 @@ mod tests {
method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams {
workspace_id: child_id.clone(),
force: false,
+ trust_repository: false,
}),
},
);
@@ -1923,6 +1969,7 @@ mod tests {
method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams {
workspace_id: child_id.clone(),
force: false,
+ trust_repository: false,
}),
},
respond_to,
@@ -1996,6 +2043,7 @@ mod tests {
method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams {
workspace_id: child_id.clone(),
force: false,
+ trust_repository: false,
}),
},
first_tx,
@@ -2006,6 +2054,7 @@ mod tests {
method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams {
workspace_id: child_id,
force: false,
+ trust_repository: false,
}),
},
second_tx,
@@ -2065,6 +2114,7 @@ mod tests {
method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams {
workspace_id: first_id,
force: true,
+ trust_repository: false,
}),
},
first_tx,
@@ -2075,6 +2125,7 @@ mod tests {
method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams {
workspace_id: second_id,
force: true,
+ trust_repository: false,
}),
},
second_tx,
@@ -2110,6 +2161,7 @@ mod tests {
path: Some(checkout.display().to_string()),
label: None,
focus: false,
+ trust_repository: false,
}),
},
respond_to,
@@ -2164,6 +2216,7 @@ mod tests {
method: crate::api::schema::Method::WorktreeRemove(WorktreeRemoveParams {
workspace_id: child_id,
force: false,
+ trust_repository: false,
}),
},
respond_to,
@@ -2175,7 +2228,7 @@ mod tests {
let error: ErrorResponse = serde_json::from_str(&response).unwrap();
assert_eq!(error.error.code, "worktree_operation_in_progress");
assert!(app.event_rx.try_recv().is_err());
- let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, true);
+ let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, true, false);
let _ = crate::worktree::run_worktree_command(&remove);
let _ = std::fs::remove_dir_all(repo);
}
diff --git a/src/app/api/worktrees/deferred.rs b/src/app/api/worktrees/deferred.rs
index 3cd5e9f1..4755b38d 100644
--- a/src/app/api/worktrees/deferred.rs
+++ b/src/app/api/worktrees/deferred.rs
@@ -199,6 +199,7 @@ impl App {
&path,
&branch,
&base,
+ params.trust_repository,
)
});
let _ = event_tx.blocking_send(AppEvent::WorktreeAddFinished(Box::new(
@@ -259,7 +260,11 @@ impl App {
#[cfg(windows)]
{
if !params.force
- && crate::worktree::checkout_has_dirty_files(&space.checkout_path).unwrap_or(false)
+ && crate::worktree::checkout_has_dirty_files(
+ &space.checkout_path,
+ params.trust_repository,
+ )
+ .unwrap_or(false)
{
Self::send_api_response(
respond_to,
@@ -311,6 +316,7 @@ impl App {
&space.repo_root,
&space.checkout_path,
params.force,
+ params.trust_repository,
);
let api_request = ApiWorktreeRemoveRequest {
id,
@@ -321,10 +327,15 @@ impl App {
let repo_root = space.repo_root;
let path = space.checkout_path;
let force = params.force;
+ let trust_repository = params.trust_repository;
let event_tx = self.event_tx.clone();
std::thread::spawn(move || {
let result = crate::worktree::run_worktree_remove_command_with_recovery(
- &command, &repo_root, &path, force,
+ &command,
+ &repo_root,
+ &path,
+ force,
+ trust_repository,
);
let _ = event_tx.blocking_send(AppEvent::WorktreeRemoveFinished(Box::new(
crate::events::WorktreeRemoveResult {
diff --git a/src/app/worktrees.rs b/src/app/worktrees.rs
index 414d74b5..9993a70e 100644
--- a/src/app/worktrees.rs
+++ b/src/app/worktrees.rs
@@ -161,7 +161,7 @@ impl App {
}
};
- let list = match crate::worktree::list_existing_worktrees(&space.repo_root) {
+ let list = match crate::worktree::list_existing_worktrees(&space.repo_root, false) {
Ok(list) => list,
Err(err) => {
self.state.config_diagnostic = Some(err);
@@ -551,6 +551,7 @@ impl App {
&path,
&branch,
"HEAD",
+ false,
)
});
let _ = event_tx.blocking_send(AppEvent::WorktreeAddFinished(Box::new(
@@ -599,6 +600,7 @@ impl App {
base: Some("HEAD".into()),
focus: true,
label: None,
+ trust_repository: false,
},
);
if let Some(message) = immediate_api_error_message(immediate_response.as_deref()) {
@@ -641,7 +643,8 @@ impl App {
}
#[cfg(windows)]
if !remove.force_confirmation
- && crate::worktree::checkout_has_dirty_files(&remove.path).unwrap_or(false)
+ && crate::worktree::checkout_has_dirty_files(&remove.path, false)
+ .unwrap_or(false)
{
remove.force_confirmation = true;
remove.error = None;
@@ -686,12 +689,13 @@ impl App {
})
.unwrap_or((None, None));
- let command = crate::worktree::build_worktree_remove_command(&repo_root, &path, force);
+ let command =
+ crate::worktree::build_worktree_remove_command(&repo_root, &path, force, false);
tracing::info!(workspace_id = %workspace_id, path = %path.display(), force, "starting git worktree remove");
let event_tx = self.event_tx.clone();
std::thread::spawn(move || {
let result = crate::worktree::run_worktree_remove_command_with_recovery(
- &command, &repo_root, &path, force,
+ &command, &repo_root, &path, force, false,
);
let _ = event_tx.blocking_send(AppEvent::WorktreeRemoveFinished(Box::new(
WorktreeRemoveResult {
@@ -728,6 +732,7 @@ impl App {
branch: None,
focus: true,
label: None,
+ trust_repository: false,
},
);
if serde_json::from_str::<crate::api::schema::SuccessResponse>(&response).is_ok() {
@@ -751,7 +756,7 @@ impl App {
}
#[cfg(windows)]
if !remove.force_confirmation
- && crate::worktree::checkout_has_dirty_files(&remove.path).unwrap_or(false)
+ && crate::worktree::checkout_has_dirty_files(&remove.path, false).unwrap_or(false)
{
remove.force_confirmation = true;
remove.error = None;
@@ -767,6 +772,7 @@ impl App {
crate::api::schema::WorktreeRemoveParams {
workspace_id,
force,
+ trust_repository: false,
},
);
if let Some(message) = immediate_api_error_message(immediate_response.as_deref()) {
@@ -1831,7 +1837,7 @@ mod tests {
}));
shutdown_test_runtimes(&mut app);
- let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false);
+ let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false, false);
crate::worktree::run_worktree_command(&remove).unwrap();
let _ = std::fs::remove_dir_all(worktree_root);
let _ = std::fs::remove_dir_all(repo);
@@ -1929,7 +1935,7 @@ mod tests {
}
assert!(checkout.join("README.md").exists());
- let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false);
+ let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false, false);
crate::worktree::run_worktree_command(&remove).unwrap();
let _ = std::fs::remove_dir_all(worktree_root);
let _ = std::fs::remove_dir_all(repo);
@@ -1988,7 +1994,7 @@ mod tests {
branch
);
- let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false);
+ let remove = crate::worktree::build_worktree_remove_command(&repo, &checkout, false, false);
crate::worktree::run_worktree_command(&remove).unwrap();
let _ = std::fs::remove_dir_all(worktree_root);
let _ = std::fs::remove_dir_all(repo);
@@ -2036,8 +2042,12 @@ mod tests {
}
assert!(checkout.join("README.md").exists());
- let remove_new =
- crate::worktree::build_worktree_remove_command(&source_checkout_path, &checkout, false);
+ let remove_new = crate::worktree::build_worktree_remove_command(
+ &source_checkout_path,
+ &checkout,
+ false,
+ false,
+ );
crate::worktree::run_worktree_command(&remove_new).unwrap();
let _ = std::fs::remove_dir_all(worktree_root);
let _ = std::fs::remove_dir_all(source_checkout_path);
@@ -2096,10 +2106,11 @@ mod tests {
}
assert!(checkout.join("SOURCE.md").exists());
- let remove_new = crate::worktree::build_worktree_remove_command(&repo, &checkout, false);
+ let remove_new =
+ crate::worktree::build_worktree_remove_command(&repo, &checkout, false, false);
crate::worktree::run_worktree_command(&remove_new).unwrap();
let remove_source =
- crate::worktree::build_worktree_remove_command(&repo, &source_checkout, false);
+ crate::worktree::build_worktree_remove_command(&repo, &source_checkout, false, false);
crate::worktree::run_worktree_command(&remove_source).unwrap();
let _ = std::fs::remove_dir_all(worktree_root);
let _ = std::fs::remove_dir_all(repo);
diff --git a/src/cli/spec.rs b/src/cli/spec.rs
index 134bf0bf..ed3c74a8 100644
--- a/src/cli/spec.rs
+++ b/src/cli/spec.rs
@@ -239,7 +239,8 @@ fn worktree_command() -> Command {
Command::new("list")
.about("List worktree workspaces")
.arg(option("workspace", "ID"))
- .arg(path_option("cwd", "PATH")),
+ .arg(path_option("cwd", "PATH"))
+ .arg(flag("trust-repository")),
)
.subcommand(
Command::new("create")
@@ -251,7 +252,8 @@ fn worktree_command() -> Command {
.arg(path_option("path", "PATH"))
.arg(option("label", "TEXT"))
.arg(flag("focus"))
- .arg(flag("no-focus")),
+ .arg(flag("no-focus"))
+ .arg(flag("trust-repository")),
)
.subcommand(
Command::new("open")
@@ -262,13 +264,15 @@ fn worktree_command() -> Command {
.arg(option("branch", "NAME"))
.arg(option("label", "TEXT"))
.arg(flag("focus"))
- .arg(flag("no-focus")),
+ .arg(flag("no-focus"))
+ .arg(flag("trust-repository")),
)
.subcommand(
Command::new("remove")
.about("Remove a worktree checkout")
.arg(option("workspace", "ID"))
- .arg(flag("force")),
+ .arg(flag("force"))
+ .arg(flag("trust-repository")),
)
}
diff --git a/src/cli/worktree.rs b/src/cli/worktree.rs
index 4a36876a..4003eb6a 100644
--- a/src/cli/worktree.rs
+++ b/src/cli/worktree.rs
@@ -28,6 +28,7 @@ pub(super) fn run_worktree_command(args: &[String]) -> std::io::Result<i32> {
fn worktree_list(args: &[String]) -> std::io::Result<i32> {
let mut workspace_id = None;
let mut cwd = None;
+ let mut trust_repository = false;
let mut index = 0;
while index < args.len() {
@@ -48,6 +49,10 @@ fn worktree_list(args: &[String]) -> std::io::Result<i32> {
cwd = Some(normalize_path_arg(value)?);
index += 2;
}
+ "--trust-repository" => {
+ trust_repository = true;
+ index += 1;
+ }
"--json" => index += 1,
other => {
eprintln!("unknown option: {other}");
@@ -56,11 +61,15 @@ fn worktree_list(args: &[String]) -> std::io::Result<i32> {
}
}
if workspace_id.is_some() && cwd.is_some() {
- eprintln!("usage: herdr worktree list [--workspace ID | --cwd PATH]");
+ eprintln!("usage: herdr worktree list [--workspace ID | --cwd PATH] [--trust-repository]");
return Ok(2);
}
- super::runtime::worktree_list(WorktreeListParams { workspace_id, cwd })
+ super::runtime::worktree_list(WorktreeListParams {
+ workspace_id,
+ cwd,
+ trust_repository,
+ })
}
fn worktree_create(args: &[String]) -> std::io::Result<i32> {
@@ -71,6 +80,7 @@ fn worktree_create(args: &[String]) -> std::io::Result<i32> {
let mut path = None;
let mut label = None;
let mut focus = false;
+ let mut trust_repository = false;
let mut index = 0;
while index < args.len() {
@@ -131,6 +141,10 @@ fn worktree_create(args: &[String]) -> std::io::Result<i32> {
focus = false;
index += 1;
}
+ "--trust-repository" => {
+ trust_repository = true;
+ index += 1;
+ }
"--json" => index += 1,
other => {
eprintln!("unknown option: {other}");
@@ -140,7 +154,7 @@ fn worktree_create(args: &[String]) -> std::io::Result<i32> {
}
if workspace_id.is_some() && cwd.is_some() {
eprintln!(
- "usage: herdr worktree create [--workspace ID | --cwd PATH] [--branch NAME] [--base REF] [--path PATH] [--label TEXT] [--focus] [--no-focus]"
+ "usage: herdr worktree create [--workspace ID | --cwd PATH] [--branch NAME] [--base REF] [--path PATH] [--label TEXT] [--focus] [--no-focus] [--trust-repository]"
);
return Ok(2);
}
@@ -153,6 +167,7 @@ fn worktree_create(args: &[String]) -> std::io::Result<i32> {
path,
label,
focus,
+ trust_repository,
})
}
@@ -163,6 +178,7 @@ fn worktree_open(args: &[String]) -> std::io::Result<i32> {
let mut branch = None;
let mut label = None;
let mut focus = false;
+ let mut trust_repository = false;
let mut index = 0;
while index < args.len() {
@@ -215,6 +231,10 @@ fn worktree_open(args: &[String]) -> std::io::Result<i32> {
focus = false;
index += 1;
}
+ "--trust-repository" => {
+ trust_repository = true;
+ index += 1;
+ }
"--json" => index += 1,
other => {
eprintln!("unknown option: {other}");
@@ -224,13 +244,13 @@ fn worktree_open(args: &[String]) -> std::io::Result<i32> {
}
if workspace_id.is_some() && cwd.is_some() {
eprintln!(
- "usage: herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus]"
+ "usage: herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus] [--trust-repository]"
);
return Ok(2);
}
if path.is_some() == branch.is_some() {
eprintln!(
- "usage: herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus]"
+ "usage: herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus] [--trust-repository]"
);
return Ok(2);
}
@@ -242,12 +262,14 @@ fn worktree_open(args: &[String]) -> std::io::Result<i32> {
branch,
label,
focus,
+ trust_repository,
})
}
fn worktree_remove(args: &[String]) -> std::io::Result<i32> {
let mut workspace_id = None;
let mut force = false;
+ let mut trust_repository = false;
let mut index = 0;
while index < args.len() {
@@ -264,6 +286,10 @@ fn worktree_remove(args: &[String]) -> std::io::Result<i32> {
force = true;
index += 1;
}
+ "--trust-repository" => {
+ trust_repository = true;
+ index += 1;
+ }
"--json" => index += 1,
other => {
eprintln!("unknown option: {other}");
@@ -273,26 +299,27 @@ fn worktree_remove(args: &[String]) -> std::io::Result<i32> {
}
let Some(workspace_id) = workspace_id else {
- eprintln!("usage: herdr worktree remove --workspace ID [--force]");
+ eprintln!("usage: herdr worktree remove --workspace ID [--force] [--trust-repository]");
return Ok(2);
};
super::runtime::worktree_remove(WorktreeRemoveParams {
workspace_id,
force,
+ trust_repository,
})
}
fn print_worktree_help() {
eprintln!("herdr worktree commands:");
- eprintln!(" herdr worktree list [--workspace ID | --cwd PATH]");
+ eprintln!(" herdr worktree list [--workspace ID | --cwd PATH] [--trust-repository]");
eprintln!(
- " herdr worktree create [--workspace ID | --cwd PATH] [--branch NAME] [--base REF] [--path PATH] [--label TEXT] [--focus] [--no-focus]"
+ " herdr worktree create [--workspace ID | --cwd PATH] [--branch NAME] [--base REF] [--path PATH] [--label TEXT] [--focus] [--no-focus] [--trust-repository]"
);
eprintln!(
- " herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus]"
+ " herdr worktree open [--workspace ID | --cwd PATH] (--path PATH | --branch NAME) [--label TEXT] [--focus] [--no-focus] [--trust-repository]"
);
- eprintln!(" herdr worktree remove --workspace ID [--force]");
+ eprintln!(" herdr worktree remove --workspace ID [--force] [--trust-repository]");
}
fn normalize_path_arg(value: &str) -> std::io::Result<String> {
diff --git a/src/worktree.rs b/src/worktree.rs
index 0bb091d7..523bc502 100644
--- a/src/worktree.rs
+++ b/src/worktree.rs
@@ -151,6 +151,23 @@ pub(crate) fn canonical_or_original(path: &Path) -> PathBuf {
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
+fn repository_git_command(repo_root: &Path, trust_repository: bool) -> std::process::Command {
+ let mut command = crate::noninteractive_process::command("git");
+ command.args(repository_git_args(repo_root, trust_repository));
+ command
+}
+
+fn repository_git_args(repo_root: &Path, trust_repository: bool) -> Vec<String> {
+ let mut args = Vec::new();
+ if trust_repository {
+ args.push("-c".to_string());
+ args.push(format!("safe.directory={}", repo_root.display()));
+ }
+ args.push("-C".to_string());
+ args.push(repo_root.display().to_string());
+ args
+}
+
pub(crate) fn default_checkout_path(root: &Path, repo_name: &str, branch: &str) -> PathBuf {
root.join(repo_name).join(branch_to_path_slug(branch))
}
@@ -159,13 +176,10 @@ pub(crate) fn build_worktree_remove_command(
repo_root: &Path,
path: &Path,
force: bool,
+ trust_repository: bool,
) -> WorktreeCommand {
- let mut args = vec![
- "-C".to_string(),
- repo_root.display().to_string(),
- "worktree".to_string(),
- "remove".to_string(),
- ];
+ let mut args = repository_git_args(repo_root, trust_repository);
+ args.extend(["worktree".to_string(), "remove".to_string()]);
if force {
args.push("--force".to_string());
}
@@ -197,16 +211,12 @@ pub(crate) fn worktree_dirty_remove_message(path: &Path) -> String {
}
#[cfg(any(windows, test))]
-pub(crate) fn checkout_has_dirty_files(path: &Path) -> Result<bool, String> {
- let path_arg = path.display().to_string();
- let output = crate::noninteractive_process::command("git")
- .args([
- "-C",
- &path_arg,
- "status",
- "--porcelain",
- "--untracked-files=all",
- ])
+pub(crate) fn checkout_has_dirty_files(
+ path: &Path,
+ trust_repository: bool,
+) -> Result<bool, String> {
+ let output = repository_git_command(path, trust_repository)
+ .args(["status", "--porcelain", "--untracked-files=all"])
.output()
.map_err(|err| err.to_string())?;
@@ -230,19 +240,20 @@ pub(crate) fn build_worktree_add_new_branch_command(
path: &Path,
branch: &str,
base: &str,
+ trust_repository: bool,
) -> WorktreeCommand {
+ let mut args = repository_git_args(repo_root, trust_repository);
+ args.extend([
+ "worktree".to_string(),
+ "add".to_string(),
+ "-b".to_string(),
+ branch.to_string(),
+ path.display().to_string(),
+ base.to_string(),
+ ]);
WorktreeCommand {
program: "git".to_string(),
- args: vec![
- "-C".to_string(),
- repo_root.display().to_string(),
- "worktree".to_string(),
- "add".to_string(),
- "-b".to_string(),
- branch.to_string(),
- path.display().to_string(),
- base.to_string(),
- ],
+ args,
}
}
@@ -250,24 +261,27 @@ pub(crate) fn build_worktree_add_existing_branch_command(
repo_root: &Path,
path: &Path,
branch: &str,
+ trust_repository: bool,
) -> WorktreeCommand {
+ let mut args = repository_git_args(repo_root, trust_repository);
+ args.extend([
+ "worktree".to_string(),
+ "add".to_string(),
+ path.display().to_string(),
+ branch.to_string(),
+ ]);
WorktreeCommand {
program: "git".to_string(),
- args: vec![
- "-C".to_string(),
- repo_root.display().to_string(),
- "worktree".to_string(),
- "add".to_string(),
- path.display().to_string(),
- branch.to_string(),
- ],
+ args,
}
}
-pub(crate) fn local_branch_exists(repo_root: &Path, branch: &str) -> Result<bool, String> {
- let output = crate::noninteractive_process::command("git")
- .arg("-C")
- .arg(repo_root)
+fn local_branch_exists(
+ repo_root: &Path,
+ branch: &str,
+ trust_repository: bool,
+) -> Result<bool, String> {
+ let output = repository_git_command(repo_root, trust_repository)
.args(["show-ref", "--verify", "--quiet"])
.arg(format!("refs/heads/{branch}"))
.output()
@@ -296,11 +310,12 @@ pub(crate) fn run_worktree_add_command(
path: &Path,
branch: &str,
base: &str,
+ trust_repository: bool,
) -> Result<(), String> {
- let command = if local_branch_exists(repo_root, branch)? {
- build_worktree_add_existing_branch_command(repo_root, path, branch)
+ let command = if local_branch_exists(repo_root, branch, trust_repository)? {
+ build_worktree_add_existing_branch_command(repo_root, path, branch, trust_repository)
} else {
- build_worktree_add_new_branch_command(repo_root, path, branch, base)
+ build_worktree_add_new_branch_command(repo_root, path, branch, base, trust_repository)
};
run_worktree_command(&command)
}
@@ -330,15 +345,16 @@ pub(crate) fn run_worktree_remove_command_with_recovery(
repo_root: &Path,
path: &Path,
force: bool,
+ trust_repository: bool,
) -> Result<(), String> {
match run_worktree_command(command) {
Ok(()) => Ok(()),
Err(err) if force && is_not_working_tree_remove_error(&err) => {
- if worktree_list_contains_path(repo_root, path)? {
+ if worktree_list_contains_path(repo_root, path, trust_repository)? {
return Err(err);
}
if path.exists() {
- if !leftover_worktree_checkout_matches_repo(repo_root, path) {
+ if !leftover_worktree_checkout_matches_repo(repo_root, path, trust_repository) {
return Err(err);
}
std::fs::remove_dir_all(path).map_err(|remove_err| {
@@ -354,7 +370,11 @@ pub(crate) fn run_worktree_remove_command_with_recovery(
}
}
-fn leftover_worktree_checkout_matches_repo(repo_root: &Path, path: &Path) -> bool {
+fn leftover_worktree_checkout_matches_repo(
+ repo_root: &Path,
+ path: &Path,
+ trust_repository: bool,
+) -> bool {
let git_file = path.join(".git");
let Ok(content) = std::fs::read_to_string(&git_file) else {
return false;
@@ -368,16 +388,14 @@ fn leftover_worktree_checkout_matches_repo(repo_root: &Path, path: &Path) -> boo
} else {
path.join(gitdir)
};
- let Some(worktrees_dir) = git_common_worktrees_dir(repo_root) else {
+ let Some(worktrees_dir) = git_common_worktrees_dir(repo_root, trust_repository) else {
return false;
};
canonical_or_original(&gitdir).starts_with(canonical_or_original(&worktrees_dir))
}
-fn git_common_worktrees_dir(repo_root: &Path) -> Option<PathBuf> {
- let output = crate::noninteractive_process::command("git")
- .arg("-C")
- .arg(repo_root)
+fn git_common_worktrees_dir(repo_root: &Path, trust_repository: bool) -> Option<PathBuf> {
+ let output = repository_git_command(repo_root, trust_repository)
.args(["rev-parse", "--git-common-dir"])
.output()
.ok()?;
@@ -470,10 +488,11 @@ pub(crate) fn parse_worktree_list_porcelain(output: &str) -> Vec<ExistingWorktre
entries
}
-pub(crate) fn list_existing_worktrees(repo_root: &Path) -> Result<Vec<ExistingWorktree>, String> {
- let output = crate::noninteractive_process::command("git")
- .arg("-C")
- .arg(repo_root)
+pub(crate) fn list_existing_worktrees(
+ repo_root: &Path,
+ trust_repository: bool,
+) -> Result<Vec<ExistingWorktree>, String> {
+ let output = repository_git_command(repo_root, trust_repository)
.args(["worktree", "list", "--porcelain"])
.output()
.map_err(|err| err.to_string())?;
@@ -491,9 +510,13 @@ pub(crate) fn list_existing_worktrees(repo_root: &Path) -> Result<Vec<ExistingWo
})
}
-pub(crate) fn worktree_list_contains_path(repo_root: &Path, path: &Path) -> Result<bool, String> {
+fn worktree_list_contains_path(
+ repo_root: &Path,
+ path: &Path,
+ trust_repository: bool,
+) -> Result<bool, String> {
let expected = canonical_or_original(path);
- Ok(list_existing_worktrees(repo_root)?
+ Ok(list_existing_worktrees(repo_root, trust_repository)?
.into_iter()
.any(|entry| canonical_or_original(&entry.path) == expected))
}
@@ -538,6 +561,18 @@ mod tests {
}
#[test]
+ fn trusted_repository_git_args_are_request_scoped() {
+ assert_eq!(
+ repository_git_args(Path::new("/repo/herdr"), false),
+ ["-C", "/repo/herdr"]
+ );
+ assert_eq!(
+ repository_git_args(Path::new("/repo/herdr"), true),
+ ["-c", "safe.directory=/repo/herdr", "-C", "/repo/herdr",]
+ );
+ }
+
+ #[test]
fn generated_branch_slug_is_worktree_namespaced_and_stable() {
assert_eq!(generated_branch_slug(0), "worktree/brave-river-0000");
assert_eq!(generated_branch_slug(9), "worktree/calm-cloud-0009");
@@ -728,11 +763,11 @@ prunable stale
],
);
- assert_eq!(checkout_has_dirty_files(&checkout), Ok(false));
+ assert_eq!(checkout_has_dirty_files(&checkout, false), Ok(false));
std::fs::write(checkout.join("README.md"), "dirty\n").unwrap();
- assert_eq!(checkout_has_dirty_files(&checkout), Ok(true));
+ assert_eq!(checkout_has_dirty_files(&checkout, false), Ok(true));
- let remove = build_worktree_remove_command(&repo, &checkout, true);
+ let remove = build_worktree_remove_command(&repo, &checkout, true, false);
run_worktree_command(&remove).unwrap();
let _ = std::fs::remove_dir_all(repo);
}
@@ -743,6 +778,7 @@ prunable stale
Path::new("/repo/herdr"),
Path::new("/w/herdr/issue-137"),
false,
+ false,
);
assert_eq!(command.program, "git");
assert_eq!(
@@ -763,6 +799,7 @@ prunable stale
Path::new("/repo/herdr"),
Path::new("/w/herdr/issue-137"),
true,
+ false,
);
assert_eq!(
command.args,
@@ -797,6 +834,7 @@ prunable stale
Path::new("/w/herdr/worktree-brave-river"),
"worktree/brave-river",
"HEAD",
+ false,
);
assert_eq!(command.program, "git");
assert_eq!(
@@ -820,6 +858,7 @@ prunable stale
Path::new("/repo/herdr"),
Path::new("/w/herdr/worktree-brave-river"),
"worktree/brave-river",
+ false,
);
assert_eq!(command.program, "git");
assert_eq!(
@@ -841,7 +880,7 @@ prunable stale
let checkout = unique_temp_path("worktree-run-checkout");
let branch = "worktree/test-create-remove";
- let add = build_worktree_add_new_branch_command(&repo, &checkout, branch, "HEAD");
+ let add = build_worktree_add_new_branch_command(&repo, &checkout, branch, "HEAD", false);
run_worktree_command(&add).unwrap();
assert!(checkout.join("README.md").exists());
@@ -857,7 +896,7 @@ prunable stale
branch
);
- let remove = build_worktree_remove_command(&repo, &checkout, false);
+ let remove = build_worktree_remove_command(&repo, &checkout, false, false);
run_worktree_command(&remove).unwrap();
assert!(!checkout.exists());
@@ -870,12 +909,14 @@ prunable stale
let checkout = unique_temp_path("worktree-recovery-checkout");
let branch = "worktree/recovery";
- let add = build_worktree_add_new_branch_command(&repo, &checkout, branch, "HEAD");
+ let add = build_worktree_add_new_branch_command(&repo, &checkout, branch, "HEAD", false);
run_worktree_command(&add).unwrap();
- let remove = build_worktree_remove_command(&repo, &checkout, true);
+ let remove = build_worktree_remove_command(&repo, &checkout, true, false);
run_worktree_command(&remove).unwrap();
std::fs::create_dir_all(&checkout).unwrap();
- let stale_admin_dir = git_common_worktrees_dir(&repo).unwrap().join("stale");
+ let stale_admin_dir = git_common_worktrees_dir(&repo, false)
+ .unwrap()
+ .join("stale");
std::fs::write(
checkout.join(".git"),
format!("gitdir: {}\n", stale_admin_dir.display()),
@@ -883,7 +924,7 @@ prunable stale
.unwrap();
std::fs::write(checkout.join("leftover"), "leftover\n").unwrap();
- run_worktree_remove_command_with_recovery(&remove, &repo, &checkout, true).unwrap();
+ run_worktree_remove_command_with_recovery(&remove, &repo, &checkout, true, false).unwrap();
assert!(!checkout.exists());
let _ = std::fs::remove_dir_all(repo);
@@ -895,14 +936,14 @@ prunable stale
let checkout = unique_temp_path("worktree-recovery-unrelated-checkout");
let branch = "worktree/recovery-unrelated";
- let add = build_worktree_add_new_branch_command(&repo, &checkout, branch, "HEAD");
+ let add = build_worktree_add_new_branch_command(&repo, &checkout, branch, "HEAD", false);
run_worktree_command(&add).unwrap();
- let remove = build_worktree_remove_command(&repo, &checkout, true);
+ let remove = build_worktree_remove_command(&repo, &checkout, true, false);
run_worktree_command(&remove).unwrap();
std::fs::create_dir_all(&checkout).unwrap();
std::fs::write(checkout.join("unrelated"), "do not delete\n").unwrap();
- let err = run_worktree_remove_command_with_recovery(&remove, &repo, &checkout, true)
+ let err = run_worktree_remove_command_with_recovery(&remove, &repo, &checkout, true, false)
.expect_err("unrelated replacement directory should not be removed");
assert!(is_not_working_tree_remove_error(&err));