anvilsign in

collin/anvil

1//! Push mirroring: after a successful push to an anvil repo, mirror all refs
2//! to a configured remote (e.g. a GitHub repo).
3//!
4//! The actual protocol work is the pure-gitoxide send-pack client in
5//! [`crate::push`] — no `git` binary involved (a design rule; see CLAUDE.md).
6//! Mirroring is best-effort and runs in the background: a failure is logged
7//! (with credentials stripped) and never affects the push that triggered it.
8//!
9//! For GitHub over HTTPS, use a token URL:
10//! `https://x-access-token:<token>@github.com/you/repo.git`. The URL is
11//! stored as-is in the database — treat it like a secret.
12
13use std::path::PathBuf;
14
15/// Spawn a background mirror push of `repo_path` to `url`. Returns
16/// immediately; the result is only logged.
17pub fn spawn_push(repo_path: PathBuf, url: String) {
18 tokio::spawn(async move {
19 let shown = redact(&url);
20 match crate::push::mirror(&repo_path, &url).await {
21 Ok(outcome) if outcome.up_to_date => {
22 tracing::info!(
23 "mirror: {} already up to date at {shown}",
24 repo_path.display()
25 )
26 }
27 Ok(outcome) => tracing::info!(
28 "mirror: pushed {} to {shown} ({} updated, {} deleted)",
29 repo_path.display(),
30 outcome.updated,
31 outcome.deleted
32 ),
33 Err(e) => tracing::error!(
34 "mirror: push of {} to {shown} failed: {}",
35 repo_path.display(),
36 redact(&e)
37 ),
38 }
39 });
40}
41
42/// Strip the userinfo (`user:token@`) out of anything URL-shaped so secrets
43/// never reach the log.
44fn redact(text: &str) -> String {
45 let mut out = String::with_capacity(text.len());
46 for (i, part) in text.split("://").enumerate() {
47 if i == 0 {
48 out.push_str(part);
49 continue;
50 }
51 out.push_str("://");
52 match part.split_once('@') {
53 // Heuristic: an '@' before the next '/' is userinfo.
54 Some((userinfo, rest)) if !userinfo.contains('/') => {
55 out.push_str("***@");
56 out.push_str(rest);
57 }
58 _ => out.push_str(part),
59 }
60 }
61 out
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn redacts_userinfo_only() {
70 assert_eq!(
71 redact("https://x-access-token:ghp_abc@github.com/a/b.git"),
72 "https://***@github.com/a/b.git"
73 );
74 assert_eq!(
75 redact("error: https://github.com/a/b.git denied"),
76 "error: https://github.com/a/b.git denied"
77 );
78 assert_eq!(redact("no urls here"), "no urls here");
79 }
80}