git_sync_rs/
sync.rs

1use crate::error::{Result, SyncError};
2use chrono::Local;
3use git2::{BranchType, Repository, Status, StatusOptions};
4use std::path::{Path, PathBuf};
5use tracing::{debug, error, info, warn};
6
7/// Configuration for the synchronizer
8#[derive(Debug, Clone)]
9pub struct SyncConfig {
10    /// Whether to sync new/untracked files
11    pub sync_new_files: bool,
12
13    /// Whether to skip git hooks when committing
14    pub skip_hooks: bool,
15
16    /// Custom commit message (can include {hostname} and {timestamp} placeholders)
17    pub commit_message: Option<String>,
18
19    /// Remote name to sync with (e.g., "origin")
20    pub remote_name: String,
21
22    /// Branch name to sync
23    pub branch_name: String,
24}
25
26impl Default for SyncConfig {
27    fn default() -> Self {
28        Self {
29            sync_new_files: true, // Default to syncing untracked files
30            skip_hooks: false,
31            commit_message: None,
32            remote_name: "origin".to_string(),
33            branch_name: "main".to_string(),
34        }
35    }
36}
37
38/// Repository state that might prevent syncing
39#[derive(Debug, Clone, PartialEq)]
40pub enum RepositoryState {
41    /// Repository is clean and ready
42    Clean,
43
44    /// Repository has uncommitted changes
45    Dirty,
46
47    /// Repository is in the middle of a rebase
48    Rebasing,
49
50    /// Repository is in the middle of a merge
51    Merging,
52
53    /// Repository is cherry-picking
54    CherryPicking,
55
56    /// Repository is bisecting
57    Bisecting,
58
59    /// Repository is applying patches (git am)
60    ApplyingPatches,
61
62    /// HEAD is detached
63    DetachedHead,
64}
65
66/// Sync state relative to remote
67#[derive(Debug, Clone, PartialEq)]
68pub enum SyncState {
69    /// Local and remote are equal
70    Equal,
71
72    /// Local is ahead of remote
73    Ahead(usize),
74
75    /// Local is behind remote
76    Behind(usize),
77
78    /// Local and remote have diverged
79    Diverged { ahead: usize, behind: usize },
80
81    /// No upstream branch
82    NoUpstream,
83}
84
85/// Unhandled file state that prevents sync
86#[derive(Debug, Clone, PartialEq)]
87pub enum UnhandledFileState {
88    /// File has merge conflicts
89    Conflicted { path: String },
90}
91
92/// Main synchronizer struct
93pub struct RepositorySynchronizer {
94    repo: Repository,
95    config: SyncConfig,
96    _repo_path: PathBuf,
97}
98
99impl RepositorySynchronizer {
100    /// Create a new synchronizer for the given repository path
101    pub fn new(repo_path: impl AsRef<Path>, config: SyncConfig) -> Result<Self> {
102        let repo_path = repo_path.as_ref().to_path_buf();
103        let repo = Repository::open(&repo_path).map_err(|_| SyncError::NotARepository {
104            path: repo_path.display().to_string(),
105        })?;
106
107        Ok(Self {
108            repo,
109            config,
110            _repo_path: repo_path,
111        })
112    }
113
114    /// Create a new synchronizer with auto-detected branch name
115    pub fn new_with_detected_branch(
116        repo_path: impl AsRef<Path>,
117        mut config: SyncConfig,
118    ) -> Result<Self> {
119        let repo_path = repo_path.as_ref().to_path_buf();
120        let repo = Repository::open(&repo_path).map_err(|_| SyncError::NotARepository {
121            path: repo_path.display().to_string(),
122        })?;
123
124        // Try to detect current branch
125        if let Ok(head) = repo.head() {
126            if head.is_branch() {
127                if let Some(branch_name) = head.shorthand() {
128                    config.branch_name = branch_name.to_string();
129                }
130            }
131        }
132
133        Ok(Self {
134            repo,
135            config,
136            _repo_path: repo_path,
137        })
138    }
139
140    /// Get the current repository state
141    pub fn get_repository_state(&self) -> Result<RepositoryState> {
142        // Check if HEAD is detached
143        if self.repo.head_detached()? {
144            return Ok(RepositoryState::DetachedHead);
145        }
146
147        // Check for various in-progress operations
148        let state = self.repo.state();
149        match state {
150            git2::RepositoryState::Clean => {
151                // Check if working directory is dirty
152                let mut status_opts = StatusOptions::new();
153                status_opts.include_untracked(true);
154                let statuses = self.repo.statuses(Some(&mut status_opts))?;
155
156                if statuses.is_empty() {
157                    Ok(RepositoryState::Clean)
158                } else {
159                    Ok(RepositoryState::Dirty)
160                }
161            }
162            git2::RepositoryState::Merge => Ok(RepositoryState::Merging),
163            git2::RepositoryState::Rebase
164            | git2::RepositoryState::RebaseInteractive
165            | git2::RepositoryState::RebaseMerge => Ok(RepositoryState::Rebasing),
166            git2::RepositoryState::CherryPick | git2::RepositoryState::CherryPickSequence => {
167                Ok(RepositoryState::CherryPicking)
168            }
169            git2::RepositoryState::Bisect => Ok(RepositoryState::Bisecting),
170            git2::RepositoryState::ApplyMailbox | git2::RepositoryState::ApplyMailboxOrRebase => {
171                Ok(RepositoryState::ApplyingPatches)
172            }
173            _ => Ok(RepositoryState::Clean),
174        }
175    }
176
177    /// Check if there are local changes that need to be committed
178    pub fn has_local_changes(&self) -> Result<bool> {
179        let mut status_opts = StatusOptions::new();
180        status_opts.include_untracked(self.config.sync_new_files);
181
182        let statuses = self.repo.statuses(Some(&mut status_opts))?;
183
184        for entry in statuses.iter() {
185            let status = entry.status();
186
187            if self.config.sync_new_files {
188                // Check for any changes including new files
189                if status.intersects(
190                    Status::WT_MODIFIED
191                        | Status::WT_DELETED
192                        | Status::WT_RENAMED
193                        | Status::WT_TYPECHANGE
194                        | Status::WT_NEW,
195                ) {
196                    return Ok(true);
197                }
198            } else {
199                // Only check for modifications to tracked files
200                if status.intersects(
201                    Status::WT_MODIFIED
202                        | Status::WT_DELETED
203                        | Status::WT_RENAMED
204                        | Status::WT_TYPECHANGE,
205                ) {
206                    return Ok(true);
207                }
208            }
209        }
210
211        Ok(false)
212    }
213
214    /// Check if there are unhandled file states that should prevent sync
215    pub fn check_unhandled_files(&self) -> Result<Option<UnhandledFileState>> {
216        let mut status_opts = StatusOptions::new();
217        status_opts.include_untracked(true);
218
219        let statuses = self.repo.statuses(Some(&mut status_opts))?;
220
221        for entry in statuses.iter() {
222            let status = entry.status();
223            let path = entry.path().unwrap_or("<unknown>").to_string();
224
225            // Check for conflicted files
226            if status.is_conflicted() {
227                return Ok(Some(UnhandledFileState::Conflicted { path }));
228            }
229        }
230
231        Ok(None)
232    }
233
234    /// Get the current branch name
235    pub fn get_current_branch(&self) -> Result<String> {
236        let head = self.repo.head()?;
237
238        if !head.is_branch() {
239            return Err(SyncError::DetachedHead);
240        }
241
242        let branch_name = head
243            .shorthand()
244            .ok_or_else(|| SyncError::Other("Could not get branch name".to_string()))?;
245
246        Ok(branch_name.to_string())
247    }
248
249    /// Get the sync state relative to the remote
250    pub fn get_sync_state(&self) -> Result<SyncState> {
251        let branch_name = self.get_current_branch()?;
252        let local_branch = self.repo.find_branch(&branch_name, BranchType::Local)?;
253
254        // Get the upstream branch
255        let upstream = match local_branch.upstream() {
256            Ok(upstream) => upstream,
257            Err(_) => return Ok(SyncState::NoUpstream),
258        };
259
260        // Get the OIDs for comparison
261        let local_oid = local_branch
262            .get()
263            .target()
264            .ok_or_else(|| SyncError::Other("Could not get local branch OID".to_string()))?;
265        let upstream_oid = upstream
266            .get()
267            .target()
268            .ok_or_else(|| SyncError::Other("Could not get upstream branch OID".to_string()))?;
269
270        // If they're the same, we're in sync
271        if local_oid == upstream_oid {
272            return Ok(SyncState::Equal);
273        }
274
275        // Count commits ahead and behind
276        let (ahead, behind) = self.repo.graph_ahead_behind(local_oid, upstream_oid)?;
277
278        match (ahead, behind) {
279            (0, 0) => Ok(SyncState::Equal),
280            (a, 0) if a > 0 => Ok(SyncState::Ahead(a)),
281            (0, b) if b > 0 => Ok(SyncState::Behind(b)),
282            (a, b) if a > 0 && b > 0 => Ok(SyncState::Diverged {
283                ahead: a,
284                behind: b,
285            }),
286            _ => Ok(SyncState::Equal),
287        }
288    }
289
290    /// Auto-commit local changes
291    pub fn auto_commit(&self) -> Result<()> {
292        info!("Auto-committing local changes");
293
294        // Stage changes
295        let mut index = self.repo.index()?;
296
297        if self.config.sync_new_files {
298            // Add all changes including new files
299            index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)?;
300        } else {
301            // Only update tracked files
302            index.update_all(["*"].iter(), None)?;
303        }
304
305        index.write()?;
306
307        // Check if there's anything to commit
308        let tree_id = index.write_tree()?;
309        let tree = self.repo.find_tree(tree_id)?;
310
311        let parent_commit = self.repo.head()?.peel_to_commit()?;
312        if parent_commit.tree_id() == tree_id {
313            debug!("No changes to commit");
314            return Ok(());
315        }
316
317        // Prepare commit message
318        let message = if let Some(ref msg) = self.config.commit_message {
319            msg.replace("{hostname}", &hostname::get()?.to_string_lossy())
320                .replace(
321                    "{timestamp}",
322                    &Local::now().format("%Y-%m-%d %I:%M:%S %p %Z").to_string(),
323                )
324        } else {
325            format!(
326                "changes from {} on {}",
327                hostname::get()?.to_string_lossy(),
328                Local::now().format("%Y-%m-%d %I:%M:%S %p %Z")
329            )
330        };
331
332        // Create signature
333        let sig = self.repo.signature()?;
334
335        // Create commit
336        self.repo
337            .commit(Some("HEAD"), &sig, &sig, &message, &tree, &[&parent_commit])?;
338
339        info!("Created auto-commit: {}", message);
340        Ok(())
341    }
342
343    /// Fetch from remote
344    pub fn fetch(&self) -> Result<()> {
345        info!("Fetching from remote: {}", self.config.remote_name);
346
347        // Use git command directly as a workaround for SSH issues
348        use std::process::Command;
349
350        let output = Command::new("git")
351            .arg("fetch")
352            .arg(&self.config.remote_name)
353            .arg(&self.config.branch_name)
354            .current_dir(&self._repo_path)
355            .output()
356            .map_err(|e| SyncError::Other(format!("Failed to run git fetch: {}", e)))?;
357
358        if !output.status.success() {
359            let stderr = String::from_utf8_lossy(&output.stderr);
360            error!("Git fetch failed: {}", stderr);
361            return Err(SyncError::Other(format!("Git fetch failed: {}", stderr)));
362        }
363
364        info!(
365            "Fetch completed successfully from remote: {}",
366            self.config.remote_name
367        );
368        return Ok(());
369
370        // Original libgit2 implementation (keeping for reference)
371        #[allow(unreachable_code)]
372        {
373            let mut remote = self.repo.find_remote(&self.config.remote_name)?;
374
375            // Log the remote URL for debugging
376            if let Some(url) = remote.url() {
377                debug!("Remote URL: {}", url);
378            }
379
380            // Prepare callbacks for authentication
381            let mut callbacks = git2::RemoteCallbacks::new();
382            callbacks.credentials(|url, username_from_url, allowed_types| {
383                debug!(
384                    "Authentication callback: url={}, username={:?}, allowed_types={:?}",
385                    url, username_from_url, allowed_types
386                );
387
388                let username = username_from_url.unwrap_or("git");
389
390                // First try SSH agent
391                debug!("Trying SSH key from agent with username: {}", username);
392                match git2::Cred::ssh_key_from_agent(username) {
393                    Ok(cred) => {
394                        debug!("Successfully obtained SSH credentials from agent");
395                        return Ok(cred);
396                    }
397                    Err(e) => {
398                        debug!("SSH agent failed: {}, trying default SSH key", e);
399                    }
400                }
401
402                // Fallback to default SSH key
403                let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
404                let ssh_dir = std::path::Path::new(&home).join(".ssh");
405                let private_key = ssh_dir.join("id_rsa");
406                let public_key = ssh_dir.join("id_rsa.pub");
407
408                // Try id_rsa first
409                if private_key.exists() {
410                    debug!("Trying SSH key from {:?}", private_key);
411                    match git2::Cred::ssh_key(username, Some(&public_key), &private_key, None) {
412                        Ok(cred) => {
413                            debug!("Successfully using SSH key from disk");
414                            return Ok(cred);
415                        }
416                        Err(e) => {
417                            debug!("Failed to use id_rsa: {}", e);
418                        }
419                    }
420                }
421
422                // Try id_ed25519
423                let private_key = ssh_dir.join("id_ed25519");
424                let public_key = ssh_dir.join("id_ed25519.pub");
425                if private_key.exists() {
426                    debug!("Trying SSH key from {:?}", private_key);
427                    match git2::Cred::ssh_key(username, Some(&public_key), &private_key, None) {
428                        Ok(cred) => {
429                            debug!("Successfully using ed25519 SSH key from disk");
430                            return Ok(cred);
431                        }
432                        Err(e) => {
433                            debug!("Failed to use id_ed25519: {}", e);
434                        }
435                    }
436                }
437
438                error!("No working SSH authentication method found");
439                Err(git2::Error::from_str(
440                    "No SSH authentication method available",
441                ))
442            });
443
444            // Add progress callback
445            callbacks.transfer_progress(|stats| {
446                debug!(
447                    "Fetch progress: {}/{} objects, {} bytes received",
448                    stats.received_objects(),
449                    stats.total_objects(),
450                    stats.received_bytes()
451                );
452                true
453            });
454
455            let mut fetch_options = git2::FetchOptions::new();
456            fetch_options.remote_callbacks(callbacks);
457
458            // Try to set proxy options from git config
459            let mut proxy_options = git2::ProxyOptions::new();
460            proxy_options.auto();
461            fetch_options.proxy_options(proxy_options);
462
463            debug!("Starting fetch for branch: {}", self.config.branch_name);
464            debug!(
465                "Fetching refspec: refs/heads/{}:refs/remotes/{}/{}",
466                self.config.branch_name, self.config.remote_name, self.config.branch_name
467            );
468
469            // Fetch the branch
470            match remote.fetch(&[&self.config.branch_name], Some(&mut fetch_options), None) {
471                Ok(_) => {
472                    info!(
473                        "Fetch completed successfully from remote: {}",
474                        self.config.remote_name
475                    );
476                    Ok(())
477                }
478                Err(e) => {
479                    error!(
480                        "Fetch failed from remote {}: {}",
481                        self.config.remote_name, e
482                    );
483                    Err(e.into())
484                }
485            }
486        }
487    }
488
489    /// Push to remote
490    pub fn push(&self) -> Result<()> {
491        info!("Pushing to remote: {}", self.config.remote_name);
492
493        // Use git command directly as a workaround for SSH issues
494        use std::process::Command;
495
496        let refspec = format!("{}:{}", self.config.branch_name, self.config.branch_name);
497
498        let output = Command::new("git")
499            .arg("push")
500            .arg(&self.config.remote_name)
501            .arg(&refspec)
502            .current_dir(&self._repo_path)
503            .output()
504            .map_err(|e| SyncError::Other(format!("Failed to run git push: {}", e)))?;
505
506        if !output.status.success() {
507            let stderr = String::from_utf8_lossy(&output.stderr);
508            error!("Git push failed: {}", stderr);
509            return Err(SyncError::Other(format!("Git push failed: {}", stderr)));
510        }
511
512        info!(
513            "Push completed successfully to remote: {}",
514            self.config.remote_name
515        );
516        return Ok(());
517
518        // Original libgit2 implementation (keeping for reference)
519        #[allow(unreachable_code)]
520        {
521            let mut remote = self.repo.find_remote(&self.config.remote_name)?;
522
523            // Prepare callbacks for authentication
524            let mut callbacks = git2::RemoteCallbacks::new();
525            callbacks.credentials(|url, username_from_url, allowed_types| {
526                debug!(
527                    "Authentication callback: url={}, username={:?}, allowed_types={:?}",
528                    url, username_from_url, allowed_types
529                );
530
531                let username = username_from_url.unwrap_or("git");
532
533                // First try SSH agent
534                debug!("Trying SSH key from agent with username: {}", username);
535                match git2::Cred::ssh_key_from_agent(username) {
536                    Ok(cred) => {
537                        debug!("Successfully obtained SSH credentials from agent");
538                        return Ok(cred);
539                    }
540                    Err(e) => {
541                        debug!("SSH agent failed: {}, trying default SSH key", e);
542                    }
543                }
544
545                // Fallback to default SSH key
546                let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
547                let ssh_dir = std::path::Path::new(&home).join(".ssh");
548                let private_key = ssh_dir.join("id_rsa");
549                let public_key = ssh_dir.join("id_rsa.pub");
550
551                // Try id_rsa first
552                if private_key.exists() {
553                    debug!("Trying SSH key from {:?}", private_key);
554                    match git2::Cred::ssh_key(username, Some(&public_key), &private_key, None) {
555                        Ok(cred) => {
556                            debug!("Successfully using SSH key from disk");
557                            return Ok(cred);
558                        }
559                        Err(e) => {
560                            debug!("Failed to use id_rsa: {}", e);
561                        }
562                    }
563                }
564
565                // Try id_ed25519
566                let private_key = ssh_dir.join("id_ed25519");
567                let public_key = ssh_dir.join("id_ed25519.pub");
568                if private_key.exists() {
569                    debug!("Trying SSH key from {:?}", private_key);
570                    match git2::Cred::ssh_key(username, Some(&public_key), &private_key, None) {
571                        Ok(cred) => {
572                            debug!("Successfully using ed25519 SSH key from disk");
573                            return Ok(cred);
574                        }
575                        Err(e) => {
576                            debug!("Failed to use id_ed25519: {}", e);
577                        }
578                    }
579                }
580
581                error!("No working SSH authentication method found");
582                Err(git2::Error::from_str(
583                    "No SSH authentication method available",
584                ))
585            });
586
587            let mut push_options = git2::PushOptions::new();
588            push_options.remote_callbacks(callbacks);
589
590            // Try to set proxy options from git config
591            let mut proxy_options = git2::ProxyOptions::new();
592            proxy_options.auto();
593            push_options.proxy_options(proxy_options);
594
595            // Push the branch
596            let refspec = format!(
597                "refs/heads/{}:refs/heads/{}",
598                self.config.branch_name, self.config.branch_name
599            );
600
601            debug!("Pushing refspec: {}", refspec);
602            match remote.push(&[&refspec], Some(&mut push_options)) {
603                Ok(_) => {
604                    info!(
605                        "Push completed successfully to remote: {}",
606                        self.config.remote_name
607                    );
608                    Ok(())
609                }
610                Err(e) => {
611                    error!("Push failed to remote {}: {}", self.config.remote_name, e);
612                    Err(e.into())
613                }
614            }
615        }
616    }
617
618    /// Perform a fast-forward merge
619    pub fn fast_forward_merge(&self) -> Result<()> {
620        info!("Performing fast-forward merge");
621
622        let branch_name = self.get_current_branch()?;
623        let local_branch = self.repo.find_branch(&branch_name, BranchType::Local)?;
624        let upstream = local_branch.upstream()?;
625
626        let upstream_oid = upstream
627            .get()
628            .target()
629            .ok_or_else(|| SyncError::Other("Could not get upstream OID".to_string()))?;
630
631        // Fast-forward by moving the reference
632        let mut reference = self.repo.head()?;
633        reference.set_target(upstream_oid, "fast-forward merge")?;
634
635        // Checkout the new HEAD to update working directory
636        let object = self.repo.find_object(upstream_oid, None)?;
637        let mut checkout_builder = git2::build::CheckoutBuilder::new();
638        checkout_builder.force(); // Force update working directory files
639        self.repo
640            .checkout_tree(&object, Some(&mut checkout_builder))?;
641
642        // Update HEAD to point to the new commit
643        self.repo.set_head(&format!("refs/heads/{}", branch_name))?;
644
645        info!("Fast-forward merge completed - working tree updated");
646        Ok(())
647    }
648
649    /// Perform a rebase
650    pub fn rebase(&self) -> Result<()> {
651        info!("Performing rebase");
652
653        let branch_name = self.get_current_branch()?;
654        let local_branch = self.repo.find_branch(&branch_name, BranchType::Local)?;
655        let upstream = local_branch.upstream()?;
656
657        let upstream_commit = upstream.get().peel_to_commit()?;
658        let local_commit = local_branch.get().peel_to_commit()?;
659
660        // Find merge base
661        let merge_base = self
662            .repo
663            .merge_base(local_commit.id(), upstream_commit.id())?;
664        let _merge_base_commit = self.repo.find_commit(merge_base)?;
665
666        // Create signature
667        let sig = self.repo.signature()?;
668
669        // Get annotated commits from references
670        let local_annotated = self
671            .repo
672            .reference_to_annotated_commit(local_branch.get())?;
673        let upstream_annotated = self.repo.reference_to_annotated_commit(upstream.get())?;
674
675        // Start rebase
676        let mut rebase = self.repo.rebase(
677            Some(&local_annotated),
678            Some(&upstream_annotated),
679            None,
680            None,
681        )?;
682
683        // Process each commit
684        while let Some(operation) = rebase.next() {
685            let _operation = operation?;
686
687            // Check if we can continue
688            if self.repo.index()?.has_conflicts() {
689                warn!("Conflicts detected during rebase");
690                rebase.abort()?;
691                return Err(SyncError::ManualInterventionRequired {
692                    reason: "Rebase conflicts detected. Please resolve manually.".to_string(),
693                });
694            }
695
696            // Continue with the rebase
697            rebase.commit(None, &sig, None)?;
698        }
699
700        // Finish the rebase
701        rebase.finish(Some(&sig))?;
702
703        // Ensure working tree is properly updated after rebase
704        let head = self.repo.head()?;
705        let head_commit = head.peel_to_commit()?;
706        let mut checkout_builder = git2::build::CheckoutBuilder::new();
707        checkout_builder.force();
708        self.repo
709            .checkout_tree(head_commit.as_object(), Some(&mut checkout_builder))?;
710
711        info!("Rebase completed successfully - working tree updated");
712        Ok(())
713    }
714
715    /// Main sync operation
716    pub fn sync(&self, check_only: bool) -> Result<()> {
717        info!("Starting sync operation (check_only: {})", check_only);
718
719        // Check repository state
720        let repo_state = self.get_repository_state()?;
721        match repo_state {
722            RepositoryState::Clean | RepositoryState::Dirty => {
723                // These states are OK to continue
724            }
725            RepositoryState::DetachedHead => {
726                return Err(SyncError::DetachedHead);
727            }
728            _ => {
729                return Err(SyncError::UnsafeRepositoryState {
730                    state: format!("{:?}", repo_state),
731                });
732            }
733        }
734
735        // Check for unhandled files
736        if let Some(unhandled) = self.check_unhandled_files()? {
737            let reason = match unhandled {
738                UnhandledFileState::Conflicted { path } => format!("Conflicted file: {}", path),
739            };
740            return Err(SyncError::ManualInterventionRequired { reason });
741        }
742
743        // If we're only checking, we're done
744        if check_only {
745            info!("Check passed, sync can proceed");
746            return Ok(());
747        }
748
749        // Auto-commit if there are local changes
750        if self.has_local_changes()? {
751            self.auto_commit()?;
752        }
753
754        // Fetch from remote
755        self.fetch()?;
756
757        // Get sync state and handle accordingly
758        let sync_state = self.get_sync_state()?;
759        match sync_state {
760            SyncState::Equal => {
761                info!("Already in sync");
762            }
763            SyncState::Ahead(_) => {
764                info!("Local is ahead, pushing");
765                self.push()?;
766            }
767            SyncState::Behind(_) => {
768                info!("Local is behind, fast-forwarding");
769                self.fast_forward_merge()?;
770            }
771            SyncState::Diverged { .. } => {
772                info!("Branches have diverged, rebasing");
773                self.rebase()?;
774                // After successful rebase, push the changes
775                self.push()?;
776            }
777            SyncState::NoUpstream => {
778                return Err(SyncError::NoRemoteConfigured {
779                    branch: self.config.branch_name.clone(),
780                });
781            }
782        }
783
784        // Verify we're in sync
785        let final_state = self.get_sync_state()?;
786        if final_state != SyncState::Equal {
787            warn!(
788                "Sync completed but repository is not in sync: {:?}",
789                final_state
790            );
791            return Err(SyncError::Other(
792                "Sync completed but repository is not in sync".to_string(),
793            ));
794        }
795
796        info!("Sync completed successfully");
797        Ok(())
798    }
799}