pub mod branch;
pub mod compact;
pub mod doctor;
pub mod gc;
pub(crate) mod gc_output;
pub(crate) mod snapshot;
use std::fmt;
use std::io;
use thiserror::Error;
use crate::keys;
use crate::object_store::{ObjectMeta, ObjectStoreError};
#[allow(clippy::ptr_arg, clippy::trivially_copy_pass_by_ref)]
fn fmt_partial_delete(
branch: &String,
undeleted: &Vec<String>,
attempted: &usize,
f: &mut fmt::Formatter<'_>,
) -> fmt::Result {
let n = undeleted.len();
let noun = if n == 1 { "key" } else { "keys" };
write!(
f,
"delete-branch {branch} failed: {n} of {attempted} {noun} could not be deleted: {} (retry to converge)",
undeleted.join(", "),
)
}
pub use crate::protocol::push::DEFAULT_LOCK_TTL_SECONDS;
#[allow(clippy::case_sensitive_file_extension_comparisons)]
pub(crate) fn is_lock_key(key: &str) -> bool {
key.ends_with(".lock")
}
pub(crate) fn has_branch_data(entries: &[ObjectMeta]) -> bool {
entries.iter().any(|entry| {
let last = entry
.key
.rsplit_once('/')
.map_or(entry.key.as_str(), |(_, s)| s);
!is_lock_key(&entry.key) && !keys::is_protected_marker_segment(last)
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StaleReason {
Deleted,
ResidueOnly,
}
impl fmt::Display for StaleReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Deleted => f.write_str("was deleted between selection and write"),
Self::ResidueOnly => f.write_str(
"is considered gone — only operational metadata \
(lock files / PROTECTED# marker) remains under its prefix",
),
}
}
}
#[derive(Debug, Error)]
pub enum ManageError {
#[error(transparent)]
Store(#[from] ObjectStoreError),
#[error("branch not found: {0}")]
BranchNotFound(String),
#[error(
"ref is protected. Run git-remote-object-store unprotect <url> <branch> to remove protection before deleting."
)]
Protected(String),
#[error(
"could not acquire ref lock at {lock}. Another client may be pushing or deleting. If this persists beyond {ttl_seconds}s, run git-remote-object-store doctor to inspect and optionally clear stale locks."
)]
LockContended {
branch: String,
lock: String,
ttl_seconds: i64,
},
#[error(fmt = fmt_partial_delete)]
PartialDelete {
branch: String,
undeleted: Vec<String>,
attempted: usize,
},
#[error("invalid branch name: {0}")]
InvalidBranch(String),
#[error("operation cancelled")]
Cancelled,
#[error(transparent)]
Io(#[from] io::Error),
#[error("internal management error: {0}")]
Internal(String),
#[error("doctor snapshot is stale: {entity} {reason}; re-run doctor")]
StaleSnapshot {
entity: String,
reason: StaleReason,
},
#[error(transparent)]
Packchain(#[from] crate::packchain::PackchainError),
}
pub trait Prompter: Send + Sync {
fn select(&self, prompt: &str, options: &[String]) -> Result<usize, ManageError>;
fn confirm(&self, prompt: &str) -> Result<bool, ManageError>;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct DialoguerPrompter;
impl Prompter for DialoguerPrompter {
fn select(&self, prompt: &str, options: &[String]) -> Result<usize, ManageError> {
Ok(dialoguer::Select::new()
.with_prompt(prompt)
.items(options)
.default(0)
.interact()?)
}
fn confirm(&self, prompt: &str) -> Result<bool, ManageError> {
Ok(dialoguer::Confirm::new()
.with_prompt(prompt)
.default(false)
.interact()?)
}
}
impl From<dialoguer::Error> for ManageError {
fn from(err: dialoguer::Error) -> Self {
match err {
dialoguer::Error::IO(io_err) if io_err.kind() == io::ErrorKind::Interrupted => {
ManageError::Cancelled
}
dialoguer::Error::IO(io_err) => ManageError::Io(io_err),
}
}
}
#[cfg(any(test, feature = "test-util"))]
pub use scripted::ScriptedPrompter;
#[cfg(any(test, feature = "test-util"))]
mod scripted {
use std::collections::VecDeque;
use std::sync::Mutex;
use super::{ManageError, Prompter};
pub struct ScriptedPrompter {
answers: Mutex<VecDeque<Answer>>,
}
#[derive(Debug, Clone)]
pub enum Answer {
Select(usize),
Confirm(bool),
Cancel,
}
impl ScriptedPrompter {
#[must_use]
pub fn new(answers: impl IntoIterator<Item = Answer>) -> Self {
Self {
answers: Mutex::new(answers.into_iter().collect()),
}
}
#[must_use]
pub fn remaining(&self) -> usize {
self.answers.lock().expect("scripted mutex poisoned").len()
}
fn pop(&self) -> Result<Answer, ManageError> {
self.answers
.lock()
.expect("scripted mutex poisoned")
.pop_front()
.ok_or(ManageError::Cancelled)
}
}
impl Prompter for ScriptedPrompter {
fn select(&self, _prompt: &str, _options: &[String]) -> Result<usize, ManageError> {
match self.pop()? {
Answer::Select(i) => Ok(i),
Answer::Cancel => Err(ManageError::Cancelled),
Answer::Confirm(_) => panic!("expected Select answer, got Confirm"),
}
}
fn confirm(&self, _prompt: &str) -> Result<bool, ManageError> {
match self.pop()? {
Answer::Confirm(b) => Ok(b),
Answer::Cancel => Err(ManageError::Cancelled),
Answer::Select(_) => panic!("expected Confirm answer, got Select"),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stale_snapshot_deleted_display_names_branch_and_uses_deleted_wording() {
let err = ManageError::StaleSnapshot {
entity: "refs/heads/main".to_owned(),
reason: StaleReason::Deleted,
};
let rendered = err.to_string();
assert!(
rendered.contains("refs/heads/main"),
"Display must name the entity: {rendered}",
);
assert!(
rendered.contains("was deleted between selection and write"),
"Deleted branch must use the 'was deleted' wording: {rendered}",
);
assert!(
rendered.contains("re-run doctor"),
"Display must instruct the operator to re-run: {rendered}",
);
}
#[test]
fn stale_snapshot_residue_only_display_names_branch_and_uses_residue_wording() {
let err = ManageError::StaleSnapshot {
entity: "refs/heads/main".to_owned(),
reason: StaleReason::ResidueOnly,
};
let rendered = err.to_string();
assert!(
rendered.contains("refs/heads/main"),
"Display must name the entity: {rendered}",
);
assert!(
rendered.contains("only operational metadata"),
"ResidueOnly must mention operational metadata: {rendered}",
);
assert!(
rendered.contains("PROTECTED# marker"),
"ResidueOnly must mention the PROTECTED# marker: {rendered}",
);
assert!(
!rendered.contains("was deleted between selection and write"),
"ResidueOnly must NOT use the 'was deleted' wording — that's \
precisely the bug issue #199 fixed: {rendered}",
);
assert!(
rendered.contains("re-run doctor"),
"Display must instruct the operator to re-run: {rendered}",
);
}
}