chronicle/sync/
push_fetch.rs1use std::path::PathBuf;
2use std::process::Command;
3
4use crate::error::chronicle_error::GitSnafu;
5use crate::error::git_error::CommandFailedSnafu;
6use crate::error::{GitError, Result};
7use snafu::ResultExt;
8
9const NOTES_REF: &str = "refs/notes/chronicle";
10
11#[derive(Debug, Clone)]
13pub struct SyncConfig {
14 pub remote: String,
15 pub push_refspec: Option<String>,
16 pub fetch_refspec: Option<String>,
17}
18
19impl SyncConfig {
20 pub fn is_enabled(&self) -> bool {
21 self.push_refspec.is_some() && self.fetch_refspec.is_some()
22 }
23}
24
25#[derive(Debug, Clone)]
27pub struct SyncStatus {
28 pub enabled: bool,
29 pub local_count: usize,
30 pub remote_count: Option<usize>,
31 pub unpushed_count: usize,
32}
33
34fn run_git(repo_dir: &PathBuf, args: &[&str]) -> std::result::Result<String, GitError> {
36 let output = Command::new("git")
37 .args(args)
38 .current_dir(repo_dir)
39 .output()
40 .map_err(|e| {
41 CommandFailedSnafu {
42 message: format!("failed to run git: {e}"),
43 }
44 .build()
45 })?;
46
47 if output.status.success() {
48 Ok(String::from_utf8_lossy(&output.stdout).to_string())
49 } else {
50 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
51 Err(CommandFailedSnafu {
52 message: stderr.trim().to_string(),
53 }
54 .build())
55 }
56}
57
58fn run_git_raw(
60 repo_dir: &PathBuf,
61 args: &[&str],
62) -> std::result::Result<(bool, String, String), GitError> {
63 let output = Command::new("git")
64 .args(args)
65 .current_dir(repo_dir)
66 .output()
67 .map_err(|e| {
68 CommandFailedSnafu {
69 message: format!("failed to run git: {e}"),
70 }
71 .build()
72 })?;
73
74 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
75 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
76 Ok((output.status.success(), stdout, stderr))
77}
78
79pub fn get_sync_config(repo_dir: &PathBuf, remote: &str) -> Result<SyncConfig> {
81 let push_refspec = get_config_values(repo_dir, &format!("remote.{remote}.push"))
82 .context(GitSnafu)?
83 .into_iter()
84 .find(|r| r.contains(NOTES_REF));
85
86 let fetch_refspec = get_config_values(repo_dir, &format!("remote.{remote}.fetch"))
87 .context(GitSnafu)?
88 .into_iter()
89 .find(|r| r.contains(NOTES_REF));
90
91 Ok(SyncConfig {
92 remote: remote.to_string(),
93 push_refspec,
94 fetch_refspec,
95 })
96}
97
98pub fn enable_sync(repo_dir: &PathBuf, remote: &str) -> Result<()> {
100 let config = get_sync_config(repo_dir, remote)?;
101
102 if config.push_refspec.is_none() {
104 run_git(
105 repo_dir,
106 &[
107 "config",
108 "--add",
109 &format!("remote.{remote}.push"),
110 NOTES_REF,
111 ],
112 )
113 .context(GitSnafu)?;
114 }
115
116 if config.fetch_refspec.is_none() {
118 let fetch_spec = format!("+{NOTES_REF}:{NOTES_REF}");
119 run_git(
120 repo_dir,
121 &[
122 "config",
123 "--add",
124 &format!("remote.{remote}.fetch"),
125 &fetch_spec,
126 ],
127 )
128 .context(GitSnafu)?;
129 }
130
131 Ok(())
132}
133
134pub fn get_sync_status(repo_dir: &PathBuf, remote: &str) -> Result<SyncStatus> {
136 let config = get_sync_config(repo_dir, remote)?;
137 let enabled = config.is_enabled();
138
139 let local_count = count_local_notes(repo_dir).context(GitSnafu)?;
140
141 let remote_count = count_remote_notes(repo_dir, remote).ok();
143
144 let unpushed_count = if let Some(rc) = remote_count {
145 local_count.saturating_sub(rc)
146 } else {
147 0
148 };
149
150 Ok(SyncStatus {
151 enabled,
152 local_count,
153 remote_count,
154 unpushed_count,
155 })
156}
157
158pub fn pull_notes(repo_dir: &PathBuf, remote: &str) -> Result<()> {
160 run_git(
161 repo_dir,
162 &["fetch", remote, &format!("+{NOTES_REF}:{NOTES_REF}")],
163 )
164 .context(GitSnafu)?;
165
166 Ok(())
167}
168
169fn count_local_notes(repo_dir: &PathBuf) -> std::result::Result<usize, GitError> {
171 let (success, stdout, _) = run_git_raw(repo_dir, &["notes", "--ref", NOTES_REF, "list"])?;
172 if !success {
173 return Ok(0);
174 }
175 Ok(stdout.lines().filter(|l| !l.is_empty()).count())
176}
177
178fn count_remote_notes(repo_dir: &PathBuf, _remote: &str) -> Result<usize> {
180 let count = count_local_notes(repo_dir).context(GitSnafu)?;
184 Ok(count)
185}
186
187fn get_config_values(repo_dir: &PathBuf, key: &str) -> std::result::Result<Vec<String>, GitError> {
189 let (success, stdout, _) = run_git_raw(repo_dir, &["config", "--get-all", key])?;
190 if !success {
191 return Ok(Vec::new());
192 }
193 Ok(stdout
194 .lines()
195 .filter(|l| !l.is_empty())
196 .map(|l| l.to_string())
197 .collect())
198}