Skip to content

Commit

Permalink
feat!: add all mode for worktrees
Browse files Browse the repository at this point in the history
Added a new mode to handle how tuxmux handles worktrees. The new mode
`all` will create a window foreach worktree found.

BREAKING CHANGE: default_worktree is replaced with workspace which is an
option that takes a string listing the mode.
  • Loading branch information
EdenEast committed Nov 20, 2023
1 parent ae9b95f commit 1a900c6
Show file tree
Hide file tree
Showing 11 changed files with 190 additions and 97 deletions.
12 changes: 8 additions & 4 deletions config.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,16 @@
//
// height 10

// Select the repositories remote default branch if multiple worktrees are found. If the default
// worktree cannot be found the fallback will be to select the correct one.
// Specifies the handling of git worktrees. There are three available modes:
//
// Default: false
// - 'ask': Lists all worktrees and prompts the user for selection.
// - 'default': For bare repositories, uses the worktree related to the default branch; for non-bare repositories
// uses the default working directory.
// - 'all': Opens a separate window for each worktree.
//
// default_worktree true
// Default "ask"
//
// worktree "default"

// Workspace directory crawler will prune the paths containing any of these components.
// Options:
Expand Down
16 changes: 10 additions & 6 deletions doc/configuration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,22 @@ Default: `50%`
height 10
----

=== default_worktree
=== worktree

Select the repositories remote default branch if multiple worktrees are found. If the default
worktree cannot be found the fallback will be to select the correct one.
Specifies the handling of git worktrees. There are three available modes

`ask`:: Lists all worktrees and prompts the user for selection.
`default`:: For bare repositories, uses the worktree related to the default branch; for non-bare repositories
uses the default working directory.
`all`:: Opens a separate window for each worktree.

[%hardbreaks]
Type: `boolean`
Default: `false`
Type: `string`
Default: `ask`

[source,javascript]
----
default_worktree true
worktree "ask"
----

=== exclude_paths
Expand Down
16 changes: 10 additions & 6 deletions readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -199,18 +199,22 @@ Default: `50%`
height 10
----

=== default_worktree
=== worktree

Select the repositories remote default branch if multiple worktrees are found. If the default
worktree cannot be found the fallback will be to select the correct one.
Specifies the handling of git worktrees. There are three available modes

`ask`:: Lists all worktrees and prompts the user for selection.
`default`:: For bare repositories, uses the worktree related to the default branch; for non-bare repositories
uses the default working directory.
`all`:: Opens a separate window for each worktree.

[%hardbreaks]
Type: `boolean`
Default: `false`
Type: `string`
Default: `ask`

[source,javascript]
----
default_worktree true
worktree "ask"
----

=== exclude_paths
Expand Down
150 changes: 91 additions & 59 deletions src/cmd/attach.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::{

use crate::{
cmd::cli::Attach,
config::Config,
config::{Config, WorktreeMode},
finder::{self, FinderOptions},
util,
walker::Walker,
Expand All @@ -16,7 +16,7 @@ use dialoguer::{
theme::ColorfulTheme,
FuzzySelect,
};
use gix::{bstr::ByteSlice, Repository};
use gix::bstr::ByteSlice;
use itertools::Itertools;
use miette::{miette, IntoDiagnostic, Result};
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
Expand Down Expand Up @@ -121,71 +121,103 @@ impl Attach {
}

let repo = gix::open(selected).ok();
let worktree = self.get_worktree(repo.as_ref(), config);
let branch = repo.as_ref().and_then(head_branch);
mux.create_session(&name, selected.to_str().unwrap(), branch.as_deref())?;

if let Some(worktree) = worktree {
mux.send_command(&name, &format!("cd {}", worktree.display()))?;
// Check worktree mode
if let Some(repo) = repo {
let worktrees = repo.worktrees().into_diagnostic()?;
let is_bare = is_bare(&repo);

if config.worktree_mode == WorktreeMode::All || self.all {
let mut iter = worktrees.iter();

// A bare repo that contains worktrees only uses worktrees and does not contain
// a normally contain a valid work_dir. When creating the session use the first
// worktree as the window name.
if is_bare && !worktrees.is_empty() {
let first = iter.next();
let path = first.and_then(|tree| tree.base().ok());
let window_name = first.map(|f| f.id().to_string());
mux.create_session(
&name,
path.expect("worktrees is not empty"),
window_name.as_deref(),
)?;
} else {
let head_branch = head_branch(&repo);
mux.create_session(
&name,
selected.to_string_lossy().as_ref(),
head_branch.as_deref(),
)?;
};

for tree in iter {
let path = tree.base().ok();
let window_name = tree.id().to_str_lossy();
mux.create_window(&window_name, path.as_deref())?;
}

return mux.attach_session(&name);
} else if config.worktree_mode == WorktreeMode::Default || self.default {
let len = worktrees.len();
// If the repo is not bare then the default would be the work_dir of the repo.
// This will be used instead of any worktree
if !is_bare || len == 0 {
mux.create_session(&name, selected.to_string_lossy().as_ref(), None)?;
return mux.attach_session(&name);
}

// If there is only one worktree and it is a bare repo it is the only option
if len == 1 {
let proxy = worktrees.first().expect("worktree contains values");
let path = proxy.base().into_diagnostic()?;
let proxy_repo = proxy
.clone()
.into_repo_with_possibly_inaccessible_worktree()
.into_diagnostic()?;
let window_name = head_branch(&proxy_repo);
mux.create_session(&name, path, window_name.as_deref())?;
return mux.attach_session(&name);
}

if let Some(default_branch) = default_branch(&repo) {
if let Some(default_worktree) =
worktrees.iter().find(|tree| tree.id() == default_branch)
{
let path = default_worktree.base().into_diagnostic()?;
mux.create_session(&name, path, Some(&default_branch))?;
return mux.attach_session(&name);
}
}

return Err(miette!("Could not find default branch / worktree"));
} else {
let items = worktrees
.iter()
.map(|tree| tree.id().to_string())
.collect_vec();

if let Some(choice) = fuzzy(&items, "Worktree") {
let tree = worktrees
.get(choice)
.expect("choice value comes from worktree index");
let path = tree.base().into_diagnostic()?;
let window_name = tree.id();
mux.create_session(&name, path, Some(window_name.to_str_lossy().as_ref()))?;
return mux.attach_session(&name);
}
}
} else {
// This is not a git repo so just create and attach.
mux.create_session(&name, selected.to_str().unwrap(), None)?;
return mux.attach_session(&name);
}
mux.attach_session(&name)?;

Ok(())
}

pub fn use_cwd(&self, config: &Config) -> Result<()> {
self.execute_selected(&std::env::current_dir().into_diagnostic()?, config)
}

fn get_worktree(&self, repo: Option<&Repository>, config: &Config) -> Option<PathBuf> {
let repo = repo?;
let worktrees = repo.worktrees().ok()?;
let use_default = self.default || config.default_worktree;
let worktree_length = worktrees.len();
let bare = is_bare(repo);

if worktree_length == 0 {
return None;
}

// If the repository is not bare then worktree's are in addition to the main default
// worktree. If we are to use 'default' we should not use any worktrees
if !bare && use_default {
return None;
}

// NOTE: A worktree's id() (name) can be different then it's branch name. To get the branch
// name you have to get the proxy repo and get the head branch of that.
let items = worktrees.iter().map(|t| t.id().to_string()).collect_vec();
if worktree_length == 1 {
// If the repo is a bare repo then there is only one valid working tree
if bare {
return worktrees[0].base().ok();
}

let default_branch = head_branch(repo)?;
let mut choices = vec![default_branch];
choices.extend(items);
let choice = fuzzy(&choices, "Worktree")?;
if choice == 0 {
return None;
}

return worktrees[choice - 1].base().ok();
}

if use_default {
return default_branch(repo)
.and_then(|name| {
let s = name.as_str();
items.iter().position(|x| x == s)
})
.and_then(|index| worktrees[index].base().ok());
}

let choice = fuzzy(&items, "Worktree")?;
worktrees[choice].base().ok()
}
}

fn default_branch(repo: &gix::Repository) -> Option<String> {
Expand Down
4 changes: 4 additions & 0 deletions src/cmd/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ pub struct Attach {
#[arg(short, long, default_value_t = false)]
pub default: bool,

/// Create mux window for each worktree
#[arg(short, long, default_value_t = false)]
pub all: bool,

/// Exact path to either attach to existing session or create a new one if
/// none exist
#[arg(short, long, default_value = None)]
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/wcmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ impl Run for Wcmd {
let target = format!("{}:{}", session_name.trim(), name);

if !mux.session_exists(&target) {
mux.create_window(name)?;
mux.create_window(name, None)?;
}

let cmd: String = intersperse(self.cmds.iter().map(|f| f.as_str()), " ").collect();
Expand Down
10 changes: 10 additions & 0 deletions src/config/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ pub enum ParseError {
#[label("expected a percentage from 1-100%")] SourceSpan,
),

#[error("Invalid worktree mode")]
#[diagnostic(
code("tm::invalid_worktree_mode"),
help("possible values ['all', 'deafult', 'ask']")
)]
InvalidWorktreeMode(
#[source_code] Source,
#[label("unknown worktree mode")] SourceSpan,
),

#[error(transparent)]
#[diagnostic(transparent)]
Kdl(#[from] KdlError),
Expand Down
12 changes: 10 additions & 2 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub struct Config {
pub exclude_path: IndexSet<String>,
pub depth: usize,
pub mode: Mode,
pub default_worktree: bool,
pub worktree_mode: WorktreeMode,
pub mux: Mux,
}

Expand All @@ -54,12 +54,20 @@ impl Default for Config {
exclude_path: indexset! { "node_modules".to_string(), ".direnv".to_string(), ".cache".to_string(), ".local".to_string()},
depth: 5,
mode: Mode::default(),
default_worktree: false,
worktree_mode: WorktreeMode::default(),
mux: Mux::default(),
}
}
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum WorktreeMode {
All,
Default,
#[default]
Ask,
}

impl Config {
pub fn load() -> Result<Config, ParseError> {
let mut config = Config::default();
Expand Down
49 changes: 36 additions & 13 deletions src/config/parser.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use itertools::Itertools;
use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue};

use super::{error::ParseError, source::Source, Config, Mode};
use super::{error::ParseError, source::Source, Config, Mode, WorktreeMode};

#[derive(Debug)]
pub struct Parser {
Expand Down Expand Up @@ -84,8 +84,31 @@ impl Parser {
config.depth = usize::try_from(self.first_entry_as_i64(node)?).unwrap_or(0);
}

if let Some(node) = doc.get("default_worktree") {
config.default_worktree = self.first_entry_as_bool(node).unwrap_or(false);
if let Some(node) = doc.get("worktree") {
let entry = self.first_entry(node)?;
let value = entry.value();
let mode =
self.first_entry(node)?
.value()
.as_string()
.ok_or(ParseError::TypeMismatch(
"string",
type_from_value(value),
self.src.clone(),
*entry.span(),
))?;

config.worktree_mode = match mode {
"all" => WorktreeMode::All,
"default" => WorktreeMode::Default,
"ask" => WorktreeMode::Ask,
_ => {
return Err(ParseError::InvalidWorktreeMode(
self.src.clone(),
*entry.span(),
))
}
};
}

if let Some(node) = doc.get("height") {
Expand Down Expand Up @@ -155,16 +178,16 @@ impl Parser {
})
}

fn first_entry_as_bool<'a>(&'a self, node: &'a KdlNode) -> Result<bool, ParseError> {
self.first_entry(node).and_then(|entry| {
entry.value().as_bool().ok_or(ParseError::TypeMismatch(
"bool",
type_from_value(entry.value()),
self.src.clone(),
*entry.span(),
))
})
}
// fn first_entry_as_bool<'a>(&'a self, node: &'a KdlNode) -> Result<bool, ParseError> {
// self.first_entry(node).and_then(|entry| {
// entry.value().as_bool().ok_or(ParseError::TypeMismatch(
// "bool",
// type_from_value(entry.value()),
// self.src.clone(),
// *entry.span(),
// ))
// })
// }

fn get_default_optional(&self, node: &KdlNode) -> Result<bool, ParseError> {
match node.get("default") {
Expand Down
Loading

0 comments on commit 1a900c6

Please sign in to comment.