use crate::autostash_carry::carry_stash;
use crate::conclusion_cleanup::reproduce_conclusion_cleanup;
use crate::diagnostic_log::warn;
use crate::index_holder_wait::{IndexWait, wait_for_index_release_within};
use crate::wait_ceilings::LOCK_CEILING;
use crate::wait_progress::Cadence;
use crate::inner_git_words::record_words;
use crate::service_index_lock::IndexLock;
pub use crate::service_land_copy::PrivateCopy;
use crate::service_protocol::{Cleanup, FailureCode, is_zero_object};
use crate::transaction_git::{GitContext, GitOutput, GitRequest, run_git};
use crate::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", "--no-refresh", 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", "--no-refresh", 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", "--no-refresh", 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, index: &Path, new: &str, paths: IndexPaths<'_>) -> Result<(), FailureCode> {
return update_index_within(git, repository, index, LOCK_CEILING, new, paths);
}
pub fn update_index_within(git: &GitContext, repository: &Path, index: &Path, ceiling: std::time::Duration, 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 tries: usize = 0;
loop {
let code: FailureCode = match reset_once(git, repository, new, paths, None) {
Ok(()) => return Ok(()),
Err(code) => code,
};
if code == FailureCode::IndexInUse {
match wait_for_index_release_within(index, "commit", ceiling, Cadence::from_environment(), &mut std::io::stderr()) {
IndexWait::OwnerGone(_) | IndexWait::GaveUp(_) => return Err(code),
IndexWait::Released(_) => continue,
IndexWait::NotHeld => {}
}
}
tries += 1;
if tries >= INDEX_ATTEMPTS {
return Err(code);
}
std::thread::sleep(INDEX_PAUSE);
}
}
pub fn build_index(git: &GitContext, repository: &Path, new: &str, paths: IndexPaths<'_>, lock: &IndexLock, seq: u64) -> Result<PrivateCopy, FailureCode> {
let copy: PrivateCopy = PrivateCopy::make(lock, seq)?;
let built: std::path::PathBuf = copy.path().to_path_buf();
let empty: bool = matches!(paths, IndexPaths::Listed(listed) if listed.is_empty()) || matches!(paths, IndexPaths::Named(named) if named.is_empty());
if !empty && !reset_in_process(git, repository, new, paths, (lock, ©)) && let Err(code) = reset_once(git, repository, new, paths, Some(built.as_path())) {
return Err(code);
}
return Ok(copy);
}
#[cfg(target_os = "linux")]
fn reset_in_process(git: &GitContext, repository: &Path, new: &str, paths: IndexPaths<'_>, (lock, copy): (&IndexLock, &PrivateCopy)) -> bool {
return crate::index_reset_file::reset_index_file(git, repository, new, paths, lock, copy);
}
#[cfg(not(target_os = "linux"))]
fn reset_in_process(_git: &GitContext, _repository: &Path, _new: &str, _paths: IndexPaths<'_>, _landing: (&IndexLock, &PrivateCopy)) -> bool {
return false;
}
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");
if !carry_stash(git, repository, cleanup) {
return false;
}
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;