1use crate::error::{GwmError, Result};
14use crate::worktree;
15use git2::{BranchType, Repository};
16use std::path::Path;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum SyncStrategy {
23 Rebase,
24 Merge,
25}
26
27impl SyncStrategy {
28 fn verb(self) -> &'static str {
30 match self {
31 SyncStrategy::Rebase => "rebase",
32 SyncStrategy::Merge => "merge",
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum SyncAction {
40 UpToDate,
43 Integrated,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct SyncReport {
53 pub branch: String,
55 pub upstream: String,
57 pub strategy: SyncStrategy,
59 pub ahead_before: usize,
62 pub behind_before: usize,
65 pub action: SyncAction,
67}
68
69pub fn sync(start: &Path, strategy: SyncStrategy) -> Result<SyncReport> {
82 let repo = Repository::discover(start).map_err(|_| GwmError::NotInGitRepo)?;
83 let workdir = repo.workdir().ok_or(GwmError::NotInGitRepo)?.to_path_buf();
84
85 if worktree::is_dirty(&repo)? {
88 return Err(GwmError::Other(
89 "worktree has uncommitted changes; commit or stash before syncing".into(),
90 ));
91 }
92
93 let head = repo.head().map_err(|_| GwmError::UnbornHead {
95 reason: "sync: cannot read HEAD (unborn or unreadable)".into(),
96 })?;
97 if !head.is_branch() {
98 return Err(GwmError::UnbornHead {
99 reason: "sync: HEAD is detached — check out a branch first".into(),
100 });
101 }
102 let branch_short = head
103 .shorthand()
104 .ok()
105 .ok_or_else(|| GwmError::UnbornHead {
106 reason: "sync: HEAD has no branch name".into(),
107 })?
108 .to_string();
109 let head_refname = head.name().ok().map(|s| s.to_string());
110
111 let local = repo
112 .find_branch(&branch_short, BranchType::Local)
113 .map_err(|_| GwmError::Other(format!("sync: local branch '{branch_short}' not found")))?;
114 let upstream = local.upstream().map_err(|_| {
115 GwmError::Other(format!(
116 "branch '{branch_short}' has no upstream configured; set one with `git branch --set-upstream-to=<remote>/{branch_short}`"
117 ))
118 })?;
119 let upstream_short = upstream
120 .name()
121 .ok()
122 .flatten()
123 .ok_or_else(|| GwmError::Other("sync: upstream tracking ref has no name".into()))?
124 .to_string();
125
126 let remote = head_refname
130 .as_deref()
131 .and_then(|rn| repo.branch_upstream_remote(rn).ok())
132 .and_then(|buf| buf.as_str().ok().map(|s| s.to_string()));
133
134 match &remote {
137 Some(r) => worktree::run_git_logged(&workdir, &["fetch", r])?,
138 None => worktree::run_git_logged(&workdir, &["fetch"])?,
139 };
140
141 let repo = Repository::discover(start).map_err(|_| GwmError::NotInGitRepo)?;
143 let (ahead_before, behind_before) = ahead_behind(&repo, &branch_short)?;
144
145 if behind_before == 0 {
146 return Ok(SyncReport {
147 branch: branch_short,
148 upstream: upstream_short,
149 strategy,
150 ahead_before,
151 behind_before,
152 action: SyncAction::UpToDate,
153 });
154 }
155
156 let integrate = match strategy {
159 SyncStrategy::Rebase => worktree::run_git_logged(&workdir, &["rebase", &upstream_short]),
160 SyncStrategy::Merge => worktree::run_git_logged(&workdir, &["merge", "--no-edit", &upstream_short]),
161 };
162 if let Err(e) = integrate {
163 let conflicted = Repository::discover(start)
169 .ok()
170 .and_then(|r| r.index().ok())
171 .map(|idx| idx.has_conflicts())
172 .unwrap_or(false);
173 let _ = worktree::run_git_logged(&workdir, &[strategy.verb(), "--abort"]);
174 if conflicted {
175 return Err(GwmError::Other(format!(
176 "{} onto {} hit conflicts and was aborted; reconcile manually with `git {} {}`",
177 strategy.verb(),
178 upstream_short,
179 strategy.verb(),
180 upstream_short
181 )));
182 }
183 return Err(GwmError::Other(format!(
186 "git {} onto {} failed and was aborted: {}",
187 strategy.verb(),
188 upstream_short,
189 e
190 )));
191 }
192
193 Ok(SyncReport {
194 branch: branch_short,
195 upstream: upstream_short,
196 strategy,
197 ahead_before,
198 behind_before,
199 action: SyncAction::Integrated,
200 })
201}
202
203fn ahead_behind(repo: &Repository, branch: &str) -> Result<(usize, usize)> {
206 let local = repo
207 .find_branch(branch, BranchType::Local)
208 .map_err(|_| GwmError::Other(format!("sync: local branch '{branch}' not found")))?;
209 let upstream = local
210 .upstream()
211 .map_err(|_| GwmError::Other(format!("branch '{branch}' has no upstream configured")))?;
212 let local_oid = local
213 .get()
214 .target()
215 .ok_or_else(|| GwmError::Other(format!("sync: branch '{branch}' has no commit")))?;
216 let up_oid = upstream
217 .get()
218 .target()
219 .ok_or_else(|| GwmError::Other("sync: upstream has no commit".into()))?;
220 let (ahead, behind) = repo.graph_ahead_behind(local_oid, up_oid)?;
221 Ok((ahead, behind))
222}