git_policy_cli/
worktree_copy_install_log.rs1use super::json_record::quote;
15use super::private_storage::sync_directory;
16use super::worktree_copy_error::{WorktreeCopyError, failure_with_cause, raw_failure};
17use super::worktree_copy_install_entry::InstalledPath;
18use super::worktree_copy_journal::{
19 Role, assert_private_path, encode_installed_entry, installed_entry, string_array,
20};
21use super::worktree_copy_selection::assert_safe_repository_path;
22use std::io::Write;
23use std::os::unix::fs::OpenOptionsExt;
24use std::path::{Path, PathBuf};
25
26pub const INSTALL_LOG_NAME: &str = "install-log.jsonl";
36
37#[derive(Clone, Debug, Default, Eq, PartialEq)]
47pub struct InstallLogRecords {
48 pub intended: Vec<String>,
50 pub created: Vec<InstalledPath>,
52}
53
54#[derive(Debug)]
64pub struct InstallLog {
65 file: std::fs::File,
67 path: PathBuf,
69}
70
71#[derive(Debug)]
81pub struct OpenedLog {
82 pub log: InstallLog,
84 pub recorded: InstallLogRecords,
86}
87
88fn parse_line(line: &str, path: &Path) -> Result<InstallLogRecords, WorktreeCopyError> {
98 let corrupt = || {
99 return failure_with_cause(
100 format!(
101 "cli-git: worktree-copy install log is corrupt: {}.",
102 quote(path.to_string_lossy().as_ref())
103 )
104 .as_str(),
105 None,
106 );
107 };
108 let value: serde_json::Value = serde_json::from_str(line).map_err(|_| return corrupt())?;
109 let object = value.as_object().ok_or_else(corrupt)?;
110 if let Some(intended) = object.get("intended").and_then(serde_json::Value::as_array) {
111 let names: Vec<String> = intended
112 .iter()
113 .map(|item| return item.as_str().map(str::to_string))
114 .collect::<Option<Vec<String>>>()
115 .ok_or_else(corrupt)?;
116 for name in &names {
117 assert_safe_repository_path(name.as_str())?;
118 }
119 return Ok(InstallLogRecords {
120 intended: names,
121 created: Vec::new(),
122 });
123 }
124 if let Some(created) = object.get("created").and_then(serde_json::Value::as_array) {
125 let entries: Vec<InstalledPath> = created
126 .iter()
127 .map(installed_entry)
128 .collect::<Option<Vec<InstalledPath>>>()
129 .ok_or_else(corrupt)?;
130 for entry in &entries {
131 assert_safe_repository_path(entry.relative_path.as_str())?;
132 }
133 return Ok(InstallLogRecords {
134 intended: Vec::new(),
135 created: entries,
136 });
137 }
138 return Err(corrupt());
139}
140
141fn read_complete_records(path: &Path) -> Result<(InstallLogRecords, u64, u64), WorktreeCopyError> {
151 assert_private_path(path, Role::JournalFile)?;
152 let content: Vec<u8> = std::fs::read(path).map_err(|error| return raw_failure(&error))?;
153 let complete_length: usize = content
154 .iter()
155 .rposition(|byte| return *byte == b'\n')
156 .map_or(0, |index| return index + 1);
157 let text: String = String::from_utf8_lossy(&content[..complete_length]).into_owned();
158 let mut records: InstallLogRecords = InstallLogRecords::default();
159 for line in text.split('\n').filter(|line| return !line.is_empty()) {
160 let parsed: InstallLogRecords = parse_line(line, path)?;
161 records.intended.extend(parsed.intended);
162 records.created.extend(parsed.created);
163 }
164 return Ok((records, complete_length as u64, content.len() as u64));
165}
166
167pub fn open_install_log(stage_container: &Path) -> Result<OpenedLog, WorktreeCopyError> {
177 let path: PathBuf = stage_container.join(INSTALL_LOG_NAME);
178 let existed: bool = std::fs::symlink_metadata(path.as_path()).is_ok();
179 let (recorded, complete_length, total_length) = if existed {
180 read_complete_records(path.as_path())?
181 } else {
182 (InstallLogRecords::default(), 0, 0)
183 };
184 let opened: std::io::Result<std::fs::File> = std::fs::OpenOptions::new()
185 .create(true)
186 .append(true)
187 .custom_flags(libc::O_NOFOLLOW)
188 .mode(0o600)
189 .open(path.as_path());
190 let file: std::fs::File = match opened {
191 Ok(file) => file,
192 Err(error) => {
193 let message: String = format!(
194 "cli-git: could not open worktree-copy install log {}.",
195 quote(path.to_string_lossy().as_ref())
196 );
197 return Err(failure_with_cause(message.as_str(), Some(&error)));
198 }
199 };
200 if complete_length < total_length {
201 file.set_len(complete_length)
202 .map_err(|error| return raw_failure(&error))?;
203 file.sync_all()
204 .map_err(|error| return raw_failure(&error))?;
205 }
206 if !existed {
207 sync_directory(stage_container).map_err(|error| return raw_failure(&error))?;
208 }
209 return Ok(OpenedLog {
210 log: InstallLog { file, path },
211 recorded,
212 });
213}
214
215impl InstallLog {
216 fn append_line(&mut self, line: &str) -> Result<(), WorktreeCopyError> {
218 let written: std::io::Result<()> = self
219 .file
220 .write_all(line.as_bytes())
221 .and_then(|()| return self.file.sync_all());
222 return written.map_err(|error| {
223 let message: String = format!(
224 "cli-git: could not persist worktree-copy install log {}.",
225 quote(self.path.to_string_lossy().as_ref())
226 );
227 return failure_with_cause(message.as_str(), Some(&error));
228 });
229 }
230
231 pub fn append_intents(&mut self, relative_paths: &[String]) -> Result<(), WorktreeCopyError> {
233 if relative_paths.is_empty() {
234 return Ok(());
235 }
236 let line: String = format!("{{\"intended\":{}}}\n", string_array(relative_paths));
237 return self.append_line(line.as_str());
238 }
239
240 pub fn append_creations(&mut self, entries: &[InstalledPath]) -> Result<(), WorktreeCopyError> {
242 if entries.is_empty() {
243 return Ok(());
244 }
245 let encoded: Vec<String> = entries.iter().map(encode_installed_entry).collect();
246 let line: String = format!("{{\"created\":[{}]}}\n", encoded.join(","));
247 return self.append_line(line.as_str());
248 }
249}
250
251#[cfg(test)]
253#[path = "worktree_copy_install_log_tests.rs"]
254mod tests;