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