Skip to main content

git_policy_cli/
landing_objects.rs

1//! What:
2//!  Migration of a prepared commit's shadow-only objects into the real store as one pack,
3//!  optionally kept by a `.keep` file naming the transaction.
4//! Why:
5//!  The real target may name the commit only after the compare-and-swap, so its objects must be
6//!  in the real store first, and the keep stops prune or repack from removing them mid-landing
7//!  (`src/policy-engine/commit-landing-objects.ts`).
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! await migrateShadowObjects({ gitPath, cwd, shadowPath, newOid, oldBase, keepMessage });
12//! ```
13
14/// Shadow-scoped Git requests.
15use super::shadow_git::shadow_request;
16/// Journal target value.
17use super::transaction_journal::Base;
18/// Running real Git and failing on a nonzero exit.
19use super::transaction_git::{GitContext, GitFailure, GitRequest, run_git_checked};
20/// `Path` is a borrowed filesystem path.
21use std::path::Path;
22
23/// What:
24///  Packs the commit's objects that the shadow holds and the real store lacks,
25///  and indexes the pack into the real store.
26/// Why:
27///   `pack-objects --local` leaves out objects reached through the shadow's alternates,
28///  so the pack carries only what the landing introduces.
29///
30/// In TS you'd write (pseudocode):
31/// ```ts
32/// const pack = await runShadowGit({ args: ['pack-objects', '--revs', '--local', '--stdout', '--quiet'] });
33/// await runTransactionGit({ args: ['index-pack', '--stdin', `--keep=${keepMessage}`], input: pack });
34/// ```
35pub fn migrate_shadow_objects(
36    context: &GitContext,
37    cwd: &Path,
38    shadow: &Path,
39    new_oid: &str,
40    old_base: &Base,
41    keep_message: Option<&str>,
42) -> Result<String, GitFailure> {
43    let mut revisions: String = format!("{new_oid}\n");
44    if let Base::Commit(oid) = old_base {
45        revisions.push_str(&format!("^{oid}\n"));
46    }
47    let mut pack_request = shadow_request(
48        shadow,
49        cwd,
50        &["pack-objects", "--revs", "--local", "--stdout", "--quiet"],
51    );
52    pack_request.input = Some(revisions.into_bytes());
53    let pack = run_git_checked(context, &pack_request)?;
54    let mut index_arguments: Vec<String> = vec!["index-pack".to_string(), "--stdin".to_string()];
55    if let Some(message) = keep_message {
56        index_arguments.push(format!("--keep={message}"));
57    }
58    let mut index_request = GitRequest::new(cwd, &index_arguments);
59    index_request.input = Some(pack.stdout);
60    let indexed = run_git_checked(context, &index_request)?;
61    let report: String = String::from_utf8(indexed.stdout)
62        .map_err(|error| { return GitFailure(format!("git index-pack printed non-UTF-8 text: {error}")); })?;
63    let trimmed: &str = report.trim();
64    let Some((_, name)) = trimmed.split_once('\t') else {
65        return Err(GitFailure(format!(
66            "git index-pack reported no pack: {}",
67            serde_json::to_string(trimmed).unwrap_or_default()
68        )));
69    };
70    return Ok(name.to_string());
71}
72
73/// Migration controls stay out of the production code path.
74#[cfg(test)]
75#[path = "landing_objects_tests.rs"]
76mod tests;