Skip to main content

git_policy_cli/
conclusion_copy.rs

1//! What:
2//!  The merge, cherry-pick and revert conclusion state copied into the shadow at preparation,
3//!       and into the transaction's comparison copies.
4//! Why:
5//!  Native `git commit` in the shadow reads the conclusion state the real worktree holds at invocation.
6//!  Landing later compares the real entries with these copies, so an entry changed by another command
7//!  after invocation is kept (`conclusion_cleanup.rs` holds the landing side).
8//!  Pseudorefs of a reftable repository live in the ref store and are copied with `update-ref`
9//!  (`src/shadow-repository/shadow-conclusion-state.ts`).
10//!
11//! In TS you'd write (pseudocode):
12//! ```ts
13//! await copyConclusionState({ gitPath, cwd, gitDir, shadowPath, transactionDirectory, refFormat });
14//! ```
15
16/// Names and helpers the landing side already uses.
17use super::conclusion_cleanup::{
18    CONCLUSION_COPY_DIRECTORY, CONCLUSION_STATE_FILES, SEQUENCER_DIRECTORY, STORE_RECORD_FILENAME,
19    is_store_held, resolve_pseudoref,
20};
21/// Private file writes and directory creation.
22use super::private_storage::{create_private_directory, write_private_file};
23/// Optional reads that treat a missing entry as absent.
24use super::recovery_files::{path_present, read_optional};
25/// JSON object writer for the store-held record.
26use super::json_record::ObjectWriter;
27/// The shadow ref helper and the process context.
28use super::shadow_git::shadow_request;
29use super::transaction_git::{GitContext, GitFailure, GitRequest, run_git_checked};
30/// Ref storage backend.
31use super::transaction_journal::RefFormat;
32/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
33use std::path::{Path, PathBuf};
34
35/// What:
36///  Copies a directory tree, keeping links as links.
37/// Why:
38///   `cp(..., { recursive: true })` copies the sequencer directory into the shadow and the copies.
39///
40/// In TS you'd write (pseudocode):
41/// ```ts
42/// await cp(source, destination, { recursive: true });
43/// ```
44fn copy_tree(source: &Path, destination: &Path) -> std::io::Result<()> {
45    create_private_directory(destination)?;
46    for entry_result in std::fs::read_dir(source)? {
47        let entry: std::fs::DirEntry = entry_result?;
48        let target: PathBuf = destination.join(entry.file_name());
49        let kind: std::fs::FileType = entry.file_type()?;
50        if kind.is_dir() {
51            copy_tree(entry.path().as_path(), target.as_path())?;
52        } else if kind.is_symlink() {
53            #[cfg(unix)]
54            std::os::unix::fs::symlink(std::fs::read_link(entry.path())?, &target)?;
55            // UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
56            #[cfg(not(unix))]
57            std::fs::copy(entry.path(), &target).map(|_| ())?;
58        } else {
59            std::fs::copy(entry.path(), &target)?;
60        }
61    }
62    return Ok(());
63}
64
65/// What:
66///  Reads one pseudoref's value in the owning worktree, if it resolves.
67/// Why:
68///   A reftable pseudoref has no file to copy, so its value is copied through the ref store.
69///
70/// In TS you'd write (pseudocode):
71/// ```ts
72/// await resolvePseudoref({ gitPath, cwd, name });
73/// ```
74fn owning_pseudoref(context: &GitContext, cwd: &Path, name: &str) -> Option<String> {
75    let mut request: GitRequest = GitRequest::new(cwd, &["rev-parse", "--verify", "--quiet", name]);
76    request.without_prefix = true;
77    return resolve_pseudoref(context, request);
78}
79
80/// What:
81///  Copies every present conclusion entry of the owning worktree into the shadow and the transaction.
82/// Why:
83///   The shadow copy is what native commit reads; the transaction copy is what landing compares against.
84///
85/// In TS you'd write (pseudocode):
86/// ```ts
87/// await copyConclusionState({ gitPath, cwd, gitDir, shadowPath, transactionDirectory, refFormat });
88/// ```
89pub fn copy_conclusion_state(
90    context: &GitContext,
91    cwd: &Path,
92    git_dir: &Path,
93    shadow: &Path,
94    transaction_directory: &Path,
95    ref_format: RefFormat,
96) -> Result<(), GitFailure> {
97    let io = |step: &str, error: std::io::Error| { return GitFailure(format!("{step} failed: {error}")); };
98    let copies: PathBuf = transaction_directory.join(CONCLUSION_COPY_DIRECTORY);
99    create_private_directory(copies.as_path()).map_err(|error| { return io("creating the conclusion copies", error); })?;
100    // Store-held values in the order of the copied names, as the incumbent's JSON record lists them.
101    let mut store_held: ObjectWriter = ObjectWriter::new();
102    for name in CONCLUSION_STATE_FILES {
103        if is_store_held(ref_format, name) {
104            if let Some(value) = owning_pseudoref(context, cwd, name) {
105                run_git_checked(
106                    context,
107                    &shadow_request(shadow, shadow, &["update-ref", name, value.as_str()]),
108                )?;
109                store_held.string(name, value.as_str());
110            }
111            continue;
112        }
113        let Some(bytes) = read_optional(git_dir.join(name).as_path())
114            .map_err(|error| { return GitFailure(error.0); })?
115        else {
116            continue;
117        };
118        write_private_file(shadow.join(name).as_path(), bytes.as_slice())
119            .map_err(|error| { return io("copying a conclusion entry into the shadow", error); })?;
120        write_private_file(copies.join(name).as_path(), bytes.as_slice())
121            .map_err(|error| { return io("copying a conclusion entry for comparison", error); })?;
122    }
123    let record: String = store_held.finish();
124    write_private_file(
125        copies.join(STORE_RECORD_FILENAME).as_path(),
126        format!("{record}\n").as_bytes(),
127    )
128    .map_err(|error| { return io("writing the store-held record", error); })?;
129    let sequencer: PathBuf = git_dir.join(SEQUENCER_DIRECTORY);
130    if path_present(sequencer.as_path()).map_err(|error| { return GitFailure(error.0); })? {
131        copy_tree(sequencer.as_path(), shadow.join(SEQUENCER_DIRECTORY).as_path())
132            .map_err(|error| { return io("copying the sequencer into the shadow", error); })?;
133        copy_tree(sequencer.as_path(), copies.join(SEQUENCER_DIRECTORY).as_path())
134            .map_err(|error| { return io("copying the sequencer for comparison", error); })?;
135    }
136    return Ok(());
137}
138
139/// Conclusion copy controls stay out of the release executable.
140#[cfg(test)]
141#[path = "conclusion_copy_tests.rs"]
142mod tests;