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::GitOutput;
34use super::git_types::Reference;
35
36pub use super::git_lowlevel::get_head_revision;
37
38pub use super::git_lowlevel::check_git_version;
39
40pub use super::git_lowlevel::get_repository_root;
41
42pub fn is_shallow_repository() -> Result<bool> {
44 super::git_lowlevel::is_shallow_repo()
45 .map_err(|e| anyhow!("Failed to check if repository is shallow: {}", e))
46}
47
48fn map_git_error_for_backoff(e: GitError) -> ::backoff::Error<GitError> {
49 match e {
50 GitError::RefFailedToPush { .. }
51 | GitError::RefFailedToLock { .. }
52 | GitError::RefConcurrentModification { .. }
53 | GitError::BadObject { .. } => ::backoff::Error::transient(e),
54 GitError::ExecError { .. }
55 | GitError::IoError(..)
56 | GitError::ShallowRepository
57 | GitError::MissingHead { .. }
58 | GitError::NoRemoteMeasurements { .. }
59 | GitError::NoUpstream { .. }
60 | GitError::MissingMeasurements => ::backoff::Error::permanent(e),
61 }
62}
63
64fn default_backoff() -> ExponentialBackoff {
66 let max_elapsed = config::backoff_max_elapsed_seconds();
67 ExponentialBackoffBuilder::default()
68 .with_max_elapsed_time(Some(Duration::from_secs(max_elapsed)))
69 .build()
70}
71
72pub fn add_note_line_to_head(line: &str) -> Result<()> {
73 let op = || -> Result<(), ::backoff::Error<GitError>> {
74 raw_add_note_line_to_head(line).map_err(map_git_error_for_backoff)
75 };
76
77 let backoff = default_backoff();
78
79 ::backoff::retry(backoff, op).map_err(|e| match e {
80 ::backoff::Error::Permanent(err) => {
81 anyhow!(err).context("Permanent failure while adding note line to head")
82 }
83 ::backoff::Error::Transient { err, .. } => {
84 anyhow!(err).context("Timed out while adding note line to head")
85 }
86 })?;
87
88 Ok(())
89}
90
91fn raw_add_note_line_to_head(line: &str) -> Result<(), GitError> {
92 ensure_symbolic_write_ref_exists()?;
93
94 let current_note_head =
98 git_rev_parse(REFS_NOTES_WRITE_SYMBOLIC_REF).unwrap_or(EMPTY_OID.to_string());
99 let current_symbolic_ref_target = git_rev_parse_symbolic_ref(REFS_NOTES_WRITE_SYMBOLIC_REF)
100 .expect("Missing symbolic-ref for target");
101 let temp_target = create_temp_add_head(¤t_note_head)?;
102
103 defer!(remove_reference(&temp_target)
104 .expect("Deleting our own temp ref for adding should never fail"));
105
106 if internal_get_head_revision().is_err() {
108 return Err(GitError::MissingHead {
109 reference: "HEAD".to_string(),
110 });
111 }
112
113 capture_git_output(
114 &["notes", "--ref", &temp_target, "append", "-m", line],
115 &None,
116 )?;
117
118 git_update_ref(unindent(
124 format!(
125 r#"
126 start
127 update {current_symbolic_ref_target} {temp_target} {current_note_head}
128 commit
129 "#
130 )
131 .as_str(),
132 ))?;
133
134 Ok(())
135}
136
137fn ensure_remote_exists() -> Result<(), GitError> {
138 if get_git_perf_remote(GIT_PERF_REMOTE).is_some() {
139 return Ok(());
140 }
141
142 if let Some(x) = get_git_perf_remote(GIT_ORIGIN) {
143 return set_git_perf_remote(GIT_PERF_REMOTE, &x);
144 }
145
146 Err(GitError::NoUpstream {})
147}
148
149fn create_temp_ref_name(prefix: &str) -> String {
151 let suffix = random_suffix();
152 format!("{prefix}{suffix}")
153}
154
155fn ensure_symbolic_write_ref_exists() -> Result<(), GitError> {
156 if git_rev_parse(REFS_NOTES_WRITE_SYMBOLIC_REF).is_err() {
157 let target = create_temp_ref_name(REFS_NOTES_WRITE_TARGET_PREFIX);
158
159 git_symbolic_ref_create_or_update(REFS_NOTES_WRITE_SYMBOLIC_REF, &target).or_else(
163 |err| {
164 if git_rev_parse(REFS_NOTES_WRITE_SYMBOLIC_REF).is_ok() {
166 Ok(())
167 } else {
168 Err(err)
169 }
170 },
171 )?;
172 }
173 Ok(())
174}
175
176fn random_suffix() -> String {
177 let suffix: u32 = thread_rng().gen();
178 format!("{suffix:08x}")
179}
180
181fn fetch(work_dir: Option<&Path>) -> Result<(), GitError> {
182 ensure_remote_exists()?;
183
184 let ref_before = git_rev_parse(REFS_NOTES_BRANCH).ok();
185 capture_git_output(
187 &[
188 "fetch",
189 "--atomic",
190 "--no-write-fetch-head",
191 GIT_PERF_REMOTE,
192 format!("+{REFS_NOTES_BRANCH}:{REFS_NOTES_BRANCH}").as_str(),
196 ],
197 &work_dir,
198 )
199 .map_err(map_git_error)?;
200
201 let ref_after = git_rev_parse(REFS_NOTES_BRANCH).ok();
202
203 if ref_before == ref_after {
204 println!("Already up to date");
205 }
206
207 Ok(())
208}
209
210fn reconcile_branch_with(target: &str, branch: &str) -> Result<(), GitError> {
211 _ = capture_git_output(
212 &[
213 "notes",
214 "--ref",
215 target,
216 "merge",
217 "-s",
218 "cat_sort_uniq",
219 branch,
220 ],
221 &None,
222 )?;
223 Ok(())
224}
225
226fn create_temp_ref(prefix: &str, current_head: &str) -> Result<String, GitError> {
227 let target = create_temp_ref_name(prefix);
228 if current_head != EMPTY_OID {
229 git_update_ref(unindent(
230 format!(
231 r#"
232 start
233 create {target} {current_head}
234 commit
235 "#
236 )
237 .as_str(),
238 ))?;
239 }
240 Ok(target)
241}
242
243fn create_temp_rewrite_head(current_notes_head: &str) -> Result<String, GitError> {
244 create_temp_ref(REFS_NOTES_REWRITE_TARGET_PREFIX, current_notes_head)
245}
246
247fn create_temp_add_head(current_notes_head: &str) -> Result<String, GitError> {
248 create_temp_ref(REFS_NOTES_ADD_TARGET_PREFIX, current_notes_head)
249}
250
251fn compact_head(target: &str) -> Result<(), GitError> {
252 let new_removal_head = git_rev_parse(format!("{target}^{{tree}}").as_str())?;
253
254 let compaction_head = capture_git_output(
256 &["commit-tree", "-m", "cutoff history", &new_removal_head],
257 &None,
258 )?
259 .stdout;
260
261 let compaction_head = compaction_head.trim();
262
263 git_update_ref(unindent(
264 format!(
265 r#"
266 start
267 update {target} {compaction_head}
268 commit
269 "#
270 )
271 .as_str(),
272 ))?;
273
274 Ok(())
275}
276
277fn retry_notify(err: GitError, dur: Duration) {
278 debug!("Error happened at {dur:?}: {err}");
279 warn!("Retrying...");
280}
281
282pub fn remove_measurements_from_commits(older_than: DateTime<Utc>, prune: bool) -> Result<()> {
283 let op = || -> Result<(), ::backoff::Error<GitError>> {
284 raw_remove_measurements_from_commits(older_than, prune).map_err(map_git_error_for_backoff)
285 };
286
287 let backoff = default_backoff();
288
289 ::backoff::retry_notify(backoff, op, retry_notify).map_err(|e| match e {
290 ::backoff::Error::Permanent(err) => {
291 anyhow!(err).context("Permanent failure while removing measurements")
292 }
293 ::backoff::Error::Transient { err, .. } => {
294 anyhow!(err).context("Timed out while removing measurements")
295 }
296 })?;
297
298 Ok(())
299}
300
301fn execute_notes_operation<F>(operation: F) -> Result<(), GitError>
302where
303 F: FnOnce(&str) -> Result<(), GitError>,
304{
305 pull_internal(None)?;
306
307 let current_notes_head = git_rev_parse(REFS_NOTES_BRANCH)?;
308 let target = create_temp_rewrite_head(¤t_notes_head)?;
309
310 operation(&target)?;
311
312 compact_head(&target)?;
313
314 git_push_notes_ref(¤t_notes_head, &target, &None, None)?;
315
316 git_update_ref(unindent(
317 format!(
318 r#"
319 start
320 update {REFS_NOTES_BRANCH} {target}
321 commit
322 "#
323 )
324 .as_str(),
325 ))?;
326
327 remove_reference(&target)?;
328
329 Ok(())
330}
331
332fn raw_remove_measurements_from_commits(
333 older_than: DateTime<Utc>,
334 prune: bool,
335) -> Result<(), GitError> {
336 if prune && is_shallow_repo()? {
338 return Err(GitError::ShallowRepository);
339 }
340
341 execute_notes_operation(|target| {
342 remove_measurements_from_reference(target, older_than)?;
344
345 if prune {
347 capture_git_output(&["notes", "--ref", target, "prune"], &None).map(|_| ())?;
348 }
349
350 Ok(())
351 })
352}
353
354fn remove_measurements_from_reference(
356 reference: &str,
357 older_than: DateTime<Utc>,
358) -> Result<(), GitError> {
359 let oldest_timestamp = older_than.timestamp();
360 let mut list_notes = spawn_git_command(&["notes", "--ref", reference, "list"], &None, None)?;
362 let notes_out = list_notes.stdout.take().unwrap();
363
364 let mut get_commit_dates = spawn_git_command(
365 &[
366 "log",
367 "--ignore-missing",
368 "--no-walk",
369 "--pretty=format:%H %ct",
370 "--stdin",
371 ],
372 &None,
373 Some(Stdio::piped()),
374 )?;
375 let dates_in = get_commit_dates.stdin.take().unwrap();
376 let dates_out = get_commit_dates.stdout.take().unwrap();
377
378 let mut remove_measurements = spawn_git_command(
379 &[
380 "notes",
381 "--ref",
382 reference,
383 "remove",
384 "--stdin",
385 "--ignore-missing",
386 ],
387 &None,
388 Some(Stdio::piped()),
389 )?;
390 let removal_in = remove_measurements.stdin.take().unwrap();
391 let removal_out = remove_measurements.stdout.take().unwrap();
392
393 let removal_handler = thread::spawn(move || {
394 let reader = BufReader::new(dates_out);
395 let mut writer = BufWriter::new(removal_in);
396 for line in reader.lines().map_while(Result::ok) {
397 if let Some((commit, timestamp)) = line.split_whitespace().take(2).collect_tuple() {
398 if let Ok(timestamp) = timestamp.parse::<i64>() {
399 if timestamp <= oldest_timestamp {
400 writeln!(writer, "{commit}").expect("Could not write to stream");
401 }
402 }
403 }
404 }
405 });
406
407 let debugging_handler = thread::spawn(move || {
408 let reader = BufReader::new(removal_out);
409 reader
410 .lines()
411 .map_while(Result::ok)
412 .for_each(|l| println!("{l}"))
413 });
414
415 {
416 let reader = BufReader::new(notes_out);
417 let mut writer = BufWriter::new(dates_in);
418
419 reader.lines().map_while(Result::ok).for_each(|line| {
420 if let Some(line) = line.split_whitespace().nth(1) {
421 writeln!(writer, "{line}").expect("Failed to write to pipe");
422 }
423 });
424 }
425
426 removal_handler.join().expect("Failed to join");
427 debugging_handler.join().expect("Failed to join");
428
429 list_notes.wait()?;
430 get_commit_dates.wait()?;
431 remove_measurements.wait()?;
432
433 Ok(())
434}
435
436fn new_symbolic_write_ref() -> Result<String, GitError> {
437 let target = create_temp_ref_name(REFS_NOTES_WRITE_TARGET_PREFIX);
438
439 git_symbolic_ref_create_or_update(REFS_NOTES_WRITE_SYMBOLIC_REF, &target)?;
443 Ok(target)
444}
445
446const EMPTY_OID: &str = "0000000000000000000000000000000000000000";
447
448fn consolidate_write_branches_into(
449 current_upstream_oid: &str,
450 target: &str,
451 except_ref: Option<&str>,
452) -> Result<Vec<Reference>, GitError> {
453 git_update_ref(unindent(
456 format!(
457 r#"
458 start
459 verify {REFS_NOTES_BRANCH} {current_upstream_oid}
460 update {target} {current_upstream_oid} {EMPTY_OID}
461 commit
462 "#
463 )
464 .as_str(),
465 ))?;
466
467 let additional_args = vec![format!("{REFS_NOTES_WRITE_TARGET_PREFIX}*")];
472 let refs = get_refs(additional_args)?
473 .into_iter()
474 .filter(|r| r.refname != except_ref.unwrap_or_default())
475 .collect_vec();
476
477 for reference in &refs {
478 reconcile_branch_with(target, &reference.oid)?;
479 }
480
481 Ok(refs)
482}
483
484fn remove_reference(ref_name: &str) -> Result<(), GitError> {
485 git_update_ref(unindent(
486 format!(
487 r#"
488 start
489 delete {ref_name}
490 commit
491 "#
492 )
493 .as_str(),
494 ))
495}
496
497fn raw_push(work_dir: Option<&Path>, remote: Option<&str>) -> Result<(), GitError> {
498 ensure_remote_exists()?;
499 let new_write_ref = new_symbolic_write_ref()?;
507
508 let merge_ref = create_temp_ref_name(REFS_NOTES_MERGE_BRANCH_PREFIX);
509
510 defer!(remove_reference(&merge_ref).expect("Deleting our own branch should never fail"));
511
512 let current_upstream_oid = git_rev_parse(REFS_NOTES_BRANCH).unwrap_or(EMPTY_OID.to_string());
519 let refs =
520 consolidate_write_branches_into(¤t_upstream_oid, &merge_ref, Some(&new_write_ref))?;
521
522 if refs.is_empty() && current_upstream_oid == EMPTY_OID {
523 return Err(GitError::MissingMeasurements);
524 }
525
526 git_push_notes_ref(¤t_upstream_oid, &merge_ref, &work_dir, remote)?;
527
528 fetch(None)?;
530
531 let mut commands = Vec::new();
533 commands.push(String::from("start"));
534 for Reference { refname, oid } in &refs {
535 commands.push(format!("delete {refname} {oid}"));
536 }
537 commands.push(String::from("commit"));
538 commands.push(String::new());
540 let commands = commands.join("\n");
541 git_update_ref(commands)?;
542
543 Ok(())
544}
545
546fn git_push_notes_ref(
547 expected_upstream: &str,
548 push_ref: &str,
549 working_dir: &Option<&Path>,
550 remote: Option<&str>,
551) -> Result<(), GitError> {
552 let remote_name = remote.unwrap_or(GIT_PERF_REMOTE);
555 let output = capture_git_output(
556 &[
557 "push",
558 "--porcelain",
559 format!("--force-with-lease={REFS_NOTES_BRANCH}:{expected_upstream}").as_str(),
560 remote_name,
561 format!("{push_ref}:{REFS_NOTES_BRANCH}").as_str(),
562 ],
563 working_dir,
564 );
565
566 match output {
570 Ok(output) => {
571 print!("{}", &output.stdout);
572 Ok(())
573 }
574 Err(GitError::ExecError { command: _, output }) => {
575 let successful_push = output.stdout.lines().any(|l| {
576 l.contains(format!("{REFS_NOTES_BRANCH}:").as_str()) && !l.starts_with('!')
577 });
578 if successful_push {
579 Ok(())
580 } else {
581 Err(GitError::RefFailedToPush { output })
582 }
583 }
584 Err(e) => Err(e),
585 }?;
586
587 Ok(())
588}
589
590pub fn prune() -> Result<()> {
591 let op = || -> Result<(), ::backoff::Error<GitError>> {
592 raw_prune().map_err(map_git_error_for_backoff)
593 };
594
595 let backoff = default_backoff();
596
597 ::backoff::retry_notify(backoff, op, retry_notify).map_err(|e| match e {
598 ::backoff::Error::Permanent(err) => {
599 anyhow!(err).context("Permanent failure while pruning refs")
600 }
601 ::backoff::Error::Transient { err, .. } => anyhow!(err).context("Timed out pushing refs"),
602 })?;
603
604 Ok(())
605}
606
607fn raw_prune() -> Result<(), GitError> {
608 if is_shallow_repo()? {
609 return Err(GitError::ShallowRepository);
610 }
611
612 execute_notes_operation(|target| {
613 capture_git_output(&["notes", "--ref", target, "prune"], &None).map(|_| ())
614 })
615}
616
617pub fn list_commits_with_measurements() -> Result<Vec<String>> {
622 let temp_ref = update_read_branch()?;
624
625 let mut list_notes =
628 spawn_git_command(&["notes", "--ref", &temp_ref.ref_name, "list"], &None, None)?;
629
630 let stdout = list_notes
631 .stdout
632 .take()
633 .ok_or_else(|| anyhow!("Failed to capture stdout from git notes list"))?;
634
635 let commits: Vec<String> = BufReader::new(stdout)
639 .lines()
640 .filter_map(|line_result| {
641 line_result
642 .ok()
643 .and_then(|line| line.split_whitespace().nth(1).map(|s| s.to_string()))
644 })
645 .collect();
646
647 Ok(commits)
648}
649
650pub struct ReadBranchGuard {
653 temp_ref: TempRef,
654}
655
656impl ReadBranchGuard {
657 pub fn ref_name(&self) -> &str {
659 &self.temp_ref.ref_name
660 }
661}
662
663pub fn create_consolidated_read_branch() -> Result<ReadBranchGuard> {
667 let temp_ref = update_read_branch()?;
668 Ok(ReadBranchGuard { temp_ref })
669}
670
671fn get_refs(additional_args: Vec<String>) -> Result<Vec<Reference>, GitError> {
672 let mut args = vec!["for-each-ref", "--format=%(refname)%00%(objectname)"];
673 args.extend(additional_args.iter().map(|s| s.as_str()));
674
675 let output = capture_git_output(&args, &None)?;
676 let refs: Result<Vec<Reference>, _> = output
677 .stdout
678 .lines()
679 .filter(|s| !s.is_empty())
680 .map(|s| {
681 let items = s.split('\0').take(2).collect_vec();
682 if items.len() != 2 {
683 return Err(GitError::ExecError {
684 command: format!("git {}", args.join(" ")),
685 output: GitOutput {
686 stdout: format!("Unexpected git for-each-ref output format: {}", s),
687 stderr: String::new(),
688 },
689 });
690 }
691 Ok(Reference {
692 refname: items[0].to_string(),
693 oid: items[1].to_string(),
694 })
695 })
696 .collect();
697 refs
698}
699
700struct TempRef {
701 ref_name: String,
702}
703
704impl TempRef {
705 fn new(prefix: &str) -> Result<Self, GitError> {
706 Ok(TempRef {
707 ref_name: create_temp_ref(prefix, EMPTY_OID)?,
708 })
709 }
710}
711
712impl Drop for TempRef {
713 fn drop(&mut self) {
714 remove_reference(&self.ref_name)
715 .unwrap_or_else(|_| panic!("Failed to remove reference: {}", self.ref_name))
716 }
717}
718
719fn update_read_branch() -> Result<TempRef> {
720 let temp_ref = TempRef::new(REFS_NOTES_READ_PREFIX)
721 .map_err(|e| anyhow!("Failed to create temporary ref: {:?}", e))?;
722 let current_upstream_oid = git_rev_parse(REFS_NOTES_BRANCH).unwrap_or(EMPTY_OID.to_string());
726
727 consolidate_write_branches_into(¤t_upstream_oid, &temp_ref.ref_name, None)
728 .map_err(|e| anyhow!("Failed to consolidate write branches: {:?}", e))?;
729
730 Ok(temp_ref)
731}
732
733pub fn walk_commits(num_commits: usize) -> Result<Vec<(String, Vec<String>)>> {
734 let temp_ref = update_read_branch()?;
736
737 let output = capture_git_output(
738 &[
739 "--no-pager",
740 "log",
741 "--no-color",
742 "--ignore-missing",
743 "-n",
744 num_commits.to_string().as_str(),
745 "--first-parent",
746 "--pretty=--,%H,%D%n%N",
747 "--decorate=full",
748 format!("--notes={}", temp_ref.ref_name).as_str(),
749 "HEAD",
750 ],
751 &None,
752 )
753 .context("Failed to retrieve commits")?;
754
755 let mut commits: Vec<(String, Vec<String>)> = Vec::new();
756 let mut detected_shallow = false;
757 let mut current_commit: Option<String> = None;
758
759 for l in output.stdout.lines() {
760 if l.starts_with("--") {
761 let info = l.split(',').collect_vec();
762 let commit_hash = info
763 .get(1)
764 .expect("No commit header found before measurement line in git log output");
765 detected_shallow |= info[2..].contains(&"grafted");
766 current_commit = Some(commit_hash.to_string());
767 commits.push((commit_hash.to_string(), Vec::new()));
768 } else if let Some(commit_hash) = current_commit.as_ref() {
769 if let Some(last) = commits.last_mut() {
770 last.1.push(l.to_string());
771 } else {
772 commits.push((commit_hash.to_string(), vec![l.to_string()]));
774 }
775 }
776 }
777
778 if detected_shallow && commits.len() < num_commits {
779 bail!("Refusing to continue as commit log depth was limited by shallow clone");
780 }
781
782 Ok(commits)
783}
784
785pub fn pull(work_dir: Option<&Path>) -> Result<()> {
786 pull_internal(work_dir)?;
787 Ok(())
788}
789
790fn pull_internal(work_dir: Option<&Path>) -> Result<(), GitError> {
791 fetch(work_dir)?;
792 Ok(())
793}
794
795pub fn push(work_dir: Option<&Path>, remote: Option<&str>) -> Result<()> {
796 let op = || {
797 raw_push(work_dir, remote)
798 .map_err(map_git_error_for_backoff)
799 .map_err(|e: ::backoff::Error<GitError>| match e {
800 ::backoff::Error::Transient { .. } => {
801 let pull_result = pull_internal(work_dir).map_err(map_git_error_for_backoff);
803
804 let pull_succeeded = pull_result.is_ok()
808 || matches!(
809 pull_result,
810 Err(::backoff::Error::Permanent(
811 GitError::RefConcurrentModification { .. }
812 | GitError::RefFailedToLock { .. }
813 ))
814 );
815
816 if pull_succeeded {
817 e
820 } else {
821 pull_result.unwrap_err()
823 }
824 }
825 ::backoff::Error::Permanent { .. } => e,
826 })
827 };
828
829 let backoff = default_backoff();
830
831 ::backoff::retry_notify(backoff, op, retry_notify).map_err(|e| match e {
832 ::backoff::Error::Permanent(err) => {
833 anyhow!(err).context("Permanent failure while pushing refs")
834 }
835 ::backoff::Error::Transient { err, .. } => anyhow!(err).context("Timed out pushing refs"),
836 })?;
837
838 Ok(())
839}
840
841#[cfg(test)]
842mod test {
843 use super::*;
844 use crate::test_helpers::{dir_with_repo, hermetic_git_env, init_repo, run_git_command};
845 use std::env::set_current_dir;
846 use std::process::Command;
847 use tempfile::tempdir;
848
849 use httptest::{
850 http::{header::AUTHORIZATION, Uri},
851 matchers::{self, request},
852 responders::status_code,
853 Expectation, Server,
854 };
855
856 fn add_server_remote(origin_url: Uri, extra_header: &str, dir: &Path) {
857 let url = origin_url.to_string();
858
859 run_git_command(&["remote", "add", "origin", &url], dir);
860 run_git_command(
861 &[
862 "config",
863 "--add",
864 format!("http.{}.extraHeader", url).as_str(),
865 extra_header,
866 ],
867 dir,
868 );
869 }
870
871 #[test]
872 fn test_customheader_pull() {
873 let tempdir = dir_with_repo();
874 set_current_dir(tempdir.path()).expect("Failed to change dir");
875
876 let mut test_server = Server::run();
877 add_server_remote(
878 test_server.url(""),
879 "AUTHORIZATION: sometoken",
880 tempdir.path(),
881 );
882
883 test_server.expect(
884 Expectation::matching(request::headers(matchers::contains((
885 AUTHORIZATION.as_str(),
886 "sometoken",
887 ))))
888 .times(1..)
889 .respond_with(status_code(200)),
890 );
891
892 hermetic_git_env();
896 let _ = pull(None); test_server.verify_and_clear();
900 }
901
902 #[test]
903 fn test_customheader_push() {
904 let tempdir = dir_with_repo();
905 set_current_dir(tempdir.path()).expect("Failed to change dir");
906
907 let test_server = Server::run();
908 add_server_remote(
909 test_server.url(""),
910 "AUTHORIZATION: someothertoken",
911 tempdir.path(),
912 );
913
914 test_server.expect(
915 Expectation::matching(request::headers(matchers::contains((
916 AUTHORIZATION.as_str(),
917 "someothertoken",
918 ))))
919 .times(1..)
920 .respond_with(status_code(200)),
921 );
922
923 hermetic_git_env();
924
925 ensure_symbolic_write_ref_exists().expect("Failed to ensure symbolic write ref exists");
927 add_note_line_to_head("test note line").expect("Failed to add note line");
928
929 let error = push(None, None);
930 error
931 .as_ref()
932 .expect_err("We have no valid git http server setup -> should fail");
933 dbg!(&error);
934 }
935
936 #[test]
937 fn test_random_suffix() {
938 for _ in 1..1000 {
939 let first = random_suffix();
940 dbg!(&first);
941 let second = random_suffix();
942 dbg!(&second);
943
944 let all_hex = |s: &String| s.chars().all(|c| c.is_ascii_hexdigit());
945
946 assert_ne!(first, second);
947 assert_eq!(first.len(), 8);
948 assert_eq!(second.len(), 8);
949 assert!(all_hex(&first));
950 assert!(all_hex(&second));
951 }
952 }
953
954 #[test]
955 fn test_empty_or_never_pushed_remote_error_for_fetch() {
956 let tempdir = tempdir().unwrap();
957 init_repo(tempdir.path());
958 set_current_dir(tempdir.path()).expect("Failed to change dir");
959 let git_dir_url = format!("file://{}", tempdir.path().display());
961 run_git_command(&["remote", "add", "origin", &git_dir_url], tempdir.path());
962
963 std::env::set_var("GIT_TRACE", "true");
965
966 let result = super::fetch(Some(tempdir.path()));
968 match result {
969 Err(GitError::NoRemoteMeasurements { output }) => {
970 assert!(
971 output.stderr.contains(GIT_PERF_REMOTE),
972 "Expected output to contain {GIT_PERF_REMOTE}. Output: '{}'",
973 output.stderr
974 )
975 }
976 other => panic!("Expected NoRemoteMeasurements error, got: {:?}", other),
977 }
978 }
979
980 #[test]
981 fn test_empty_or_never_pushed_remote_error_for_push() {
982 let tempdir = tempdir().unwrap();
983 init_repo(tempdir.path());
984 set_current_dir(tempdir.path()).expect("Failed to change dir");
985
986 hermetic_git_env();
987
988 run_git_command(
989 &["remote", "add", "origin", "invalid invalid"],
990 tempdir.path(),
991 );
992
993 std::env::set_var("GIT_TRACE", "true");
995
996 add_note_line_to_head("test line, invalid measurement, does not matter").unwrap();
997
998 let result = super::raw_push(Some(tempdir.path()), None);
999 match result {
1000 Err(GitError::RefFailedToPush { output }) => {
1001 assert!(
1002 output.stderr.contains(GIT_PERF_REMOTE),
1003 "Expected output to contain {GIT_PERF_REMOTE}, got: {}",
1004 output.stderr
1005 )
1006 }
1007 other => panic!("Expected RefFailedToPush error, got: {:?}", other),
1008 }
1009 }
1010
1011 #[test]
1016 fn test_new_symbolic_write_ref_returns_valid_ref() {
1017 let tempdir = dir_with_repo();
1018 set_current_dir(tempdir.path()).unwrap();
1019 hermetic_git_env();
1020
1021 let result = new_symbolic_write_ref();
1023 assert!(
1024 result.is_ok(),
1025 "Should create symbolic write ref: {:?}",
1026 result
1027 );
1028
1029 let ref_name = result.unwrap();
1030
1031 assert!(
1033 !ref_name.is_empty(),
1034 "Reference name should not be empty, got: '{}'",
1035 ref_name
1036 );
1037
1038 assert!(
1040 ref_name.starts_with(REFS_NOTES_WRITE_TARGET_PREFIX),
1041 "Reference should start with {}, got: {}",
1042 REFS_NOTES_WRITE_TARGET_PREFIX,
1043 ref_name
1044 );
1045
1046 let suffix = ref_name
1048 .strip_prefix(REFS_NOTES_WRITE_TARGET_PREFIX)
1049 .expect("Should have prefix");
1050 assert!(
1051 !suffix.is_empty() && suffix.chars().all(|c| c.is_ascii_hexdigit()),
1052 "Suffix should be non-empty hex string, got: {}",
1053 suffix
1054 );
1055 }
1056
1057 #[test]
1060 fn test_add_and_retrieve_notes() {
1061 let tempdir = dir_with_repo();
1062 set_current_dir(tempdir.path()).unwrap();
1063 hermetic_git_env();
1064
1065 let result = add_note_line_to_head("test: 100");
1067 assert!(
1068 result.is_ok(),
1069 "Should add note (requires valid ref from new_symbolic_write_ref): {:?}",
1070 result
1071 );
1072
1073 let result2 = add_note_line_to_head("test: 200");
1075 assert!(result2.is_ok(), "Should add second note: {:?}", result2);
1076
1077 let commits = walk_commits(10);
1079 assert!(commits.is_ok(), "Should walk commits: {:?}", commits);
1080
1081 let commits = commits.unwrap();
1082 assert!(!commits.is_empty(), "Should have commits");
1083
1084 let (_, notes) = &commits[0];
1086 assert!(!notes.is_empty(), "HEAD should have notes");
1087 assert!(
1088 notes.iter().any(|n| n.contains("test:")),
1089 "Notes should contain our test data"
1090 );
1091 }
1092
1093 #[test]
1097 fn test_walk_commits_shallow_repo_detection() {
1098 let tempdir = dir_with_repo();
1099 hermetic_git_env();
1100
1101 set_current_dir(tempdir.path()).unwrap();
1103 for i in 2..=5 {
1104 run_git_command(
1105 &["commit", "--allow-empty", "-m", &format!("Commit {}", i)],
1106 tempdir.path(),
1107 );
1108 }
1109
1110 let shallow_dir = tempdir.path().join("shallow");
1112 let output = Command::new("git")
1113 .args(&[
1114 "clone",
1115 "--depth",
1116 "2",
1117 tempdir.path().to_str().unwrap(),
1118 shallow_dir.to_str().unwrap(),
1119 ])
1120 .output()
1121 .unwrap();
1122
1123 assert!(
1124 output.status.success(),
1125 "Shallow clone failed: {}",
1126 String::from_utf8_lossy(&output.stderr)
1127 );
1128
1129 set_current_dir(&shallow_dir).unwrap();
1130 hermetic_git_env();
1131
1132 add_note_line_to_head("test: 100").expect("Should add note");
1134
1135 let result = walk_commits(10);
1137 assert!(result.is_ok(), "walk_commits should succeed: {:?}", result);
1138
1139 let commits = result.unwrap();
1140
1141 assert!(
1147 !commits.is_empty(),
1148 "Should have found commits in shallow repo"
1149 );
1150 }
1151
1152 #[test]
1154 fn test_walk_commits_normal_repo_not_shallow() {
1155 let tempdir = dir_with_repo();
1156 set_current_dir(tempdir.path()).unwrap();
1157 hermetic_git_env();
1158
1159 for i in 2..=3 {
1161 run_git_command(
1162 &["commit", "--allow-empty", "-m", &format!("Commit {}", i)],
1163 tempdir.path(),
1164 );
1165 }
1166
1167 add_note_line_to_head("test: 100").expect("Should add note");
1169
1170 let result = walk_commits(10);
1171 assert!(result.is_ok(), "walk_commits should succeed");
1172
1173 let commits = result.unwrap();
1174
1175 assert!(!commits.is_empty(), "Should have found commits");
1177 }
1178}