Skip to main content

git_workflow/commands/
sync.rs

1//! `gw sync` command - Sync current branch after base PR merge
2//!
3//! When the base PR of your current branch has been merged, this command:
4//! 1. Updates the PR's base branch to main via `gh pr edit --base main`
5//! 2. Rebases the branch on origin/main
6//! 3. Force pushes with --force-with-lease
7//!
8//! # Example
9//!
10//! ```text
11//! $ gw status
12//!   Branch: feature/child
13//!   PR: #42 (open)
14//!   Base: feature/base (merged ✓)
15//!
16//!   Next: gw sync
17//!
18//! $ gw sync
19//!   Updating base: feature/base → main
20//!   Rebasing on origin/main...
21//!   Force pushing...
22//!   ✓ Synced
23//! ```
24
25use crate::error::{GwError, Result};
26use crate::git;
27use crate::github::{self, PrState};
28use crate::output;
29use crate::state::{RepoType, WorkingDirState};
30
31/// Execute the `sync` command
32pub fn run(verbose: bool) -> Result<()> {
33    // 1. Check prerequisites
34    if !git::is_git_repo() {
35        return Err(GwError::NotAGitRepository);
36    }
37
38    let working_dir = WorkingDirState::detect();
39    if !working_dir.is_clean() {
40        output::error(&format!(
41            "You have uncommitted changes ({}).",
42            working_dir.description()
43        ));
44        output::action("git stash -u -m 'WIP before sync'");
45        return Err(GwError::UncommittedChanges);
46    }
47
48    // 2. Get current branch info
49    let repo_type = RepoType::detect()?;
50    let home_branch = repo_type.home_branch();
51    let current = git::current_branch()?;
52
53    // Don't run on home branch
54    if current == home_branch {
55        output::warn("Already on home branch. Nothing to sync.");
56        output::hints(&["mise run git:new feature/...  # Create a feature branch first"]);
57        return Ok(());
58    }
59
60    println!();
61    output::info(&format!("Branch: {}", output::bold(&current)));
62
63    // 3. Fetch latest first to get accurate PR/branch state
64    output::info("Fetching from origin...");
65    git::fetch_prune(verbose)?;
66
67    // 4. Check GitHub CLI
68    if !github::is_gh_available() {
69        return Err(GwError::Other(
70            "GitHub CLI (gh) is not available. Install it from https://cli.github.com/".into(),
71        ));
72    }
73
74    // 5. Get PR info for current branch
75    let pr = match github::get_pr_for_branch(&current)? {
76        Some(pr) => pr,
77        None => {
78            output::warn("No PR found for this branch.");
79            output::hints(&["gh pr create  # Create a PR first"]);
80            return Ok(());
81        }
82    };
83
84    output::info(&format!("PR: #{} ({})", pr.number, pr.title));
85    output::info(&format!("Base: {}", pr.base_branch));
86
87    // Detect default remote branch
88    let default_remote = git::get_default_remote_branch()?;
89    let default_branch = default_remote.strip_prefix("origin/").unwrap_or("main");
90
91    // 6. Check if base is already the default branch
92    if pr.base_branch == default_branch {
93        output::success(&format!(
94            "Base is already '{}'. Nothing to sync.",
95            default_branch
96        ));
97        output::hints(&[&format!(
98            "git rebase {}  # If you need to update",
99            default_remote
100        )]);
101        return Ok(());
102    }
103
104    // 7. Check if base branch's PR is merged
105    let base_pr = match github::get_pr_for_branch(&pr.base_branch)? {
106        Some(base_pr) => base_pr,
107        None => {
108            output::warn(&format!(
109                "No PR found for base branch '{}'. Cannot determine if it's merged.",
110                pr.base_branch
111            ));
112            return Ok(());
113        }
114    };
115
116    if !base_pr.state.is_merged() {
117        let state_str = match &base_pr.state {
118            PrState::Open => "still open",
119            PrState::Closed => "closed (not merged)",
120            PrState::Merged { .. } => "merged",
121        };
122        output::warn(&format!(
123            "Base PR #{} ({}) is {}.",
124            base_pr.number, pr.base_branch, state_str
125        ));
126        output::hints(&["Wait for the base PR to be merged first"]);
127        return Ok(());
128    }
129
130    // Base PR is merged - proceed with sync
131    output::success(&format!(
132        "Base PR #{} ({}) is merged ✓",
133        base_pr.number, pr.base_branch
134    ));
135
136    println!();
137    output::info("Syncing...");
138
139    // 8. Update PR base to default branch
140    output::info(&format!("  Updating PR base to {}...", default_branch));
141    github::update_pr_base(pr.number, default_branch)?;
142
143    // 9. Rebase on default remote
144    output::info(&format!("  Rebasing on {}...", default_remote));
145    if let Err(e) = git::rebase(&default_remote, verbose) {
146        output::error("Rebase failed. You may need to resolve conflicts manually.");
147        output::action("git rebase --continue  # After resolving conflicts");
148        output::action("git rebase --abort     # To cancel");
149        return Err(e);
150    }
151
152    // 10. Force push
153    output::info("  Force pushing...");
154    git::force_push_with_lease(&current, verbose)?;
155
156    println!();
157    output::ready("Synced", &current);
158    output::hints(&[
159        &format!("PR #{} base is now '{}'", pr.number, default_branch),
160        "gw status  # Check status",
161    ]);
162
163    Ok(())
164}