Skip to content

Commit cb4f271

Browse files
timabellclaude
andcommitted
fix: Move repo into folder when target path ends with slash (#264)
When new_path ends with / or \, treat it as a directory target and move the repo into it (appending the repo's basename), matching unix mv behaviour. E.g. 'move repo myrepo dst/' results in dst/myrepo. The resolved destination path is now shown in the output message. Prompts: - repo move should move to folder when folder/ specified · Issue #264 - test for moving to existing folder? Fixes #264 bump: minor Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c6af029 commit cb4f271

3 files changed

Lines changed: 107 additions & 6 deletions

File tree

src/gitopolis.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -354,10 +354,20 @@ impl Gitopolis {
354354
Ok(folder_name)
355355
}
356356

357-
pub fn move_repo(&mut self, old_path: &str, new_path: &str) -> Result<(), GitopolisError> {
357+
pub fn move_repo(&mut self, old_path: &str, new_path: &str) -> Result<String, GitopolisError> {
358358
let mut repos = self.load()?;
359359
let normalized_old = normalize_folder(old_path.to_string());
360-
let normalized_new = normalize_folder(new_path.to_string());
360+
let is_dir_target = new_path.ends_with('/') || new_path.ends_with('\\');
361+
let normalized_new = if is_dir_target {
362+
let base = std::path::Path::new(&normalized_old)
363+
.file_name()
364+
.map(|f| f.to_string_lossy().to_string())
365+
.unwrap_or_else(|| normalized_old.clone());
366+
let dir = normalize_folder(new_path.to_string());
367+
format!("{}/{}", dir, base)
368+
} else {
369+
normalize_folder(new_path.to_string())
370+
};
361371

362372
// Find the repo in the config
363373
let repo = repos
@@ -381,10 +391,10 @@ impl Gitopolis {
381391

382392
// Update the config: remove old entry and add new one with same tags/remotes
383393
repos.remove(vec![normalized_old]);
384-
repos.add_with_tags_and_remotes(normalized_new, repo.tags, repo.remotes);
394+
repos.add_with_tags_and_remotes(normalized_new.clone(), repo.tags, repo.remotes);
385395

386396
self.save(repos)?;
387-
Ok(())
397+
Ok(normalized_new)
388398
}
389399

390400
fn save(&self, repos: Repos) -> Result<(), GitopolisError> {

src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,8 @@ fn main() {
261261
Some(Commands::Move { entity }) => match entity {
262262
MoveEntity::Repo { old_path, new_path } => {
263263
match init_gitopolis().move_repo(old_path, new_path) {
264-
Ok(_) => {
265-
eprintln!("Moved {} to {}", old_path, new_path);
264+
Ok(resolved_path) => {
265+
eprintln!("Moved {} to {}", old_path, resolved_path);
266266
}
267267
Err(error) => {
268268
eprintln!("Error: {}", error.message());

tests/end_to_end_tests.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2360,6 +2360,97 @@ url = \"git://example.org/test_url\"
23602360
assert_eq!(expected_toml, read_gitopolis_state_toml(&temp));
23612361
}
23622362

2363+
#[test]
2364+
fn move_repo_into_folder() {
2365+
// When new_path ends with /, treat it as a directory and move repo into it
2366+
// Issue: https://github.com/timabell/gitopolis/issues/264
2367+
let temp = temp_folder();
2368+
add_a_repo(&temp, "my_repo", "git://example.org/test_url");
2369+
2370+
gitopolis_executable()
2371+
.current_dir(&temp)
2372+
.args(vec!["move", "repo", "my_repo", "dst/"])
2373+
.assert()
2374+
.success()
2375+
.stderr(predicate::str::contains("Moved my_repo to dst/my_repo"));
2376+
2377+
// Verify old location doesn't exist
2378+
assert!(!temp.path().join("my_repo").exists());
2379+
2380+
// Verify new location exists inside dst/
2381+
assert!(temp.path().join("dst/my_repo").exists());
2382+
2383+
// Verify config is updated
2384+
let expected_toml = "[[repos]]
2385+
path = \"dst/my_repo\"
2386+
tags = []
2387+
2388+
[repos.remotes.origin]
2389+
name = \"origin\"
2390+
url = \"git://example.org/test_url\"
2391+
";
2392+
assert_eq!(expected_toml, read_gitopolis_state_toml(&temp));
2393+
}
2394+
2395+
#[test]
2396+
fn move_repo_into_folder_nested_source() {
2397+
// When source is nested and new_path ends with /, use only the final component
2398+
let temp = temp_folder();
2399+
add_a_repo(&temp, "services/auth", "git://example.org/test_url");
2400+
2401+
gitopolis_executable()
2402+
.current_dir(&temp)
2403+
.args(vec!["move", "repo", "services/auth", "apps/"])
2404+
.assert()
2405+
.success()
2406+
.stderr(predicate::str::contains("Moved services/auth to apps/auth"));
2407+
2408+
assert!(!temp.path().join("services/auth").exists());
2409+
assert!(temp.path().join("apps/auth").exists());
2410+
2411+
let expected_toml = "[[repos]]
2412+
path = \"apps/auth\"
2413+
tags = []
2414+
2415+
[repos.remotes.origin]
2416+
name = \"origin\"
2417+
url = \"git://example.org/test_url\"
2418+
";
2419+
assert_eq!(expected_toml, read_gitopolis_state_toml(&temp));
2420+
}
2421+
2422+
#[test]
2423+
fn move_repo_into_existing_folder() {
2424+
// When destination folder already exists and path ends with /, move repo into it
2425+
let temp = temp_folder();
2426+
add_a_repo(&temp, "my_repo", "git://example.org/test_url");
2427+
2428+
// Create the destination folder ahead of time
2429+
std::fs::create_dir_all(temp.path().join("existing_dir")).unwrap();
2430+
2431+
gitopolis_executable()
2432+
.current_dir(&temp)
2433+
.args(vec!["move", "repo", "my_repo", "existing_dir/"])
2434+
.assert()
2435+
.success()
2436+
.stderr(predicate::str::contains(
2437+
"Moved my_repo to existing_dir/my_repo",
2438+
));
2439+
2440+
assert!(!temp.path().join("my_repo").exists());
2441+
assert!(temp.path().join("existing_dir/my_repo").exists());
2442+
2443+
let expected_toml = "[[repos]]
2444+
path = \"existing_dir/my_repo\"
2445+
tags = []
2446+
2447+
[repos.remotes.origin]
2448+
name = \"origin\"
2449+
url = \"git://example.org/test_url\"
2450+
";
2451+
assert_eq!(expected_toml, read_gitopolis_state_toml(&temp));
2452+
}
2453+
23632454
#[test]
23642455
fn move_repo_not_found() {
23652456
// Test that move fails when repo doesn't exist

0 commit comments

Comments
 (0)