#![forbid(unsafe_code)]
#![deny(
mismatched_lifetime_syntaxes,
unused_imports,
unused_must_use,
dead_code,
unstable_name_collisions,
unused_assignments
)]
#![deny(clippy::all, clippy::perf, clippy::pedantic, clippy::nursery)]
#![allow(
clippy::missing_errors_doc,
clippy::must_use_candidate,
clippy::module_name_repetitions
)]
mod error;
mod hookspath;
use std::{
fs::File,
io::{Read, Write},
path::{Path, PathBuf},
};
pub use error::HooksError;
use error::Result;
use hookspath::HookPaths;
use git2::{Oid, Repository};
pub const HOOK_POST_COMMIT: &str = "post-commit";
pub const HOOK_PRE_COMMIT: &str = "pre-commit";
pub const HOOK_COMMIT_MSG: &str = "commit-msg";
pub const HOOK_PREPARE_COMMIT_MSG: &str = "prepare-commit-msg";
pub const HOOK_PRE_PUSH: &str = "pre-push";
const HOOK_COMMIT_MSG_TEMP_FILE: &str = "COMMIT_EDITMSG";
pub fn hook_available(
repo: &Repository,
other_paths: Option<&[&str]>,
hook: &str,
) -> Result<bool> {
let hook = HookPaths::new(repo, other_paths, hook)?;
Ok(hook.found())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PrePushRef {
pub local_ref: String,
pub local_oid: Option<Oid>,
pub remote_ref: String,
pub remote_oid: Option<Oid>,
}
impl PrePushRef {
pub fn new(
local_ref: impl Into<String>,
local_oid: Option<Oid>,
remote_ref: impl Into<String>,
remote_oid: Option<Oid>,
) -> Self {
Self {
local_ref: local_ref.into(),
local_oid,
remote_ref: remote_ref.into(),
remote_oid,
}
}
fn format_oid(oid: Option<Oid>) -> String {
oid.map_or_else(|| "0".repeat(40), |id| id.to_string())
}
pub fn to_line(&self) -> String {
format!(
"{} {} {} {}",
self.local_ref,
Self::format_oid(self.local_oid),
self.remote_ref,
Self::format_oid(self.remote_oid)
)
}
pub fn to_stdin(updates: &[Self]) -> String {
let mut stdin = String::new();
for update in updates {
stdin.push_str(&update.to_line());
stdin.push('\n');
}
stdin
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct HookRunResponse {
pub hook: PathBuf,
pub stdout: String,
pub stderr: String,
pub code: i32,
}
#[derive(Debug, PartialEq, Eq)]
pub enum HookResult {
NoHookFound,
Run(HookRunResponse),
}
impl HookResult {
pub const fn is_successful(&self) -> bool {
matches!(self, Self::Run(response) if response.is_successful())
}
}
impl HookRunResponse {
pub const fn is_successful(&self) -> bool {
self.code == 0
}
}
pub fn create_hook(
r: &Repository,
hook: &str,
hook_script: &[u8],
) -> PathBuf {
let hook = HookPaths::new(r, None, hook).unwrap();
let path = hook.hook.clone();
create_hook_in_path(&hook.hook, hook_script);
path
}
fn create_hook_in_path(path: &Path, hook_script: &[u8]) {
File::create(path).unwrap().write_all(hook_script).unwrap();
#[cfg(unix)]
{
std::process::Command::new("chmod")
.arg("+x")
.arg(path)
.output()
.unwrap();
}
}
pub fn hooks_commit_msg(
repo: &Repository,
other_paths: Option<&[&str]>,
msg: &mut String,
) -> Result<HookResult> {
let hook = HookPaths::new(repo, other_paths, HOOK_COMMIT_MSG)?;
if !hook.found() {
return Ok(HookResult::NoHookFound);
}
let temp_file = hook.git.join(HOOK_COMMIT_MSG_TEMP_FILE);
File::create(&temp_file)?.write_all(msg.as_bytes())?;
let res = hook.run_hook_os_str([&temp_file])?;
msg.clear();
File::open(temp_file)?.read_to_string(msg)?;
Ok(res)
}
pub fn hooks_pre_commit(
repo: &Repository,
other_paths: Option<&[&str]>,
) -> Result<HookResult> {
let hook = HookPaths::new(repo, other_paths, HOOK_PRE_COMMIT)?;
if !hook.found() {
return Ok(HookResult::NoHookFound);
}
hook.run_hook(&[])
}
pub fn hooks_post_commit(
repo: &Repository,
other_paths: Option<&[&str]>,
) -> Result<HookResult> {
let hook = HookPaths::new(repo, other_paths, HOOK_POST_COMMIT)?;
if !hook.found() {
return Ok(HookResult::NoHookFound);
}
hook.run_hook(&[])
}
pub fn hooks_pre_push(
repo: &Repository,
other_paths: Option<&[&str]>,
remote: Option<&str>,
url: &str,
updates: &[PrePushRef],
) -> Result<HookResult> {
let hook = HookPaths::new(repo, other_paths, HOOK_PRE_PUSH)?;
if !hook.found() {
return Ok(HookResult::NoHookFound);
}
let remote_name = match remote {
Some(r) if !r.is_empty() => r,
_ => url,
};
let stdin_data = PrePushRef::to_stdin(updates);
hook.run_hook_os_str_with_stdin(
[remote_name, url],
Some(stdin_data.as_bytes()),
)
}
pub enum PrepareCommitMsgSource {
Message,
Template,
Merge,
Squash,
Commit(git2::Oid),
}
#[allow(clippy::needless_pass_by_value)]
pub fn hooks_prepare_commit_msg(
repo: &Repository,
other_paths: Option<&[&str]>,
source: PrepareCommitMsgSource,
msg: &mut String,
) -> Result<HookResult> {
let hook =
HookPaths::new(repo, other_paths, HOOK_PREPARE_COMMIT_MSG)?;
if !hook.found() {
return Ok(HookResult::NoHookFound);
}
let temp_file = hook.git.join(HOOK_COMMIT_MSG_TEMP_FILE);
File::create(&temp_file)?.write_all(msg.as_bytes())?;
let temp_file_path = temp_file.as_os_str().to_string_lossy();
let vec = vec![
temp_file_path.as_ref(),
match source {
PrepareCommitMsgSource::Message => "message",
PrepareCommitMsgSource::Template => "template",
PrepareCommitMsgSource::Merge => "merge",
PrepareCommitMsgSource::Squash => "squash",
PrepareCommitMsgSource::Commit(_) => "commit",
},
];
let mut args = vec;
let id = if let PrepareCommitMsgSource::Commit(id) = &source {
Some(id.to_string())
} else {
None
};
if let Some(id) = &id {
args.push(id);
}
let res = hook.run_hook(args.as_slice())?;
msg.clear();
File::open(temp_file)?.read_to_string(msg)?;
Ok(res)
}
#[cfg(test)]
mod tests {
use super::*;
use git2_testing::{repo_init, repo_init_bare};
use pretty_assertions::assert_eq;
use tempfile::TempDir;
fn branch_update(
repo: &Repository,
remote: Option<&str>,
branch: &str,
remote_branch: Option<&str>,
delete: bool,
) -> PrePushRef {
let local_ref = format!("refs/heads/{branch}");
let local_oid = (!delete).then(|| {
repo.find_branch(branch, git2::BranchType::Local)
.unwrap()
.get()
.peel_to_commit()
.unwrap()
.id()
});
let remote_branch = remote_branch.unwrap_or(branch);
let remote_ref = format!("refs/heads/{remote_branch}");
let remote_oid = remote.and_then(|remote_name| {
repo.find_reference(&format!(
"refs/remotes/{remote_name}/{remote_branch}"
))
.ok()
.and_then(|r| r.peel_to_commit().ok())
.map(|c| c.id())
});
PrePushRef::new(local_ref, local_oid, remote_ref, remote_oid)
}
fn head_branch(repo: &Repository) -> String {
repo.head().unwrap().shorthand().unwrap().to_string()
}
#[test]
fn test_pre_push_ref_format() {
let zero_oid = "0".repeat(40);
let oid_a = "a".repeat(40);
let oid_b = "b".repeat(40);
let update = PrePushRef::new(
"refs/heads/main",
Some(git2::Oid::from_str(&oid_a).unwrap()),
"refs/heads/main",
Some(git2::Oid::from_str(&oid_b).unwrap()),
);
assert_eq!(
update.to_line(),
format!(
"refs/heads/main {oid_a} refs/heads/main {oid_b}"
)
);
let update = PrePushRef::new(
"refs/heads/feature",
Some(git2::Oid::from_str(&oid_a).unwrap()),
"refs/heads/feature",
None,
);
assert_eq!(
update.to_line(),
format!("refs/heads/feature {oid_a} refs/heads/feature {zero_oid}")
);
let update = PrePushRef::new(
"refs/heads/old",
None,
"refs/heads/old",
Some(git2::Oid::from_str(&oid_b).unwrap()),
);
assert_eq!(
update.to_line(),
format!(
"refs/heads/old {zero_oid} refs/heads/old {oid_b}"
)
);
let updates = [
PrePushRef::new(
"refs/heads/a",
Some(git2::Oid::from_str(&oid_a).unwrap()),
"refs/heads/a",
None,
),
PrePushRef::new(
"refs/heads/b",
Some(git2::Oid::from_str(&oid_b).unwrap()),
"refs/heads/b",
None,
),
];
assert_eq!(
PrePushRef::to_stdin(&updates),
format!(
"refs/heads/a {oid_a} refs/heads/a {zero_oid}\nrefs/heads/b {oid_b} refs/heads/b {zero_oid}\n"
)
);
}
#[test]
fn test_smoke() {
let (_td, repo) = repo_init();
let mut msg = String::from("test");
let res = hooks_commit_msg(&repo, None, &mut msg).unwrap();
assert_eq!(res, HookResult::NoHookFound);
let hook = b"#!/bin/sh
exit 0
";
create_hook(&repo, HOOK_POST_COMMIT, hook);
let res = hooks_post_commit(&repo, None).unwrap();
assert!(res.is_successful());
}
#[test]
fn test_hooks_commit_msg_ok() {
let (_td, repo) = repo_init();
let hook = b"#!/bin/sh
exit 0
";
create_hook(&repo, HOOK_COMMIT_MSG, hook);
let mut msg = String::from("test");
let res = hooks_commit_msg(&repo, None, &mut msg).unwrap();
assert!(res.is_successful());
assert_eq!(msg, String::from("test"));
}
#[test]
fn test_hooks_commit_msg_with_shell_command_ok() {
let (_td, repo) = repo_init();
let hook = br#"#!/bin/sh
COMMIT_MSG="$(cat "$1")"
printf "$COMMIT_MSG" | sed 's/sth/shell_command/g' > "$1"
exit 0
"#;
create_hook(&repo, HOOK_COMMIT_MSG, hook);
let mut msg = String::from("test_sth");
let res = hooks_commit_msg(&repo, None, &mut msg).unwrap();
assert!(res.is_successful());
assert_eq!(msg, String::from("test_shell_command"));
}
#[test]
fn test_pre_commit_sh() {
let (_td, repo) = repo_init();
let hook = b"#!/bin/sh
exit 0
";
create_hook(&repo, HOOK_PRE_COMMIT, hook);
let res = hooks_pre_commit(&repo, None).unwrap();
assert!(res.is_successful());
}
#[test]
fn test_hook_with_missing_shebang() {
const TEXT: &str = "Hello, world!";
let (_td, repo) = repo_init();
let hook = b"echo \"$@\"\nexit 42";
create_hook(&repo, HOOK_PRE_COMMIT, hook);
let hook =
HookPaths::new(&repo, None, HOOK_PRE_COMMIT).unwrap();
assert!(hook.found());
let result = hook.run_hook(&[TEXT]).unwrap();
let HookResult::Run(response) = result else {
unreachable!("run_hook should've run");
};
let stdout = response.stdout.as_str().trim_ascii_end();
assert_eq!(response.code, 42);
assert_eq!(response.hook, hook.hook);
assert_eq!(stdout, TEXT, "{:?} != {TEXT:?}", stdout);
assert!(response.stderr.is_empty());
}
#[test]
fn test_no_hook_found() {
let (_td, repo) = repo_init();
let res = hooks_pre_commit(&repo, None).unwrap();
assert_eq!(res, HookResult::NoHookFound);
}
#[test]
fn test_other_path() {
let (td, repo) = repo_init();
let hook = b"#!/bin/sh
exit 0
";
let custom_hooks_path = td.path().join(".myhooks");
std::fs::create_dir(dbg!(&custom_hooks_path)).unwrap();
create_hook_in_path(
dbg!(custom_hooks_path.join(HOOK_PRE_COMMIT).as_path()),
hook,
);
let res =
hooks_pre_commit(&repo, Some(&["../.myhooks"])).unwrap();
assert!(res.is_successful());
}
#[test]
fn test_other_path_precedence() {
let (td, repo) = repo_init();
{
let hook = b"#!/bin/sh
exit 0
";
create_hook(&repo, HOOK_PRE_COMMIT, hook);
}
{
let reject_hook = b"#!/bin/sh
exit 1
";
let custom_hooks_path = td.path().join(".myhooks");
std::fs::create_dir(dbg!(&custom_hooks_path)).unwrap();
create_hook_in_path(
dbg!(custom_hooks_path
.join(HOOK_PRE_COMMIT)
.as_path()),
reject_hook,
);
}
let res =
hooks_pre_commit(&repo, Some(&["../.myhooks"])).unwrap();
assert!(res.is_successful());
}
#[test]
fn test_pre_commit_fail_sh() {
let (_td, repo) = repo_init();
let hook = b"#!/bin/sh
echo 'rejected'
exit 1
";
create_hook(&repo, HOOK_PRE_COMMIT, hook);
let res = hooks_pre_commit(&repo, None).unwrap();
assert!(!res.is_successful());
}
#[test]
fn test_env_containing_path() {
const PATH_EXPORT: &str = "export PATH";
let (_td, repo) = repo_init();
let hook = b"#!/bin/sh
export
exit 1
";
create_hook(&repo, HOOK_PRE_COMMIT, hook);
let res = hooks_pre_commit(&repo, None).unwrap();
let HookResult::Run(response) = res else {
unreachable!()
};
assert!(
response
.stdout
.lines()
.any(|line| line.starts_with(PATH_EXPORT)),
"Could not find line starting with {PATH_EXPORT:?} in: {:?}",
response.stdout
);
}
#[test]
fn test_pre_commit_fail_hookspath() {
let (_td, repo) = repo_init();
let hooks = TempDir::new().unwrap();
let hook = b"#!/bin/sh
echo 'rejected'
exit 1
";
create_hook_in_path(&hooks.path().join("pre-commit"), hook);
repo.config()
.unwrap()
.set_str(
"core.hooksPath",
hooks.path().as_os_str().to_str().unwrap(),
)
.unwrap();
let res = hooks_pre_commit(&repo, None).unwrap();
let HookResult::Run(response) = res else {
unreachable!()
};
assert_eq!(response.code, 1);
assert_eq!(&response.stdout, "rejected\n");
}
#[test]
fn test_pre_commit_fail_bare() {
let (_td, repo) = repo_init_bare();
let hook = b"#!/bin/sh
echo 'rejected'
exit 1
";
create_hook(&repo, HOOK_PRE_COMMIT, hook);
let res = hooks_pre_commit(&repo, None).unwrap();
assert!(!res.is_successful());
}
#[test]
fn test_pre_commit_py() {
let (_td, repo) = repo_init();
#[cfg(not(windows))]
let hook = b"#!/usr/bin/env python
import sys
sys.exit(0)
";
#[cfg(windows)]
let hook = b"#!/bin/env python.exe
import sys
sys.exit(0)
";
create_hook(&repo, HOOK_PRE_COMMIT, hook);
let res = hooks_pre_commit(&repo, None).unwrap();
assert!(res.is_successful(), "{res:?}");
}
#[test]
fn test_pre_commit_fail_py() {
let (_td, repo) = repo_init();
#[cfg(not(windows))]
let hook = b"#!/usr/bin/env python
import sys
sys.exit(1)
";
#[cfg(windows)]
let hook = b"#!/bin/env python.exe
import sys
sys.exit(1)
";
create_hook(&repo, HOOK_PRE_COMMIT, hook);
let res = hooks_pre_commit(&repo, None).unwrap();
assert!(!res.is_successful());
}
#[test]
fn test_hooks_commit_msg_reject() {
let (_td, repo) = repo_init();
let hook = b"#!/bin/sh
echo 'msg' > \"$1\"
echo 'rejected'
exit 1
";
create_hook(&repo, HOOK_COMMIT_MSG, hook);
let mut msg = String::from("test");
let res = hooks_commit_msg(&repo, None, &mut msg).unwrap();
let HookResult::Run(response) = res else {
unreachable!()
};
assert_eq!(response.code, 1);
assert_eq!(&response.stdout, "rejected\n");
assert_eq!(msg, String::from("msg\n"));
}
#[test]
fn test_commit_msg_no_block_but_alter() {
let (_td, repo) = repo_init();
let hook = b"#!/bin/sh
echo 'msg' > \"$1\"
exit 0
";
create_hook(&repo, HOOK_COMMIT_MSG, hook);
let mut msg = String::from("test");
let res = hooks_commit_msg(&repo, None, &mut msg).unwrap();
assert!(res.is_successful());
assert_eq!(msg, String::from("msg\n"));
}
#[test]
fn test_hook_pwd_in_bare_without_workdir() {
let (_td, repo) = repo_init_bare();
let git_root = repo.path().to_path_buf();
let hook =
HookPaths::new(&repo, None, HOOK_POST_COMMIT).unwrap();
assert_eq!(hook.pwd, git_root);
}
#[test]
fn test_hook_pwd() {
let (_td, repo) = repo_init();
let git_root = repo.path().to_path_buf();
let hook =
HookPaths::new(&repo, None, HOOK_POST_COMMIT).unwrap();
assert_eq!(hook.pwd, git_root.parent().unwrap());
}
#[test]
fn test_hooks_prep_commit_msg_success() {
let (_td, repo) = repo_init();
let hook = b"#!/bin/sh
echo \"msg:$2\" > \"$1\"
exit 0
";
create_hook(&repo, HOOK_PREPARE_COMMIT_MSG, hook);
let mut msg = String::from("test");
let res = hooks_prepare_commit_msg(
&repo,
None,
PrepareCommitMsgSource::Message,
&mut msg,
)
.unwrap();
assert!(res.is_successful());
assert_eq!(msg, String::from("msg:message\n"));
}
#[test]
fn test_hooks_prep_commit_msg_reject() {
let (_td, repo) = repo_init();
let hook = b"#!/bin/sh
echo \"$2,$3\" > \"$1\"
echo 'rejected'
exit 2
";
create_hook(&repo, HOOK_PREPARE_COMMIT_MSG, hook);
let mut msg = String::from("test");
let res = hooks_prepare_commit_msg(
&repo,
None,
PrepareCommitMsgSource::Commit(git2::Oid::zero()),
&mut msg,
)
.unwrap();
let HookResult::Run(response) = res else {
unreachable!()
};
assert_eq!(response.code, 2);
assert_eq!(&response.stdout, "rejected\n");
assert_eq!(
msg,
String::from(
"commit,0000000000000000000000000000000000000000\n"
)
);
}
#[test]
fn test_pre_push_sh() {
let (_td, repo) = repo_init();
let hook = b"#!/bin/sh
exit 0
";
create_hook(&repo, HOOK_PRE_PUSH, hook);
let branch = head_branch(&repo);
let updates = [branch_update(
&repo,
Some("origin"),
&branch,
None,
false,
)];
let res = hooks_pre_push(
&repo,
None,
Some("origin"),
"https://example.com/repo.git",
&updates,
)
.unwrap();
assert!(res.is_successful());
}
#[test]
fn test_pre_push_fail_sh() {
let (_td, repo) = repo_init();
let hook = b"#!/bin/sh
echo 'failed'
exit 3
";
create_hook(&repo, HOOK_PRE_PUSH, hook);
let branch = head_branch(&repo);
let updates = [branch_update(
&repo,
Some("origin"),
&branch,
None,
false,
)];
let res = hooks_pre_push(
&repo,
None,
Some("origin"),
"https://example.com/repo.git",
&updates,
)
.unwrap();
let HookResult::Run(response) = res else {
unreachable!()
};
assert_eq!(response.code, 3);
assert_eq!(&response.stdout, "failed\n");
}
#[test]
fn test_pre_push_no_remote_name() {
let (_td, repo) = repo_init();
let hook = b"#!/bin/sh
# Verify that when remote is None, URL is passed for both arguments
echo \"arg1=$1 arg2=$2\"
exit 0
";
create_hook(&repo, HOOK_PRE_PUSH, hook);
let branch = head_branch(&repo);
let updates =
[branch_update(&repo, None, &branch, None, false)];
let res = hooks_pre_push(
&repo,
None,
None,
"https://example.com/repo.git",
&updates,
)
.unwrap();
let HookResult::Run(response) = res else {
panic!("Expected Run result, got: {res:?}");
};
assert!(response.is_successful());
assert_eq!(
response.stdout,
"arg1=https://example.com/repo.git arg2=https://example.com/repo.git\n"
);
}
#[test]
fn test_pre_push_with_arguments() {
let (_td, repo) = repo_init();
let hook = b"#!/bin/sh
echo \"remote_name=$1\"
echo \"remote_url=$2\"
exit 0
";
create_hook(&repo, HOOK_PRE_PUSH, hook);
let branch = head_branch(&repo);
let updates = [branch_update(
&repo,
Some("origin"),
&branch,
None,
false,
)];
let res = hooks_pre_push(
&repo,
None,
Some("origin"),
"https://example.com/repo.git",
&updates,
)
.unwrap();
let HookResult::Run(response) = res else {
unreachable!("Expected Run result, got: {res:?}")
};
assert!(response.is_successful());
assert_eq!(
response.stdout,
"remote_name=origin\nremote_url=https://example.com/repo.git\n"
);
}
#[test]
fn test_pre_push_multiple_updates() {
let (_td, repo) = repo_init();
let hook = b"#!/bin/sh
cat
exit 0
";
create_hook(&repo, HOOK_PRE_PUSH, hook);
let branch = head_branch(&repo);
let branch_update = branch_update(
&repo,
Some("origin"),
&branch,
None,
false,
);
let head_commit =
repo.head().unwrap().peel_to_commit().unwrap();
repo.tag_lightweight("v1", head_commit.as_object(), false)
.unwrap();
let tag_ref = repo.find_reference("refs/tags/v1").unwrap();
let tag_oid = tag_ref.target().unwrap();
let tag_update = PrePushRef::new(
"refs/tags/v1",
Some(tag_oid),
"refs/tags/v1",
None,
);
let updates = [branch_update, tag_update];
let expected_stdin = PrePushRef::to_stdin(&updates);
let res = hooks_pre_push(
&repo,
None,
Some("origin"),
"https://example.com/repo.git",
&updates,
)
.unwrap();
let HookResult::Run(response) = res else {
unreachable!("Expected Run result, got: {res:?}")
};
assert!(
response.is_successful(),
"Hook should succeed: stdout {} stderr {}",
response.stdout,
response.stderr
);
assert_eq!(
response.stdout, expected_stdin,
"stdin should include all refspec lines"
);
}
#[test]
fn test_pre_push_delete_ref_uses_zero_oid() {
let (_td, repo) = repo_init();
let hook = b"#!/bin/sh
cat
exit 0
";
create_hook(&repo, HOOK_PRE_PUSH, hook);
let branch = head_branch(&repo);
let updates = [branch_update(
&repo,
Some("origin"),
&branch,
None,
true,
)];
let expected_stdin = PrePushRef::to_stdin(&updates);
let res = hooks_pre_push(
&repo,
None,
Some("origin"),
"https://example.com/repo.git",
&updates,
)
.unwrap();
let HookResult::Run(response) = res else {
unreachable!("Expected Run result, got: {res:?}")
};
assert!(response.is_successful());
assert_eq!(response.stdout, expected_stdin);
}
#[test]
fn test_pre_push_stdin() {
let (_td, repo) = repo_init();
let hook = b"#!/bin/sh
cat
exit 0
";
create_hook(&repo, HOOK_PRE_PUSH, hook);
let branch = head_branch(&repo);
let updates = [branch_update(
&repo,
Some("origin"),
&branch,
None,
false,
)];
let expected_stdin = PrePushRef::to_stdin(&updates);
let res = hooks_pre_push(
&repo,
None,
Some("origin"),
"https://github.com/user/repo.git",
&updates,
)
.unwrap();
let HookResult::Run(response) = res else {
unreachable!("Expected Run result, got: {res:?}")
};
assert!(response.is_successful());
assert_eq!(response.stdout, expected_stdin);
}
#[test]
fn test_pre_push_uses_push_target_remote_not_upstream() {
let (_td, repo) = repo_init();
let head = repo.head().unwrap();
let local_commit = head.target().unwrap();
repo.reference(
"refs/remotes/origin/master",
local_commit,
true,
"create origin/master",
)
.unwrap();
let sig = repo.signature().unwrap();
let tree_id = {
let mut index = repo.index().unwrap();
index.write_tree().unwrap()
};
let tree = repo.find_tree(tree_id).unwrap();
let old_commit = repo
.commit(None, &sig, &sig, "old backup commit", &tree, &[])
.unwrap();
repo.reference(
"refs/remotes/backup/master",
old_commit,
true,
"create backup/master at old commit",
)
.unwrap();
{
let mut config = repo.config().unwrap();
config.set_str("branch.master.remote", "origin").unwrap();
config
.set_str("branch.master.merge", "refs/heads/master")
.unwrap();
}
let hook = b"#!/bin/sh
cat
exit 0
";
create_hook(&repo, HOOK_PRE_PUSH, hook);
let branch = head_branch(&repo);
let updates = [branch_update(
&repo,
Some("backup"),
&branch,
None,
false,
)];
let expected_stdin = PrePushRef::to_stdin(&updates);
let res = hooks_pre_push(
&repo,
None,
Some("backup"),
"https://github.com/user/backup-repo.git",
&updates,
)
.unwrap();
let HookResult::Run(response) = res else {
panic!("Expected Run result, got: {res:?}")
};
assert!(response.is_successful());
assert_eq!(response.stdout, expected_stdin);
}
}