Skip to content

Commit b6cd56a

Browse files
committed
Invoke hooks using bash explicitly
To run correctly on Windows, all invocations of hooks need to be executed as an argument to bash. This changes the code to build commands using get_bash() with the `-c` flag. When discovering which bash to use, we are careful on Windows to pick "git-bash" aka the bash.exe that comes with git. This is neccesary as git-bash uses different path translations than regular bash.exe, and using the wrong one would result in git.exe not being able to discover the git-branchless.exe binary correctly (when invoked in hook).
1 parent 16b18ee commit b6cd56a

File tree

4 files changed

+63
-12
lines changed

4 files changed

+63
-12
lines changed

src/core/rewrite.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use fn_error_context::context;
1111
use indicatif::{ProgressBar, ProgressStyle};
1212

1313
use crate::core::formatting::printable_styled_string;
14-
use crate::util::{run_git, wrap_git_error, GitExecutable};
14+
use crate::util::{get_bash, run_git, wrap_git_error, GitExecutable};
1515

1616
use super::eventlog::{
1717
Event, EventCursor, EventReplayer, EventTransactionId, BRANCHLESS_TRANSACTION_ID_ENV_VAR,
@@ -385,21 +385,25 @@ fn post_rebase_in_memory(
385385
rewritten_oids: &[(git2::Oid, git2::Oid)],
386386
event_tx_id: EventTransactionId,
387387
) -> anyhow::Result<()> {
388-
let post_rewrite_hook_path = repo
388+
let hooks_path = repo
389389
.config()?
390390
.get_path("core.hooksPath")
391-
.unwrap_or_else(|_| repo.path().join("hooks"))
392-
.join("post-rewrite");
393-
if post_rewrite_hook_path.exists() {
394-
let mut child = Command::new(post_rewrite_hook_path.as_path())
395-
.arg("rebase")
391+
.unwrap_or_else(|_| repo.path().join("hooks"));
392+
if hooks_path.join("post-rewrite").exists() {
393+
let mut command = Command::new(get_bash().context("bash needed to run post-rewrite")?);
394+
let mut child = command
395+
.arg("-c")
396+
// Always use Unix pathing here as this is an arg to bash.
397+
// Prefix with ./ as hooks aren't in the PATH.
398+
.arg("./post-rewrite rebase")
396399
.env(BRANCHLESS_TRANSACTION_ID_ENV_VAR, event_tx_id.to_string())
400+
.current_dir(&hooks_path)
397401
.stdin(Stdio::piped())
398402
.spawn()
399403
.with_context(|| {
400404
format!(
401405
"Invoking post-rewrite hook at: {:?}",
402-
post_rewrite_hook_path.as_path()
406+
hooks_path.join("post-rewrite").as_path()
403407
)
404408
})?;
405409

src/testing.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};
77
use std::process::Command;
88
use std::str::FromStr;
99

10-
use crate::util::{wrap_git_error, GitExecutable, GitVersion};
10+
use crate::util::{get_bash, wrap_git_error, GitExecutable, GitVersion};
1111
use anyhow::Context;
1212
use fn_error_context::context;
1313

@@ -99,11 +99,16 @@ impl Git {
9999
.git_executable
100100
.parent()
101101
.expect("Unable to find git path parent");
102-
std::env::join_paths(vec![
102+
let bash = get_bash().expect("bash missing?");
103+
let bash_path = bash.parent().unwrap();
104+
std::env::join_paths(
105+
vec![
103106
// For Git to be able to launch `git-branchless`.
104107
branchless_path,
105108
// For our hooks to be able to call back into `git`.
106109
git_path,
110+
// For branchless to manually invoke bash when needed.
111+
bash_path,
107112
]
108113
.iter()
109114
.map(|path| path.to_str().expect("Unable to decode path component")),

src/util.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use std::collections::{HashMap, HashSet};
44
use std::convert::TryInto;
5+
use std::env;
56
use std::io::{stderr, stdout, Write};
67
use std::path::PathBuf;
78
use std::process::Command;
@@ -274,6 +275,42 @@ impl FromStr for GitVersion {
274275
}
275276
}
276277

278+
/// Returns a path for a given file, searching through PATH to find it.
279+
pub fn get_from_path(exe_name: &str) -> Option<PathBuf> {
280+
env::var_os("PATH").and_then(|paths| {
281+
env::split_paths(&paths).find_map(|dir| {
282+
let bash_path = dir.join(exe_name);
283+
if bash_path.is_file() {
284+
Some(bash_path.to_path_buf())
285+
} else {
286+
None
287+
}
288+
})
289+
})
290+
}
291+
292+
/// Returns the path to bash.
293+
pub fn get_bash() -> Option<PathBuf> {
294+
let exe_name = if cfg!(target_os = "windows") {
295+
"bash.exe"
296+
} else {
297+
"bash"
298+
};
299+
// If we are on Windows, first look for git.exe, and try to use it's bash, otherwise it won't
300+
// be able to find git-branchless correctly.
301+
if cfg!(target_os = "windows") {
302+
// Git is typically installed at C:\Program Files\Git\cmd\git.exe with the cmd\ directory
303+
// in the path, however git-bash is usually not in PATH and is in bin\ directory:
304+
let git_path = get_from_path("git.exe").expect("Couldn't find git.exe");
305+
let git_dir = git_path.parent().unwrap().parent().unwrap();
306+
let git_bash = git_dir.join("bin").join(exe_name);
307+
if git_bash.is_file() {
308+
return Some(git_bash);
309+
}
310+
}
311+
get_from_path(exe_name)
312+
}
313+
277314
#[cfg(test)]
278315
mod tests {
279316
use super::*;

tests/core/test_hooks.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
use anyhow::Context;
12
use branchless::testing::with_git;
3+
use branchless::util::get_bash;
24
use std::process::Command;
35

46
fn preprocess_stderr(stderr: String) -> String {
@@ -166,8 +168,11 @@ fn test_pre_auto_gc() -> anyhow::Result<()> {
166168
// `pre-auto-gc` hook to be invoked at all. We'll just invoke the hook
167169
// directly to make sure that it's installed properly.
168170
std::env::set_current_dir(&git.repo_path)?;
169-
let hook_path = git.repo_path.join(".git").join("hooks").join("pre-auto-gc");
170-
let output = Command::new(hook_path)
171+
let output = Command::new(get_bash().context("bash needed to run pre-auto-gc")?)
172+
.arg("-c")
173+
// Always use a unix style path here, as we are handing it to bash (even on Windows).
174+
.arg("./.git/hooks/pre-auto-gc")
175+
.current_dir(&git.repo_path)
171176
.env("PATH", git.get_path_for_env())
172177
.output()?;
173178

0 commit comments

Comments
 (0)