1use std::{
2 io::{BufRead, BufReader, BufWriter, Write},
3 path::Path,
4 process::Stdio,
5 thread,
6 time::Duration,
7};
8
9use defer::defer;
10use log::{debug, warn};
11use unindent::unindent;
12
13use anyhow::{anyhow, bail, Context, Result};
14use backoff::{ExponentialBackoff, ExponentialBackoffBuilder};
15use itertools::Itertools;
16
17use chrono::prelude::*;
18use rand::{thread_rng, Rng};
19
20use crate::config;
21
22use super::git_definitions::{
23 GIT_ORIGIN, GIT_PERF_REMOTE, REFS_NOTES_ADD_TARGET_PREFIX, REFS_NOTES_BRANCH,
24 REFS_NOTES_MERGE_BRANCH_PREFIX, REFS_NOTES_READ_PREFIX, REFS_NOTES_REWRITE_TARGET_PREFIX,
25 REFS_NOTES_WRITE_SYMBOLIC_REF, REFS_NOTES_WRITE_TARGET_PREFIX,
26};
27use super::git_lowlevel::{
28 capture_git_output, get_git_perf_remote, git_rev_parse, git_rev_parse_symbolic_ref,
29 git_update_ref, internal_get_head_revision, is_shallow_repo, map_git_error,
30 set_git_perf_remote, spawn_git_command,
31};
32use super::git_types::GitError;
33use super::git_types::Reference;
34
35pub use super::git_lowlevel::get_head_revision;
36
37pub use super::git_lowlevel::check_git_version;
38
39fn map_git_error_for_backoff(e: GitError) -> ::backoff::Error<GitError> {
42 match e {
43 GitError::RefFailedToPush { .. }
44 | GitError::RefFailedToLock { .. }
45 | GitError::RefConcurrentModification { .. }
46 | GitError::BadObject { .. } => ::backoff::Error::transient(e),
47 GitError::ExecError { .. }
48 | GitError::IoError(..)
49 | GitError::ShallowRepository
50 | GitError::MissingHead { .. }
51 | GitError::NoRemoteMeasurements { .. }
52 | GitError::NoUpstream { .. }
53 | GitError::MissingMeasurements => ::backoff::Error::permanent(e),
54 }
55}
56
57fn default_backoff() -> ExponentialBackoff {
59 let max_elapsed = config::backoff_max_elapsed_seconds();
60 ExponentialBackoffBuilder::default()
61 .with_max_elapsed_time(Some(Duration::from_secs(max_elapsed)))
62 .build()
63}
64
65pub fn add_note_line_to_head(line: &str) -> Result<()> {
66 let op = || -> Result<(), ::backoff::Error<GitError>> {
67 raw_add_note_line_to_head(line).map_err(map_git_error_for_backoff)
68 };
69
70 let backoff = default_backoff();
71
72 ::backoff::retry(backoff, op).map_err(|e| match e {
73 ::backoff::Error::Permanent(err) => {
74 anyhow!(err).context("Permanent failure while adding note line to head")
75 }
76 ::backoff::Error::Transient { err, .. } => {
77 anyhow!(err).context("Timed out while adding note line to head")
78 }
79 })?;
80
81 Ok(())
82}
83
84fn raw_add_note_line_to_head(line: &str) -> Result<(), GitError> {
85 ensure_symbolic_write_ref_exists()?;
86
87 let current_note_head =
91 git_rev_parse(REFS_NOTES_WRITE_SYMBOLIC_REF).unwrap_or(EMPTY_OID.to_string());
92 let current_symbolic_ref_target = git_rev_parse_symbolic_ref(REFS_NOTES_WRITE_SYMBOLIC_REF)
93 .expect("Missing symbolic-ref for target");
94 let temp_target = create_temp_add_head(¤t_note_head)?;
95
96 defer!(remove_reference(&temp_target)
97 .expect("Deleting our own temp ref for adding should never fail"));
98
99 if internal_get_head_revision().is_err() {
101 return Err(GitError::MissingHead {
102 reference: "HEAD".to_string(),
103 });
104 }
105
106 capture_git_output(
107 &[
108 "notes",
109 "--ref",
110 &temp_target,
111 "append",
112 "-m",
115 line,
116 ],
117 &None,
118 )?;
119
120 git_update_ref(unindent(
122 format!(
123 r#"
124 start
125 symref-verify {REFS_NOTES_WRITE_SYMBOLIC_REF} {current_symbolic_ref_target}
126 update {current_symbolic_ref_target} {temp_target} {current_note_head}
127 commit
128 "#
129 )
130 .as_str(),
131 ))?;
132
133 Ok(())
134}
135
136fn ensure_remote_exists() -> Result<(), GitError> {
137 if get_git_perf_remote(GIT_PERF_REMOTE).is_some() {
138 return Ok(());
139 }
140
141 if let Some(x) = get_git_perf_remote(GIT_ORIGIN) {
142 return set_git_perf_remote(GIT_PERF_REMOTE, &x);
143 }
144
145 Err(GitError::NoUpstream {})
146}
147
148fn create_temp_ref_name(prefix: &str) -> String {
150 let suffix = random_suffix();
151 format!("{prefix}{suffix}")
152}
153
154fn ensure_symbolic_write_ref_exists() -> Result<(), GitError> {
155 if git_rev_parse(REFS_NOTES_WRITE_SYMBOLIC_REF).is_err() {
156 let target = create_temp_ref_name(REFS_NOTES_WRITE_TARGET_PREFIX);
157
158 git_update_ref(unindent(
159 format!(
160 r#"
161 start
162 symref-create {REFS_NOTES_WRITE_SYMBOLIC_REF} {target}
163 commit
164 "#
165 )
166 .as_str(),
167 ))
168 .or_else(|err| {
169 if let GitError::RefFailedToLock { .. } = err {
170 Ok(())
171 } else {
172 Err(err)
173 }
174 })?;
175 }
176 Ok(())
177}
178
179fn random_suffix() -> String {
180 let suffix: u32 = thread_rng().gen();
181 format!("{suffix:08x}")
182}
183
184fn fetch(work_dir: Option<&Path>) -> Result<(), GitError> {
185 ensure_remote_exists()?;
186
187 let ref_before = git_rev_parse(REFS_NOTES_BRANCH).ok();
188 capture_git_output(
190 &[
191 "fetch",
192 "--atomic",
193 "--no-write-fetch-head",
194 GIT_PERF_REMOTE,
195 format!("+{REFS_NOTES_BRANCH}:{REFS_NOTES_BRANCH}").as_str(),
199 ],
200 &work_dir,
201 )
202 .map_err(map_git_error)?;
203
204 let ref_after = git_rev_parse(REFS_NOTES_BRANCH).ok();
205
206 if ref_before == ref_after {
207 println!("Already up to date");
208 }
209
210 Ok(())
211}
212
213fn reconcile_branch_with(target: &str, branch: &str) -> Result<(), GitError> {
214 _ = capture_git_output(
215 &[
216 "notes",
217 "--ref",
218 target,
219 "merge",
220 "-s",
221 "cat_sort_uniq",
222 branch,
223 ],
224 &None,
225 )?;
226 Ok(())
227}
228
229fn create_temp_ref(prefix: &str, current_head: &str) -> Result<String, GitError> {
230 let target = create_temp_ref_name(prefix);
231 if current_head != EMPTY_OID {
232 git_update_ref(unindent(
233 format!(
234 r#"
235 start
236 create {target} {current_head}
237 commit
238 "#
239 )
240 .as_str(),
241 ))?;
242 }
243 Ok(target)
244}
245
246fn create_temp_rewrite_head(current_notes_head: &str) -> Result<String, GitError> {
247 create_temp_ref(REFS_NOTES_REWRITE_TARGET_PREFIX, current_notes_head)
248}
249
250fn create_temp_add_head(current_notes_head: &str) -> Result<String, GitError> {
251 create_temp_ref(REFS_NOTES_ADD_TARGET_PREFIX, current_notes_head)
252}
253
254fn compact_head(target: &str) -> Result<(), GitError> {
255 let new_removal_head = git_rev_parse(format!("{target}^{{tree}}").as_str())?;
256
257 let compaction_head = capture_git_output(
259 &["commit-tree", "-m", "cutoff history", &new_removal_head],
260 &None,
261 )?
262 .stdout;
263
264 let compaction_head = compaction_head.trim();
265
266 git_update_ref(unindent(
267 format!(
268 r#"
269 start
270 update {target} {compaction_head}
271 commit
272 "#
273 )
274 .as_str(),
275 ))?;
276
277 Ok(())
278}
279
280fn retry_notify(err: GitError, dur: Duration) {
281 debug!("Error happened at {dur:?}: {err}");
282 warn!("Retrying...");
283}
284
285pub fn remove_measurements_from_commits(older_than: DateTime<Utc>) -> Result<()> {
286 let op = || -> Result<(), ::backoff::Error<GitError>> {
287 raw_remove_measurements_from_commits(older_than).map_err(map_git_error_for_backoff)
288 };
289
290 let backoff = default_backoff();
291
292 ::backoff::retry_notify(backoff, op, retry_notify).map_err(|e| match e {
293 ::backoff::Error::Permanent(err) => {
294 anyhow!(err).context("Permanent failure while adding note line to head")
295 }
296 ::backoff::Error::Transient { err, .. } => {
297 anyhow!(err).context("Timed out while adding note line to head")
298 }
299 })?;
300
301 Ok(())
302}
303
304fn raw_remove_measurements_from_commits(older_than: DateTime<Utc>) -> Result<(), GitError> {
305 fetch(None)?;
310
311 let current_notes_head = git_rev_parse(REFS_NOTES_BRANCH)?;
312
313 let target = create_temp_rewrite_head(¤t_notes_head)?;
314
315 remove_measurements_from_reference(&target, older_than)?;
316
317 compact_head(&target)?;
318
319 git_push_notes_ref(¤t_notes_head, &target, &None)?;
320
321 git_update_ref(unindent(
322 format!(
323 r#"
324 start
325 update {REFS_NOTES_BRANCH} {target}
326 commit
327 "#
328 )
329 .as_str(),
330 ))?;
331
332 remove_reference(&target)?;
334
335 Ok(())
336}
337
338fn remove_measurements_from_reference(
340 reference: &str,
341 older_than: DateTime<Utc>,
342) -> Result<(), GitError> {
343 let oldest_timestamp = older_than.timestamp();
344 let mut list_notes = spawn_git_command(&["notes", "--ref", reference, "list"], &None, None)?;
346 let notes_out = list_notes.stdout.take().unwrap();
347
348 let mut get_commit_dates = spawn_git_command(
349 &[
350 "log",
351 "--ignore-missing",
352 "--no-walk",
353 "--pretty=format:%H %ct",
354 "--stdin",
355 ],
356 &None,
357 Some(Stdio::piped()),
358 )?;
359 let dates_in = get_commit_dates.stdin.take().unwrap();
360 let dates_out = get_commit_dates.stdout.take().unwrap();
361
362 let mut remove_measurements = spawn_git_command(
363 &[
364 "notes",
365 "--ref",
366 reference,
367 "remove",
368 "--stdin",
369 "--ignore-missing",
370 ],
371 &None,
372 Some(Stdio::piped()),
373 )?;
374 let removal_in = remove_measurements.stdin.take().unwrap();
375 let removal_out = remove_measurements.stdout.take().unwrap();
376
377 let removal_handler = thread::spawn(move || {
378 let reader = BufReader::new(dates_out);
379 let mut writer = BufWriter::new(removal_in);
380 for line in reader.lines().map_while(Result::ok) {
381 if let Some((commit, timestamp)) = line.split_whitespace().take(2).collect_tuple() {
382 if let Ok(timestamp) = timestamp.parse::<i64>() {
383 if timestamp <= oldest_timestamp {
384 writeln!(writer, "{commit}").expect("Could not write to stream");
385 }
386 }
387 }
388 }
389 });
390
391 let debugging_handler = thread::spawn(move || {
392 let reader = BufReader::new(removal_out);
393 reader
394 .lines()
395 .map_while(Result::ok)
396 .for_each(|l| println!("{l}"))
397 });
398
399 {
400 let reader = BufReader::new(notes_out);
401 let mut writer = BufWriter::new(dates_in);
402
403 reader.lines().map_while(Result::ok).for_each(|line| {
404 if let Some(line) = line.split_whitespace().nth(1) {
405 writeln!(writer, "{line}").expect("Failed to write to pipe");
406 }
407 });
408 }
409
410 removal_handler.join().expect("Failed to join");
411 debugging_handler.join().expect("Failed to join");
412
413 list_notes.wait()?;
414 get_commit_dates.wait()?;
415 remove_measurements.wait()?;
416
417 Ok(())
418}
419
420fn new_symbolic_write_ref() -> Result<String, GitError> {
421 let target = create_temp_ref_name(REFS_NOTES_WRITE_TARGET_PREFIX);
422
423 git_update_ref(unindent(
424 format!(
425 r#"
426 start
427 symref-update {REFS_NOTES_WRITE_SYMBOLIC_REF} {target}
428 commit
429 "#
430 )
431 .as_str(),
432 ))?;
433 Ok(target)
434}
435
436const EMPTY_OID: &str = "0000000000000000000000000000000000000000";
437
438fn consolidate_write_branches_into(
439 current_upstream_oid: &str,
440 target: &str,
441 except_ref: Option<&str>,
442) -> Result<Vec<Reference>, GitError> {
443 git_update_ref(unindent(
446 format!(
447 r#"
448 start
449 verify {REFS_NOTES_BRANCH} {current_upstream_oid}
450 update {target} {current_upstream_oid} {EMPTY_OID}
451 commit
452 "#
453 )
454 .as_str(),
455 ))?;
456
457 let additional_args = vec![format!("{REFS_NOTES_WRITE_TARGET_PREFIX}*")];
462 let refs = get_refs(additional_args)?
463 .into_iter()
464 .filter(|r| r.refname != except_ref.unwrap_or_default())
465 .collect_vec();
466
467 for reference in &refs {
468 reconcile_branch_with(target, &reference.oid)?;
469 }
470
471 Ok(refs)
472}
473
474fn remove_reference(ref_name: &str) -> Result<(), GitError> {
475 git_update_ref(unindent(
476 format!(
477 r#"
478 start
479 delete {ref_name}
480 commit
481 "#
482 )
483 .as_str(),
484 ))
485}
486
487fn raw_push(work_dir: Option<&Path>) -> Result<(), GitError> {
488 ensure_remote_exists()?;
489 let new_write_ref = new_symbolic_write_ref()?;
497
498 let merge_ref = create_temp_ref_name(REFS_NOTES_MERGE_BRANCH_PREFIX);
499
500 defer!(remove_reference(&merge_ref).expect("Deleting our own branch should never fail"));
501
502 let current_upstream_oid = git_rev_parse(REFS_NOTES_BRANCH).unwrap_or(EMPTY_OID.to_string());
509 let refs =
510 consolidate_write_branches_into(¤t_upstream_oid, &merge_ref, Some(&new_write_ref))?;
511
512 if refs.is_empty() && current_upstream_oid == EMPTY_OID {
513 return Err(GitError::MissingMeasurements);
514 }
515
516 git_push_notes_ref(¤t_upstream_oid, &merge_ref, &work_dir)?;
517
518 fetch(None)?;
520
521 let mut commands = Vec::new();
523 commands.push(String::from("start"));
524 for Reference { refname, oid } in &refs {
525 commands.push(format!("delete {refname} {oid}"));
526 }
527 commands.push(String::from("commit"));
528 commands.push(String::new());
530 let commands = commands.join("\n");
531 git_update_ref(commands)?;
532
533 Ok(())
534}
535
536fn git_push_notes_ref(
537 expected_upstream: &str,
538 push_ref: &str,
539 working_dir: &Option<&Path>,
540) -> Result<(), GitError> {
541 let output = capture_git_output(
544 &[
545 "push",
546 "--porcelain",
547 format!("--force-with-lease={REFS_NOTES_BRANCH}:{expected_upstream}").as_str(),
548 GIT_PERF_REMOTE,
549 format!("{push_ref}:{REFS_NOTES_BRANCH}").as_str(),
550 ],
551 working_dir,
552 );
553
554 match output {
558 Ok(output) => {
559 print!("{}", &output.stdout);
560 Ok(())
561 }
562 Err(GitError::ExecError { command: _, output }) => {
563 let successful_push = output.stdout.lines().any(|l| {
564 l.contains(format!("{REFS_NOTES_BRANCH}:").as_str()) && !l.starts_with('!')
565 });
566 if successful_push {
567 Ok(())
568 } else {
569 Err(GitError::RefFailedToPush { output })
570 }
571 }
572 Err(e) => Err(e),
573 }?;
574
575 Ok(())
576}
577
578pub fn prune() -> Result<()> {
580 let op = || -> Result<(), ::backoff::Error<GitError>> {
581 raw_prune().map_err(map_git_error_for_backoff)
582 };
583
584 let backoff = default_backoff();
585
586 ::backoff::retry_notify(backoff, op, retry_notify).map_err(|e| match e {
587 ::backoff::Error::Permanent(err) => {
588 anyhow!(err).context("Permanent failure while pruning refs")
589 }
590 ::backoff::Error::Transient { err, .. } => anyhow!(err).context("Timed out pushing refs"),
591 })?;
592
593 Ok(())
594}
595
596fn raw_prune() -> Result<(), GitError> {
597 if is_shallow_repo()? {
598 return Err(GitError::ShallowRepository);
599 }
600
601 pull_internal(None)?;
605
606 let current_notes_head = git_rev_parse(REFS_NOTES_BRANCH)?;
608 let target = create_temp_rewrite_head(¤t_notes_head)?;
609
610 capture_git_output(&["notes", "--ref", &target, "prune"], &None)?;
612
613 compact_head(&target)?;
615
616 git_push_notes_ref(¤t_notes_head, &target, &None)?;
621 git_update_ref(unindent(
622 format!(
623 r#"
624 start
625 update {REFS_NOTES_BRANCH} {target}
626 commit
627 "#
628 )
629 .as_str(),
630 ))?;
631
632 remove_reference(&target)?;
634
635 Ok(())
636}
637
638fn get_refs(additional_args: Vec<String>) -> Result<Vec<Reference>, GitError> {
639 let mut args = vec!["for-each-ref", "--format=%(refname)%00%(objectname)"];
640 args.extend(additional_args.iter().map(|s| s.as_str()));
641
642 let output = capture_git_output(&args, &None)?;
643 Ok(output
644 .stdout
645 .lines()
646 .map(|s| {
647 let items = s.split('\0').take(2).collect_vec();
648 assert!(items.len() == 2);
649 Reference {
650 refname: items[0].to_string(),
651 oid: items[1].to_string(),
652 }
653 })
654 .collect_vec())
655}
656
657struct TempRef {
658 ref_name: String,
659}
660
661impl TempRef {
662 fn new(prefix: &str) -> Result<Self, GitError> {
663 Ok(TempRef {
664 ref_name: create_temp_ref(prefix, EMPTY_OID)?,
665 })
666 }
667}
668
669impl Drop for TempRef {
670 fn drop(&mut self) {
671 remove_reference(&self.ref_name)
672 .unwrap_or_else(|_| panic!("Failed to remove reference: {}", self.ref_name))
673 }
674}
675
676fn update_read_branch() -> Result<TempRef, GitError> {
677 let temp_ref = TempRef::new(REFS_NOTES_READ_PREFIX)?;
678 let current_upstream_oid = git_rev_parse(REFS_NOTES_BRANCH).unwrap_or(EMPTY_OID.to_string());
693
694 let _ = consolidate_write_branches_into(¤t_upstream_oid, &temp_ref.ref_name, None)?;
695
696 Ok(temp_ref)
697}
698
699pub fn walk_commits(num_commits: usize) -> Result<Vec<(String, Vec<String>)>> {
700 let temp_ref = update_read_branch()?;
702
703 let output = capture_git_output(
704 &[
705 "--no-pager",
706 "log",
707 "--no-color",
708 "--ignore-missing",
709 "-n",
710 num_commits.to_string().as_str(),
711 "--first-parent",
712 "--pretty=--,%H,%D%n%N",
713 "--decorate=full",
714 format!("--notes={}", temp_ref.ref_name).as_str(),
715 "HEAD",
716 ],
717 &None,
718 )
719 .context("Failed to retrieve commits")?;
720
721 let mut commits: Vec<(String, Vec<String>)> = Vec::new();
722 let mut detected_shallow = false;
723 let mut current_commit: Option<String> = None;
724
725 for l in output.stdout.lines() {
726 if l.starts_with("--") {
727 let info = l.split(',').collect_vec();
728 let commit_hash = info
729 .get(1)
730 .expect("No commit header found before measurement line in git log output");
731 detected_shallow |= info[2..].contains(&"grafted");
732 current_commit = Some(commit_hash.to_string());
733 commits.push((commit_hash.to_string(), Vec::new()));
734 } else if let Some(commit_hash) = current_commit.as_ref() {
735 if let Some(last) = commits.last_mut() {
736 last.1.push(l.to_string());
737 } else {
738 commits.push((commit_hash.to_string(), vec![l.to_string()]));
740 }
741 }
742 }
743
744 if detected_shallow && commits.len() < num_commits {
745 bail!("Refusing to continue as commit log depth was limited by shallow clone");
746 }
747
748 Ok(commits)
749}
750
751pub fn pull(work_dir: Option<&Path>) -> Result<()> {
752 pull_internal(work_dir)?;
753 Ok(())
754}
755
756fn pull_internal(work_dir: Option<&Path>) -> Result<(), GitError> {
757 fetch(work_dir).or_else(|err| match err {
758 GitError::RefConcurrentModification { .. } | GitError::RefFailedToLock { .. } => Ok(()),
763 _ => Err(err),
764 })?;
765
766 Ok(())
767}
768
769pub fn push(work_dir: Option<&Path>) -> Result<()> {
770 let op = || {
771 raw_push(work_dir)
772 .map_err(map_git_error_for_backoff)
773 .map_err(|e: ::backoff::Error<GitError>| match e {
774 ::backoff::Error::Transient { .. } => {
775 match pull_internal(work_dir).map_err(map_git_error_for_backoff) {
776 Ok(_) => e,
777 Err(e) => e,
778 }
779 }
780 ::backoff::Error::Permanent { .. } => e,
781 })
782 };
783
784 let backoff = default_backoff();
785
786 ::backoff::retry_notify(backoff, op, retry_notify).map_err(|e| match e {
787 ::backoff::Error::Permanent(err) => {
788 anyhow!(err).context("Permanent failure while pushing refs")
789 }
790 ::backoff::Error::Transient { err, .. } => anyhow!(err).context("Timed out pushing refs"),
791 })?;
792
793 Ok(())
794}
795
796#[cfg(test)]
797mod test {
798 use super::*;
799 use std::env::{self, set_current_dir};
800 use std::process;
801
802 use httptest::{
803 http::{header::AUTHORIZATION, Uri},
804 matchers::{self, request},
805 responders::status_code,
806 Expectation, Server,
807 };
808 use serial_test::serial;
809 use tempfile::{tempdir, TempDir};
810
811 fn run_git_command(args: &[&str], dir: &Path) {
812 assert!(process::Command::new("git")
813 .args(args)
814 .envs([
815 ("GIT_CONFIG_NOSYSTEM", "true"),
816 ("GIT_CONFIG_GLOBAL", "/dev/null"),
817 ("GIT_AUTHOR_NAME", "testuser"),
818 ("GIT_AUTHOR_EMAIL", "testuser@example.com"),
819 ("GIT_COMMITTER_NAME", "testuser"),
820 ("GIT_COMMITTER_EMAIL", "testuser@example.com"),
821 ])
822 .stdout(Stdio::null())
823 .stderr(Stdio::null())
824 .current_dir(dir)
825 .status()
826 .expect("Failed to spawn git command")
827 .success());
828 }
829
830 fn init_repo(dir: &Path) {
831 run_git_command(&["init", "--initial-branch", "master"], dir);
832 run_git_command(&["commit", "--allow-empty", "-m", "Initial commit"], dir);
833 }
834
835 fn dir_with_repo() -> TempDir {
836 let tempdir = tempdir().unwrap();
837 init_repo(tempdir.path());
838 tempdir
839 }
840
841 fn add_server_remote(origin_url: Uri, extra_header: &str, dir: &Path) {
842 let url = origin_url.to_string();
843
844 run_git_command(&["remote", "add", "origin", &url], dir);
845 run_git_command(
846 &[
847 "config",
848 "--add",
849 format!("http.{}.extraHeader", url).as_str(),
850 extra_header,
851 ],
852 dir,
853 );
854 }
855
856 fn hermetic_git_env() {
857 env::set_var("GIT_CONFIG_NOSYSTEM", "true");
858 env::set_var("GIT_CONFIG_GLOBAL", "/dev/null");
859 env::set_var("GIT_AUTHOR_NAME", "testuser");
860 env::set_var("GIT_AUTHOR_EMAIL", "testuser@example.com");
861 env::set_var("GIT_COMMITTER_NAME", "testuser");
862 env::set_var("GIT_COMMITTER_EMAIL", "testuser@example.com");
863 }
864
865 #[test]
866 #[serial]
867 fn test_customheader_pull() {
868 let tempdir = dir_with_repo();
869 set_current_dir(tempdir.path()).expect("Failed to change dir");
870
871 let test_server = Server::run();
872 add_server_remote(
873 test_server.url(""),
874 "AUTHORIZATION: sometoken",
875 tempdir.path(),
876 );
877
878 test_server.expect(
879 Expectation::matching(request::headers(matchers::contains((
880 AUTHORIZATION.as_str(),
881 "sometoken",
882 ))))
883 .times(1..)
884 .respond_with(status_code(200)),
885 );
886
887 hermetic_git_env();
891 pull(None).expect_err("We have no valid git http server setup -> should fail");
892 }
893
894 #[test]
895 #[serial]
896 fn test_customheader_push() {
897 let tempdir = dir_with_repo();
898 set_current_dir(tempdir.path()).expect("Failed to change dir");
899
900 let test_server = Server::run();
901 add_server_remote(
902 test_server.url(""),
903 "AUTHORIZATION: someothertoken",
904 tempdir.path(),
905 );
906
907 test_server.expect(
908 Expectation::matching(request::headers(matchers::contains((
909 AUTHORIZATION.as_str(),
910 "someothertoken",
911 ))))
912 .times(1..)
913 .respond_with(status_code(200)),
914 );
915
916 ensure_symbolic_write_ref_exists().expect("Failed to ensure symbolic write ref exists");
918 add_note_line_to_head("test note line").expect("Failed to add note line");
919
920 hermetic_git_env();
922
923 let error = push(None);
924 error
925 .as_ref()
926 .expect_err("We have no valid git http server setup -> should fail");
927 dbg!(&error);
928 }
929
930 #[test]
931 fn test_random_suffix() {
932 for _ in 1..1000 {
933 let first = random_suffix();
934 dbg!(&first);
935 let second = random_suffix();
936 dbg!(&second);
937
938 let all_hex = |s: &String| s.chars().all(|c| c.is_ascii_hexdigit());
939
940 assert_ne!(first, second);
941 assert_eq!(first.len(), 8);
942 assert_eq!(second.len(), 8);
943 assert!(all_hex(&first));
944 assert!(all_hex(&second));
945 }
946 }
947
948 #[test]
949 #[serial]
950 fn test_empty_or_never_pushed_remote_error_for_fetch() {
951 let tempdir = tempdir().unwrap();
952 init_repo(tempdir.path());
953 set_current_dir(tempdir.path()).expect("Failed to change dir");
954 let git_dir_url = format!("file://{}", tempdir.path().display());
956 run_git_command(&["remote", "add", "origin", &git_dir_url], tempdir.path());
957
958 std::env::set_var("GIT_TRACE", "true");
960
961 let result = super::fetch(Some(tempdir.path()));
963 match result {
964 Err(GitError::NoRemoteMeasurements { output }) => {
965 assert!(
966 output.stderr.contains(GIT_PERF_REMOTE),
967 "Expected output to contain {GIT_PERF_REMOTE}. Output: '{}'",
968 output.stderr
969 )
970 }
971 other => panic!("Expected NoRemoteMeasurements error, got: {:?}", other),
972 }
973 }
974
975 #[test]
976 #[serial]
977 fn test_empty_or_never_pushed_remote_error_for_push() {
978 let tempdir = tempdir().unwrap();
979 init_repo(tempdir.path());
980 set_current_dir(tempdir.path()).expect("Failed to change dir");
981
982 run_git_command(
983 &["remote", "add", "origin", "invalid invalid"],
984 tempdir.path(),
985 );
986
987 std::env::set_var("GIT_TRACE", "true");
989
990 add_note_line_to_head("test line, invalid measurement, does not matter").unwrap();
991
992 let result = super::raw_push(Some(tempdir.path()));
993 match result {
994 Err(GitError::RefFailedToPush { output }) => {
995 assert!(
996 output.stderr.contains(GIT_PERF_REMOTE),
997 "Expected output to contain {GIT_PERF_REMOTE}, got: {}",
998 output.stderr
999 )
1000 }
1001 other => panic!("Expected RefFailedToPush error, got: {:?}", other),
1002 }
1003 }
1004}