Skip to main content

cfgd_core/daemon/
git.rs

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    // Try git CLI first with SSH hang protection.
12    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        // Fall back to libgit2
26        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    // Check if we need to fast-forward
39    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    // Check for changes
76    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        // No HEAD yet, everything in index is new
93        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    // Create commit
104    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    // Push — try git CLI first with SSH hang protection.
130    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        // Fall back to libgit2.
150        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}
167// --- Public sync functions for CLI commands ---
168
169pub fn git_pull_sync(repo_path: &Path) -> std::result::Result<bool, String> {
170    git_pull(repo_path)
171}