upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/git_mirror.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/git_mirror.rs')
-rw-r--r--src/git_mirror.rs159
1 files changed, 159 insertions, 0 deletions
diff --git a/src/git_mirror.rs b/src/git_mirror.rs
new file mode 100644
index 0000000..47c0442
--- /dev/null
+++ b/src/git_mirror.rs
@@ -0,0 +1,159 @@
1use crate::db::MirrorDb;
2use crate::discovery::DiscoveredRepo;
3use crate::health::GraspServer;
4use anyhow::{Context, Result};
5use git2::RemoteCallbacks;
6use std::path::Path;
7
8pub struct GitMirror {
9 mirror_dir: std::path::PathBuf,
10}
11
12impl GitMirror {
13 pub fn new(mirror_dir: &Path) -> Self {
14 Self {
15 mirror_dir: mirror_dir.to_path_buf(),
16 }
17 }
18
19 fn repo_path(&self, pubkey: &str, identifier: &str) -> std::path::PathBuf {
20 self.mirror_dir
21 .join(pubkey)
22 .join(format!("{}.git", identifier))
23 }
24
25 pub async fn mirror_repo_to_servers(
26 &self,
27 db: &MirrorDb,
28 repo: &DiscoveredRepo,
29 target_servers: &[GraspServer],
30 ) -> Result<()> {
31 if target_servers.is_empty() {
32 tracing::debug!(
33 identifier = %repo.identifier,
34 "no missing servers to mirror to"
35 );
36 return Ok(());
37 }
38
39 let pk_hex = repo.pubkey.to_hex();
40 let repo_path = self.repo_path(&pk_hex, &repo.identifier);
41
42 if !repo_path.exists() {
43 self.clone_bare(&repo_path, &repo.clone_urls)?;
44 }
45
46 for server in target_servers {
47 let target_url = server.clone_url(&pk_hex, &repo.identifier);
48
49 tracing::info!(
50 identifier = %repo.identifier,
51 server = %server.domain,
52 target = %target_url,
53 "mirroring git data"
54 );
55
56 let repo_id = db.get_all_repos().await.ok().and_then(|repos| {
57 repos
58 .iter()
59 .find(|r| r.pubkey == pk_hex && r.identifier == repo.identifier)
60 .map(|r| r.id)
61 });
62
63 match self.push_mirror(&repo_path, &target_url) {
64 Ok(()) => {
65 tracing::info!(
66 identifier = %repo.identifier,
67 server = %server.domain,
68 "git mirror succeeded"
69 );
70 if let Some(id) = repo_id {
71 let _ = db.mark_git_synced(id, &server.domain).await;
72 }
73 }
74 Err(e) => {
75 tracing::error!(
76 identifier = %repo.identifier,
77 server = %server.domain,
78 error = %e,
79 "git mirror failed"
80 );
81 if let Some(id) = repo_id {
82 let _ = db.mark_sync_error(id, &server.domain, &e.to_string()).await;
83 }
84 }
85 }
86 }
87
88 Ok(())
89 }
90
91 fn clone_bare(&self, repo_path: &Path, clone_urls: &[String]) -> Result<()> {
92 if let Some(parent) = repo_path.parent() {
93 std::fs::create_dir_all(parent)
94 .with_context(|| format!("failed to create {:?}", parent))?;
95 }
96
97 let mut last_error = None;
98
99 for url in clone_urls {
100 if url.is_empty() {
101 continue;
102 }
103 tracing::info!(url = %url, path = ?repo_path, "cloning bare repo");
104
105 let mut callbacks = RemoteCallbacks::new();
106 callbacks.credentials(|_url, _username, _allowed| git2::Cred::default());
107
108 let mut fetch_opts = git2::FetchOptions::new();
109 fetch_opts.remote_callbacks(callbacks);
110
111 let mut builder = git2::build::RepoBuilder::new();
112 builder.bare(true).fetch_options(fetch_opts);
113
114 match builder.clone(url, repo_path) {
115 Ok(_) => {
116 tracing::info!(url = %url, "bare clone succeeded");
117 return Ok(());
118 }
119 Err(e) => {
120 tracing::warn!(url = %url, error = %e, "clone failed, trying next URL");
121 last_error = Some(e);
122 if repo_path.exists() {
123 let _ = std::fs::remove_dir_all(repo_path);
124 }
125 }
126 }
127 }
128
129 let err = last_error.unwrap_or_else(|| git2::Error::from_str("no clone URLs available"));
130 Err(err).with_context(|| format!("all clone attempts failed for {:?}", repo_path))
131 }
132
133 fn push_mirror(&self, repo_path: &Path, target_url: &str) -> Result<()> {
134 let repo = git2::Repository::open(repo_path)
135 .with_context(|| format!("failed to open bare repo at {:?}", repo_path))?;
136
137 let mut remote = repo.remote("push_target", target_url)?;
138
139 let mut callbacks = RemoteCallbacks::new();
140 callbacks.credentials(|_url, _username, _allowed| git2::Cred::default());
141 callbacks.push_update_reference(|_refname, status| {
142 if let Some(s) = status {
143 tracing::warn!(status = %s, "push rejected");
144 }
145 Ok(())
146 });
147
148 let mut push_opts = git2::PushOptions::new();
149 push_opts.remote_callbacks(callbacks);
150
151 let refspecs = ["+refs/*:refs/*"];
152
153 remote
154 .push(&refspecs, Some(&mut push_opts))
155 .with_context(|| format!("failed to push mirror to {}", target_url))?;
156
157 Ok(())
158 }
159}