git_workflow/commands/
sync.rs1use super::helpers;
26use crate::error::{GwError, Result};
27use crate::git;
28use crate::github::{self, PrState};
29use crate::output;
30use crate::state::{RepoType, WorkingDirState};
31
32pub fn run(verbose: bool) -> Result<()> {
34 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 let repo_type = RepoType::detect()?;
51 let home_branch = repo_type.home_branch();
52 let current = git::current_branch()?;
53
54 if current == home_branch {
56 println!();
57 output::info(&format!("Branch: {}", output::bold(¤t)));
58
59 output::info("Fetching from origin...");
61 git::fetch_prune(verbose)?;
62 output::success("Fetched (stale remote branches pruned)");
63
64 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(¤t)));
75
76 output::info("Fetching from origin...");
78 git::fetch_prune(verbose)?;
79
80 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 let pr = match github::get_pr_for_branch(¤t)? {
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 let default_remote = git::get_default_remote_branch()?;
102 let default_branch = default_remote.strip_prefix("origin/").unwrap_or("main");
103
104 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 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 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 output::info(&format!(" Updating PR base to {}...", default_branch));
154 github::update_pr_base(pr.number, default_branch)?;
155
156 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 output::info(" Force pushing...");
167 git::force_push_with_lease(¤t, verbose)?;
168
169 println!();
170 output::ready("Synced", ¤t);
171 output::hints(&[
172 &format!("PR #{} base is now '{}'", pr.number, default_branch),
173 "gw status # Check status",
174 ]);
175
176 Ok(())
177}