1#![forbid(unsafe_code)]
13#![deny(
14 mismatched_lifetime_syntaxes,
15 unused_imports,
16 unused_must_use,
17 dead_code,
18 unstable_name_collisions,
19 unused_assignments
20)]
21#![deny(clippy::all, clippy::perf, clippy::pedantic, clippy::nursery)]
22#![allow(
23 clippy::missing_errors_doc,
24 clippy::must_use_candidate,
25 clippy::module_name_repetitions
26)]
27
28mod error;
29mod hookspath;
30
31use std::{
32 fs::File,
33 io::{Read, Write},
34 path::{Path, PathBuf},
35};
36
37pub use error::HooksError;
38use error::Result;
39use hookspath::HookPaths;
40
41use git2::{Oid, Repository};
42
43pub const HOOK_POST_COMMIT: &str = "post-commit";
44pub const HOOK_PRE_COMMIT: &str = "pre-commit";
45pub const HOOK_COMMIT_MSG: &str = "commit-msg";
46pub const HOOK_PREPARE_COMMIT_MSG: &str = "prepare-commit-msg";
47pub const HOOK_PRE_PUSH: &str = "pre-push";
48
49const HOOK_COMMIT_MSG_TEMP_FILE: &str = "COMMIT_EDITMSG";
50
51pub fn hook_available(
53 repo: &Repository,
54 other_paths: Option<&[&str]>,
55 hook: &str,
56) -> Result<bool> {
57 let hook = HookPaths::new(repo, other_paths, hook)?;
58 Ok(hook.found())
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct PrePushRef {
63 pub local_ref: String,
64 pub local_oid: Option<Oid>,
65 pub remote_ref: String,
66 pub remote_oid: Option<Oid>,
67}
68
69impl PrePushRef {
70 pub fn new(
71 local_ref: impl Into<String>,
72 local_oid: Option<Oid>,
73 remote_ref: impl Into<String>,
74 remote_oid: Option<Oid>,
75 ) -> Self {
76 Self {
77 local_ref: local_ref.into(),
78 local_oid,
79 remote_ref: remote_ref.into(),
80 remote_oid,
81 }
82 }
83
84 fn format_oid(oid: Option<Oid>) -> String {
85 oid.map_or_else(|| "0".repeat(40), |id| id.to_string())
88 }
89
90 pub fn to_line(&self) -> String {
91 format!(
92 "{} {} {} {}",
93 self.local_ref,
94 Self::format_oid(self.local_oid),
95 self.remote_ref,
96 Self::format_oid(self.remote_oid)
97 )
98 }
99
100 pub fn to_stdin(updates: &[Self]) -> String {
102 let mut stdin = String::new();
103 for update in updates {
104 stdin.push_str(&update.to_line());
105 stdin.push('\n');
106 }
107 stdin
108 }
109}
110
111#[derive(Debug, PartialEq, Eq)]
113pub struct HookRunResponse {
114 pub hook: PathBuf,
116 pub stdout: String,
118 pub stderr: String,
120 pub code: i32,
122}
123
124#[derive(Debug, PartialEq, Eq)]
125pub enum HookResult {
126 NoHookFound,
128 Run(HookRunResponse),
130}
131
132impl HookResult {
133 pub const fn is_successful(&self) -> bool {
135 matches!(self, Self::Run(response) if response.is_successful())
136 }
137}
138
139impl HookRunResponse {
140 pub const fn is_successful(&self) -> bool {
142 self.code == 0
143 }
144}
145
146pub fn create_hook(
151 r: &Repository,
152 hook: &str,
153 hook_script: &[u8],
154) -> PathBuf {
155 let hook = HookPaths::new(r, None, hook).unwrap();
156
157 let path = hook.hook.clone();
158
159 create_hook_in_path(&hook.hook, hook_script);
160
161 path
162}
163
164fn create_hook_in_path(path: &Path, hook_script: &[u8]) {
165 File::create(path).unwrap().write_all(hook_script).unwrap();
166
167 #[cfg(unix)]
168 {
169 std::process::Command::new("chmod")
170 .arg("+x")
171 .arg(path)
172 .output()
174 .unwrap();
175 }
176}
177
178pub fn hooks_commit_msg(
185 repo: &Repository,
186 other_paths: Option<&[&str]>,
187 msg: &mut String,
188) -> Result<HookResult> {
189 let hook = HookPaths::new(repo, other_paths, HOOK_COMMIT_MSG)?;
190
191 if !hook.found() {
192 return Ok(HookResult::NoHookFound);
193 }
194
195 let temp_file = hook.git.join(HOOK_COMMIT_MSG_TEMP_FILE);
196 File::create(&temp_file)?.write_all(msg.as_bytes())?;
197
198 let res = hook.run_hook_os_str([&temp_file])?;
199
200 msg.clear();
202 File::open(temp_file)?.read_to_string(msg)?;
203
204 Ok(res)
205}
206
207pub fn hooks_pre_commit(
209 repo: &Repository,
210 other_paths: Option<&[&str]>,
211) -> Result<HookResult> {
212 let hook = HookPaths::new(repo, other_paths, HOOK_PRE_COMMIT)?;
213
214 if !hook.found() {
215 return Ok(HookResult::NoHookFound);
216 }
217
218 hook.run_hook(&[])
219}
220
221pub fn hooks_post_commit(
223 repo: &Repository,
224 other_paths: Option<&[&str]>,
225) -> Result<HookResult> {
226 let hook = HookPaths::new(repo, other_paths, HOOK_POST_COMMIT)?;
227
228 if !hook.found() {
229 return Ok(HookResult::NoHookFound);
230 }
231
232 hook.run_hook(&[])
233}
234
235pub fn hooks_pre_push(
248 repo: &Repository,
249 other_paths: Option<&[&str]>,
250 remote: Option<&str>,
251 url: &str,
252 updates: &[PrePushRef],
253) -> Result<HookResult> {
254 let hook = HookPaths::new(repo, other_paths, HOOK_PRE_PUSH)?;
255
256 if !hook.found() {
257 return Ok(HookResult::NoHookFound);
258 }
259
260 let remote_name = match remote {
262 Some(r) if !r.is_empty() => r,
263 _ => url,
264 };
265
266 let stdin_data = PrePushRef::to_stdin(updates);
267
268 hook.run_hook_os_str_with_stdin(
269 [remote_name, url],
270 Some(stdin_data.as_bytes()),
271 )
272}
273
274pub enum PrepareCommitMsgSource {
275 Message,
276 Template,
277 Merge,
278 Squash,
279 Commit(git2::Oid),
280}
281
282#[allow(clippy::needless_pass_by_value)]
284pub fn hooks_prepare_commit_msg(
285 repo: &Repository,
286 other_paths: Option<&[&str]>,
287 source: PrepareCommitMsgSource,
288 msg: &mut String,
289) -> Result<HookResult> {
290 let hook =
291 HookPaths::new(repo, other_paths, HOOK_PREPARE_COMMIT_MSG)?;
292
293 if !hook.found() {
294 return Ok(HookResult::NoHookFound);
295 }
296
297 let temp_file = hook.git.join(HOOK_COMMIT_MSG_TEMP_FILE);
298 File::create(&temp_file)?.write_all(msg.as_bytes())?;
299
300 let temp_file_path = temp_file.as_os_str().to_string_lossy();
301
302 let vec = vec![
303 temp_file_path.as_ref(),
304 match source {
305 PrepareCommitMsgSource::Message => "message",
306 PrepareCommitMsgSource::Template => "template",
307 PrepareCommitMsgSource::Merge => "merge",
308 PrepareCommitMsgSource::Squash => "squash",
309 PrepareCommitMsgSource::Commit(_) => "commit",
310 },
311 ];
312 let mut args = vec;
313
314 let id = if let PrepareCommitMsgSource::Commit(id) = &source {
315 Some(id.to_string())
316 } else {
317 None
318 };
319
320 if let Some(id) = &id {
321 args.push(id);
322 }
323
324 let res = hook.run_hook(args.as_slice())?;
325
326 msg.clear();
328 File::open(temp_file)?.read_to_string(msg)?;
329
330 Ok(res)
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use git2_testing::{repo_init, repo_init_bare};
337 use pretty_assertions::assert_eq;
338 use tempfile::TempDir;
339
340 fn branch_update(
341 repo: &Repository,
342 remote: Option<&str>,
343 branch: &str,
344 remote_branch: Option<&str>,
345 delete: bool,
346 ) -> PrePushRef {
347 let local_ref = format!("refs/heads/{branch}");
348 let local_oid = (!delete).then(|| {
349 repo.find_branch(branch, git2::BranchType::Local)
350 .unwrap()
351 .get()
352 .peel_to_commit()
353 .unwrap()
354 .id()
355 });
356
357 let remote_branch = remote_branch.unwrap_or(branch);
358 let remote_ref = format!("refs/heads/{remote_branch}");
359 let remote_oid = remote.and_then(|remote_name| {
360 repo.find_reference(&format!(
361 "refs/remotes/{remote_name}/{remote_branch}"
362 ))
363 .ok()
364 .and_then(|r| r.peel_to_commit().ok())
365 .map(|c| c.id())
366 });
367
368 PrePushRef::new(local_ref, local_oid, remote_ref, remote_oid)
369 }
370
371 fn head_branch(repo: &Repository) -> String {
372 repo.head().unwrap().shorthand().unwrap().to_string()
373 }
374
375 #[test]
376 fn test_pre_push_ref_format() {
377 let zero_oid = "0".repeat(40);
378 let oid_a = "a".repeat(40);
379 let oid_b = "b".repeat(40);
380
381 let update = PrePushRef::new(
383 "refs/heads/main",
384 Some(git2::Oid::from_str(&oid_a).unwrap()),
385 "refs/heads/main",
386 Some(git2::Oid::from_str(&oid_b).unwrap()),
387 );
388 assert_eq!(
389 update.to_line(),
390 format!(
391 "refs/heads/main {oid_a} refs/heads/main {oid_b}"
392 )
393 );
394
395 let update = PrePushRef::new(
397 "refs/heads/feature",
398 Some(git2::Oid::from_str(&oid_a).unwrap()),
399 "refs/heads/feature",
400 None,
401 );
402 assert_eq!(
403 update.to_line(),
404 format!("refs/heads/feature {oid_a} refs/heads/feature {zero_oid}")
405 );
406
407 let update = PrePushRef::new(
409 "refs/heads/old",
410 None,
411 "refs/heads/old",
412 Some(git2::Oid::from_str(&oid_b).unwrap()),
413 );
414 assert_eq!(
415 update.to_line(),
416 format!(
417 "refs/heads/old {zero_oid} refs/heads/old {oid_b}"
418 )
419 );
420
421 let updates = [
423 PrePushRef::new(
424 "refs/heads/a",
425 Some(git2::Oid::from_str(&oid_a).unwrap()),
426 "refs/heads/a",
427 None,
428 ),
429 PrePushRef::new(
430 "refs/heads/b",
431 Some(git2::Oid::from_str(&oid_b).unwrap()),
432 "refs/heads/b",
433 None,
434 ),
435 ];
436 assert_eq!(
437 PrePushRef::to_stdin(&updates),
438 format!(
439 "refs/heads/a {oid_a} refs/heads/a {zero_oid}\nrefs/heads/b {oid_b} refs/heads/b {zero_oid}\n"
440 )
441 );
442 }
443
444 #[test]
445 fn test_smoke() {
446 let (_td, repo) = repo_init();
447
448 let mut msg = String::from("test");
449 let res = hooks_commit_msg(&repo, None, &mut msg).unwrap();
450
451 assert_eq!(res, HookResult::NoHookFound);
452
453 let hook = b"#!/bin/sh
454exit 0
455 ";
456
457 create_hook(&repo, HOOK_POST_COMMIT, hook);
458
459 let res = hooks_post_commit(&repo, None).unwrap();
460
461 assert!(res.is_successful());
462 }
463
464 #[test]
465 fn test_hooks_commit_msg_ok() {
466 let (_td, repo) = repo_init();
467
468 let hook = b"#!/bin/sh
469exit 0
470 ";
471
472 create_hook(&repo, HOOK_COMMIT_MSG, hook);
473
474 let mut msg = String::from("test");
475 let res = hooks_commit_msg(&repo, None, &mut msg).unwrap();
476
477 assert!(res.is_successful());
478
479 assert_eq!(msg, String::from("test"));
480 }
481
482 #[test]
483 fn test_hooks_commit_msg_with_shell_command_ok() {
484 let (_td, repo) = repo_init();
485
486 let hook = br#"#!/bin/sh
487COMMIT_MSG="$(cat "$1")"
488printf "$COMMIT_MSG" | sed 's/sth/shell_command/g' > "$1"
489exit 0
490 "#;
491
492 create_hook(&repo, HOOK_COMMIT_MSG, hook);
493
494 let mut msg = String::from("test_sth");
495 let res = hooks_commit_msg(&repo, None, &mut msg).unwrap();
496
497 assert!(res.is_successful());
498
499 assert_eq!(msg, String::from("test_shell_command"));
500 }
501
502 #[test]
503 fn test_pre_commit_sh() {
504 let (_td, repo) = repo_init();
505
506 let hook = b"#!/bin/sh
507exit 0
508 ";
509
510 create_hook(&repo, HOOK_PRE_COMMIT, hook);
511 let res = hooks_pre_commit(&repo, None).unwrap();
512 assert!(res.is_successful());
513 }
514
515 #[test]
516 fn test_hook_with_missing_shebang() {
517 const TEXT: &str = "Hello, world!";
518
519 let (_td, repo) = repo_init();
520
521 let hook = b"echo \"$@\"\nexit 42";
522
523 create_hook(&repo, HOOK_PRE_COMMIT, hook);
524
525 let hook =
526 HookPaths::new(&repo, None, HOOK_PRE_COMMIT).unwrap();
527
528 assert!(hook.found());
529
530 let result = hook.run_hook(&[TEXT]).unwrap();
531
532 let HookResult::Run(response) = result else {
533 unreachable!("run_hook should've run");
534 };
535
536 let stdout = response.stdout.as_str().trim_ascii_end();
537
538 assert_eq!(response.code, 42);
539 assert_eq!(response.hook, hook.hook);
540 assert_eq!(stdout, TEXT, "{:?} != {TEXT:?}", stdout);
541 assert!(response.stderr.is_empty());
542 }
543
544 #[test]
545 fn test_no_hook_found() {
546 let (_td, repo) = repo_init();
547
548 let res = hooks_pre_commit(&repo, None).unwrap();
549 assert_eq!(res, HookResult::NoHookFound);
550 }
551
552 #[test]
553 fn test_other_path() {
554 let (td, repo) = repo_init();
555
556 let hook = b"#!/bin/sh
557exit 0
558 ";
559
560 let custom_hooks_path = td.path().join(".myhooks");
561
562 std::fs::create_dir(dbg!(&custom_hooks_path)).unwrap();
563 create_hook_in_path(
564 dbg!(custom_hooks_path.join(HOOK_PRE_COMMIT).as_path()),
565 hook,
566 );
567
568 let res =
569 hooks_pre_commit(&repo, Some(&["../.myhooks"])).unwrap();
570
571 assert!(res.is_successful());
572 }
573
574 #[test]
575 fn test_other_path_precedence() {
576 let (td, repo) = repo_init();
577
578 {
579 let hook = b"#!/bin/sh
580exit 0
581 ";
582
583 create_hook(&repo, HOOK_PRE_COMMIT, hook);
584 }
585
586 {
587 let reject_hook = b"#!/bin/sh
588exit 1
589 ";
590
591 let custom_hooks_path = td.path().join(".myhooks");
592 std::fs::create_dir(dbg!(&custom_hooks_path)).unwrap();
593 create_hook_in_path(
594 dbg!(custom_hooks_path
595 .join(HOOK_PRE_COMMIT)
596 .as_path()),
597 reject_hook,
598 );
599 }
600
601 let res =
602 hooks_pre_commit(&repo, Some(&["../.myhooks"])).unwrap();
603
604 assert!(res.is_successful());
605 }
606
607 #[test]
608 fn test_pre_commit_fail_sh() {
609 let (_td, repo) = repo_init();
610
611 let hook = b"#!/bin/sh
612echo 'rejected'
613exit 1
614 ";
615
616 create_hook(&repo, HOOK_PRE_COMMIT, hook);
617 let res = hooks_pre_commit(&repo, None).unwrap();
618 assert!(!res.is_successful());
619 }
620
621 #[test]
622 fn test_env_containing_path() {
623 const PATH_EXPORT: &str = "export PATH";
624
625 let (_td, repo) = repo_init();
626
627 let hook = b"#!/bin/sh
628export
629exit 1
630 ";
631
632 create_hook(&repo, HOOK_PRE_COMMIT, hook);
633 let res = hooks_pre_commit(&repo, None).unwrap();
634
635 let HookResult::Run(response) = res else {
636 unreachable!()
637 };
638
639 assert!(
640 response
641 .stdout
642 .lines()
643 .any(|line| line.starts_with(PATH_EXPORT)),
644 "Could not find line starting with {PATH_EXPORT:?} in: {:?}",
645 response.stdout
646 );
647 }
648
649 #[test]
650 fn test_pre_commit_fail_hookspath() {
651 let (_td, repo) = repo_init();
652 let hooks = TempDir::new().unwrap();
653
654 let hook = b"#!/bin/sh
655echo 'rejected'
656exit 1
657 ";
658
659 create_hook_in_path(&hooks.path().join("pre-commit"), hook);
660
661 repo.config()
662 .unwrap()
663 .set_str(
664 "core.hooksPath",
665 hooks.path().as_os_str().to_str().unwrap(),
666 )
667 .unwrap();
668
669 let res = hooks_pre_commit(&repo, None).unwrap();
670
671 let HookResult::Run(response) = res else {
672 unreachable!()
673 };
674
675 assert_eq!(response.code, 1);
676 assert_eq!(&response.stdout, "rejected\n");
677 }
678
679 #[test]
680 fn test_pre_commit_fail_bare() {
681 let (_td, repo) = repo_init_bare();
682
683 let hook = b"#!/bin/sh
684echo 'rejected'
685exit 1
686 ";
687
688 create_hook(&repo, HOOK_PRE_COMMIT, hook);
689 let res = hooks_pre_commit(&repo, None).unwrap();
690 assert!(!res.is_successful());
691 }
692
693 #[test]
694 fn test_pre_commit_py() {
695 let (_td, repo) = repo_init();
696
697 #[cfg(not(windows))]
699 let hook = b"#!/usr/bin/env python
700import sys
701sys.exit(0)
702 ";
703 #[cfg(windows)]
704 let hook = b"#!/bin/env python.exe
705import sys
706sys.exit(0)
707 ";
708
709 create_hook(&repo, HOOK_PRE_COMMIT, hook);
710 let res = hooks_pre_commit(&repo, None).unwrap();
711 assert!(res.is_successful(), "{res:?}");
712 }
713
714 #[test]
715 fn test_pre_commit_fail_py() {
716 let (_td, repo) = repo_init();
717
718 #[cfg(not(windows))]
720 let hook = b"#!/usr/bin/env python
721import sys
722sys.exit(1)
723 ";
724 #[cfg(windows)]
725 let hook = b"#!/bin/env python.exe
726import sys
727sys.exit(1)
728 ";
729
730 create_hook(&repo, HOOK_PRE_COMMIT, hook);
731 let res = hooks_pre_commit(&repo, None).unwrap();
732 assert!(!res.is_successful());
733 }
734
735 #[test]
736 fn test_hooks_commit_msg_reject() {
737 let (_td, repo) = repo_init();
738
739 let hook = b"#!/bin/sh
740 echo 'msg' > \"$1\"
741 echo 'rejected'
742 exit 1
743 ";
744
745 create_hook(&repo, HOOK_COMMIT_MSG, hook);
746
747 let mut msg = String::from("test");
748 let res = hooks_commit_msg(&repo, None, &mut msg).unwrap();
749
750 let HookResult::Run(response) = res else {
751 unreachable!()
752 };
753
754 assert_eq!(response.code, 1);
755 assert_eq!(&response.stdout, "rejected\n");
756
757 assert_eq!(msg, String::from("msg\n"));
758 }
759
760 #[test]
761 fn test_commit_msg_no_block_but_alter() {
762 let (_td, repo) = repo_init();
763
764 let hook = b"#!/bin/sh
765echo 'msg' > \"$1\"
766exit 0
767 ";
768
769 create_hook(&repo, HOOK_COMMIT_MSG, hook);
770
771 let mut msg = String::from("test");
772 let res = hooks_commit_msg(&repo, None, &mut msg).unwrap();
773
774 assert!(res.is_successful());
775 assert_eq!(msg, String::from("msg\n"));
776 }
777
778 #[test]
779 fn test_hook_pwd_in_bare_without_workdir() {
780 let (_td, repo) = repo_init_bare();
781 let git_root = repo.path().to_path_buf();
782
783 let hook =
784 HookPaths::new(&repo, None, HOOK_POST_COMMIT).unwrap();
785
786 assert_eq!(hook.pwd, git_root);
787 }
788
789 #[test]
790 fn test_hook_pwd() {
791 let (_td, repo) = repo_init();
792 let git_root = repo.path().to_path_buf();
793
794 let hook =
795 HookPaths::new(&repo, None, HOOK_POST_COMMIT).unwrap();
796
797 assert_eq!(hook.pwd, git_root.parent().unwrap());
798 }
799
800 #[test]
801 fn test_hooks_prep_commit_msg_success() {
802 let (_td, repo) = repo_init();
803
804 let hook = b"#!/bin/sh
805echo \"msg:$2\" > \"$1\"
806exit 0
807 ";
808
809 create_hook(&repo, HOOK_PREPARE_COMMIT_MSG, hook);
810
811 let mut msg = String::from("test");
812 let res = hooks_prepare_commit_msg(
813 &repo,
814 None,
815 PrepareCommitMsgSource::Message,
816 &mut msg,
817 )
818 .unwrap();
819
820 assert!(res.is_successful());
821 assert_eq!(msg, String::from("msg:message\n"));
822 }
823
824 #[test]
825 fn test_hooks_prep_commit_msg_reject() {
826 let (_td, repo) = repo_init();
827
828 let hook = b"#!/bin/sh
829echo \"$2,$3\" > \"$1\"
830echo 'rejected'
831exit 2
832 ";
833
834 create_hook(&repo, HOOK_PREPARE_COMMIT_MSG, hook);
835
836 let mut msg = String::from("test");
837 let res = hooks_prepare_commit_msg(
838 &repo,
839 None,
840 PrepareCommitMsgSource::Commit(git2::Oid::zero()),
841 &mut msg,
842 )
843 .unwrap();
844
845 let HookResult::Run(response) = res else {
846 unreachable!()
847 };
848
849 assert_eq!(response.code, 2);
850 assert_eq!(&response.stdout, "rejected\n");
851
852 assert_eq!(
853 msg,
854 String::from(
855 "commit,0000000000000000000000000000000000000000\n"
856 )
857 );
858 }
859
860 #[test]
861 fn test_pre_push_sh() {
862 let (_td, repo) = repo_init();
863
864 let hook = b"#!/bin/sh
865exit 0
866 ";
867
868 create_hook(&repo, HOOK_PRE_PUSH, hook);
869
870 let branch = head_branch(&repo);
871 let updates = [branch_update(
872 &repo,
873 Some("origin"),
874 &branch,
875 None,
876 false,
877 )];
878
879 let res = hooks_pre_push(
880 &repo,
881 None,
882 Some("origin"),
883 "https://example.com/repo.git",
884 &updates,
885 )
886 .unwrap();
887
888 assert!(res.is_successful());
889 }
890
891 #[test]
892 fn test_pre_push_fail_sh() {
893 let (_td, repo) = repo_init();
894
895 let hook = b"#!/bin/sh
896echo 'failed'
897exit 3
898 ";
899 create_hook(&repo, HOOK_PRE_PUSH, hook);
900
901 let branch = head_branch(&repo);
902 let updates = [branch_update(
903 &repo,
904 Some("origin"),
905 &branch,
906 None,
907 false,
908 )];
909
910 let res = hooks_pre_push(
911 &repo,
912 None,
913 Some("origin"),
914 "https://example.com/repo.git",
915 &updates,
916 )
917 .unwrap();
918 let HookResult::Run(response) = res else {
919 unreachable!()
920 };
921 assert_eq!(response.code, 3);
922 assert_eq!(&response.stdout, "failed\n");
923 }
924
925 #[test]
926 fn test_pre_push_no_remote_name() {
927 let (_td, repo) = repo_init();
928
929 let hook = b"#!/bin/sh
930# Verify that when remote is None, URL is passed for both arguments
931echo \"arg1=$1 arg2=$2\"
932exit 0
933 ";
934
935 create_hook(&repo, HOOK_PRE_PUSH, hook);
936
937 let branch = head_branch(&repo);
938 let updates =
939 [branch_update(&repo, None, &branch, None, false)];
940
941 let res = hooks_pre_push(
942 &repo,
943 None,
944 None,
945 "https://example.com/repo.git",
946 &updates,
947 )
948 .unwrap();
949
950 let HookResult::Run(response) = res else {
951 panic!("Expected Run result, got: {res:?}");
952 };
953
954 assert!(response.is_successful());
955 assert_eq!(
957 response.stdout,
958 "arg1=https://example.com/repo.git arg2=https://example.com/repo.git\n"
959 );
960 }
961
962 #[test]
963 fn test_pre_push_with_arguments() {
964 let (_td, repo) = repo_init();
965
966 let hook = b"#!/bin/sh
967echo \"remote_name=$1\"
968echo \"remote_url=$2\"
969exit 0
970 ";
971
972 create_hook(&repo, HOOK_PRE_PUSH, hook);
973
974 let branch = head_branch(&repo);
975 let updates = [branch_update(
976 &repo,
977 Some("origin"),
978 &branch,
979 None,
980 false,
981 )];
982
983 let res = hooks_pre_push(
984 &repo,
985 None,
986 Some("origin"),
987 "https://example.com/repo.git",
988 &updates,
989 )
990 .unwrap();
991
992 let HookResult::Run(response) = res else {
993 unreachable!("Expected Run result, got: {res:?}")
994 };
995
996 assert!(response.is_successful());
997 assert_eq!(
998 response.stdout,
999 "remote_name=origin\nremote_url=https://example.com/repo.git\n"
1000 );
1001 }
1002
1003 #[test]
1004 fn test_pre_push_multiple_updates() {
1005 let (_td, repo) = repo_init();
1006
1007 let hook = b"#!/bin/sh
1008cat
1009exit 0
1010 ";
1011
1012 create_hook(&repo, HOOK_PRE_PUSH, hook);
1013
1014 let branch = head_branch(&repo);
1015 let branch_update = branch_update(
1016 &repo,
1017 Some("origin"),
1018 &branch,
1019 None,
1020 false,
1021 );
1022
1023 let head_commit =
1025 repo.head().unwrap().peel_to_commit().unwrap();
1026 repo.tag_lightweight("v1", head_commit.as_object(), false)
1027 .unwrap();
1028 let tag_ref = repo.find_reference("refs/tags/v1").unwrap();
1029 let tag_oid = tag_ref.target().unwrap();
1030 let tag_update = PrePushRef::new(
1031 "refs/tags/v1",
1032 Some(tag_oid),
1033 "refs/tags/v1",
1034 None,
1035 );
1036
1037 let updates = [branch_update, tag_update];
1038 let expected_stdin = PrePushRef::to_stdin(&updates);
1039
1040 let res = hooks_pre_push(
1041 &repo,
1042 None,
1043 Some("origin"),
1044 "https://example.com/repo.git",
1045 &updates,
1046 )
1047 .unwrap();
1048
1049 let HookResult::Run(response) = res else {
1050 unreachable!("Expected Run result, got: {res:?}")
1051 };
1052
1053 assert!(
1054 response.is_successful(),
1055 "Hook should succeed: stdout {} stderr {}",
1056 response.stdout,
1057 response.stderr
1058 );
1059 assert_eq!(
1060 response.stdout, expected_stdin,
1061 "stdin should include all refspec lines"
1062 );
1063 }
1064
1065 #[test]
1066 fn test_pre_push_delete_ref_uses_zero_oid() {
1067 let (_td, repo) = repo_init();
1068
1069 let hook = b"#!/bin/sh
1070cat
1071exit 0
1072 ";
1073
1074 create_hook(&repo, HOOK_PRE_PUSH, hook);
1075
1076 let branch = head_branch(&repo);
1077 let updates = [branch_update(
1078 &repo,
1079 Some("origin"),
1080 &branch,
1081 None,
1082 true,
1083 )];
1084 let expected_stdin = PrePushRef::to_stdin(&updates);
1085
1086 let res = hooks_pre_push(
1087 &repo,
1088 None,
1089 Some("origin"),
1090 "https://example.com/repo.git",
1091 &updates,
1092 )
1093 .unwrap();
1094
1095 let HookResult::Run(response) = res else {
1096 unreachable!("Expected Run result, got: {res:?}")
1097 };
1098
1099 assert!(response.is_successful());
1100 assert_eq!(response.stdout, expected_stdin);
1101 }
1102
1103 #[test]
1104 fn test_pre_push_stdin() {
1105 let (_td, repo) = repo_init();
1106
1107 let hook = b"#!/bin/sh
1108cat
1109exit 0
1110 ";
1111
1112 create_hook(&repo, HOOK_PRE_PUSH, hook);
1113
1114 let branch = head_branch(&repo);
1115 let updates = [branch_update(
1116 &repo,
1117 Some("origin"),
1118 &branch,
1119 None,
1120 false,
1121 )];
1122 let expected_stdin = PrePushRef::to_stdin(&updates);
1123
1124 let res = hooks_pre_push(
1125 &repo,
1126 None,
1127 Some("origin"),
1128 "https://github.com/user/repo.git",
1129 &updates,
1130 )
1131 .unwrap();
1132
1133 let HookResult::Run(response) = res else {
1134 unreachable!("Expected Run result, got: {res:?}")
1135 };
1136
1137 assert!(response.is_successful());
1138 assert_eq!(response.stdout, expected_stdin);
1139 }
1140
1141 #[test]
1142 fn test_pre_push_uses_push_target_remote_not_upstream() {
1143 let (_td, repo) = repo_init();
1144
1145 let head = repo.head().unwrap();
1147 let local_commit = head.target().unwrap();
1148
1149 repo.reference(
1159 "refs/remotes/origin/master",
1160 local_commit,
1161 true,
1162 "create origin/master",
1163 )
1164 .unwrap();
1165
1166 let sig = repo.signature().unwrap();
1168 let tree_id = {
1169 let mut index = repo.index().unwrap();
1170 index.write_tree().unwrap()
1171 };
1172 let tree = repo.find_tree(tree_id).unwrap();
1173 let old_commit = repo
1174 .commit(None, &sig, &sig, "old backup commit", &tree, &[])
1175 .unwrap();
1176
1177 repo.reference(
1178 "refs/remotes/backup/master",
1179 old_commit,
1180 true,
1181 "create backup/master at old commit",
1182 )
1183 .unwrap();
1184
1185 {
1187 let mut config = repo.config().unwrap();
1188 config.set_str("branch.master.remote", "origin").unwrap();
1189 config
1190 .set_str("branch.master.merge", "refs/heads/master")
1191 .unwrap();
1192 }
1193
1194 let hook = b"#!/bin/sh
1195cat
1196exit 0
1197";
1198
1199 create_hook(&repo, HOOK_PRE_PUSH, hook);
1200
1201 let branch = head_branch(&repo);
1202 let updates = [branch_update(
1203 &repo,
1204 Some("backup"),
1205 &branch,
1206 None,
1207 false,
1208 )];
1209 let expected_stdin = PrePushRef::to_stdin(&updates);
1210
1211 let res = hooks_pre_push(
1212 &repo,
1213 None,
1214 Some("backup"),
1215 "https://github.com/user/backup-repo.git",
1216 &updates,
1217 )
1218 .unwrap();
1219
1220 let HookResult::Run(response) = res else {
1221 panic!("Expected Run result, got: {res:?}")
1222 };
1223
1224 assert!(response.is_successful());
1225 assert_eq!(response.stdout, expected_stdin);
1226 }
1227}