1use crate::api::search::PullRequestStatus;
2use crate::graph::FlatDep;
3use crate::util::loop_until_confirm;
4use git2::build::CheckoutBuilder;
5use git2::{
6 CherrypickOptions,
7 Repository, Sort,
8 Revwalk, Oid, Commit,
9 Index
10};
11
12use std::error::Error;
13use tokio::process::Command;
14
15fn remote_ref(remote: &str, git_ref: &str) -> String {
16 format!("{}/{}", remote, git_ref)
17}
18
19pub fn generate_rebase_script(deps: FlatDep) -> String {
25 let deps = deps
26 .iter()
27 .filter(|(dep, _)| *dep.state() == PullRequestStatus::Open)
28 .collect::<Vec<_>>();
29
30 let mut out = String::new();
31
32 out.push_str("#!/usr/bin/env bash\n\n");
33 out.push_str("set -euo pipefail\n");
34 out.push_str("set -o xtrace\n\n");
35
36 out.push_str("# ------ THIS SCRIPT ASSUMES YOUR PR STACK IS A SINGLE CHAIN WITHOUT BRANCHING ----- #\n\n");
37 out.push_str("# It starts at the base of the stack, cherry-picking onto the new base and force-pushing as it goes.\n");
38 out.push_str("# We can't tell where the initial cherry-pick should stop (mainly because of our squash merge workflow),\n");
39 out.push_str(
40 "# so that initial stopping point for the first PR needs to be specified manually.\n\n",
41 );
42
43 out.push_str("export PREBASE=\"<enter a marker to stop the initial cherry-pick at>\"\n");
44
45 for (from, to) in deps {
46 let to = if let Some(pr) = to {
47 pr.head().to_string()
48 } else {
49 String::from("<enter a ref to rebase the stack on; usually `develop`>")
50 };
51
52 out.push_str("\n# -------------- #\n\n");
53
54 out.push_str(&format!("export TO=\"{}\"\n", remote_ref("heap", &to)));
55 out.push_str(&format!(
56 "export FROM=\"{}\"\n\n",
57 remote_ref("heap", from.head())
58 ));
59
60 out.push_str("git checkout \"$TO\"\n");
61 out.push_str("git cherry-pick \"$PREBASE\"..\"$FROM\"\n");
62 out.push_str("export PREBASE=\"$(git rev-parse --verify $FROM)\"\n");
63 out.push_str("git push -f heap HEAD:refs/heads/\"$FROM\"\n");
64 }
65
66 out
67}
68
69fn oid_to_commit(repo: &Repository, oid: Oid) -> Commit {
70 repo.find_commit(oid).unwrap()
71}
72
73fn head_commit(repo: &Repository) -> Commit {
74 repo.find_commit(repo.head().unwrap().target().unwrap()).unwrap()
75}
76
77fn checkout_commit(repo: &Repository, commit: &Commit, options: Option<&mut CheckoutBuilder>) {
78 repo.checkout_tree(&commit.as_object(), options).unwrap();
79 repo.set_head_detached(commit.id()).unwrap();
80}
81
82fn rev_to_commit<'a>(repo: &'a Repository, rev: &str) -> Commit<'a> {
83 let commit = repo.revparse_single(rev).unwrap();
84 let commit = commit.into_commit().unwrap();
85 commit
86}
87
88fn create_commit<'a>(repo: &'a Repository, index: &mut Index, message: &str) -> Commit<'a> {
90 let tree = index.write_tree_to(&repo).unwrap();
91 let tree = repo.find_tree(tree).unwrap();
92
93 let signature = repo.signature().unwrap();
94 let commit = repo
95 .commit(
96 None,
97 &signature,
98 &signature,
99 message,
100 &tree,
101 &[&head_commit(repo)]
102 )
103 .unwrap();
104
105 let commit = oid_to_commit(&repo, commit);
106
107 let mut cb = CheckoutBuilder::new();
108 cb.force();
109
110 checkout_commit(&repo, &commit, Some(&mut cb));
111
112 repo.cleanup_state().unwrap();
115
116 commit
117}
118
119fn cherry_pick_range(repo: &Repository, walk: &mut Revwalk) {
120 for from in walk {
121 let from = oid_to_commit(&repo, from.unwrap());
122
123 if from.parent_count() > 1 {
124 panic!("Exiting: I don't know how to deal with merge commits correctly.");
125 }
126
127 let mut cb = CheckoutBuilder::new();
128 cb.allow_conflicts(true);
129 let mut opts = CherrypickOptions::new();
130 opts.checkout_builder(cb);
131
132 println!("Cherry-picking: {:?}", from);
133 repo.cherrypick(&from, Some(&mut opts)).unwrap();
134
135 let mut index = repo.index().unwrap();
136
137 if index.has_conflicts() {
138 let prompt = "Conflicts! Resolve manually and `git add` each one (don't run any `git cherry-pick` commands, though).";
139 loop_until_confirm(prompt);
140
141 index = repo.index().unwrap();
143 index.read(true).unwrap();
144 }
145
146 create_commit(&repo, &mut index, from.message().unwrap());
147 }
148}
149
150pub async fn perform_rebase(
151 deps: FlatDep,
152 repo: &Repository,
153 remote: &str,
154 boundary: Option<&str>
155) -> Result<(), Box<dyn Error>> {
156 let deps = deps
157 .iter()
158 .filter(|(dep, _)| *dep.state() == PullRequestStatus::Open)
159 .collect::<Vec<_>>();
160
161 let (pr, _) = deps[0];
162
163 let base = rev_to_commit(&repo, &remote_ref(remote, pr.base()));
164 let head = rev_to_commit(&repo, pr.head());
165
166 let mut stop_cherry_pick_at = match boundary {
167 Some(rev) => rev_to_commit(&repo, rev).id(),
168 None => repo.merge_base(base.id(), head.id()).unwrap()
169 };
170 let mut update_local_branches_to = vec![];
171
172 println!("Checking out {:?}", base);
173 checkout_commit(&repo, &base, None);
174
175 let mut push_refspecs = vec![];
176
177 for (pr, _) in deps {
178 println!("\nWorking on PR: {:?}", pr.head());
179
180 let from = rev_to_commit(&repo, pr.head());
181
182 let mut walk = repo.revwalk().unwrap();
183 walk.set_sorting(Sort::TOPOLOGICAL).unwrap();
184 walk.set_sorting(Sort::REVERSE).unwrap();
185 walk.push(from.id()).unwrap();
186 walk.hide(stop_cherry_pick_at).unwrap();
187
188 cherry_pick_range(&repo, &mut walk);
191
192 update_local_branches_to.push((pr.head(), head_commit(&repo)));
196
197 let from = rev_to_commit(&repo, &remote_ref(remote, pr.head()));
199 stop_cherry_pick_at = from.id();
200
201 push_refspecs.push(format!("{}:refs/heads/{}", head_commit(&repo).id(), pr.head()));
202 }
203
204 let repo_dir = repo.workdir().unwrap().to_str().unwrap();
205
206 let mut command = Command::new("git");
209 command.arg("push").arg("-f").arg(remote);
210 command.args(push_refspecs.as_slice());
211 command.current_dir(repo_dir);
212
213 println!("\n{:?}", push_refspecs);
214 loop_until_confirm("Going to push these refspecs ☝️ ");
215
216 command.spawn()?.await?;
217
218 println!("\nUpdating local branches so they point to the new stack.\n");
219 for (branch, target) in update_local_branches_to {
220 println!(" + Branch {} now points to {}", branch, target.id());
221 repo.branch(branch, &target, true).unwrap();
222 }
223
224 Ok(())
225}