Skip to content

Commit 72ba4f3

Browse files
timabellclaude
andcommitted
fix: Skip adding remotes when repo already exists during clone
When cloning, if a repo already exists, the clone operation is skipped entirely - including not attempting to add any remotes. Previously, clone would skip the git clone but still try to add remotes, which would fail with "remote xxx already exists" warnings. Changed Git::clone() to return Result<bool, GitopolisError> where true means the repo was cloned and false means it was skipped (already exists). Prompts: - "now fix this bug [shouldn't add extra remotes to existing repo when cloning · Issue #247 · timabell/gitopolis](#247)" - in response to suggestion about checking existing remotes: "simpler than that, if a repo already exists don't clone it and don't add any remotes" Fixes #247 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5e39ddd commit 72ba4f3

4 files changed

Lines changed: 81 additions & 9 deletions

File tree

src/git.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ pub trait Git {
99
fn read_url(&self, path: String, remote_name: String) -> Result<String, GitopolisError>;
1010
fn read_all_remotes(&self, path: String) -> Result<BTreeMap<String, String>, GitopolisError>;
1111
fn add_remote(&self, path: &str, remote_name: &str, url: &str);
12-
fn clone(&self, path: &str, url: &str, remote_name: Option<&str>)
13-
-> Result<(), GitopolisError>;
12+
/// Clone a repo. Returns Ok(true) if cloned, Ok(false) if already exists (skipped).
13+
fn clone(
14+
&self,
15+
path: &str,
16+
url: &str,
17+
remote_name: Option<&str>,
18+
) -> Result<bool, GitopolisError>;
1419
}
1520

1621
pub struct GitImpl {}
@@ -75,10 +80,10 @@ impl Git for GitImpl {
7580
path: &str,
7681
url: &str,
7782
remote_name: Option<&str>,
78-
) -> Result<(), GitopolisError> {
83+
) -> Result<bool, GitopolisError> {
7984
if Path::new(path).exists() {
8085
println!("🏢 {path}> Already exists, skipped.");
81-
return Ok(());
86+
return Ok(false);
8287
}
8388
println!("🏢 {path}> Cloning {url} ...");
8489
let mut args = vec!["clone"];
@@ -103,6 +108,6 @@ impl Git for GitImpl {
103108
});
104109
}
105110

106-
Ok(())
111+
Ok(true)
107112
}
108113
}

src/gitopolis.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,17 @@ impl Gitopolis {
106106
&clone_remote.url,
107107
Some(clone_remote_name),
108108
) {
109-
Ok(()) => {
110-
// Add all other remotes
109+
Ok(true) => {
110+
// Clone succeeded, add all other remotes
111111
for (name, remote) in &repo.remotes {
112112
if name != clone_remote_name {
113113
self.git.add_remote(&repo.path, name, &remote.url);
114114
}
115115
}
116116
}
117+
Ok(false) => {
118+
// Repo already exists, skip adding remotes
119+
}
117120
Err(_) => {
118121
eprintln!("Warning: Could not clone {}", repo.path);
119122
error_count += 1;

tests/end_to_end_tests.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2673,3 +2673,67 @@ bbb source_bbb (push)
26732673
"#;
26742674
assert_eq!(expected_remotes, remotes);
26752675
}
2676+
2677+
#[test]
2678+
fn clone_skips_adding_remotes_when_repo_exists() {
2679+
// Test that when a repo already exists, clone skips it entirely
2680+
// and doesn't try to add remotes (which would fail with "remote already exists")
2681+
let temp = temp_folder();
2682+
2683+
// Create source repos
2684+
create_local_repo(&temp, "source_origin");
2685+
create_local_repo(&temp, "source_upstream");
2686+
2687+
// Pre-create the target repo with origin remote already configured
2688+
let cloned_path = temp.path().join("cloned_repo");
2689+
fs::create_dir_all(&cloned_path).expect("create repo dir failed");
2690+
Command::new("git")
2691+
.current_dir(&cloned_path)
2692+
.args(vec!["init", "--initial-branch", "main"])
2693+
.output()
2694+
.expect("git init failed");
2695+
Command::new("git")
2696+
.current_dir(&cloned_path)
2697+
.args(vec!["remote", "add", "origin", "existing_origin_url"])
2698+
.output()
2699+
.expect("git remote add failed");
2700+
2701+
// Create state with multiple remotes
2702+
let initial_state_toml = r#"[[repos]]
2703+
path = "cloned_repo"
2704+
tags = []
2705+
2706+
[repos.remotes.origin]
2707+
name = "origin"
2708+
url = "source_origin"
2709+
2710+
[repos.remotes.upstream]
2711+
name = "upstream"
2712+
url = "source_upstream"
2713+
"#;
2714+
write_gitopolis_state_toml(&temp, initial_state_toml);
2715+
2716+
// Run clone - should skip the existing repo and NOT try to add remotes
2717+
gitopolis_executable()
2718+
.current_dir(&temp)
2719+
.args(vec!["clone"])
2720+
.assert()
2721+
.success()
2722+
.stdout(predicate::str::contains("Already exists, skipped"))
2723+
// Should NOT have any warnings about failed remote adds
2724+
.stderr(predicate::str::contains("Warning").not());
2725+
2726+
// Verify the original remote is unchanged (not overwritten)
2727+
let remotes_output = Command::new("git")
2728+
.current_dir(&cloned_path)
2729+
.args(vec!["remote", "--verbose"])
2730+
.output()
2731+
.expect("git remote --verbose failed");
2732+
let remotes = String::from_utf8(remotes_output.stdout).expect("utf8 conversion failed");
2733+
2734+
// Should only have the original origin remote, not the upstream from config
2735+
let expected_remotes = r#"origin existing_origin_url (fetch)
2736+
origin existing_origin_url (push)
2737+
"#;
2738+
assert_eq!(expected_remotes, remotes);
2739+
}

tests/gitopolis_tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -519,8 +519,8 @@ impl Git for FakeGit {
519519
path: &str,
520520
url: &str,
521521
_remote_name: Option<&str>,
522-
) -> Result<(), GitopolisError> {
522+
) -> Result<bool, GitopolisError> {
523523
(self.clone_callback)(path.to_owned(), url.to_owned());
524-
Ok(())
524+
Ok(true)
525525
}
526526
}

0 commit comments

Comments
 (0)