use crate::git::attributes;
use crate::git::repo::{CONFIG_FILE, Repo};
use crate::rules::declaration::Config;
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outcome {
UpToDate,
Updated,
Stale,
}
#[derive(Debug)]
pub struct Report {
pub outcome: Outcome,
pub warnings: Vec<String>,
pub foreign: usize,
}
pub fn run(repo: &Repo, check: bool, rendering: attributes::Rendering) -> Result<Report> {
let config = Config::load(&repo.xcrypt_config_path())?;
if config.missing {
return Err(Error::Config(format!(
"{CONFIG_FILE} is missing, so there is nothing to synchronise; \
run `git-xcrypt init` to create it"
)));
}
let lines = attributes::render_lines(&config, rendering);
let path = repo.attributes_path();
let outcome = if check {
let current = attributes::ACCEPTED.into_iter().any(|rendering| {
let lines = attributes::render_lines(&config, rendering);
attributes::desired(&path, &lines).is_ok_and(|(existing, wanted)| existing == wanted)
});
if current {
Outcome::UpToDate
} else {
Outcome::Stale
}
} else if attributes::write_section(&path, &lines)? {
Outcome::Updated
} else {
Outcome::UpToDate
};
let foreign = attributes::foreign_lines_touching(&path, &["filter", "text", "eol", "crlf"])
.map(|lines| lines.len())
.unwrap_or(0);
Ok(Report {
outcome,
foreign,
warnings: config.pointless_eol,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::process::Command;
use tempfile::TempDir;
fn init_repo() -> TempDir {
let dir = TempDir::new().expect("temporary directory");
let ok = Command::new("git")
.args(["init", "-q"])
.current_dir(dir.path())
.status()
.expect("git must be on PATH")
.success();
assert!(ok, "git init failed");
dir
}
fn prepared(declarations: &str) -> (TempDir, Repo) {
let dir = init_repo();
let repo = Repo::discover(dir.path()).expect("discovery");
crate::commands::init::run(&repo).expect("init must succeed");
fs::write(repo.xcrypt_config_path(), declarations).expect("writing the declarations");
(dir, repo)
}
#[test]
fn only_the_per_pattern_section_can_go_stale() {
let (_dir, repo) = prepared("secrets/\n");
let before = fs::read_to_string(repo.attributes_path()).expect("attributes");
assert_eq!(
run(&repo, true, attributes::Rendering::Global)
.expect("check must succeed")
.outcome,
Outcome::UpToDate,
"a global section was called stale, which it cannot be"
);
fs::write(repo.xcrypt_config_path(), "secrets/\n*.env\nmore/\n")
.expect("changing the declarations");
assert_eq!(
run(&repo, true, attributes::Rendering::Global)
.expect("check must succeed")
.outcome,
Outcome::UpToDate,
"a global section went stale over a changed declaration"
);
assert_eq!(
before,
fs::read_to_string(repo.attributes_path()).expect("attributes"),
"--check must never touch the working tree"
);
let per_pattern = attributes::Rendering::PerPattern { fold_case: false };
run(&repo, false, per_pattern).expect("sync");
assert_eq!(
run(&repo, true, per_pattern).expect("check").outcome,
Outcome::UpToDate,
"the check and the write must not disagree"
);
let before = fs::read_to_string(repo.attributes_path()).expect("attributes");
fs::write(
repo.xcrypt_config_path(),
"secrets/\n*.env\nmore/\nlater/\n",
)
.expect("changing the declarations again");
assert_eq!(
run(&repo, true, per_pattern).expect("check").outcome,
Outcome::Stale,
"a split section did not notice the declaration changing under it"
);
assert_eq!(
before,
fs::read_to_string(repo.attributes_path()).expect("attributes"),
"--check must never touch the working tree"
);
}
}