1use std::path::Path;
4use std::time::Duration;
5
6use anyhow::Result;
7
8use crate::{
9 Store,
10 git::{self, RepositoryState},
11};
12
13pub const STORE_GIT_DIR: &str = ".git/";
15
16pub const GIT_PULL_OUTDATED: Duration = Duration::from_secs(30);
21
22pub struct Sync<'a> {
24 store: &'a Store,
26}
27
28impl<'a> Sync<'a> {
29 pub fn new(store: &'a Store) -> Sync<'a> {
31 Self { store }
32 }
33
34 fn path(&self) -> &Path {
36 &self.store.root
37 }
38
39 pub fn readyness(&self) -> Result<Readyness> {
45 let path = self.path();
46
47 if !self.is_init() {
48 return Ok(Readyness::NoSync);
49 }
50
51 match git::git_state(path).unwrap() {
52 RepositoryState::Clean => {
53 if is_dirty(path)? {
54 Ok(Readyness::Dirty)
55 } else {
56 Ok(Readyness::Ready)
57 }
58 }
59 state => Ok(Readyness::RepoState(state)),
60 }
61 }
62
63 pub fn prepare(&self) -> Result<()> {
68 if !self.is_init() {
72 return Ok(());
73 }
74
75 if !self.has_remote()? {
77 return Ok(());
78 }
79
80 let repo = self.path();
82 if git::git_branch_upstream(repo, "HEAD")?.is_none() {
83 let remotes = self.tracked_remote_or_remotes()?;
85 if remotes.len() != 1 {
86 return Ok(());
87 }
88
89 let remote = &remotes[0];
91 git::git_fetch(repo, Some(remote))?;
92
93 let remote_branches = git::git_branch_remote(repo)?;
95 if remote_branches.is_empty() {
96 return Ok(());
97 }
98
99 let branch = git::git_current_branch(repo)?;
101 let upstream_ref = format!("{remote}/{branch}");
102
103 if !remote_branches.contains(&upstream_ref) {
105 return Ok(());
106 }
107 git::git_branch_set_upstream(repo, None, &upstream_ref)?;
108 }
109
110 self.pull()?;
111
112 Ok(())
113 }
114
115 pub fn finalize<M: AsRef<str>>(&self, msg: M) -> Result<()> {
121 if !self.is_init() {
123 return Ok(());
124 }
125
126 if is_dirty(self.path())? {
128 self.commit_all(msg, false)?;
129 }
130
131 if !self.has_remote()? || !safe_need_to_push(self.path()) {
133 return Ok(());
134 }
135
136 let mut set_branch = None;
138 let mut set_upstream = None;
139 let repo = self.path();
140 if git::git_branch_upstream(repo, "HEAD")?.is_none() {
141 let remotes = git::git_remote(repo)?;
143 if remotes.len() == 1 {
144 let remote = &remotes[0];
146 git::git_fetch(repo, Some(remote))?;
147 let remote_branches = git::git_branch_remote(repo)?;
148
149 let branch = git::git_current_branch(repo)?;
151 let upstream_ref = format!("{remote}/{branch}");
152
153 if !remote_branches.contains(&upstream_ref) {
155 set_branch = Some(branch);
156 set_upstream = Some(remote.to_string());
157 }
158 }
159 }
160
161 self.push(set_branch.as_deref(), set_upstream.as_deref())?;
162
163 Ok(())
164 }
165
166 pub fn init(&self) -> Result<()> {
168 git::git_init(self.path())?;
169 self.commit_all("Initialize sync with git", true)?;
170 Ok(())
171 }
172
173 pub fn clone(&self, url: &str, quiet: bool) -> Result<()> {
175 let path = self
176 .path()
177 .to_str()
178 .expect("failed to determine clone path");
179 git::git_clone(self.path(), url, path, quiet)?;
180 Ok(())
181 }
182
183 pub fn is_init(&self) -> bool {
185 self.path().join(STORE_GIT_DIR).is_dir()
186 }
187
188 pub fn remotes(&self) -> Result<Vec<String>> {
190 git::git_remote(self.path())
191 }
192
193 pub fn tracked_remote_or_remotes(&self) -> Result<Vec<String>> {
195 let branch = git::git_current_branch(self.path())?;
197 let remote = git::git_config_branch_remote(self.path(), &branch);
198 if let Ok(Some(remote)) = remote {
199 return Ok(vec![remote]);
200 }
201
202 git::git_remote(self.path())
204 }
205
206 pub fn remote_url(&self, remote: &str) -> Result<String> {
208 git::git_remote_get_url(self.path(), remote)
209 }
210
211 pub fn add_remote_url(&self, remote: &str, url: &str) -> Result<()> {
213 git::git_remote_add(self.path(), remote, url)?;
214
215 let branch = git::git_current_branch(self.path());
217 if let Ok(ref branch) = branch {
218 let _ = git::git_config_branch_set_remote(self.path(), branch, remote);
219 }
220
221 Ok(())
222 }
223
224 pub fn set_remote_url(&self, remote: &str, url: &str) -> Result<()> {
226 git::git_remote_remove(self.path(), remote)?;
228 self.add_remote_url(remote, url)
229 }
230
231 pub fn has_remote(&self) -> Result<bool> {
233 if !self.is_init() {
234 return Ok(false);
235 }
236 git::git_has_remote(self.path())
237 }
238
239 fn pull(&self) -> Result<()> {
241 git::git_pull(self.path())
242 }
243
244 fn push(&self, set_branch: Option<&str>, set_upstream: Option<&str>) -> Result<()> {
246 git::git_push(self.path(), set_branch, set_upstream)
247 }
248
249 pub fn commit_all<M: AsRef<str>>(&self, msg: M, commit_empty: bool) -> Result<()> {
251 let path = self.path();
252 git::git_add_all(path)?;
253 git::git_commit(path, msg.as_ref(), commit_empty)
254 }
255
256 pub fn reset_hard_all(&self) -> Result<()> {
258 let path = self.path();
259 git::git_add_all(path)?;
260 git::git_reset_hard(path)
261 }
262
263 pub fn changed_files_raw(&self, short: bool) -> Result<String> {
268 let path = self.path();
269 let mut status = git::git_status(path, short)?;
270
271 if status.trim().is_empty() {
273 status.truncate(0);
274 }
275
276 Ok(status)
277 }
278}
279
280#[derive(Debug, Eq, PartialEq)]
286pub enum Readyness {
287 NoSync,
289
290 RepoState(git::RepositoryState),
292
293 Dirty,
295
296 Ready,
298}
299
300impl Readyness {
301 pub fn is_ready(&self) -> bool {
303 matches!(self, Self::Ready)
304 }
305}
306
307fn is_dirty(repo: &Path) -> Result<bool> {
311 git::git_has_changes(repo)
312}
313
314fn safe_need_to_push(repo: &Path) -> bool {
318 match need_to_push(repo) {
319 Ok(push) => push,
320 Err(err) => {
321 eprintln!("failed to test if local branch is different than remote, ignoring: {err}",);
322 true
323 }
324 }
325}
326
327fn need_to_push(repo: &Path) -> Result<bool> {
331 let last_pulled = git::git_last_pull_time(repo)?;
333 if last_pulled.elapsed()? > GIT_PULL_OUTDATED {
334 return Ok(true);
335 }
336
337 let branch = git::git_current_branch(repo)?;
339 let upstream = match git::git_branch_upstream(repo, &branch)? {
340 Some(upstream) => upstream,
341 None => return Ok(true),
342 };
343
344 Ok(git::git_ref_hash(repo, branch)? != git::git_ref_hash(repo, upstream)?)
346}