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    // 6. Check if base is already main
88    if pr.base_branch == "main" {
89        output::success("Base is already 'main'. Nothing to sync.");
90        output::hints(&["git rebase origin/main  # If you need to update"]);
91        return Ok(());
92    }
93
94    // 7. Check if base branch's PR is merged
95    let base_pr = match github::get_pr_for_branch(&pr.base_branch)? {
96        Some(base_pr) => base_pr,
97        None => {
98            output::warn(&format!(
99                "No PR found for base branch '{}'. Cannot determine if it's merged.",
100                pr.base_branch
101            ));
102            return Ok(());
103        }
104    };
105
106    if !base_pr.state.is_merged() {
107        let state_str = match &base_pr.state {
108            PrState::Open => "still open",
109            PrState::Closed => "closed (not merged)",
110            PrState::Merged { .. } => "merged",
111        };
112        output::warn(&format!(
113            "Base PR #{} ({}) is {}.",
114            base_pr.number, pr.base_branch, state_str
115        ));
116        output::hints(&["Wait for the base PR to be merged first"]);
117        return Ok(());
118    }
119
120    // Base PR is merged - proceed with sync
121    output::success(&format!(
122        "Base PR #{} ({}) is merged ✓",
123        base_pr.number, pr.base_branch
124    ));
125
126    println!();
127    output::info("Syncing...");
128
129    // 8. Update PR base to main
130    output::info("  Updating PR base to main...");
131    github::update_pr_base(pr.number, "main")?;
132
133    // 9. Rebase on origin/main
134    output::info("  Rebasing on origin/main...");
135    if let Err(e) = git::rebase("origin/main", verbose) {
136        output::error("Rebase failed. You may need to resolve conflicts manually.");
137        output::action("git rebase --continue  # After resolving conflicts");
138        output::action("git rebase --abort     # To cancel");
139        return Err(e);
140    }
141
142    // 10. Force push
143    output::info("  Force pushing...");
144    git::force_push_with_lease(&current, verbose)?;
145
146    println!();
147    output::ready("Synced", &current);
148    output::hints(&[
149        &format!("PR #{} base is now 'main'", pr.number),
150        "mise run git:status  # Check status",
151    ]);
152
153    Ok(())
154}