1#[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord)]
2pub struct Script {
3 pub commands: Vec<Command>,
4 pub dependents: Vec<Script>,
5}
6
7impl Script {
8 pub fn new() -> Self {
9 Default::default()
10 }
11
12 pub fn is_empty(&self) -> bool {
13 self.commands.is_empty() && self.dependents.is_empty()
14 }
15
16 pub fn branch(&self) -> Option<&str> {
17 for command in self.commands.iter().rev() {
18 if let Command::CreateBranch(name) = command {
19 return Some(name);
20 }
21 }
22
23 None
24 }
25
26 pub fn dependent_branches(&self) -> Vec<&str> {
27 let mut branches = Vec::new();
28 for dependent in self.dependents.iter() {
29 branches.push(dependent.branch().unwrap_or("detached"));
30 branches.extend(dependent.dependent_branches());
31 }
32 branches
33 }
34
35 pub fn is_branch_deleted(&self, branch: &str) -> bool {
36 for command in &self.commands {
37 if let Command::DeleteBranch(current) = command {
38 if branch == current {
39 return true;
40 }
41 }
42 }
43
44 for dependent in &self.dependents {
45 if dependent.is_branch_deleted(branch) {
46 return true;
47 }
48 }
49
50 false
51 }
52}
53
54#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
55pub enum Command {
56 SwitchCommit(git2::Oid),
58 RegisterMark(git2::Oid),
60 SwitchMark(git2::Oid),
62 CherryPick(git2::Oid),
64 Fixup(git2::Oid),
66 CreateBranch(String),
68 DeleteBranch(String),
70}
71
72pub struct Executor {
73 head_oid: git2::Oid,
74 marks: std::collections::HashMap<git2::Oid, git2::Oid>,
75 branches: Vec<(git2::Oid, String)>,
76 delete_branches: Vec<String>,
77 post_rewrite: Vec<(git2::Oid, git2::Oid)>,
78 dry_run: bool,
79 detached: bool,
80}
81
82impl Executor {
83 pub fn new(repo: &dyn crate::legacy::git::Repo, dry_run: bool) -> Executor {
84 let head_oid = repo.head_commit().id;
85 Self {
86 head_oid,
87 marks: Default::default(),
88 branches: Default::default(),
89 delete_branches: Default::default(),
90 post_rewrite: Default::default(),
91 dry_run,
92 detached: false,
93 }
94 }
95
96 pub fn run_script<'s>(
97 &mut self,
98 repo: &mut dyn crate::legacy::git::Repo,
99 script: &'s Script,
100 ) -> Vec<(git2::Error, &'s str, Vec<&'s str>)> {
101 let mut failures = Vec::new();
102 let branch_name = script.branch().unwrap_or("detached");
103
104 log::trace!("Applying `{}`", branch_name);
105 log::trace!("Script: {:#?}", script.commands);
106 #[allow(clippy::disallowed_methods)]
107 let res = script
108 .commands
109 .iter()
110 .try_for_each(|command| self.stage_single(repo, command));
111 match res.and_then(|_| self.commit(repo)) {
112 Ok(()) => {
113 log::trace!(" `{}` succeeded", branch_name);
114 for dependent in script.dependents.iter() {
115 failures.extend(self.run_script(repo, dependent));
116 }
117 if !failures.is_empty() {
118 log::trace!(" `{}`'s dependent failed", branch_name);
119 }
120 }
121 Err(err) => {
122 log::trace!(" `{}` failed: {}", branch_name, err);
123 self.abandon(repo);
124 failures.push((err, branch_name, script.dependent_branches()));
125 }
126 }
127
128 failures
129 }
130
131 pub fn stage_single(
132 &mut self,
133 repo: &mut dyn crate::legacy::git::Repo,
134 command: &Command,
135 ) -> Result<(), git2::Error> {
136 match command {
137 Command::SwitchCommit(oid) => {
138 let commit = repo.find_commit(*oid).ok_or_else(|| {
139 git2::Error::new(
140 git2::ErrorCode::NotFound,
141 git2::ErrorClass::Reference,
142 format!("could not find commit {oid:?}"),
143 )
144 })?;
145 log::trace!("git checkout {} # {}", oid, commit.summary);
146 self.head_oid = *oid;
147 }
148 Command::RegisterMark(mark_oid) => {
149 let target_oid = self.head_oid;
150 self.marks.insert(*mark_oid, target_oid);
151 }
152 Command::SwitchMark(mark_oid) => {
153 let oid = *self
154 .marks
155 .get(mark_oid)
156 .expect("We only switch to marks that are created");
157
158 let commit = repo.find_commit(oid).unwrap();
159 log::trace!("git checkout {} # {}", oid, commit.summary);
160 self.head_oid = oid;
161 }
162 Command::CherryPick(cherry_oid) => {
163 let cherry_commit = repo.find_commit(*cherry_oid).ok_or_else(|| {
164 git2::Error::new(
165 git2::ErrorCode::NotFound,
166 git2::ErrorClass::Reference,
167 format!("could not find commit {cherry_oid:?}"),
168 )
169 })?;
170 log::trace!(
171 "git cherry-pick {} # {}",
172 cherry_oid,
173 cherry_commit.summary
174 );
175 let updated_oid = if self.dry_run {
176 *cherry_oid
177 } else {
178 repo.cherry_pick(self.head_oid, *cherry_oid)?
179 };
180 self.post_rewrite.push((*cherry_oid, updated_oid));
181 self.head_oid = updated_oid;
182 }
183 Command::Fixup(squash_oid) => {
184 let cherry_commit = repo.find_commit(*squash_oid).ok_or_else(|| {
185 git2::Error::new(
186 git2::ErrorCode::NotFound,
187 git2::ErrorClass::Reference,
188 format!("could not find commit {squash_oid:?}"),
189 )
190 })?;
191 log::trace!(
192 "git merge --squash {} # {}",
193 squash_oid,
194 cherry_commit.summary
195 );
196 let updated_oid = if self.dry_run {
197 *squash_oid
198 } else {
199 repo.squash(*squash_oid, self.head_oid)?
200 };
201 for (_old_oid, new_oid) in &mut self.post_rewrite {
202 if *new_oid == self.head_oid {
203 *new_oid = updated_oid;
204 }
205 }
206 self.post_rewrite.push((*squash_oid, updated_oid));
207 self.head_oid = updated_oid;
208 }
209 Command::CreateBranch(name) => {
210 let branch_oid = self.head_oid;
211 self.branches.push((branch_oid, name.to_owned()));
212 }
213 Command::DeleteBranch(name) => {
214 self.delete_branches.push(name.to_owned());
215 }
216 }
217
218 Ok(())
219 }
220
221 pub fn commit(&mut self, repo: &mut dyn crate::legacy::git::Repo) -> Result<(), git2::Error> {
222 let hook_repo = repo.path().map(git2::Repository::open).transpose()?;
223 let hooks = if self.dry_run {
224 None
225 } else {
226 hook_repo
227 .as_ref()
228 .map(git2_ext::hooks::Hooks::with_repo)
229 .transpose()?
230 };
231
232 log::trace!("Running reference-transaction hook");
233 let reference_transaction = self.branches.clone();
234 let reference_transaction: Vec<(git2::Oid, git2::Oid, &str)> = reference_transaction
235 .iter()
236 .map(|(new_oid, name)| {
237 let old_oid = git2::Oid::zero();
240 (old_oid, *new_oid, name.as_str())
241 })
242 .collect();
243 let reference_transaction =
244 if let (Some(hook_repo), Some(hooks)) = (hook_repo.as_ref(), hooks.as_ref()) {
245 Some(
246 hooks
247 .run_reference_transaction(hook_repo, &reference_transaction)
248 .map_err(|err| {
249 git2::Error::new(
250 git2::ErrorCode::GenericError,
251 git2::ErrorClass::Os,
252 err.to_string(),
253 )
254 })?,
255 )
256 } else {
257 None
258 };
259
260 if !self.branches.is_empty() || !self.delete_branches.is_empty() {
261 if !self.dry_run {
263 repo.detach()?;
264 self.detached = true;
265 }
266
267 for (oid, name) in self.branches.iter() {
268 let commit = repo.find_commit(*oid).unwrap();
269 log::trace!("git checkout {} # {}", oid, commit.summary);
270 log::trace!("git switch -c {}", name);
271 if !self.dry_run {
272 repo.branch(name, *oid)?;
273 }
274 }
275 }
276 self.branches.clear();
277
278 for name in self.delete_branches.iter() {
279 log::trace!("git branch -D {}", name);
280 if !self.dry_run {
281 repo.delete_branch(name)?;
282 }
283 }
284 self.delete_branches.clear();
285
286 if let Some(tx) = reference_transaction {
287 tx.committed();
288 }
289 self.post_rewrite.retain(|(old, new)| old != new);
290 if !self.post_rewrite.is_empty() {
291 log::trace!("Running post-rewrite hook");
292 if let (Some(hook_repo), Some(hooks)) = (hook_repo.as_ref(), hooks.as_ref()) {
293 hooks.run_post_rewrite_rebase(hook_repo, &self.post_rewrite);
294 }
295 self.post_rewrite.clear();
296 }
297
298 Ok(())
299 }
300
301 pub fn abandon(&mut self, repo: &dyn crate::legacy::git::Repo) {
302 self.head_oid = repo.head_commit().id;
303 self.branches.clear();
304 self.delete_branches.clear();
305 self.post_rewrite.clear();
306 }
307
308 pub fn close(
309 &mut self,
310 repo: &mut dyn crate::legacy::git::Repo,
311 restore_branch: &str,
312 ) -> Result<(), git2::Error> {
313 assert_eq!(&self.branches, &[]);
314 assert_eq!(self.delete_branches, Vec::<String>::new());
315 log::trace!("git switch {}", restore_branch);
316 if !self.dry_run {
317 if self.detached {
318 repo.switch(restore_branch)?;
319 }
320 self.head_oid = repo.head_commit().id;
321 }
322
323 Ok(())
324 }
325}