branchless/core/
gc.rs

1//! Deal with Git's garbage collection mechanism.
2//!
3//! Git treats a commit as unreachable if there are no references that point to
4//! it or one of its descendants. However, the branchless workflow requires
5//! keeping such commits reachable until the user has obsoleted them.
6//!
7//! This module is responsible for adding extra references to Git, so that Git's
8//! garbage collection doesn't collect commits which branchless thinks are still
9//! active.
10
11use std::fmt::Write;
12
13use eyre::Context;
14use tracing::instrument;
15
16use crate::core::effects::Effects;
17use crate::core::eventlog::{
18    is_gc_ref, CommitActivityStatus, EventCursor, EventLogDb, EventReplayer,
19};
20use crate::core::formatting::Pluralize;
21use crate::git::{NonZeroOid, Reference, Repo};
22
23/// Find references under `refs/branchless/` which point to commits which are no
24/// longer active. These are safe to remove.
25pub fn find_dangling_references<'repo>(
26    repo: &'repo Repo,
27    event_replayer: &EventReplayer,
28    event_cursor: EventCursor,
29) -> eyre::Result<Vec<Reference<'repo>>> {
30    let mut result = Vec::new();
31    for reference in repo.get_all_references()? {
32        let reference_name = reference.get_name()?;
33        if !is_gc_ref(&reference_name) {
34            continue;
35        }
36
37        // The graph only contains commits, so we don't need to handle the
38        // case of the reference not peeling to a valid commit. (It might be
39        // a reference to a different kind of object.)
40        let commit = match reference.peel_to_commit()? {
41            Some(commit) => commit,
42            None => continue,
43        };
44
45        match event_replayer.get_cursor_commit_activity_status(event_cursor, commit.get_oid()) {
46            CommitActivityStatus::Active => {
47                // Do nothing.
48            }
49            CommitActivityStatus::Inactive => {
50                // This commit hasn't been observed, but it's possible that the user expected it
51                // to remain. Do nothing. See https://github.com/arxanas/git-branchless/issues/412.
52            }
53            CommitActivityStatus::Obsolete => {
54                // This commit was explicitly hidden by some operation.
55                result.push(reference)
56            }
57        }
58    }
59    Ok(result)
60}
61
62/// Mark a commit as reachable.
63///
64/// Once marked as reachable, the commit won't be collected by Git's garbage
65/// collection mechanism until first garbage-collected by branchless itself
66/// (using the `gc` function).
67///
68/// If the commit does not exist (such as if it was already garbage-collected), then this is a no-op.
69///
70/// Args:
71/// * `repo`: The Git repository.
72/// * `commit_oid`: The commit OID to mark as reachable.
73#[instrument]
74pub fn mark_commit_reachable(repo: &Repo, commit_oid: NonZeroOid) -> eyre::Result<()> {
75    let ref_name = format!("refs/branchless/{commit_oid}");
76    eyre::ensure!(
77        Reference::is_valid_name(&ref_name),
78        format!("Invalid ref name to mark commit as reachable: {ref_name}")
79    );
80
81    // NB: checking for the commit first with `find_commit` is racy, as the `create_reference` call
82    // could still fail if the commit is deleted by then, but it's too hard to propagate whether the
83    // commit was not found from `create_reference`.
84    if repo.find_commit(commit_oid)?.is_some() {
85        repo.create_reference(
86            &ref_name.into(),
87            commit_oid,
88            true,
89            "branchless: marking commit as reachable",
90        )
91        .wrap_err("Creating reference")?;
92    }
93
94    Ok(())
95}
96
97/// Run branchless's garbage collection.
98///
99/// Frees any references to commits which are no longer visible in the smartlog.
100#[instrument]
101pub fn gc(effects: &Effects) -> eyre::Result<()> {
102    let repo = Repo::from_current_dir()?;
103    let conn = repo.get_db_conn()?;
104    let event_log_db = EventLogDb::new(&conn)?;
105    let event_replayer = EventReplayer::from_event_log_db(effects, &repo, &event_log_db)?;
106    let event_cursor = event_replayer.make_default_cursor();
107
108    writeln!(
109        effects.get_output_stream(),
110        "branchless: collecting garbage"
111    )?;
112    let dangling_references = find_dangling_references(&repo, &event_replayer, event_cursor)?;
113    let num_dangling_references = Pluralize {
114        determiner: None,
115        amount: dangling_references.len(),
116        unit: ("dangling reference", "dangling references"),
117    }
118    .to_string();
119    for mut reference in dangling_references.into_iter() {
120        reference.delete()?;
121    }
122
123    writeln!(
124        effects.get_output_stream(),
125        "branchless: {num_dangling_references} deleted",
126    )?;
127    Ok(())
128}