1mod transport;
2
3use crate::error::{Result, SyncError};
4use chrono::Local;
5use git2::{BranchType, MergeOptions, Oid, Repository, Status, StatusOptions};
6use std::fs;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9use tracing::{debug, error, info, warn};
10
11pub use transport::{CommandGitTransport, CommitOutcome, GitTransport};
12
13pub const FALLBACK_BRANCH_PREFIX: &str = "git-sync/";
15
16#[derive(Debug, Clone)]
18pub struct SyncConfig {
19 pub sync_new_files: bool,
21
22 pub skip_hooks: bool,
24
25 pub commit_message: Option<String>,
27
28 pub remote_name: String,
30
31 pub branch_name: String,
33
34 pub conflict_branch: bool,
36
37 pub target_branch: Option<String>,
40}
41
42impl Default for SyncConfig {
43 fn default() -> Self {
44 Self {
45 sync_new_files: true, skip_hooks: false,
47 commit_message: None,
48 remote_name: "origin".to_string(),
49 branch_name: "main".to_string(),
50 conflict_branch: false,
51 target_branch: None,
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq)]
58pub enum RepositoryState {
59 Clean,
61
62 Dirty,
64
65 Rebasing,
67
68 Merging,
70
71 CherryPicking,
73
74 Bisecting,
76
77 ApplyingPatches,
79
80 Reverting,
82
83 DetachedHead,
85}
86
87#[derive(Debug, Clone, PartialEq)]
89pub enum SyncState {
90 Equal,
92
93 Ahead(usize),
95
96 Behind(usize),
98
99 Diverged { ahead: usize, behind: usize },
101
102 NoUpstream,
104}
105
106#[derive(Debug, Clone, PartialEq)]
108pub enum UnhandledFileState {
109 Conflicted { path: String },
111}
112
113#[derive(Debug, Clone, Default)]
115pub struct FallbackState {
116 pub last_checked_target_oid: Option<Oid>,
119}
120
121pub struct RepositorySynchronizer {
123 repo: Repository,
124 config: SyncConfig,
125 _repo_path: PathBuf,
126 fallback_state: FallbackState,
127 transport: Arc<dyn GitTransport>,
128}
129
130impl RepositorySynchronizer {
131 pub fn new(repo_path: impl AsRef<Path>, config: SyncConfig) -> Result<Self> {
133 Self::new_with_transport(repo_path, config, Arc::new(CommandGitTransport))
134 }
135
136 pub fn new_with_transport(
138 repo_path: impl AsRef<Path>,
139 config: SyncConfig,
140 transport: Arc<dyn GitTransport>,
141 ) -> Result<Self> {
142 let repo_path = repo_path.as_ref().to_path_buf();
143 let repo = Repository::open(&repo_path).map_err(|_| SyncError::NotARepository {
144 path: repo_path.display().to_string(),
145 })?;
146
147 Ok(Self {
148 repo,
149 config,
150 _repo_path: repo_path,
151 fallback_state: FallbackState::default(),
152 transport,
153 })
154 }
155
156 pub fn new_with_detected_branch(
158 repo_path: impl AsRef<Path>,
159 config: SyncConfig,
160 ) -> Result<Self> {
161 Self::new_with_detected_branch_and_transport(
162 repo_path,
163 config,
164 Arc::new(CommandGitTransport),
165 )
166 }
167
168 pub fn new_with_detected_branch_and_transport(
170 repo_path: impl AsRef<Path>,
171 mut config: SyncConfig,
172 transport: Arc<dyn GitTransport>,
173 ) -> Result<Self> {
174 let repo_path = repo_path.as_ref().to_path_buf();
175 let repo = Repository::open(&repo_path).map_err(|_| SyncError::NotARepository {
176 path: repo_path.display().to_string(),
177 })?;
178
179 if let Ok(head) = repo.head() {
181 if head.is_branch() {
182 if let Some(branch_name) = head.shorthand() {
183 config.branch_name = branch_name.to_string();
184 }
185 }
186 }
187
188 Ok(Self {
189 repo,
190 config,
191 _repo_path: repo_path,
192 fallback_state: FallbackState::default(),
193 transport,
194 })
195 }
196
197 pub fn get_repository_state(&self) -> Result<RepositoryState> {
199 match self.repo.head_detached() {
201 Ok(true) => return Ok(RepositoryState::DetachedHead),
202 Ok(false) => {}
203 Err(e) if e.code() == git2::ErrorCode::UnbornBranch => {}
205 Err(e) => return Err(e.into()),
206 }
207
208 let state = self.repo.state();
210 match state {
211 git2::RepositoryState::Clean => {
212 let mut status_opts = StatusOptions::new();
214 status_opts.include_untracked(true);
215 let statuses = self.repo.statuses(Some(&mut status_opts))?;
216
217 if statuses.is_empty() {
218 Ok(RepositoryState::Clean)
219 } else {
220 Ok(RepositoryState::Dirty)
221 }
222 }
223 git2::RepositoryState::Merge => Ok(RepositoryState::Merging),
224 git2::RepositoryState::Rebase
225 | git2::RepositoryState::RebaseInteractive
226 | git2::RepositoryState::RebaseMerge => Ok(RepositoryState::Rebasing),
227 git2::RepositoryState::CherryPick | git2::RepositoryState::CherryPickSequence => {
228 Ok(RepositoryState::CherryPicking)
229 }
230 git2::RepositoryState::Revert | git2::RepositoryState::RevertSequence => {
231 Ok(RepositoryState::Reverting)
232 }
233 git2::RepositoryState::Bisect => Ok(RepositoryState::Bisecting),
234 git2::RepositoryState::ApplyMailbox | git2::RepositoryState::ApplyMailboxOrRebase => {
235 Ok(RepositoryState::ApplyingPatches)
236 }
237 }
238 }
239
240 pub fn has_local_changes(&self) -> Result<bool> {
242 let mut status_opts = StatusOptions::new();
243 status_opts.include_untracked(self.config.sync_new_files);
244
245 let statuses = self.repo.statuses(Some(&mut status_opts))?;
246
247 for entry in statuses.iter() {
248 let status = entry.status();
249 let tracked_or_staged_changes = Status::WT_MODIFIED
250 | Status::WT_DELETED
251 | Status::WT_RENAMED
252 | Status::WT_TYPECHANGE
253 | Status::INDEX_MODIFIED
254 | Status::INDEX_DELETED
255 | Status::INDEX_RENAMED
256 | Status::INDEX_TYPECHANGE
257 | Status::INDEX_NEW;
258
259 if self.config.sync_new_files {
260 if status.intersects(tracked_or_staged_changes | Status::WT_NEW) {
262 return Ok(true);
263 }
264 } else {
265 if status.intersects(tracked_or_staged_changes) {
267 return Ok(true);
268 }
269 }
270 }
271
272 Ok(false)
273 }
274
275 pub fn check_unhandled_files(&self) -> Result<Option<UnhandledFileState>> {
277 let mut status_opts = StatusOptions::new();
278 status_opts.include_untracked(true);
279
280 let statuses = self.repo.statuses(Some(&mut status_opts))?;
281
282 for entry in statuses.iter() {
283 let status = entry.status();
284 let path = entry.path().unwrap_or("<unknown>").to_string();
285
286 if status.is_conflicted() {
288 return Ok(Some(UnhandledFileState::Conflicted { path }));
289 }
290 }
291
292 Ok(None)
293 }
294
295 pub fn get_current_branch(&self) -> Result<String> {
297 let head = match self.repo.head() {
298 Ok(head) => head,
299 Err(e) if e.code() == git2::ErrorCode::UnbornBranch => {
300 if let Some(branch) = self.unborn_head_branch_name()? {
301 return Ok(branch);
302 }
303 if !self.config.branch_name.is_empty() {
304 return Ok(self.config.branch_name.clone());
305 }
306 return Err(SyncError::Other(
307 "Repository HEAD is unborn and branch name could not be determined".to_string(),
308 ));
309 }
310 Err(e) => return Err(e.into()),
311 };
312
313 if !head.is_branch() {
314 return Err(SyncError::DetachedHead);
315 }
316
317 let branch_name = head
318 .shorthand()
319 .ok_or_else(|| SyncError::Other("Could not get branch name".to_string()))?;
320
321 Ok(branch_name.to_string())
322 }
323
324 pub fn get_sync_state(&self) -> Result<SyncState> {
326 let branch_name = self.get_current_branch()?;
327 let local_branch = self.repo.find_branch(&branch_name, BranchType::Local)?;
328
329 let upstream = match local_branch.upstream() {
331 Ok(upstream) => upstream,
332 Err(_) => return Ok(SyncState::NoUpstream),
333 };
334
335 let local_oid = local_branch
337 .get()
338 .target()
339 .ok_or_else(|| SyncError::Other("Could not get local branch OID".to_string()))?;
340 let upstream_oid = upstream
341 .get()
342 .target()
343 .ok_or_else(|| SyncError::Other("Could not get upstream branch OID".to_string()))?;
344
345 if local_oid == upstream_oid {
347 return Ok(SyncState::Equal);
348 }
349
350 let (ahead, behind) = self.repo.graph_ahead_behind(local_oid, upstream_oid)?;
352
353 match (ahead, behind) {
354 (0, 0) => Ok(SyncState::Equal),
355 (a, 0) if a > 0 => Ok(SyncState::Ahead(a)),
356 (0, b) if b > 0 => Ok(SyncState::Behind(b)),
357 (a, b) if a > 0 && b > 0 => Ok(SyncState::Diverged {
358 ahead: a,
359 behind: b,
360 }),
361 _ => Ok(SyncState::Equal),
362 }
363 }
364
365 pub fn auto_commit(&self) -> Result<()> {
367 info!("Auto-committing local changes");
368
369 let mut index = self.repo.index()?;
371
372 if self.config.sync_new_files {
373 let repo_root = self._repo_path.clone();
383 let mut nested_repo_prefixes: Vec<String> = Vec::new();
384 let mut cb = |path: &Path, _matched_spec: &[u8]| -> i32 {
385 let path_s = path.to_string_lossy();
386
387 if nested_repo_prefixes.iter().any(|p| path_s.starts_with(p)) {
389 return 1;
390 }
391
392 if path_s.contains("/.git/") || path_s.ends_with("/.git") {
394 return 1;
395 }
396
397 if path_s.ends_with('/') {
400 let no_slash = path_s.trim_end_matches('/');
401 if repo_root.join(no_slash).join(".git").exists() {
402 nested_repo_prefixes.push(format!("{}/", no_slash));
403 }
404 return 1;
405 }
406
407 0
408 };
409
410 index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, Some(&mut cb))?;
411 } else {
412 index.update_all(["*"].iter(), None)?;
414 }
415
416 index.write()?;
417
418 let message = if let Some(ref msg) = self.config.commit_message {
420 msg.replace("{hostname}", &hostname::get()?.to_string_lossy())
421 .replace(
422 "{timestamp}",
423 &Local::now().format("%Y-%m-%d %I:%M:%S %p %Z").to_string(),
424 )
425 } else {
426 format!(
427 "changes from {} on {}",
428 hostname::get()?.to_string_lossy(),
429 Local::now().format("%Y-%m-%d %I:%M:%S %p %Z")
430 )
431 };
432
433 match self
434 .transport
435 .commit(&self._repo_path, &message, self.config.skip_hooks)?
436 {
437 CommitOutcome::Created => info!("Created auto-commit: {}", message),
438 CommitOutcome::NoChanges => {
439 debug!("No changes to commit");
440 }
441 }
442
443 Ok(())
444 }
445
446 pub fn fetch_branch(&self, branch: &str) -> Result<()> {
448 info!(
449 "Fetching branch {} from remote: {}",
450 branch, self.config.remote_name
451 );
452
453 if let Err(e) =
454 self.transport
455 .fetch_branch(&self._repo_path, &self.config.remote_name, branch)
456 {
457 error!("Git fetch failed: {}", e);
458 return Err(e);
459 }
460
461 info!(
462 "Fetch completed successfully for branch {} from remote: {}",
463 branch, self.config.remote_name
464 );
465 Ok(())
466 }
467
468 pub fn fetch(&self) -> Result<()> {
470 let current_branch = self.get_current_branch()?;
471 self.fetch_branch(¤t_branch)?;
472
473 if self.config.conflict_branch {
475 if let Ok(target) = self.get_target_branch() {
476 if target != current_branch {
477 let _ = self.fetch_branch(&target);
479 }
480 }
481 }
482
483 Ok(())
484 }
485
486 pub fn push(&self) -> Result<()> {
488 info!("Pushing to remote: {}", self.config.remote_name);
489
490 let current_branch = self.get_current_branch()?;
491 let refspec = format!("{}:{}", current_branch, current_branch);
492 self.transport
493 .push_refspec(&self._repo_path, &self.config.remote_name, &refspec)?;
494
495 info!(
496 "Push completed successfully to remote: {}",
497 self.config.remote_name
498 );
499 Ok(())
500 }
501
502 pub fn fast_forward_merge(&self) -> Result<()> {
504 info!("Performing fast-forward merge");
505
506 let branch_name = self.get_current_branch()?;
507 let local_branch = self.repo.find_branch(&branch_name, BranchType::Local)?;
508 let upstream = local_branch.upstream()?;
509
510 let upstream_oid = upstream
511 .get()
512 .target()
513 .ok_or_else(|| SyncError::Other("Could not get upstream OID".to_string()))?;
514
515 let mut reference = self.repo.head()?;
517 reference.set_target(upstream_oid, "fast-forward merge")?;
518
519 let object = self.repo.find_object(upstream_oid, None)?;
521 let mut checkout_builder = git2::build::CheckoutBuilder::new();
522 checkout_builder.force(); self.repo
524 .checkout_tree(&object, Some(&mut checkout_builder))?;
525
526 self.repo.set_head(&format!("refs/heads/{}", branch_name))?;
528
529 info!("Fast-forward merge completed - working tree updated");
530 Ok(())
531 }
532
533 pub fn rebase(&self) -> Result<()> {
535 info!("Performing rebase");
536
537 let branch_name = self.get_current_branch()?;
538 let local_branch = self.repo.find_branch(&branch_name, BranchType::Local)?;
539 let upstream = local_branch.upstream()?;
540
541 let upstream_commit = upstream.get().peel_to_commit()?;
542 let local_commit = local_branch.get().peel_to_commit()?;
543
544 let merge_base = self
546 .repo
547 .merge_base(local_commit.id(), upstream_commit.id())?;
548 let _merge_base_commit = self.repo.find_commit(merge_base)?;
549
550 let sig = self.repo.signature()?;
552
553 let local_annotated = self
555 .repo
556 .reference_to_annotated_commit(local_branch.get())?;
557 let upstream_annotated = self.repo.reference_to_annotated_commit(upstream.get())?;
558
559 let mut rebase = self.repo.rebase(
561 Some(&local_annotated),
562 Some(&upstream_annotated),
563 None,
564 None,
565 )?;
566
567 while let Some(operation) = rebase.next() {
569 let _operation = operation?;
570
571 if self.repo.index()?.has_conflicts() {
573 warn!("Conflicts detected during rebase");
574 rebase.abort()?;
575
576 if self.config.conflict_branch {
578 return self.handle_conflict_with_fallback();
579 }
580
581 return Err(SyncError::ManualInterventionRequired {
582 reason: "Rebase conflicts detected. Please resolve manually.".to_string(),
583 });
584 }
585
586 rebase.commit(None, &sig, None)?;
588 }
589
590 rebase.finish(Some(&sig))?;
592
593 let head = self.repo.head()?;
595 let head_commit = head.peel_to_commit()?;
596 let mut checkout_builder = git2::build::CheckoutBuilder::new();
597 checkout_builder.force();
598 self.repo
599 .checkout_tree(head_commit.as_object(), Some(&mut checkout_builder))?;
600
601 info!("Rebase completed successfully - working tree updated");
602 Ok(())
603 }
604
605 pub fn detect_default_branch(&self) -> Result<String> {
607 if let Ok(reference) = self.repo.find_reference("refs/remotes/origin/HEAD") {
609 if let Ok(resolved) = reference.resolve() {
610 if let Some(name) = resolved.shorthand() {
611 if let Some(branch) = name.strip_prefix("origin/") {
613 debug!("Detected default branch from origin/HEAD: {}", branch);
614 return Ok(branch.to_string());
615 }
616 }
617 }
618 }
619
620 if self.repo.find_branch("main", BranchType::Local).is_ok()
622 || self.repo.find_reference("refs/remotes/origin/main").is_ok()
623 {
624 debug!("Falling back to 'main' as default branch");
625 return Ok("main".to_string());
626 }
627
628 if self.repo.find_branch("master", BranchType::Local).is_ok()
629 || self
630 .repo
631 .find_reference("refs/remotes/origin/master")
632 .is_ok()
633 {
634 debug!("Falling back to 'master' as default branch");
635 return Ok("master".to_string());
636 }
637
638 self.get_current_branch()
640 }
641
642 pub fn get_target_branch(&self) -> Result<String> {
644 if let Some(ref target) = self.config.target_branch {
645 if !target.is_empty() {
646 return Ok(target.clone());
647 }
648 }
649 self.detect_default_branch()
650 }
651
652 pub fn is_on_fallback_branch(&self) -> Result<bool> {
654 let current = self.get_current_branch()?;
655 Ok(current.starts_with(FALLBACK_BRANCH_PREFIX))
656 }
657
658 fn generate_fallback_branch_name() -> String {
660 let hostname = hostname::get()
661 .map(|h| h.to_string_lossy().to_string())
662 .unwrap_or_else(|_| "unknown".to_string());
663 let timestamp = Local::now().format("%Y-%m-%d-%H%M%S");
664 format!("{}{}-{}", FALLBACK_BRANCH_PREFIX, hostname, timestamp)
665 }
666
667 pub fn create_fallback_branch(&self) -> Result<String> {
669 let branch_name = Self::generate_fallback_branch_name();
670 info!("Creating fallback branch: {}", branch_name);
671
672 let head_commit = self.repo.head()?.peel_to_commit()?;
674
675 self.repo
677 .branch(&branch_name, &head_commit, false)
678 .map_err(|e| SyncError::Other(format!("Failed to create fallback branch: {}", e)))?;
679
680 let refname = format!("refs/heads/{}", branch_name);
682 self.repo.set_head(&refname)?;
683
684 let mut checkout_builder = git2::build::CheckoutBuilder::new();
686 checkout_builder.force();
687 self.repo
688 .checkout_head(Some(&mut checkout_builder))
689 .map_err(|e| SyncError::Other(format!("Failed to checkout fallback branch: {}", e)))?;
690
691 info!("Switched to fallback branch: {}", branch_name);
692 Ok(branch_name)
693 }
694
695 pub fn push_branch(&self, branch_name: &str) -> Result<()> {
697 info!("Pushing branch {} to remote", branch_name);
698
699 self.transport.push_branch_upstream(
700 &self._repo_path,
701 &self.config.remote_name,
702 branch_name,
703 )?;
704
705 info!("Successfully pushed branch {} to remote", branch_name);
706 Ok(())
707 }
708
709 pub fn can_merge_cleanly(&self, target_branch: &str) -> Result<bool> {
711 let target_ref = format!("refs/remotes/{}/{}", self.config.remote_name, target_branch);
713 let target_reference = self.repo.find_reference(&target_ref).map_err(|e| {
714 SyncError::Other(format!(
715 "Failed to find target branch {}: {}",
716 target_branch, e
717 ))
718 })?;
719 let target_commit = target_reference.peel_to_commit()?;
720
721 let head_commit = self.repo.head()?.peel_to_commit()?;
723
724 if self
726 .repo
727 .graph_descendant_of(target_commit.id(), head_commit.id())?
728 {
729 debug!(
730 "Target branch {} is descendant of current HEAD, clean merge possible",
731 target_branch
732 );
733 return Ok(true);
734 }
735
736 let merge_opts = MergeOptions::new();
738 let index = self
739 .repo
740 .merge_commits(&head_commit, &target_commit, Some(&merge_opts))
741 .map_err(|e| SyncError::Other(format!("Failed to perform merge check: {}", e)))?;
742
743 let has_conflicts = index.has_conflicts();
744 debug!("In-memory merge check: has_conflicts={}", has_conflicts);
745
746 Ok(!has_conflicts)
747 }
748
749 fn get_target_branch_oid(&self, target_branch: &str) -> Result<Oid> {
751 let target_ref = format!("refs/remotes/{}/{}", self.config.remote_name, target_branch);
752 let reference = self.repo.find_reference(&target_ref)?;
753 reference
754 .target()
755 .ok_or_else(|| SyncError::Other("Target branch has no OID".to_string()))
756 }
757
758 pub fn try_return_to_target(&mut self) -> Result<bool> {
760 if !self.is_on_fallback_branch()? {
761 return Ok(false);
762 }
763
764 let target_branch = self.get_target_branch()?;
765 info!(
766 "On fallback branch, checking if we can return to {}",
767 target_branch
768 );
769
770 let target_oid = match self.get_target_branch_oid(&target_branch) {
772 Ok(oid) => oid,
773 Err(e) => {
774 warn!("Could not find target branch {}: {}", target_branch, e);
775 return Ok(false);
776 }
777 };
778
779 if let Some(last_checked) = self.fallback_state.last_checked_target_oid {
781 if last_checked == target_oid {
782 debug!(
783 "Target branch {} hasn't changed since last check, skipping merge check",
784 target_branch
785 );
786 return Ok(false);
787 }
788 }
789
790 if !self.can_merge_cleanly(&target_branch)? {
792 info!(
793 "Cannot cleanly merge {} into current branch, staying on fallback",
794 target_branch
795 );
796 self.fallback_state.last_checked_target_oid = Some(target_oid);
797 return Ok(false);
798 }
799
800 info!(
801 "Clean merge possible, returning to target branch {}",
802 target_branch
803 );
804
805 let current_branch = self.get_current_branch()?;
807 let current_oid = self
808 .repo
809 .head()?
810 .target()
811 .ok_or_else(|| SyncError::Other("Current HEAD has no OID".to_string()))?;
812
813 let merge_base = self.repo.merge_base(current_oid, target_oid)?;
815
816 let (ahead, _) = self.repo.graph_ahead_behind(current_oid, merge_base)?;
818 let has_commits_to_rebase = ahead > 0;
819
820 let target_ref = format!("refs/heads/{}", target_branch);
822
823 let remote_target_ref =
825 format!("refs/remotes/{}/{}", self.config.remote_name, target_branch);
826 let remote_target = self.repo.find_reference(&remote_target_ref)?;
827 let remote_target_oid = remote_target
828 .target()
829 .ok_or_else(|| SyncError::Other("Remote target has no OID".to_string()))?;
830
831 if self.repo.find_reference(&target_ref).is_ok() {
833 self.repo.reference(
835 &target_ref,
836 remote_target_oid,
837 true,
838 "git-sync: updating target branch before return",
839 )?;
840 } else {
841 let remote_commit = self.repo.find_commit(remote_target_oid)?;
843 self.repo.branch(&target_branch, &remote_commit, false)?;
844 }
845
846 self.repo.set_head(&target_ref)?;
848 let mut checkout_builder = git2::build::CheckoutBuilder::new();
849 checkout_builder.force();
850 self.repo.checkout_head(Some(&mut checkout_builder))?;
851
852 if has_commits_to_rebase {
853 info!(
854 "Rebasing {} commits from {} onto {}",
855 ahead, current_branch, target_branch
856 );
857
858 let fallback_ref = format!("refs/heads/{}", current_branch);
861 let fallback_reference = self.repo.find_reference(&fallback_ref)?;
862 let fallback_annotated = self
863 .repo
864 .reference_to_annotated_commit(&fallback_reference)?;
865
866 let target_reference = self.repo.find_reference(&target_ref)?;
867 let target_annotated = self.repo.reference_to_annotated_commit(&target_reference)?;
868
869 let sig = self.repo.signature()?;
870
871 let mut rebase = self.repo.rebase(
873 Some(&fallback_annotated),
874 Some(&target_annotated),
875 None,
876 None,
877 )?;
878
879 while let Some(operation) = rebase.next() {
881 let _operation = operation?;
882
883 if self.repo.index()?.has_conflicts() {
884 warn!("Conflicts during rebase back to target, aborting");
885 rebase.abort()?;
886 self.repo.set_head(&fallback_ref)?;
888 self.repo.checkout_head(Some(&mut checkout_builder))?;
889 self.fallback_state.last_checked_target_oid = Some(target_oid);
890 return Ok(false);
891 }
892
893 rebase.commit(None, &sig, None)?;
894 }
895
896 rebase.finish(Some(&sig))?;
897
898 let head = self.repo.head()?;
900 let head_commit = head.peel_to_commit()?;
901 self.repo
902 .checkout_tree(head_commit.as_object(), Some(&mut checkout_builder))?;
903 }
904
905 self.fallback_state.last_checked_target_oid = None;
907
908 info!("Successfully returned to target branch {}", target_branch);
909 Ok(true)
910 }
911
912 fn handle_conflict_with_fallback(&self) -> Result<()> {
914 if !self.config.conflict_branch {
915 return Err(SyncError::ManualInterventionRequired {
916 reason: "Rebase conflicts detected. Please resolve manually.".to_string(),
917 });
918 }
919
920 info!("Conflict detected with conflict_branch enabled, creating fallback branch");
921
922 let fallback_branch = self.create_fallback_branch()?;
924
925 if self.has_local_changes()? {
927 self.auto_commit()?;
928 }
929
930 self.push_branch(&fallback_branch)?;
932
933 info!(
934 "Switched to fallback branch {} due to conflicts. \
935 Will automatically return to target branch when conflicts are resolved.",
936 fallback_branch
937 );
938
939 Ok(())
940 }
941
942 pub fn sync(&mut self, check_only: bool) -> Result<()> {
944 info!("Starting sync operation (check_only: {})", check_only);
945
946 let repo_state = self.get_repository_state()?;
948 match repo_state {
949 RepositoryState::Clean | RepositoryState::Dirty => {
950 }
952 RepositoryState::DetachedHead => {
953 return Err(SyncError::DetachedHead);
954 }
955 _ => {
956 return Err(SyncError::UnsafeRepositoryState {
957 state: format!("{:?}", repo_state),
958 });
959 }
960 }
961
962 if let Some(unhandled) = self.check_unhandled_files()? {
964 let reason = match unhandled {
965 UnhandledFileState::Conflicted { path } => format!("Conflicted file: {}", path),
966 };
967 return Err(SyncError::ManualInterventionRequired { reason });
968 }
969
970 if check_only {
972 info!("Check passed, sync can proceed");
973 return Ok(());
974 }
975
976 if self.is_head_unborn()? {
979 info!("Repository HEAD is unborn; attempting initial publish");
980 if self.has_local_changes()? {
981 self.auto_commit()?;
982 let branch = self.get_current_branch()?;
983 self.push_branch(&branch)?;
984 } else {
985 info!("HEAD is unborn and there are no local changes to publish");
986 }
987 return Ok(());
988 }
989
990 self.fetch()?;
992
993 if self.config.conflict_branch
996 && self.is_on_fallback_branch()?
997 && self.try_return_to_target()?
998 {
999 info!("Returned to target branch, continuing with normal sync");
1001 }
1002
1003 if self.has_local_changes()? {
1005 self.auto_commit()?;
1006 }
1007
1008 let sync_state = self.get_sync_state()?;
1010 match sync_state {
1011 SyncState::Equal => {
1012 info!("Already in sync");
1013 }
1014 SyncState::Ahead(_) => {
1015 info!("Local is ahead, pushing");
1016 self.push()?;
1017 }
1018 SyncState::Behind(_) => {
1019 info!("Local is behind, fast-forwarding");
1020 self.fast_forward_merge()?;
1021 }
1022 SyncState::Diverged { .. } => {
1023 info!("Branches have diverged, rebasing");
1024 self.rebase()?;
1025 self.push()?;
1026 }
1027 SyncState::NoUpstream => {
1028 if self.is_on_fallback_branch()? {
1030 info!("Fallback branch has no upstream, pushing");
1031 let branch = self.get_current_branch()?;
1032 self.push_branch(&branch)?;
1033 } else {
1034 let branch = self
1035 .get_current_branch()
1036 .unwrap_or_else(|_| "<unknown>".into());
1037 return Err(SyncError::NoRemoteConfigured { branch });
1038 }
1039 }
1040 }
1041
1042 let final_state = self.get_sync_state()?;
1044 if final_state != SyncState::Equal && final_state != SyncState::NoUpstream {
1045 warn!(
1046 "Sync completed but repository is not in sync: {:?}",
1047 final_state
1048 );
1049 return Err(SyncError::Other(
1050 "Sync completed but repository is not in sync".to_string(),
1051 ));
1052 }
1053
1054 info!("Sync completed successfully");
1055 Ok(())
1056 }
1057
1058 fn is_head_unborn(&self) -> Result<bool> {
1060 match self.repo.head() {
1061 Ok(head) => match head.peel_to_commit() {
1062 Ok(_) => Ok(false),
1063 Err(e) if e.code() == git2::ErrorCode::UnbornBranch => Ok(true),
1064 Err(e) => Err(e.into()),
1065 },
1066 Err(e) if e.code() == git2::ErrorCode::UnbornBranch => Ok(true),
1067 Err(e) => Err(e.into()),
1068 }
1069 }
1070
1071 fn unborn_head_branch_name(&self) -> Result<Option<String>> {
1072 let head_path = self.repo.path().join("HEAD");
1073 let head_contents = fs::read_to_string(head_path)?;
1074 Ok(head_contents
1075 .trim()
1076 .strip_prefix("ref: refs/heads/")
1077 .map(|s| s.to_string()))
1078 }
1079}