aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian Korotkiewicz <skorotkiewicz@gmail.com>2026-08-10 03:41:50 +0200
committerSebastian Korotkiewicz <skorotkiewicz@gmail.com>2026-08-10 03:41:50 +0200
signature Sebastian Korotkiewicz <skorotkiewicz@gmail.com>

This commit was signed with a verified signature.

Signer
Sebastian Korotkiewicz <skorotkiewicz@gmail.com>
Signing key
5BDC557B496BDB0D
Fingerprint
B498E2E410902F8AEC108F4F5BDC557B496BDB0D
commit8474b525c601adc463ea978bb6fde7e4b3363ee6 (patch)
tree431193187cd2fc2f633a640f77a22d52f9f2f336
parent72f9ac7f014d755135e7ced6ed075b4b992ac7ee (diff)
downloadaurcade-8474b525c601adc463ea978bb6fde7e4b3363ee6.tar.gz
aurcade-8474b525c601adc463ea978bb6fde7e4b3363ee6.zip
Add push victory tracking and display
Introduce `PushVictory` struct and related functions to track and display statistics for git pushes: number of commits, verified signatures, and branches updated. Capture branch refs before `git-receive-pack`, compute the delta after a successful push, and print a summary message. Add test to verify push victory calculation with a sample repository.
-rw-r--r--src/main.rs171
1 files changed, 170 insertions, 1 deletions
diff --git a/src/main.rs b/src/main.rs
index ffe3cff..f7c46a7 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,6 @@
use serde::Deserialize;
use std::{
- collections::{BTreeSet, HashSet},
+ collections::{BTreeMap, BTreeSet, HashSet},
env,
fs::{self, OpenOptions},
io::{self, Write},
@@ -741,6 +741,120 @@ fn ssh_lobby(config: &Config, account: &Account, root: &Path) -> Result<String,
Ok(output)
}
+struct PushVictory {
+ commits: usize,
+ verified: usize,
+ branches: Vec<String>,
+}
+
+fn branch_refs(repository: &Path) -> Result<BTreeMap<String, String>, Error> {
+ let output = Command::new("git")
+ .arg("--git-dir")
+ .arg(repository)
+ .args([
+ "for-each-ref",
+ "--format=%(refname:strip=2)%09%(objectname)",
+ "refs/heads",
+ ])
+ .output()?;
+ if !output.status.success() {
+ return Err("failed to read branch refs".into());
+ }
+ Ok(String::from_utf8(output.stdout)?
+ .lines()
+ .filter_map(|line| line.split_once('\t'))
+ .map(|(branch, commit)| (branch.to_owned(), commit.to_owned()))
+ .collect())
+}
+
+fn push_victory(
+ repository: &Path,
+ before: &BTreeMap<String, String>,
+) -> Result<PushVictory, Error> {
+ let after = branch_refs(repository)?;
+ let branches: Vec<String> = before
+ .keys()
+ .chain(after.keys())
+ .filter(|branch| before.get(*branch) != after.get(*branch))
+ .cloned()
+ .collect::<BTreeSet<_>>()
+ .into_iter()
+ .collect();
+ let new_tips: Vec<&str> = after
+ .iter()
+ .filter(|(branch, commit)| before.get(*branch) != Some(*commit))
+ .map(|(_, commit)| commit.as_str())
+ .collect();
+ if new_tips.is_empty() {
+ return Ok(PushVictory {
+ commits: 0,
+ verified: 0,
+ branches,
+ });
+ }
+
+ let mut command = Command::new("git");
+ command
+ .arg("-c")
+ .arg(format!(
+ "gpg.ssh.allowedSignersFile={}",
+ signing_root().join("allowed_signers").display()
+ ))
+ .arg("-c")
+ .arg("gpg.openpgp.program=/usr/bin/gpg")
+ .env("GNUPGHOME", signing_root().join("gnupg"))
+ .arg("--git-dir")
+ .arg(repository)
+ .args(["log", "--format=%H%x09%G?%x09%GS"])
+ .args(new_tips);
+ if !before.is_empty() {
+ command.arg("--not").args(before.values());
+ }
+ let output = command.stderr(Stdio::null()).output()?;
+ if !output.status.success() {
+ return Err("failed to inspect pushed commits".into());
+ }
+ let mut commits = 0;
+ let mut verified = 0;
+ for line in String::from_utf8_lossy(&output.stdout).lines() {
+ commits += 1;
+ let mut fields = line.split('\t');
+ if matches!(
+ (fields.next(), fields.next(), fields.next(), fields.next()),
+ (Some(_), Some("G"), Some(signer), None) if !signer.is_empty()
+ ) {
+ verified += 1;
+ }
+ }
+ Ok(PushVictory {
+ commits,
+ verified,
+ branches,
+ })
+}
+
+fn print_push_victory(victory: &PushVictory) {
+ let commits = if victory.commits == 1 {
+ "commit"
+ } else {
+ "commits"
+ };
+ let signatures = if victory.verified == 1 {
+ "signature"
+ } else {
+ "signatures"
+ };
+ let update = if victory.branches.is_empty() {
+ "refs unchanged".to_owned()
+ } else {
+ format!("{} updated", victory.branches.join(", "))
+ };
+ eprintln!(
+ "\nNEW HIGH SCORE!\n{} {commits} · {} verified {signatures} · {update}",
+ victory.commits, victory.verified
+ );
+}
+
fn serve(config: &Config, account_name: &str) -> Result<(), Error> {
let account = config
.accounts
@@ -800,6 +914,11 @@ fn serve(config: &Config, account_name: &str) -> Result<(), Error> {
} else {
None
};
+ let refs_before = if action == "git-receive-pack" {
+ branch_refs(&path).ok()
+ } else {
+ None
+ };
let status = match Command::new(action).arg(&path).status() {
Ok(status) => status,
@@ -812,6 +931,10 @@ fn serve(config: &Config, account_name: &str) -> Result<(), Error> {
if !status.success() {
return Err(format!("{action} failed for {repository}").into());
}
+ let victory = refs_before
+ .as_ref()
+ .and_then(|before| push_victory(&path, before).ok())
+ .filter(|victory| !victory.branches.is_empty());
if created {
fs::create_dir_all(destination.parent().expect("repository parent"))?;
if let Err(error) = fs::rename(&path, &destination) {
@@ -826,6 +949,9 @@ fn serve(config: &Config, account_name: &str) -> Result<(), Error> {
write_cgit_config(config, &root)?;
}
}
+ if let Some(victory) = victory {
+ print_push_victory(&victory);
+ }
Ok(())
}
@@ -1114,6 +1240,49 @@ mod tests {
}
#[test]
+ fn calculates_push_victory() {
+ let directory = test_directory("push-victory");
+ let source = directory.join("source");
+ let repository = directory.join("repository.git");
+ let git = |arguments: &[&str]| {
+ assert!(
+ Command::new("git")
+ .arg("-C")
+ .arg(&source)
+ .args(arguments)
+ .status()
+ .unwrap()
+ .success()
+ );
+ };
+ assert!(
+ Command::new("git")
+ .args(["init", "--quiet", "--initial-branch=main"])
+ .arg(&source)
+ .status()
+ .unwrap()
+ .success()
+ );
+ git(&["config", "user.name", "AURcade"]);
+ git(&["config", "user.email", "aurcade@example.invalid"]);
+ fs::write(source.join("score"), "one\n").unwrap();
+ git(&["add", "score"]);
+ git(&["commit", "--quiet", "-m", "one"]);
+ fs::write(source.join("score"), "two\n").unwrap();
+ git(&["commit", "--quiet", "-am", "two"]);
+ init_repository(&directory, "repository").unwrap();
+ let before = branch_refs(&repository).unwrap();
+ git(&["remote", "add", "origin", repository.to_str().unwrap()]);
+ git(&["push", "--quiet", "origin", "main"]);
+
+ let victory = push_victory(&repository, &before).unwrap();
+ assert_eq!(victory.commits, 2);
+ assert_eq!(victory.verified, 0);
+ assert_eq!(victory.branches, ["main"]);
+ fs::remove_dir_all(directory).unwrap();
+ }
+
+ #[test]
fn metadata_id_only_changes_with_metadata() {
let directory = test_directory("metadata-id");
let repository = directory.join("repository");