diff --git a/README.md b/README.md index 7de2b1e..b38ebcd 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,14 @@ Options: Color of the prompt in the picker --session-sort-order Set the sort order of the sessions in the switch command [possible values: Alphabetical, LastAttached] + --clone-repo-switch + Whether to automatically switch to the new session after the `clone-repo` command finishes + `Always` will always switch tmux to the new session + `Never` will always create the new session in the background + When set to `Foreground`, the new session will only be opened in the background if the active + tmux session has changed since starting the clone process (for long clone processes on larger repos) [possible values: Always, Never, Foreground] + --enable-list-worktrees + Enable listing of woktrees for bare repositories [possible values: true, false] -h, --help Print help ``` diff --git a/src/cli.rs b/src/cli.rs index 67c8597..8e6395e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -21,7 +21,7 @@ use ratatui::style::Color; #[derive(Debug, Parser)] #[command(author, version)] -///Scan for all git folders in specified directorires, select one and open it as a new tmux session +///Scan for all git folders in specified directories, select one and open it as a new tmux session pub struct Cli { #[command(subcommand)] command: Option, @@ -115,7 +115,7 @@ pub struct ConfigArgs { /// Background color of the highlighted item in the picker picker_highlight_color: Option, #[arg(long, value_name = "#rrggbb")] - /// Text color of the hightlighted item in the picker + /// Text color of the highlighted item in the picker picker_highlight_text_color: Option, #[arg(long, value_name = "#rrggbb")] /// Color of the borders between widgets in the picker @@ -136,6 +136,9 @@ pub struct ConfigArgs { /// When set to `Foreground`, the new session will only be opened in the background if the active /// tmux session has changed since starting the clone process (for long clone processes on larger repos) clone_repo_switch: Option, + #[arg(long, value_name = "true | false")] + /// Enable listing of woktrees for bare repositories + enable_list_worktrees: Option, } #[derive(Debug, Args)] @@ -422,6 +425,10 @@ fn config_command(cmd: &ConfigCommand, mut config: Config) -> Result<()> { config.switch_filter_unknown = Some(switch_filter_unknown.to_owned()); } + if let Some(enable_list_worktrees) = args.enable_list_worktrees { + config.list_worktrees = Some(enable_list_worktrees.to_owned()); + } + if let Some(dirs) = &args.excluded_dirs { let current_excluded = config.excluded_dirs; match current_excluded { diff --git a/src/configs.rs b/src/configs.rs index 3408d9c..a573a58 100644 --- a/src/configs.rs +++ b/src/configs.rs @@ -52,6 +52,7 @@ pub struct Config { pub session_configs: Option>, pub marks: Option>, pub clone_repo_switch: Option, + pub list_worktrees: Option, } #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] diff --git a/src/repos.rs b/src/repos.rs index f4c0f44..9888f2e 100644 --- a/src/repos.rs +++ b/src/repos.rs @@ -30,52 +30,134 @@ pub fn find_repos(config: &Config) -> Result>> { }; while let Some(file) = to_search.pop_front() { - if let Some(ref excluder) = excluder { - if excluder.is_match(&file.path.to_string()?) { - continue; - } + if should_skip_file(&file, &excluder)? { + continue; } - if let Ok(repo) = git2::Repository::open(&file.path) { - if repo.is_worktree() { - continue; - } + if let Ok(repo) = git2::Repository::open(file.path.clone()) { + process_repository(&file, repo, &mut repos, config)?; + } else if should_search_directory(&file) { + process_directory(&file, &mut to_search)?; + } + } + Ok(repos) +} - let session_name = file - .path - .file_name() - .expect("The file name doesn't end in `..`") - .to_string()?; - - let session = Session::new(session_name, SessionType::Git(repo)); - if let Some(list) = repos.get_mut(&session.name) { - list.push(session); - } else { - repos.insert(session.name.clone(), vec![session]); - } - } else if file.path.is_dir() && file.depth > 0 { - match fs::read_dir(&file.path) { - Err(ref e) if e.kind() == std::io::ErrorKind::PermissionDenied => { - eprintln!( - "Warning: insufficient permissions to read '{0}'. Skipping directory...", - file.path.to_string()? - ); - } - result => { - let read_dir = result - .change_context(TmsError::IoError) - .attach_printable_lazy(|| { - format!("Could not read directory {:?}", file.path) - })? - .map(|dir_entry| dir_entry.expect("Found non-valid utf8 path").path()); - for dir in read_dir { - to_search.push_back(SearchDirectory::new(dir, file.depth - 1)) +fn should_skip_file( + file: &SearchDirectory, + excluder: &Option, +) -> Result { + if let Some(ref excluder) = excluder { + if excluder.is_match(&file.path.to_string()?) { + return Ok(true); + } + } + Ok(false) +} + +fn should_search_directory(file: &SearchDirectory) -> bool { + file.path.is_dir() && file.depth > 0 +} + +fn process_bare_repository( + file: &SearchDirectory, + repos: &mut HashMap>, +) -> Result<()> { + match fs::read_dir(&file.path) { + Ok(entries) => { + for entry in entries.flatten() { + let entry_path = entry.path(); + if entry_path.is_dir() { + let git_file = entry_path.join(".git"); + if git_file.exists() && git_file.is_file() { + if let Ok(worktree_repo) = git2::Repository::open(&entry_path) { + let session_name = entry_path + .file_name() + .expect("The file name doesn't end in `..`") + .to_string_lossy() + .to_string(); + + let parent = file + .path + .file_name() + .expect("The file name doesn't end in `..`") + .to_string_lossy() + .to_string(); + let session = Session::new( + format!("{}#{}", parent, session_name.clone()), + SessionType::Git(worktree_repo), + ); + repos.insert(session_name, vec![session]); + } } } } } + Err(e) => { + eprintln!( + "Warning: couldn't read bare repository directory '{}': {}", + file.path.to_string_lossy(), + e + ); + } + } + Ok(()) +} + +fn process_repository( + file: &SearchDirectory, + repo: git2::Repository, + repos: &mut HashMap>, + config: &Config, +) -> Result<()> { + if repo.is_worktree() { + return Ok(()); + } + + if repo.is_bare() && config.list_worktrees == Some(true) { + process_bare_repository(file, repos)?; + return Ok(()); + } + + let session_name = file + .path + .file_name() + .expect("The file name doesn't end in `..`") + .to_string()?; + + let session = Session::new(session_name.clone(), SessionType::Git(repo)); + if let Some(list) = repos.get_mut(&session.name) { + list.push(session); + } else { + repos.insert(session.name.clone(), vec![session]); + } + Ok(()) +} + +fn process_directory( + file: &SearchDirectory, + to_search: &mut VecDeque, +) -> Result<()> { + match fs::read_dir(&file.path) { + Err(ref e) if e.kind() == std::io::ErrorKind::PermissionDenied => { + eprintln!( + "Warning: insufficient permissions to read '{0}'. Skipping directory...", + file.path.to_string()? + ); + Ok(()) + } + result => { + let read_dir = result + .change_context(TmsError::IoError) + .attach_printable_lazy(|| format!("Could not read directory {:?}", file.path))? + .map(|dir_entry| dir_entry.expect("Found non-valid utf8 path").path()); + + for dir in read_dir { + to_search.push_back(SearchDirectory::new(dir, file.depth - 1)); + } + Ok(()) + } } - Ok(repos) } pub fn find_submodules( diff --git a/tests/cli.rs b/tests/cli.rs index ccffa89..6e35cd7 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -67,6 +67,7 @@ fn tms_config() -> anyhow::Result<()> { session_configs: None, marks: None, clone_repo_switch: Some(CloneRepoSwitchConfig::Always), + list_worktrees: None, }; let mut tms = Command::cargo_bin("tms")?;