use super::conclusion_cleanup::reproduce_conclusion_cleanup;
use super::diagnostic_log::warn;
use super::inner_git_words::record_words;
use super::service_protocol::{Cleanup, FailureCode, is_zero_object};
use super::transaction_git::{GitContext, GitOutput, GitRequest, run_git};
use super::transaction_journal::RefFormat;
use std::path::Path;
const INDEX_ATTEMPTS: usize = 5;
const INDEX_PAUSE: std::time::Duration = std::time::Duration::from_millis(100);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum IndexPaths<'a> {
Listed(&'a [String]),
Named(&'a [Vec<u8>]),
Derived {
old: &'a str,
},
}
fn run(git: &GitContext, repository: &Path, arguments: &[&str], input: Option<Vec<u8>>, index: Option<&Path>) -> Result<GitOutput, FailureCode> {
let mut request: GitRequest = GitRequest::new(repository, arguments);
request.input = input;
request.index = index.map(Path::to_path_buf);
return run_git(git, &request).map_err(|_| { return FailureCode::Unavailable; });
}
pub fn classify(output: &GitOutput) -> FailureCode {
let text: String = output.error_text().to_lowercase();
if text.contains("no space left") || text.contains("quota exceeded") {
return FailureCode::RepositoryFull;
}
if text.contains("read-only file system") || text.contains("permission denied") {
return FailureCode::RepositoryReadOnly;
}
if text.contains("index.lock") {
return FailureCode::IndexInUse;
}
return FailureCode::Unavailable;
}
fn literal(path: &[u8]) -> Vec<u8> {
let mut spec: Vec<u8> = b":(top,literal)".to_vec();
spec.extend_from_slice(path);
return spec;
}
fn derived_names(git: &GitContext, repository: &Path, old: &str, new: &str) -> Result<Vec<u8>, FailureCode> {
let mut arguments: Vec<&str> = vec!["diff-tree", "-r", "--name-only", "-z", "--no-renames"];
if is_zero_object(old) {
arguments.push("--root");
arguments.push(new);
} else {
arguments.push(old);
arguments.push(new);
}
let output: GitOutput = run(git, repository, &arguments, None, None)?;
if !output.succeeded() {
return Err(classify(&output));
}
return Ok(output.stdout);
}
fn pathspec_input(names: &[u8]) -> Vec<u8> {
let mut input: Vec<u8> = Vec::new();
for name in names.split(|byte| { return *byte == 0; }).filter(|name| { return !name.is_empty(); }) {
input.extend(literal(name));
input.push(0);
}
return input;
}
fn reset_once(git: &GitContext, repository: &Path, new: &str, paths: IndexPaths<'_>, index: Option<&Path>) -> Result<(), FailureCode> {
let output: GitOutput = match paths {
IndexPaths::Listed(listed) => {
let specs: Vec<String> = listed.iter().map(|path| { return format!(":(top,literal){path}"); }).collect();
let mut arguments: Vec<&str> = vec!["reset", new, "--"];
arguments.extend(specs.iter().map(String::as_str));
run(git, repository, &arguments, None, index)?
}
IndexPaths::Named(names) => {
let joined: Vec<u8> = names.iter().flat_map(|name| { return name.iter().copied().chain(std::iter::once(0)); }).collect();
let input: Vec<u8> = pathspec_input(joined.as_slice());
if input.is_empty() {
return Ok(());
}
run(git, repository, &["reset", new, "--pathspec-from-file=-", "--pathspec-file-nul"], Some(input), index)?
}
IndexPaths::Derived { old } => {
let input: Vec<u8> = pathspec_input(derived_names(git, repository, old, new)?.as_slice());
if input.is_empty() {
return Ok(());
}
run(git, repository, &["reset", new, "--pathspec-from-file=-", "--pathspec-file-nul"], Some(input), index)?
}
};
if !output.succeeded() {
return Err(classify(&output));
}
record_words(&["reset", "<commit>", "--", "<paths>"], &output);
return Ok(());
}
pub fn update_index(git: &GitContext, repository: &Path, new: &str, paths: IndexPaths<'_>) -> Result<(), FailureCode> {
if matches!(paths, IndexPaths::Listed(listed) if listed.is_empty()) || matches!(paths, IndexPaths::Named(named) if named.is_empty()) {
return Ok(());
}
let mut last: FailureCode = FailureCode::Unavailable;
for attempt in 0..INDEX_ATTEMPTS {
match reset_once(git, repository, new, paths, None) {
Ok(()) => return Ok(()),
Err(code) => last = code,
}
if attempt + 1 < INDEX_ATTEMPTS {
std::thread::sleep(INDEX_PAUSE);
}
}
return Err(last);
}
pub fn build_index(git: &GitContext, repository: &Path, new: &str, paths: IndexPaths<'_>, index: &Path, seq: u64) -> Result<std::path::PathBuf, FailureCode> {
let built: std::path::PathBuf = index.with_file_name(format!("index.cli-git-{seq}"));
let _stale = std::fs::remove_file(&built);
if index.exists() {
std::fs::copy(index, &built).map_err(|error| { return storage_failure(&error); })?;
}
let empty: bool = matches!(paths, IndexPaths::Listed(listed) if listed.is_empty()) || matches!(paths, IndexPaths::Named(named) if named.is_empty());
if !empty && let Err(code) = reset_once(git, repository, new, paths, Some(built.as_path())) {
let _removed = std::fs::remove_file(&built);
return Err(code);
}
return Ok(built);
}
pub fn storage_failure(error: &std::io::Error) -> FailureCode {
return match error.raw_os_error() {
Some(code) if code == 28 || code == 122 => FailureCode::RepositoryFull,
Some(code) if code == 30 || code == 13 => FailureCode::RepositoryReadOnly,
_ => FailureCode::Unavailable,
};
}
pub fn remove_conclusion_state(git: &GitContext, repository: &Path, cleanup: &Cleanup) -> bool {
if !cleanup.scratch.exists() {
return true;
}
let format: RefFormat = if cleanup.reftable { RefFormat::Reftable } else { RefFormat::Files };
let shadow = cleanup.scratch.join("shadow");
return match reproduce_conclusion_cleanup(git, repository, cleanup.git_dir.as_path(), shadow.as_path(), cleanup.scratch.as_path(), format) {
Ok(()) => true,
Err(error) => {
warn("serviceLandFollow", format!("removing the conclusion state failed: {}", error.0).as_str());
false
}
};
}
#[cfg(test)]
#[path = "service_land_follow_tests.rs"]
mod tests;