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