1#![warn(missing_docs)]
11#![warn(
12 clippy::all,
13 clippy::as_conversions,
14 clippy::clone_on_ref_ptr,
15 clippy::dbg_macro
16)]
17#![allow(clippy::too_many_arguments, clippy::blocks_in_if_conditions)]
18
19use std::fmt::Write;
20use std::fs::File;
21use std::io::{stdin, BufRead};
22use std::time::SystemTime;
23
24use eyre::Context;
25use git_branchless_invoke::CommandContext;
26use git_branchless_opts::{HookArgs, HookSubcommand};
27use itertools::Itertools;
28use lib::core::dag::Dag;
29use lib::core::repo_ext::RepoExt;
30use lib::core::rewrite::rewrite_hooks::get_deferred_commits_path;
31use lib::util::EyreExitOr;
32use tracing::{error, instrument, warn};
33
34use lib::core::eventlog::{should_ignore_ref_updates, Event, EventLogDb, EventReplayer};
35use lib::core::formatting::{Glyphs, Pluralize};
36use lib::core::gc::{gc, mark_commit_reachable};
37use lib::git::{CategorizedReferenceName, MaybeZeroOid, NonZeroOid, ReferenceName, Repo};
38
39use lib::core::effects::Effects;
40pub use lib::core::rewrite::rewrite_hooks::{
41 hook_drop_commit_if_empty, hook_post_rewrite, hook_register_extra_post_rewrite_hook,
42 hook_skip_upstream_applied_commit,
43};
44
45#[instrument]
49fn hook_post_checkout(
50 effects: &Effects,
51 previous_head_oid: &str,
52 current_head_oid: &str,
53 is_branch_checkout: isize,
54) -> eyre::Result<()> {
55 if is_branch_checkout == 0 {
56 return Ok(());
57 }
58
59 let now = SystemTime::now();
60 let timestamp = now.duration_since(SystemTime::UNIX_EPOCH)?;
61 writeln!(
62 effects.get_output_stream(),
63 "branchless: processing checkout"
64 )?;
65
66 let repo = Repo::from_current_dir()?;
67 let conn = repo.get_db_conn()?;
68 let event_log_db = EventLogDb::new(&conn)?;
69 let event_tx_id = event_log_db.make_transaction_id(now, "hook-post-checkout")?;
70 event_log_db.add_events(vec![Event::RefUpdateEvent {
71 timestamp: timestamp.as_secs_f64(),
72 event_tx_id,
73 old_oid: previous_head_oid.parse()?,
74 new_oid: {
75 let oid: MaybeZeroOid = current_head_oid.parse()?;
76 oid
77 },
78 ref_name: ReferenceName::from("HEAD"),
79 message: None,
80 }])?;
81 Ok(())
82}
83
84fn hook_post_commit_common(effects: &Effects, hook_name: &str) -> eyre::Result<()> {
85 let now = SystemTime::now();
86 let glyphs = Glyphs::detect();
87 let repo = Repo::from_current_dir()?;
88 let conn = repo.get_db_conn()?;
89 let event_log_db = EventLogDb::new(&conn)?;
90
91 let commit_oid = match repo.get_head_info()?.oid {
92 Some(commit_oid) => commit_oid,
93 None => {
94 warn!(
96 "`{}` hook called, but could not determine the OID of `HEAD`",
97 hook_name
98 );
99 return Ok(());
100 }
101 };
102
103 let commit = repo
104 .find_commit_or_fail(commit_oid)
105 .wrap_err("Looking up `HEAD` commit")?;
106 mark_commit_reachable(&repo, commit_oid)
107 .wrap_err("Marking commit as reachable for GC purposes")?;
108
109 let event_replayer = EventReplayer::from_event_log_db(effects, &repo, &event_log_db)?;
110 let event_cursor = event_replayer.make_default_cursor();
111 let references_snapshot = repo.get_references_snapshot()?;
112 Dag::open_and_sync(
113 effects,
114 &repo,
115 &event_replayer,
116 event_cursor,
117 &references_snapshot,
118 )?;
119
120 if repo.is_rebase_underway()? {
121 let deferred_commits_path = get_deferred_commits_path(&repo);
122 let mut deferred_commits_file = File::options()
123 .create(true)
124 .append(true)
125 .open(&deferred_commits_path)
126 .with_context(|| {
127 format!("Opening deferred commits file at {deferred_commits_path:?}")
128 })?;
129
130 use std::io::Write;
131 writeln!(deferred_commits_file, "{commit_oid}")?;
132 return Ok(());
133 }
134
135 let timestamp = commit.get_time().to_system_time()?;
136
137 let timestamp = timestamp
143 .duration_since(SystemTime::UNIX_EPOCH)?
144 .as_secs_f64();
145
146 let event_tx_id = event_log_db.make_transaction_id(now, hook_name)?;
147 event_log_db.add_events(vec![Event::CommitEvent {
148 timestamp,
149 event_tx_id,
150 commit_oid: commit.get_oid(),
151 }])?;
152 writeln!(
153 effects.get_output_stream(),
154 "branchless: processed commit: {}",
155 glyphs.render(commit.friendly_describe(&glyphs)?)?,
156 )?;
157
158 Ok(())
159}
160
161#[instrument]
165fn hook_post_commit(effects: &Effects) -> eyre::Result<()> {
166 hook_post_commit_common(effects, "post-commit")
167}
168
169#[instrument]
175fn hook_post_merge(effects: &Effects, _is_squash_merge: isize) -> eyre::Result<()> {
176 hook_post_commit_common(effects, "post-merge")
177}
178
179#[instrument]
183fn hook_post_applypatch(effects: &Effects) -> eyre::Result<()> {
184 hook_post_commit_common(effects, "post-applypatch")
185}
186
187mod reference_transaction {
188 use std::collections::HashMap;
189 use std::fs::File;
190 use std::io::{BufRead, BufReader};
191 use std::str::FromStr;
192
193 use eyre::Context;
194 use itertools::Itertools;
195 use lazy_static::lazy_static;
196 use tracing::{instrument, warn};
197
198 use lib::git::{MaybeZeroOid, ReferenceName, Repo};
199
200 #[derive(Clone, Debug, PartialEq, Eq)]
202 pub enum ReferenceTarget {
203 Direct { oid: MaybeZeroOid },
205
206 Symbolic { name: ReferenceName },
209 }
210
211 impl ReferenceTarget {
212 #[instrument]
221 pub fn as_oid(&self, repo: &Repo) -> eyre::Result<MaybeZeroOid> {
222 match self {
223 ReferenceTarget::Direct { oid } => Ok(*oid),
224 ReferenceTarget::Symbolic { name } => Ok(repo.reference_name_to_oid(name)?),
225 }
226 }
227 }
228
229 impl FromStr for ReferenceTarget {
230 type Err = eyre::ErrReport;
231
232 fn from_str(value: &str) -> Result<Self, Self::Err> {
239 match value.strip_prefix("ref:") {
240 Some(refname) => Ok(ReferenceTarget::Symbolic {
241 name: ReferenceName::from(refname),
242 }),
243 None => Ok(ReferenceTarget::Direct {
244 oid: value.parse()?,
245 }),
246 }
247 }
248 }
249
250 #[instrument]
251 fn parse_packed_refs_line(line: &str) -> Option<(ReferenceName, MaybeZeroOid)> {
252 if line.is_empty() {
253 return None;
254 }
255 if line.starts_with('#') {
256 return None;
258 }
259 if line.starts_with('^') {
260 return None;
263 }
264 if !line.starts_with(|c: char| c.is_ascii_hexdigit()) {
265 warn!(?line, "Unrecognized pack-refs line starting character");
266 return None;
267 }
268
269 lazy_static! {
270 static ref RE: regex::Regex = regex::Regex::new(r"^([^ ]+) (.+)$").unwrap();
271 };
272 match RE.captures(line) {
273 None => {
274 warn!(?line, "No regex match for pack-refs line");
275 None
276 }
277
278 Some(captures) => {
279 let oid = &captures[1];
280 let oid = match MaybeZeroOid::from_str(oid) {
281 Ok(oid) => oid,
282 Err(err) => {
283 warn!(?oid, ?err, "Could not parse OID for pack-refs line");
284 return None;
285 }
286 };
287
288 let reference_name = &captures[2];
289 let reference_name = ReferenceName::from(reference_name);
290
291 Some((reference_name, oid))
292 }
293 }
294 }
295
296 #[cfg(test)]
297 #[test]
298 fn test_parse_packed_refs_line() {
299 use super::*;
300
301 let line = "1234567812345678123456781234567812345678 refs/foo/bar";
302 let name = ReferenceName::from("refs/foo/bar");
303 let oid = MaybeZeroOid::from_str("1234567812345678123456781234567812345678").unwrap();
304 assert_eq!(parse_packed_refs_line(line), Some((name, oid)));
305 }
306
307 #[instrument]
308 pub fn read_packed_refs_file(
309 repo: &Repo,
310 ) -> eyre::Result<HashMap<ReferenceName, MaybeZeroOid>> {
311 let packed_refs_file_path = repo.get_packed_refs_path();
312 let file = match File::open(packed_refs_file_path) {
313 Ok(file) => file,
314 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(HashMap::new()),
315 Err(err) => return Err(err.into()),
316 };
317
318 let reader = BufReader::new(file);
319 let mut result = HashMap::new();
320 for line in reader.lines() {
321 let line = line.wrap_err("Reading line from packed-refs")?;
322 if line.is_empty() {
323 continue;
324 }
325 if let Some((k, v)) = parse_packed_refs_line(&line) {
326 result.insert(k, v);
327 }
328 }
329 Ok(result)
330 }
331
332 #[derive(Debug, PartialEq, Eq)]
333 pub struct ParsedReferenceTransactionLine {
334 pub ref_name: ReferenceName,
335 pub old_value: ReferenceTarget,
336 pub new_value: ReferenceTarget,
337 }
338
339 #[instrument]
340 pub fn parse_reference_transaction_line(
341 line: &str,
342 ) -> eyre::Result<ParsedReferenceTransactionLine> {
343 let fields = line.split(' ').collect_vec();
344 match fields.as_slice() {
345 [old_value, new_value, ref_name] => Ok(ParsedReferenceTransactionLine {
346 ref_name: ReferenceName::from(*ref_name),
347 old_value: ReferenceTarget::from_str(old_value)?,
348 new_value: ReferenceTarget::from_str(new_value)?,
349 }),
350 _ => {
351 eyre::bail!(
352 "Unexpected number of fields in reference-transaction line: {:?}",
353 &line
354 )
355 }
356 }
357 }
358
359 #[cfg(test)]
360 #[test]
361 fn test_parse_reference_transaction_line() -> eyre::Result<()> {
362 use lib::{core::eventlog::should_ignore_ref_updates, testing::make_git};
363
364 let git = make_git()?;
365 git.init_repo()?;
366 let oid1 = git.commit_file("README", 1)?;
367 let oid2 = git.commit_file("README2", 2)?;
368
369 let zero = "0000000000000000000000000000000000000000";
370 let branch_ref = "refs/heads/mybranch";
371 let orig_head_ref = "ORIG_HEAD";
372 let master_tx_ref = "ref:refs/heads/master";
373 let master_ref = "refs/heads/master";
374 let head_ref = "HEAD";
375
376 let line = format!("{oid1} {oid2} {branch_ref}");
377 assert_eq!(
378 parse_reference_transaction_line(&line)?,
379 ParsedReferenceTransactionLine {
380 old_value: ReferenceTarget::Direct { oid: oid1.into() },
381 new_value: ReferenceTarget::Direct { oid: oid2.into() },
382 ref_name: ReferenceName::from(branch_ref),
383 }
384 );
385
386 let line = format!("{zero} {master_tx_ref} HEAD");
387 assert_eq!(
388 parse_reference_transaction_line(&line)?,
389 ParsedReferenceTransactionLine {
390 old_value: ReferenceTarget::Direct { oid: zero.parse()? },
391 new_value: ReferenceTarget::Symbolic {
392 name: ReferenceName::from(master_ref)
393 },
394 ref_name: ReferenceName::from(head_ref)
395 }
396 );
397
398 {
399 let line = &format!("{oid1} {oid2} ORIG_HEAD");
400 let parsed_line = parse_reference_transaction_line(line)?;
401 assert_eq!(
402 parsed_line,
403 ParsedReferenceTransactionLine {
404 old_value: ReferenceTarget::Direct { oid: oid1.into() },
405 new_value: ReferenceTarget::Direct { oid: oid2.into() },
406 ref_name: ReferenceName::from(orig_head_ref)
407 }
408 );
409 assert!(should_ignore_ref_updates(&parsed_line.ref_name));
410 }
411
412 let line = "there are not three fields here";
413 assert!(parse_reference_transaction_line(line).is_err());
414
415 Ok(())
416 }
417
418 fn reftarget_matches_refname(
419 reftarget: &ReferenceTarget,
420 refname: &ReferenceName,
421 packed_references: &HashMap<ReferenceName, MaybeZeroOid>,
422 ) -> bool {
423 match reftarget {
424 ReferenceTarget::Direct { oid } => packed_references.get(refname) == Some(oid),
425 ReferenceTarget::Symbolic { name } => name == refname,
426 }
427 }
428 #[instrument]
453 pub fn fix_packed_reference_oid(
454 repo: &Repo,
455 packed_references: &HashMap<ReferenceName, MaybeZeroOid>,
456 parsed_line: ParsedReferenceTransactionLine,
457 ) -> ParsedReferenceTransactionLine {
458 match parsed_line {
459 ParsedReferenceTransactionLine {
460 ref_name,
461 old_value:
462 ReferenceTarget::Direct {
463 oid: MaybeZeroOid::Zero,
464 },
465 new_value,
466 } if reftarget_matches_refname(&new_value, &ref_name, packed_references) => {
467 ParsedReferenceTransactionLine {
471 ref_name,
472 old_value: new_value.clone(),
473 new_value: new_value.clone(),
474 }
475 }
476
477 ParsedReferenceTransactionLine {
478 ref_name,
479 old_value,
480 new_value:
481 ReferenceTarget::Direct {
482 oid: MaybeZeroOid::Zero,
483 },
484 } if reftarget_matches_refname(&old_value, &ref_name, packed_references) => {
485 ParsedReferenceTransactionLine {
489 ref_name,
490 old_value: old_value.clone(),
491 new_value: old_value.clone(),
492 }
493 }
494
495 other => other,
496 }
497 }
498}
499
500#[instrument]
504fn hook_reference_transaction(effects: &Effects, transaction_state: &str) -> eyre::Result<()> {
505 use reference_transaction::{
506 fix_packed_reference_oid, parse_reference_transaction_line, read_packed_refs_file,
507 ParsedReferenceTransactionLine,
508 };
509
510 if transaction_state != "committed" {
511 return Ok(());
512 }
513 let now = SystemTime::now();
514
515 let repo = Repo::from_current_dir()?;
516 let conn = repo.get_db_conn()?;
517 let event_log_db = EventLogDb::new(&conn)?;
518 let event_tx_id = event_log_db.make_transaction_id(now, "reference-transaction")?;
519
520 let packed_references = read_packed_refs_file(&repo)?;
521
522 let parsed_lines: Vec<ParsedReferenceTransactionLine> = stdin()
523 .lock()
524 .split(b'\n')
525 .filter_map(|line| {
526 let line = match line {
527 Ok(line) => line,
528 Err(_) => return None,
529 };
530 let line = match std::str::from_utf8(&line) {
531 Ok(line) => line,
532 Err(err) => {
533 error!(?err, ?line, "Could not parse reference-transaction line");
534 return None;
535 }
536 };
537 match parse_reference_transaction_line(line) {
538 Ok(line) => Some(line),
539 Err(err) => {
540 error!(?err, ?line, "Could not parse reference-transaction-line");
541 None
542 }
543 }
544 })
545 .filter(
546 |ParsedReferenceTransactionLine {
547 ref_name,
548 old_value: _,
549 new_value: _,
550 }| !should_ignore_ref_updates(ref_name),
551 )
552 .map(|parsed_line| fix_packed_reference_oid(&repo, &packed_references, parsed_line))
553 .collect();
554 if parsed_lines.is_empty() {
555 return Ok(());
556 }
557
558 let num_reference_updates = Pluralize {
559 determiner: None,
560 amount: parsed_lines.len(),
561 unit: ("update", "updates"),
562 };
563 writeln!(
564 effects.get_output_stream(),
565 "branchless: processing {}: {}",
566 num_reference_updates,
567 parsed_lines
568 .iter()
569 .map(
570 |ParsedReferenceTransactionLine {
571 ref_name,
572 old_value: _,
573 new_value: _,
574 }| { CategorizedReferenceName::new(ref_name).friendly_describe() }
575 )
576 .map(|description| format!("{}", console::style(description).green()))
577 .sorted()
578 .collect::<Vec<_>>()
579 .join(", ")
580 )?;
581
582 let timestamp = now
583 .duration_since(SystemTime::UNIX_EPOCH)
584 .wrap_err("Calculating timestamp")?
585 .as_secs_f64();
586 let events: eyre::Result<Vec<Event>> = parsed_lines
587 .into_iter()
588 .map(
589 |ParsedReferenceTransactionLine {
590 ref_name,
591 old_value,
592 new_value,
593 }| {
594 let old_oid = old_value.as_oid(&repo)?;
595 let new_oid = new_value.as_oid(&repo)?;
596 Ok(Event::RefUpdateEvent {
597 timestamp,
598 event_tx_id,
599 ref_name,
600 old_oid,
601 new_oid,
602 message: None,
603 })
604 },
605 )
606 .collect();
607 event_log_db.add_events(events?)?;
608
609 Ok(())
610}
611
612#[instrument]
614pub fn command_main(ctx: CommandContext, args: HookArgs) -> EyreExitOr<()> {
615 let CommandContext {
616 effects,
617 git_run_info,
618 } = ctx;
619 let HookArgs { subcommand } = args;
620
621 match subcommand {
622 HookSubcommand::DetectEmptyCommit { old_commit_oid } => {
623 let old_commit_oid: NonZeroOid = old_commit_oid.parse()?;
624 hook_drop_commit_if_empty(&effects, old_commit_oid)?;
625 }
626
627 HookSubcommand::PreAutoGc => {
628 gc(&effects)?;
629 }
630
631 HookSubcommand::PostApplypatch => {
632 hook_post_applypatch(&effects)?;
633 }
634
635 HookSubcommand::PostCheckout {
636 previous_commit,
637 current_commit,
638 is_branch_checkout,
639 } => {
640 hook_post_checkout(
641 &effects,
642 &previous_commit,
643 ¤t_commit,
644 is_branch_checkout,
645 )?;
646 }
647
648 HookSubcommand::PostCommit => {
649 hook_post_commit(&effects)?;
650 }
651
652 HookSubcommand::PostMerge { is_squash_merge } => {
653 hook_post_merge(&effects, is_squash_merge)?;
654 }
655
656 HookSubcommand::PostRewrite { rewrite_type } => {
657 hook_post_rewrite(&effects, &git_run_info, &rewrite_type)?;
658 }
659
660 HookSubcommand::ReferenceTransaction { transaction_state } => {
661 hook_reference_transaction(&effects, &transaction_state)?;
662 }
663
664 HookSubcommand::RegisterExtraPostRewriteHook => {
665 hook_register_extra_post_rewrite_hook()?;
666 }
667
668 HookSubcommand::SkipUpstreamAppliedCommit { commit_oid } => {
669 let commit_oid: NonZeroOid = commit_oid.parse()?;
670 hook_skip_upstream_applied_commit(&effects, commit_oid)?;
671 }
672 }
673
674 Ok(Ok(()))
675}