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