Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/adapter/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,11 @@ impl GitAdapter {

/// Extract commit authors across a revision or range (e.g. `base..head`).
pub fn authors(&self, rev_range: &str) -> Result<Vec<String>> {
// The range goes before `--`. After it git reads the range as a
// pathspec, which matches nothing and exits 0, so authors came back
// empty and every git docket fell through to the @git-author fallback.
let output = Command::new("git")
.args(["log", "--format=%an", "--", rev_range])
.args(["log", "--format=%an", rev_range, "--"])
.current_dir(&self.repo_root)
.output()
.with_context(|| format!("Failed to query authors for range '{rev_range}'"))?;
Expand Down
62 changes: 62 additions & 0 deletions tests/git_adapter_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,36 @@ impl TempGitRepo {
.expect("git checkout failed");
assert!(status.success());
}

/// Commit under a specific author identity, so docket provenance is real.
fn commit_as(&self, msg: &str, name: &str, email: &str) -> String {
let prev_name = self.git_config("user.name");
let prev_email = self.git_config("user.email");
self.set_git_config("user.name", name);
self.set_git_config("user.email", email);
let sha = self.commit(msg);
self.set_git_config("user.name", &prev_name);
self.set_git_config("user.email", &prev_email);
sha
}

fn git_config(&self, key: &str) -> String {
let out = Command::new("git")
.args(["config", "--get", key])
.current_dir(&self.path)
.output()
.expect("git config read failed");
String::from_utf8_lossy(&out.stdout).trim().to_string()
}

fn set_git_config(&self, key: &str, value: &str) {
let status = Command::new("git")
.args(["config", key, value])
.current_dir(&self.path)
.status()
.expect("git config write failed");
assert!(status.success());
}
}

impl Drop for TempGitRepo {
Expand Down Expand Up @@ -275,6 +305,38 @@ fn test_git_adapter_visibility_violation_cloaked() {
assert!(docket.disputes[0].detail.contains("private path"));
}

#[test]
fn test_git_adapter_3way_reports_real_commit_authors() {
let repo = TempGitRepo::new("3way_authors");

repo.write_file("src/lib.rs", "pub fn common() -> i32 { 0 }\n");
repo.commit_as("base version", "Base Author", "base@example.com");

repo.create_and_checkout_branch("feature/auth");
repo.write_file("src/lib.rs", "pub fn common() -> i32 { 1 }\n");
repo.commit_as("feature commit", "Dana Feature", "dana@example.com");

let adapter = GitAdapter::new(&repo.path).expect("valid git repo");
let engine = Engine::new().expect("valid engine");

let docket = adapter
.adjudicate_3way(
"main",
"feature/auth",
&engine,
&MeaningPolicy::default(),
&VisibilityPolicy::default(),
&GitAdjudicateOptions::default(),
)
.expect("adjudicate 3way");

assert_eq!(
docket.authors,
vec!["Dana Feature".to_string()],
"docket must name the commits' real author, not the @git-author fallback"
);
}

#[test]
fn test_cli_git_adjudication_flags() {
let repo = TempGitRepo::new("cli_git");
Expand Down
Loading