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 index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)?;
375 } else {
376 index.update_all(["*"].iter(), None)?;
378 }
379
380 index.write()?;
381
382 let message = if let Some(ref msg) = self.config.commit_message {
384 msg.replace("{hostname}", &hostname::get()?.to_string_lossy())
385 .replace(
386 "{timestamp}",
387 &Local::now().format("%Y-%m-%d %I:%M:%S %p %Z").to_string(),
388 )
389 } else {
390 format!(
391 "changes from {} on {}",
392 hostname::get()?.to_string_lossy(),
393 Local::now().format("%Y-%m-%d %I:%M:%S %p %Z")
394 )
395 };
396
397 match self
398 .transport
399 .commit(&self._repo_path, &message, self.config.skip_hooks)?
400 {
401 CommitOutcome::Created => info!("Created auto-commit: {}", message),
402 CommitOutcome::NoChanges => {
403 debug!("No changes to commit");
404 }
405 }
406
407 Ok(())
408 }
409
410 pub fn fetch_branch(&self, branch: &str) -> Result<()> {
412 info!(
413 "Fetching branch {} from remote: {}",
414 branch, self.config.remote_name
415 );
416
417 if let Err(e) =
418 self.transport
419 .fetch_branch(&self._repo_path, &self.config.remote_name, branch)
420 {
421 error!("Git fetch failed: {}", e);
422 return Err(e);
423 }
424
425 info!(
426 "Fetch completed successfully for branch {} from remote: {}",
427 branch, self.config.remote_name
428 );
429 Ok(())
430 }
431
432 pub fn fetch(&self) -> Result<()> {
434 let current_branch = self.get_current_branch()?;
435 self.fetch_branch(¤t_branch)?;
436
437 if self.config.conflict_branch {
439 if let Ok(target) = self.get_target_branch() {
440 if target != current_branch {
441 let _ = self.fetch_branch(&target);
443 }
444 }
445 }
446
447 Ok(())
448 }
449
450 pub fn push(&self) -> Result<()> {
452 info!("Pushing to remote: {}", self.config.remote_name);
453
454 let current_branch = self.get_current_branch()?;
455 let refspec = format!("{}:{}", current_branch, current_branch);
456 self.transport
457 .push_refspec(&self._repo_path, &self.config.remote_name, &refspec)?;
458
459 info!(
460 "Push completed successfully to remote: {}",
461 self.config.remote_name
462 );
463 Ok(())
464 }
465
466 pub fn fast_forward_merge(&self) -> Result<()> {
468 info!("Performing fast-forward merge");
469
470 let branch_name = self.get_current_branch()?;
471 let local_branch = self.repo.find_branch(&branch_name, BranchType::Local)?;
472 let upstream = local_branch.upstream()?;
473
474 let upstream_oid = upstream
475 .get()
476 .target()
477 .ok_or_else(|| SyncError::Other("Could not get upstream OID".to_string()))?;
478
479 let mut reference = self.repo.head()?;
481 reference.set_target(upstream_oid, "fast-forward merge")?;
482
483 let object = self.repo.find_object(upstream_oid, None)?;
485 let mut checkout_builder = git2::build::CheckoutBuilder::new();
486 checkout_builder.force(); self.repo
488 .checkout_tree(&object, Some(&mut checkout_builder))?;
489
490 self.repo.set_head(&format!("refs/heads/{}", branch_name))?;
492
493 info!("Fast-forward merge completed - working tree updated");
494 Ok(())
495 }
496
497 pub fn rebase(&self) -> Result<()> {
499 info!("Performing rebase");
500
501 let branch_name = self.get_current_branch()?;
502 let local_branch = self.repo.find_branch(&branch_name, BranchType::Local)?;
503 let upstream = local_branch.upstream()?;
504
505 let upstream_commit = upstream.get().peel_to_commit()?;
506 let local_commit = local_branch.get().peel_to_commit()?;
507
508 let merge_base = self
510 .repo
511 .merge_base(local_commit.id(), upstream_commit.id())?;
512 let _merge_base_commit = self.repo.find_commit(merge_base)?;
513
514 let sig = self.repo.signature()?;
516
517 let local_annotated = self
519 .repo
520 .reference_to_annotated_commit(local_branch.get())?;
521 let upstream_annotated = self.repo.reference_to_annotated_commit(upstream.get())?;
522
523 let mut rebase = self.repo.rebase(
525 Some(&local_annotated),
526 Some(&upstream_annotated),
527 None,
528 None,
529 )?;
530
531 while let Some(operation) = rebase.next() {
533 let _operation = operation?;
534
535 if self.repo.index()?.has_conflicts() {
537 warn!("Conflicts detected during rebase");
538 rebase.abort()?;
539
540 if self.config.conflict_branch {
542 return self.handle_conflict_with_fallback();
543 }
544
545 return Err(SyncError::ManualInterventionRequired {
546 reason: "Rebase conflicts detected. Please resolve manually.".to_string(),
547 });
548 }
549
550 rebase.commit(None, &sig, None)?;
552 }
553
554 rebase.finish(Some(&sig))?;
556
557 let head = self.repo.head()?;
559 let head_commit = head.peel_to_commit()?;
560 let mut checkout_builder = git2::build::CheckoutBuilder::new();
561 checkout_builder.force();
562 self.repo
563 .checkout_tree(head_commit.as_object(), Some(&mut checkout_builder))?;
564
565 info!("Rebase completed successfully - working tree updated");
566 Ok(())
567 }
568
569 pub fn detect_default_branch(&self) -> Result<String> {
571 if let Ok(reference) = self.repo.find_reference("refs/remotes/origin/HEAD") {
573 if let Ok(resolved) = reference.resolve() {
574 if let Some(name) = resolved.shorthand() {
575 if let Some(branch) = name.strip_prefix("origin/") {
577 debug!("Detected default branch from origin/HEAD: {}", branch);
578 return Ok(branch.to_string());
579 }
580 }
581 }
582 }
583
584 if self.repo.find_branch("main", BranchType::Local).is_ok()
586 || self.repo.find_reference("refs/remotes/origin/main").is_ok()
587 {
588 debug!("Falling back to 'main' as default branch");
589 return Ok("main".to_string());
590 }
591
592 if self.repo.find_branch("master", BranchType::Local).is_ok()
593 || self
594 .repo
595 .find_reference("refs/remotes/origin/master")
596 .is_ok()
597 {
598 debug!("Falling back to 'master' as default branch");
599 return Ok("master".to_string());
600 }
601
602 self.get_current_branch()
604 }
605
606 pub fn get_target_branch(&self) -> Result<String> {
608 if let Some(ref target) = self.config.target_branch {
609 if !target.is_empty() {
610 return Ok(target.clone());
611 }
612 }
613 self.detect_default_branch()
614 }
615
616 pub fn is_on_fallback_branch(&self) -> Result<bool> {
618 let current = self.get_current_branch()?;
619 Ok(current.starts_with(FALLBACK_BRANCH_PREFIX))
620 }
621
622 fn generate_fallback_branch_name() -> String {
624 let hostname = hostname::get()
625 .map(|h| h.to_string_lossy().to_string())
626 .unwrap_or_else(|_| "unknown".to_string());
627 let timestamp = Local::now().format("%Y-%m-%d-%H%M%S");
628 format!("{}{}-{}", FALLBACK_BRANCH_PREFIX, hostname, timestamp)
629 }
630
631 pub fn create_fallback_branch(&self) -> Result<String> {
633 let branch_name = Self::generate_fallback_branch_name();
634 info!("Creating fallback branch: {}", branch_name);
635
636 let head_commit = self.repo.head()?.peel_to_commit()?;
638
639 self.repo
641 .branch(&branch_name, &head_commit, false)
642 .map_err(|e| SyncError::Other(format!("Failed to create fallback branch: {}", e)))?;
643
644 let refname = format!("refs/heads/{}", branch_name);
646 self.repo.set_head(&refname)?;
647
648 let mut checkout_builder = git2::build::CheckoutBuilder::new();
650 checkout_builder.force();
651 self.repo
652 .checkout_head(Some(&mut checkout_builder))
653 .map_err(|e| SyncError::Other(format!("Failed to checkout fallback branch: {}", e)))?;
654
655 info!("Switched to fallback branch: {}", branch_name);
656 Ok(branch_name)
657 }
658
659 pub fn push_branch(&self, branch_name: &str) -> Result<()> {
661 info!("Pushing branch {} to remote", branch_name);
662
663 self.transport.push_branch_upstream(
664 &self._repo_path,
665 &self.config.remote_name,
666 branch_name,
667 )?;
668
669 info!("Successfully pushed branch {} to remote", branch_name);
670 Ok(())
671 }
672
673 pub fn can_merge_cleanly(&self, target_branch: &str) -> Result<bool> {
675 let target_ref = format!("refs/remotes/{}/{}", self.config.remote_name, target_branch);
677 let target_reference = self.repo.find_reference(&target_ref).map_err(|e| {
678 SyncError::Other(format!(
679 "Failed to find target branch {}: {}",
680 target_branch, e
681 ))
682 })?;
683 let target_commit = target_reference.peel_to_commit()?;
684
685 let head_commit = self.repo.head()?.peel_to_commit()?;
687
688 if self
690 .repo
691 .graph_descendant_of(target_commit.id(), head_commit.id())?
692 {
693 debug!(
694 "Target branch {} is descendant of current HEAD, clean merge possible",
695 target_branch
696 );
697 return Ok(true);
698 }
699
700 let merge_opts = MergeOptions::new();
702 let index = self
703 .repo
704 .merge_commits(&head_commit, &target_commit, Some(&merge_opts))
705 .map_err(|e| SyncError::Other(format!("Failed to perform merge check: {}", e)))?;
706
707 let has_conflicts = index.has_conflicts();
708 debug!("In-memory merge check: has_conflicts={}", has_conflicts);
709
710 Ok(!has_conflicts)
711 }
712
713 fn get_target_branch_oid(&self, target_branch: &str) -> Result<Oid> {
715 let target_ref = format!("refs/remotes/{}/{}", self.config.remote_name, target_branch);
716 let reference = self.repo.find_reference(&target_ref)?;
717 reference
718 .target()
719 .ok_or_else(|| SyncError::Other("Target branch has no OID".to_string()))
720 }
721
722 pub fn try_return_to_target(&mut self) -> Result<bool> {
724 if !self.is_on_fallback_branch()? {
725 return Ok(false);
726 }
727
728 let target_branch = self.get_target_branch()?;
729 info!(
730 "On fallback branch, checking if we can return to {}",
731 target_branch
732 );
733
734 let target_oid = match self.get_target_branch_oid(&target_branch) {
736 Ok(oid) => oid,
737 Err(e) => {
738 warn!("Could not find target branch {}: {}", target_branch, e);
739 return Ok(false);
740 }
741 };
742
743 if let Some(last_checked) = self.fallback_state.last_checked_target_oid {
745 if last_checked == target_oid {
746 debug!(
747 "Target branch {} hasn't changed since last check, skipping merge check",
748 target_branch
749 );
750 return Ok(false);
751 }
752 }
753
754 if !self.can_merge_cleanly(&target_branch)? {
756 info!(
757 "Cannot cleanly merge {} into current branch, staying on fallback",
758 target_branch
759 );
760 self.fallback_state.last_checked_target_oid = Some(target_oid);
761 return Ok(false);
762 }
763
764 info!(
765 "Clean merge possible, returning to target branch {}",
766 target_branch
767 );
768
769 let current_branch = self.get_current_branch()?;
771 let current_oid = self
772 .repo
773 .head()?
774 .target()
775 .ok_or_else(|| SyncError::Other("Current HEAD has no OID".to_string()))?;
776
777 let merge_base = self.repo.merge_base(current_oid, target_oid)?;
779
780 let (ahead, _) = self.repo.graph_ahead_behind(current_oid, merge_base)?;
782 let has_commits_to_rebase = ahead > 0;
783
784 let target_ref = format!("refs/heads/{}", target_branch);
786
787 let remote_target_ref =
789 format!("refs/remotes/{}/{}", self.config.remote_name, target_branch);
790 let remote_target = self.repo.find_reference(&remote_target_ref)?;
791 let remote_target_oid = remote_target
792 .target()
793 .ok_or_else(|| SyncError::Other("Remote target has no OID".to_string()))?;
794
795 if self.repo.find_reference(&target_ref).is_ok() {
797 self.repo.reference(
799 &target_ref,
800 remote_target_oid,
801 true,
802 "git-sync: updating target branch before return",
803 )?;
804 } else {
805 let remote_commit = self.repo.find_commit(remote_target_oid)?;
807 self.repo.branch(&target_branch, &remote_commit, false)?;
808 }
809
810 self.repo.set_head(&target_ref)?;
812 let mut checkout_builder = git2::build::CheckoutBuilder::new();
813 checkout_builder.force();
814 self.repo.checkout_head(Some(&mut checkout_builder))?;
815
816 if has_commits_to_rebase {
817 info!(
818 "Rebasing {} commits from {} onto {}",
819 ahead, current_branch, target_branch
820 );
821
822 let fallback_ref = format!("refs/heads/{}", current_branch);
825 let fallback_reference = self.repo.find_reference(&fallback_ref)?;
826 let fallback_annotated = self
827 .repo
828 .reference_to_annotated_commit(&fallback_reference)?;
829
830 let target_reference = self.repo.find_reference(&target_ref)?;
831 let target_annotated = self.repo.reference_to_annotated_commit(&target_reference)?;
832
833 let sig = self.repo.signature()?;
834
835 let mut rebase = self.repo.rebase(
837 Some(&fallback_annotated),
838 Some(&target_annotated),
839 None,
840 None,
841 )?;
842
843 while let Some(operation) = rebase.next() {
845 let _operation = operation?;
846
847 if self.repo.index()?.has_conflicts() {
848 warn!("Conflicts during rebase back to target, aborting");
849 rebase.abort()?;
850 self.repo.set_head(&fallback_ref)?;
852 self.repo.checkout_head(Some(&mut checkout_builder))?;
853 self.fallback_state.last_checked_target_oid = Some(target_oid);
854 return Ok(false);
855 }
856
857 rebase.commit(None, &sig, None)?;
858 }
859
860 rebase.finish(Some(&sig))?;
861
862 let head = self.repo.head()?;
864 let head_commit = head.peel_to_commit()?;
865 self.repo
866 .checkout_tree(head_commit.as_object(), Some(&mut checkout_builder))?;
867 }
868
869 self.fallback_state.last_checked_target_oid = None;
871
872 info!("Successfully returned to target branch {}", target_branch);
873 Ok(true)
874 }
875
876 fn handle_conflict_with_fallback(&self) -> Result<()> {
878 if !self.config.conflict_branch {
879 return Err(SyncError::ManualInterventionRequired {
880 reason: "Rebase conflicts detected. Please resolve manually.".to_string(),
881 });
882 }
883
884 info!("Conflict detected with conflict_branch enabled, creating fallback branch");
885
886 let fallback_branch = self.create_fallback_branch()?;
888
889 if self.has_local_changes()? {
891 self.auto_commit()?;
892 }
893
894 self.push_branch(&fallback_branch)?;
896
897 info!(
898 "Switched to fallback branch {} due to conflicts. \
899 Will automatically return to target branch when conflicts are resolved.",
900 fallback_branch
901 );
902
903 Ok(())
904 }
905
906 pub fn sync(&mut self, check_only: bool) -> Result<()> {
908 info!("Starting sync operation (check_only: {})", check_only);
909
910 let repo_state = self.get_repository_state()?;
912 match repo_state {
913 RepositoryState::Clean | RepositoryState::Dirty => {
914 }
916 RepositoryState::DetachedHead => {
917 return Err(SyncError::DetachedHead);
918 }
919 _ => {
920 return Err(SyncError::UnsafeRepositoryState {
921 state: format!("{:?}", repo_state),
922 });
923 }
924 }
925
926 if let Some(unhandled) = self.check_unhandled_files()? {
928 let reason = match unhandled {
929 UnhandledFileState::Conflicted { path } => format!("Conflicted file: {}", path),
930 };
931 return Err(SyncError::ManualInterventionRequired { reason });
932 }
933
934 if check_only {
936 info!("Check passed, sync can proceed");
937 return Ok(());
938 }
939
940 if self.is_head_unborn()? {
943 info!("Repository HEAD is unborn; attempting initial publish");
944 if self.has_local_changes()? {
945 self.auto_commit()?;
946 let branch = self.get_current_branch()?;
947 self.push_branch(&branch)?;
948 } else {
949 info!("HEAD is unborn and there are no local changes to publish");
950 }
951 return Ok(());
952 }
953
954 self.fetch()?;
956
957 if self.config.conflict_branch
960 && self.is_on_fallback_branch()?
961 && self.try_return_to_target()?
962 {
963 info!("Returned to target branch, continuing with normal sync");
965 }
966
967 if self.has_local_changes()? {
969 self.auto_commit()?;
970 }
971
972 let sync_state = self.get_sync_state()?;
974 match sync_state {
975 SyncState::Equal => {
976 info!("Already in sync");
977 }
978 SyncState::Ahead(_) => {
979 info!("Local is ahead, pushing");
980 self.push()?;
981 }
982 SyncState::Behind(_) => {
983 info!("Local is behind, fast-forwarding");
984 self.fast_forward_merge()?;
985 }
986 SyncState::Diverged { .. } => {
987 info!("Branches have diverged, rebasing");
988 self.rebase()?;
989 self.push()?;
990 }
991 SyncState::NoUpstream => {
992 if self.is_on_fallback_branch()? {
994 info!("Fallback branch has no upstream, pushing");
995 let branch = self.get_current_branch()?;
996 self.push_branch(&branch)?;
997 } else {
998 let branch = self
999 .get_current_branch()
1000 .unwrap_or_else(|_| "<unknown>".into());
1001 return Err(SyncError::NoRemoteConfigured { branch });
1002 }
1003 }
1004 }
1005
1006 let final_state = self.get_sync_state()?;
1008 if final_state != SyncState::Equal && final_state != SyncState::NoUpstream {
1009 warn!(
1010 "Sync completed but repository is not in sync: {:?}",
1011 final_state
1012 );
1013 return Err(SyncError::Other(
1014 "Sync completed but repository is not in sync".to_string(),
1015 ));
1016 }
1017
1018 info!("Sync completed successfully");
1019 Ok(())
1020 }
1021
1022 fn is_head_unborn(&self) -> Result<bool> {
1024 match self.repo.head() {
1025 Ok(head) => match head.peel_to_commit() {
1026 Ok(_) => Ok(false),
1027 Err(e) if e.code() == git2::ErrorCode::UnbornBranch => Ok(true),
1028 Err(e) => Err(e.into()),
1029 },
1030 Err(e) if e.code() == git2::ErrorCode::UnbornBranch => Ok(true),
1031 Err(e) => Err(e.into()),
1032 }
1033 }
1034
1035 fn unborn_head_branch_name(&self) -> Result<Option<String>> {
1036 let head_path = self.repo.path().join("HEAD");
1037 let head_contents = fs::read_to_string(head_path)?;
1038 Ok(head_contents
1039 .trim()
1040 .strip_prefix("ref: refs/heads/")
1041 .map(|s| s.to_string()))
1042 }
1043}