Skip to content

Commit

Permalink
Run commands when adding worktrees (#57)
Browse files Browse the repository at this point in the history
Closes #6
  • Loading branch information
9999years authored Oct 19, 2024
1 parent 36fbe92 commit e8f910f
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 1 deletion.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ toml = "0.8.19"
tracing = { version = "0.1.40", features = ["attributes"] }
tracing-human-layer = "0.1.3"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "registry"] }
unindent = "0.2.3"
utf8-command = "1.0.1"
walkdir = "2.5.0"
which = "6.0.3"
Expand Down
10 changes: 10 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,13 @@ copy_untracked = true
#
# See: https://cli.github.com/
enable_gh = false

# Commands to run when a new worktree is added.
commands = [
# "direnv allow",
# { sh = '''
# if [ -e flake.nix ]; then
# nix develop --command true
# fi
# ''' },
]
28 changes: 27 additions & 1 deletion src/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,37 @@ impl<'a> WorktreePlan<'a> {
tracing::debug!("{self:#?}");

if self.git.config.cli.dry_run {
tracing::info!("$ {}", Utf8ProgramAndArgs::from(&command));
tracing::info!(
"{} {}",
'$'.if_supports_color(Stream::Stdout, |text| text.green()),
Utf8ProgramAndArgs::from(&command)
);
} else {
command.status_checked()?;
self.copy_untracked()?;
}
self.run_commands()?;
Ok(())
}

#[instrument(level = "trace")]
fn run_commands(&self) -> miette::Result<()> {
for command in self.git.config.file.commands() {
let mut command = command.as_command();
let command_display = Utf8ProgramAndArgs::from(&command);
tracing::info!(
"{} {command_display}",
'$'.if_supports_color(Stream::Stdout, |text| text.green())
);
let status = command
.current_dir(&self.destination)
.status_checked()
.into_diagnostic();
if let Err(err) = status {
tracing::error!("{err}");
}
}

Ok(())
}
}
Expand Down
67 changes: 67 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use std::process::Command;

use camino::Utf8PathBuf;
use clap::Parser;
use miette::Context;
use miette::IntoDiagnostic;
use serde::de::Error;
use serde::Deserialize;
use unindent::unindent;
use xdg::BaseDirectories;

use crate::cli::Cli;
Expand Down Expand Up @@ -78,6 +82,9 @@ pub struct ConfigFile {

#[serde(default)]
enable_gh: Option<bool>,

#[serde(default)]
commands: Vec<ShellCommand>,
}

impl ConfigFile {
Expand Down Expand Up @@ -106,6 +113,65 @@ impl ConfigFile {
pub fn enable_gh(&self) -> bool {
self.enable_gh.unwrap_or(false)
}

pub fn commands(&self) -> Vec<ShellCommand> {
self.commands.clone()
}
}

#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
#[serde(untagged)]
pub enum ShellCommand {
Simple(ShellArgs),
Shell { sh: String },
}

impl ShellCommand {
pub fn as_command(&self) -> Command {
match self {
ShellCommand::Simple(args) => {
let mut command = Command::new(&args.program);
command.args(&args.args);
command
}
ShellCommand::Shell { sh } => {
let mut command = Command::new("sh");
let sh = unindent(sh);
command.args(["-c", sh.trim_ascii()]);
command
}
}
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ShellArgs {
program: String,
args: Vec<String>,
}

impl<'de> Deserialize<'de> for ShellArgs {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let quoted: String = Deserialize::deserialize(deserializer)?;
let mut args = shell_words::split(&quoted).map_err(D::Error::custom)?;

if args.is_empty() {
return Err(D::Error::invalid_value(
serde::de::Unexpected::Str(&quoted),
// TODO: This error message doesn't actually get propagated upward
// correctly, so you get "data did not match any variant of untagged enum
// ShellCommand" instead.
&"a shell command (you are missing a program)",
));
}

let program = args.remove(0);

Ok(Self { program, args })
}
}

#[cfg(test)]
Expand All @@ -122,6 +188,7 @@ mod tests {
default_branches: vec!["main".to_owned(), "master".to_owned(), "trunk".to_owned(),],
copy_untracked: Some(true),
enable_gh: Some(false),
commands: vec![],
}
);
}
Expand Down
43 changes: 43 additions & 0 deletions tests/config_commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use command_error::CommandExt;
use expect_test::expect;
use test_harness::GitProle;
use test_harness::WorktreeState;

#[test]
fn config_commands() -> miette::Result<()> {
let prole = GitProle::new()?;
prole.setup_worktree_repo("my-repo")?;

prole.write_config(
r#"
commands = [
"sh -c 'echo Puppy wuz here > puppy-log'",
{ sh = '''
echo 2wice the Pupyluv >> puppy-log
''' },
]
"#,
)?;

prole
.cd_cmd("my-repo")
.args(["add", "puppy"])
.status_checked()?;

prole
.repo_state("my-repo")
.worktrees([
WorktreeState::new_bare(),
WorktreeState::new("main").branch("main"),
WorktreeState::new("puppy").branch("puppy").file(
"puppy-log",
expect![[r#"
Puppy wuz here
2wice the Pupyluv
"#]],
),
])
.assert();

Ok(())
}
26 changes: 26 additions & 0 deletions tests/config_commands_default.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use command_error::CommandExt;
use test_harness::GitProle;
use test_harness::WorktreeState;

#[test]
fn config_commands_default() -> miette::Result<()> {
let prole = GitProle::new()?;
prole.setup_worktree_repo("my-repo")?;

prole
.cd_cmd("my-repo")
.args(["add", "puppy"])
.status_checked()?;

prole
.repo_state("my-repo")
.worktrees([
WorktreeState::new_bare(),
WorktreeState::new("main").branch("main"),
// Wow girl give us nothing!
WorktreeState::new("puppy").branch("puppy").status([]),
])
.assert();

Ok(())
}

0 comments on commit e8f910f

Please sign in to comment.