From ff32759058ee15175223e368d90b3813b6a05ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Fri, 26 Nov 2021 16:13:17 +0100 Subject: [PATCH] Add subcommand that converts existing repository Close #6 --- docs/src/worktrees.md | 17 ++++++ src/cmd.rs | 5 ++ src/lib.rs | 124 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 131 insertions(+), 15 deletions(-) diff --git a/docs/src/worktrees.md b/docs/src/worktrees.md index a99b55a..fd7380d 100644 --- a/docs/src/worktrees.md +++ b/docs/src/worktrees.md @@ -192,6 +192,23 @@ can also use the following: $ grm wt clean ``` +### Converting an existing repository + +It is possible to convert an existing directory to a worktree setup, using `grm +wt convert`. This command has to be run in the root of the repository you want +to convert: + +``` +grm wt convert +[✔] Conversion successful +``` + +This command will refuse to run if you have any changes in your repository. +Commit them and try again! + +Afterwards, the directory is empty, as there are no worktrees checked out yet. +Now you can use the usual commands to set up worktrees. + ### Manual access GRM isn't doing any magic, it's just git under the hood. If you need to have access diff --git a/src/cmd.rs b/src/cmd.rs index e75bbd2..6cf4a18 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -83,6 +83,8 @@ pub enum WorktreeAction { Delete(WorktreeDeleteArgs), #[clap(about = "Show state of existing worktrees")] Status(WorktreeStatusArgs), + #[clap(about = "Convert a normal repository to a worktree setup")] + Convert(WorktreeConvertArgs), #[clap(about = "Clean all worktrees that do not contain uncommited/unpushed changes")] Clean(WorktreeCleanArgs), } @@ -116,6 +118,9 @@ pub struct WorktreeDeleteArgs { #[derive(Parser)] pub struct WorktreeStatusArgs {} +#[derive(Parser)] +pub struct WorktreeConvertArgs {} + #[derive(Parser)] pub struct WorktreeCleanArgs {} diff --git a/src/lib.rs b/src/lib.rs index a387abb..e96f22e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,8 @@ use repo::{ const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree"; const BRANCH_NAMESPACE_SEPARATOR: &str = "/"; +const GIT_CONFIG_BARE_KEY: &str = "core.bare"; + #[cfg(test)] mod tests { use super::*; @@ -868,28 +870,33 @@ pub fn run() { } }; - let repo = match open_repo(&dir, true) { - Ok(r) => r, - Err(e) => { - match e.kind { - RepoErrorKind::NotFound => { - print_error("Current directory does not contain a worktree setup") + fn get_repo(dir: &Path) -> git2::Repository { + match open_repo(dir, true) { + Ok(r) => r, + Err(e) => { + match e.kind { + RepoErrorKind::NotFound => { + print_error("Current directory does not contain a worktree setup") + } + _ => print_error(&format!("Error opening repo: {}", e)), } - _ => print_error(&format!("Error opening repo: {}", e)), + process::exit(1); } - process::exit(1); } - }; + } - let worktrees = repo - .worktrees() - .unwrap() - .iter() - .map(|e| e.unwrap().to_string()) - .collect::>(); + fn get_worktrees(repo: &git2::Repository) -> Vec { + repo.worktrees() + .unwrap() + .iter() + .map(|e| e.unwrap().to_string()) + .collect::>() + } match args.action { cmd::WorktreeAction::Add(action_args) => { + let repo = get_repo(&dir); + let worktrees = get_worktrees(&repo); if worktrees.contains(&action_args.name) { print_error("Worktree already exists"); process::exit(1); @@ -1014,6 +1021,7 @@ pub fn run() { cmd::WorktreeAction::Delete(action_args) => { let worktree_dir = dir.join(&action_args.name); + let repo = get_repo(&dir); match remove_worktree( &action_args.name, @@ -1040,6 +1048,8 @@ pub fn run() { } } cmd::WorktreeAction::Status(_args) => { + let repo = get_repo(&dir); + let worktrees = get_worktrees(&repo); let mut table = Table::new(); add_worktree_table_header(&mut table); for worktree in &worktrees { @@ -1081,7 +1091,91 @@ pub fn run() { } println!("{}", table); } + cmd::WorktreeAction::Convert(_args) => { + // Converting works like this: + // * Check whether there are uncommitted/unpushed changes + // * Move the contents of .git dir to the worktree directory + // * Remove all files + // * Set `core.bare` to `true` + + let repo = open_repo(&dir, false).unwrap_or_else(|error| { + if error.kind == RepoErrorKind::NotFound { + print_error("Directory does not contain a git repository"); + } else { + print_error(&format!("Opening repository failed: {}", error)); + } + process::exit(1); + }); + + let status = get_repo_status(&repo, false); + if status.changes.unwrap().is_some() { + print_error("Changes found in repository, refusing to convert"); + } + + if let Err(error) = std::fs::rename(".git", GIT_MAIN_WORKTREE_DIRECTORY) { + print_error(&format!("Error moving .git directory: {}", error)); + } + + for entry in match std::fs::read_dir(&dir) { + Ok(iterator) => iterator, + Err(error) => { + print_error(&format!("Opening directory failed: {}", error)); + process::exit(1); + } + } { + match entry { + Ok(entry) => { + let path = entry.path(); + // The path will ALWAYS have a file component + if path.file_name().unwrap() == GIT_MAIN_WORKTREE_DIRECTORY { + continue; + } + if path.is_file() || path.is_symlink() { + if let Err(error) = std::fs::remove_file(&path) { + print_error(&format!("Failed removing {}", error)); + process::exit(1); + } + } else if let Err(error) = std::fs::remove_dir_all(&path) { + print_error(&format!("Failed removing {}", error)); + process::exit(1); + } + } + Err(error) => { + print_error(&format!("Error getting directory entry: {}", error)); + process::exit(1); + } + } + } + + let worktree_repo = open_repo(&dir, true).unwrap_or_else(|error| { + print_error(&format!( + "Opening newly converted repository failed: {}", + error + )); + process::exit(1); + }); + + let mut config = worktree_repo.config().unwrap_or_else(|error| { + print_error(&format!( + "Opening getting repository configuration: {}", + error + )); + process::exit(1); + }); + + config + .set_bool(GIT_CONFIG_BARE_KEY, true) + .unwrap_or_else(|error| { + print_error(&format!( + "Error setting {}: {}", + GIT_CONFIG_BARE_KEY, error + )); + process::exit(1); + }); + } cmd::WorktreeAction::Clean(_args) => { + let repo = get_repo(&dir); + let worktrees = get_worktrees(&repo); for worktree in &worktrees { let repo_dir = &dir.join(&worktree); if repo_dir.exists() {