1use super::*;
2
3pub(crate) fn git_pull(repo_path: &Path) -> std::result::Result<bool, String> {
4 let repo = git2::Repository::open(repo_path).map_err(|e| format!("open repo: {}", e))?;
5
6 let head = repo.head().map_err(|e| format!("get HEAD: {}", e))?;
7 let branch_name = head
8 .shorthand()
9 .ok_or_else(|| "cannot determine branch name".to_string())?;
10
11 let remote_url = repo
13 .find_remote("origin")
14 .ok()
15 .and_then(|r| r.url().map(String::from));
16 let repo_dir = &repo_path.display().to_string();
17 let cli_ok = crate::try_git_cmd(
18 remote_url.as_deref(),
19 &["-C", repo_dir, "fetch", "origin", branch_name],
20 "fetch",
21 None,
22 );
23
24 if !cli_ok {
25 let mut remote = repo
27 .find_remote("origin")
28 .map_err(|e| format!("find remote: {}", e))?;
29 let mut fetch_opts = git2::FetchOptions::new();
30 let mut callbacks = git2::RemoteCallbacks::new();
31 callbacks.credentials(crate::git_ssh_credentials);
32 fetch_opts.remote_callbacks(callbacks);
33 remote
34 .fetch(&[branch_name], Some(&mut fetch_opts), None)
35 .map_err(|e| format!("fetch: {}", e))?;
36 }
37
38 let fetch_head = repo
40 .find_reference("FETCH_HEAD")
41 .map_err(|e| format!("find FETCH_HEAD: {}", e))?;
42 let fetch_commit = repo
43 .reference_to_annotated_commit(&fetch_head)
44 .map_err(|e| format!("resolve FETCH_HEAD: {}", e))?;
45
46 let (analysis, _) = repo
47 .merge_analysis(&[&fetch_commit])
48 .map_err(|e| format!("merge analysis: {}", e))?;
49
50 if analysis.is_up_to_date() {
51 return Ok(false);
52 }
53
54 if analysis.is_fast_forward() {
55 let refname = format!("refs/heads/{}", branch_name);
56 let mut reference = repo
57 .find_reference(&refname)
58 .map_err(|e| format!("find ref: {}", e))?;
59 reference
60 .set_target(fetch_commit.id(), "cfgd: fast-forward pull")
61 .map_err(|e| format!("set target: {}", e))?;
62 repo.set_head(&refname)
63 .map_err(|e| format!("set HEAD: {}", e))?;
64 repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))
65 .map_err(|e| format!("checkout: {}", e))?;
66 return Ok(true);
67 }
68
69 Err("cannot fast-forward — remote has diverged".to_string())
70}
71
72pub(crate) fn git_auto_commit_push(repo_path: &Path) -> std::result::Result<bool, String> {
73 let repo = git2::Repository::open(repo_path).map_err(|e| format!("open repo: {}", e))?;
74
75 let mut index = repo.index().map_err(|e| format!("get index: {}", e))?;
77 index
78 .add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)
79 .map_err(|e| format!("stage changes: {}", e))?;
80 index.write().map_err(|e| format!("write index: {}", e))?;
81
82 let diff = repo
83 .diff_index_to_workdir(Some(&index), None)
84 .map_err(|e| format!("diff: {}", e))?;
85
86 let head_tree = repo.head().ok().and_then(|h| h.peel_to_tree().ok());
87
88 let staged_diff = if let Some(ref tree) = head_tree {
89 repo.diff_tree_to_index(Some(tree), Some(&index), None)
90 .map_err(|e| format!("staged diff: {}", e))?
91 } else {
92 repo.diff_tree_to_index(None, Some(&index), None)
94 .map_err(|e| format!("staged diff: {}", e))?
95 };
96
97 if diff.stats().map(|s| s.files_changed()).unwrap_or(0) == 0
98 && staged_diff.stats().map(|s| s.files_changed()).unwrap_or(0) == 0
99 {
100 return Ok(false);
101 }
102
103 let tree_oid = index
105 .write_tree()
106 .map_err(|e| format!("write tree: {}", e))?;
107 let tree = repo
108 .find_tree(tree_oid)
109 .map_err(|e| format!("find tree: {}", e))?;
110
111 let signature = repo
112 .signature()
113 .map_err(|e| format!("get signature: {}", e))?;
114
115 let parent = repo.head().ok().and_then(|h| h.peel_to_commit().ok());
116
117 let parents: Vec<&git2::Commit> = parent.as_ref().map(|p| vec![p]).unwrap_or_default();
118
119 repo.commit(
120 Some("HEAD"),
121 &signature,
122 &signature,
123 "cfgd: auto-commit configuration changes",
124 &tree,
125 &parents,
126 )
127 .map_err(|e| format!("commit: {}", e))?;
128
129 let head = repo.head().map_err(|e| format!("get HEAD: {}", e))?;
131 let branch_name = head
132 .shorthand()
133 .ok_or_else(|| "cannot determine branch name".to_string())?;
134
135 let remote_url = repo
136 .find_remote("origin")
137 .ok()
138 .and_then(|r| r.url().map(String::from));
139
140 let repo_dir = &repo_path.display().to_string();
141 let cli_ok = crate::try_git_cmd(
142 remote_url.as_deref(),
143 &["-C", repo_dir, "push", "origin", branch_name],
144 "push",
145 None,
146 );
147
148 if !cli_ok {
149 let mut remote = repo
151 .find_remote("origin")
152 .map_err(|e| format!("find remote: {}", e))?;
153
154 let mut push_opts = git2::PushOptions::new();
155 let mut callbacks = git2::RemoteCallbacks::new();
156 callbacks.credentials(crate::git_ssh_credentials);
157 push_opts.remote_callbacks(callbacks);
158
159 let refspec = format!("refs/heads/{}:refs/heads/{}", branch_name, branch_name);
160 remote
161 .push(&[&refspec], Some(&mut push_opts))
162 .map_err(|e| format!("push: {}", e))?;
163 }
164
165 Ok(true)
166}
167pub fn git_pull_sync(repo_path: &Path) -> std::result::Result<bool, String> {
170 git_pull(repo_path)
171}