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