Skip to main content

git_policy_cli/commit/
commit_worktree_records.rs

1//! What:
2//!  The worktree completion records a landed commit owes the worktree: selected final-newline corrections.
3//! Why:
4//!  The commit keeps what the private index holds, so the real worktree still holds the bytes the user wrote.
5//!  After the landing, each recorded file takes the committed bytes only while it still holds its earlier bytes
6//!  (`src/policy-engine/commit-transaction-selected-worktree.ts`).
7//!
8//! In TS you'd write (pseudocode):
9//! ```ts
10//! const selected = await selectedWorktreeRecords({ gitPath, cwd, repositoryRoot, indexPath, initialCandidates, newlinePaths });
11//! ```
12
13use crate::candidate_object::CandidateMode;
14use crate::direct_fix_install::InstallChange;
15use crate::path_bytes::os_from_path_bytes;
16use crate::transaction_git::{GitContext, GitFailure, GitOutput, GitRequest, run_git_checked};
17use crate::transaction_journal::AddedPath;
18use crate::worktree_completion::{WorktreeFileState, inspect_worktree_file};
19use std::path::Path;
20
21/// What:
22///  The Git mode text a candidate mode carries, when it is an ordinary file.
23/// Why:
24///   Only regular and executable files take a corrected worktree copy.
25///
26/// In TS you'd write (pseudocode):
27/// ```ts
28/// candidate.mode === 'executable' ? '100755' : '100644'
29/// ```
30fn ordinary_git_mode(mode: CandidateMode) -> Option<&'static str> {
31    return match mode {
32        CandidateMode::Regular => Some("100644"),
33        CandidateMode::Executable => Some("100755"),
34        _ => None,
35    };
36}
37
38/// What:
39///  The blob hash of some bytes, without writing an object.
40/// Why:
41///   The record names the original staged blob, which only its bytes determine.
42///
43/// In TS you'd write (pseudocode):
44/// ```ts
45/// gitBlobId(bytes)
46/// ```
47fn blob_id(context: &GitContext, cwd: &Path, bytes: &[u8]) -> Result<String, GitFailure> {
48    let mut request: GitRequest = GitRequest::new(cwd, &["hash-object", "--stdin"]);
49    request.without_prefix = true;
50    request.input = Some(bytes.to_vec());
51    let output: GitOutput = run_git_checked(context, &request)?;
52    return Ok(String::from_utf8_lossy(output.stdout.as_slice()).trim().to_string());
53}
54
55/// What:
56///  The settled mode and object of one path in the private index, or nothing when it has no stage-zero entry.
57/// Why:
58///   A correction counts only while the settled entry still has the ordinary mode of the original.
59///
60/// In TS you'd write (pseudocode):
61/// ```ts
62/// (await loadIndexEntries({ gitPath, cwd, indexPath, paths })).get(path)
63/// ```
64pub fn settled_entry(
65    context: &GitContext,
66    cwd: &Path,
67    index: &Path,
68    path: &[u8],
69) -> Result<Option<(String, String)>, GitFailure> {
70    let mut pathspec: Vec<u8> = b":(top,literal)".to_vec();
71    pathspec.extend_from_slice(path);
72    let mut request: GitRequest = GitRequest::new(cwd, &["ls-files", "--stage", "-z", "--"]);
73    request.arguments.push(os_from_path_bytes(pathspec.as_slice()));
74    request.without_prefix = true;
75    request.index = Some(index.to_path_buf());
76    let output: GitOutput = run_git_checked(context, &request)?;
77    for record in output.stdout.split(|byte| { return *byte == 0; }) {
78        let Some(tab) = record.iter().position(|byte| { return *byte == b'\t'; }) else {
79            continue;
80        };
81        let metadata: String = String::from_utf8_lossy(&record[..tab]).into_owned();
82        let fields: Vec<&str> = metadata.split(' ').collect();
83        if fields.len() == 3 && fields[2] == "0" {
84            return Ok(Some((String::from(fields[0]), String::from(fields[1]))));
85        }
86    }
87    return Ok(None);
88}
89
90/// What:
91///  The records for selected files that final-newline corrected, only where the worktree copy still equals the
92///  original staged bytes.
93/// Why:
94///   A partially staged edit cannot be replaced by a commit correction; the installer re-checks before replacing.
95///
96/// In TS you'd write (pseudocode):
97/// ```ts
98/// await selectedWorktreeRecords({ gitPath, cwd, repositoryRoot, indexPath, initialCandidates, newlinePaths });
99/// ```
100pub fn selected_worktree_records(
101    context: &GitContext,
102    cwd: &Path,
103    repository_root: &Path,
104    index: &Path,
105    changes: &[InstallChange],
106) -> Result<Vec<AddedPath>, GitFailure> {
107    let mut records: Vec<AddedPath> = Vec::new();
108    for change in changes {
109        let Some(git_mode) = ordinary_git_mode(change.mode) else {
110            continue;
111        };
112        let original_oid: String = blob_id(context, cwd, &change.original)?;
113        let Some((entry_mode, entry_oid)) = settled_entry(context, cwd, index, change.path.as_slice())? else {
114            continue;
115        };
116        if entry_mode != git_mode || entry_oid == original_oid {
117            continue;
118        }
119        let destination = repository_root.join(path_from_bytes(change.path.as_slice()));
120        match inspect_worktree_file(destination.as_path(), git_mode, &change.original, &change.replacement) {
121            Ok(WorktreeFileState::Original(_)) => {}
122            // A missing copy is reported as a conflict by the inspection, so it is skipped here with the other conflicts.
123            Ok(_) => continue,
124            Err(error) => {
125                return Err(GitFailure(format!(
126                    "reading {} failed: {error}",
127                    destination.display()
128                )));
129            }
130        }
131        records.push(AddedPath {
132            path: change.path.clone(),
133            git_mode: String::from(git_mode),
134            original_oid,
135            intended_oid: entry_oid,
136        });
137    }
138    return Ok(records);
139}
140
141/// What:
142///  A repository-relative path from its bytes.
143/// Why:
144///   The worktree path is joined from the exact bytes Git reported.
145///
146/// In TS you'd write (pseudocode):
147/// ```ts
148/// join(repositoryRoot, path)
149/// ```
150fn path_from_bytes(bytes: &[u8]) -> std::path::PathBuf {
151    return std::path::PathBuf::from(os_from_path_bytes(bytes));
152}
153
154/// Controls for this module stay out of the release executable.
155#[cfg(test)]
156#[path = "commit_worktree_records_tests.rs"]
157mod tests;