Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,17 @@ jobs:
toolchain: stable
- name: Run Rust tests
run: PATH_TO_GIT="$PWD"/git cargo test

test-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Set up Rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- name: Run Rust tests
run: |
$env:PATH_TO_GIT='C:\Program Files\Git\cmd\git.exe'
cargo test
6 changes: 5 additions & 1 deletion src/commands/wrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,11 @@ mod tests {
"branchless",
"wrap",
"--git-executable",
"/bin/echo",
if cfg!(target_os = "windows") {
"echo"
} else {
"/bin/echo"
},
"hello",
"world",
])?;
Expand Down
47 changes: 26 additions & 21 deletions src/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};
use std::process::Command;
use std::str::FromStr;

use crate::util::{wrap_git_error, GitExecutable, GitVersion};
use crate::util::{get_sh, wrap_git_error, GitExecutable, GitVersion};
use anyhow::Context;
use fn_error_context::context;

Expand Down Expand Up @@ -99,16 +99,23 @@ impl Git {
.git_executable
.parent()
.expect("Unable to find git path parent");
vec![
// For Git to be able to launch `git-branchless`.
branchless_path,
// For our hooks to be able to call back into `git`.
git_path,
]
.iter()
.map(|path| path.to_str().expect("Unable to decode path component"))
.collect::<Vec<_>>()
.join(":")
let bash = get_sh().expect("bash missing?");
let bash_path = bash.parent().unwrap();
std::env::join_paths(
vec![
// For Git to be able to launch `git-branchless`.
branchless_path,
// For our hooks to be able to call back into `git`.
git_path,
// For branchless to manually invoke bash when needed.
bash_path,
]
.iter()
.map(|path| path.to_str().expect("Unable to decode path component")),
)
.expect("joining paths")
.to_string_lossy()
.to_string()
}

/// Run a Git command.
Expand All @@ -124,7 +131,11 @@ impl Git {
expected_exit_code,
} = options;

let default_path = Path::new("/usr/bin/git");
let default_path = if cfg!(target_os = "windows") {
Path::new("C:\\Program Files\\Git\\bin\\git.exe")
} else {
Path::new("/usr/bin/git")
};
let git_executable = if !use_system_git {
&self.git_executable
} else {
Expand All @@ -150,15 +161,9 @@ impl Git {
// messages. Usually, we can set this with `git commit -m`, but we have
// no such option for things such as `git rebase`, which may call `git
// commit` later as a part of their execution.
("GIT_EDITOR", {
// Some systems have `/bin/true` and others have
// `/usr/bin/true`. Pick whichever one we can find.
if Path::new("/bin/true").exists() {
"/bin/true"
} else {
"/usr/bin/true"
}
}),
//
// ":" is understood by `git` to skip editing.
("GIT_EDITOR", ":"),
(
"PATH_TO_GIT",
git_executable
Expand Down
50 changes: 46 additions & 4 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use std::collections::{HashMap, HashSet};
use std::convert::TryInto;
use std::env;
use std::io::{stderr, stdout, Write};
use std::path::PathBuf;
use std::process::{Command, ExitStatus, Stdio};
Expand Down Expand Up @@ -261,12 +262,17 @@ pub fn run_hook(
let hook_path = repo
.config()?
.get_path("core.hooksPath")
.unwrap_or_else(|_| repo.path().join("hooks"))
.join(hook_name);
if hook_path.exists() {
let mut child = Command::new(hook_path.as_path())
.unwrap_or_else(|_| repo.path().join("hooks"));
if hook_path.join(hook_name).exists() {
let mut child = Command::new(get_sh().context("shell needed to run hook")?)
.arg("-c")
// This is always passed as a current dir unix path, as `sh` is processing it as an
// argument, even on Windows.
.arg(format!("./{} \"$@\"", hook_name))
.arg(hook_name) // "$@" expands "$1" "$2" "$3" ... but we also must specify $0.
.args(args.iter().map(|arg| arg.as_ref()).collect::<Vec<_>>())
.env(BRANCHLESS_TRANSACTION_ID_ENV_VAR, event_tx_id.to_string())
.current_dir(&hook_path)
.stdin(Stdio::piped())
.spawn()
.with_context(|| {
Expand Down Expand Up @@ -310,6 +316,42 @@ impl FromStr for GitVersion {
}
}

/// Returns a path for a given file, searching through PATH to find it.
pub fn get_from_path(exe_name: &str) -> Option<PathBuf> {
env::var_os("PATH").and_then(|paths| {
env::split_paths(&paths).find_map(|dir| {
let bash_path = dir.join(exe_name);
if bash_path.is_file() {
Some(bash_path)
} else {
None
}
})
})
}

/// Returns the path to a shell suitable for running hooks.
pub fn get_sh() -> Option<PathBuf> {
let exe_name = if cfg!(target_os = "windows") {
"bash.exe"
} else {
"sh"
};
// If we are on Windows, first look for git.exe, and try to use it's bash, otherwise it won't
// be able to find git-branchless correctly.
if cfg!(target_os = "windows") {
// Git is typically installed at C:\Program Files\Git\cmd\git.exe with the cmd\ directory
// in the path, however git-bash is usually not in PATH and is in bin\ directory:
let git_path = get_from_path("git.exe").expect("Couldn't find git.exe");
let git_dir = git_path.parent().unwrap().parent().unwrap();
let git_bash = git_dir.join("bin").join(exe_name);
if git_bash.is_file() {
return Some(git_bash);
}
}
get_from_path(exe_name)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
11 changes: 9 additions & 2 deletions tests/core/test_hooks.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use anyhow::Context;
use branchless::testing::with_git;
use branchless::util::get_sh;
use std::process::Command;

fn preprocess_stderr(stderr: String) -> String {
stderr
// Interactive progress displays may update the same line multiple times
// with a carriage return before emitting the final newline.
.replace("\r", "\n")
// Window pseudo console may emit EL 'Erase in Line' VT sequences.
.replace("\x1b[K", "")
.lines()
.filter(|line| {
!line.chars().all(|c| c.is_whitespace()) && !line.starts_with("branchless: processing")
Expand Down Expand Up @@ -164,8 +168,11 @@ fn test_pre_auto_gc() -> anyhow::Result<()> {
// `pre-auto-gc` hook to be invoked at all. We'll just invoke the hook
// directly to make sure that it's installed properly.
std::env::set_current_dir(&git.repo_path)?;
let hook_path = git.repo_path.join(".git").join("hooks").join("pre-auto-gc");
let output = Command::new(hook_path)
let output = Command::new(get_sh().context("bash needed to run pre-auto-gc")?)
.arg("-c")
// Always use a unix style path here, as we are handing it to bash (even on Windows).
.arg("./.git/hooks/pre-auto-gc")
.current_dir(&git.repo_path)
.env("PATH", git.get_path_for_env())
.output()?;

Expand Down