1#[macro_use]
2extern crate slog;
3use anyhow::{anyhow, Result};
4
5mod commute;
6mod config;
7mod owned;
8mod stack;
9
10use std::io::Write;
11use std::path::Path;
12
13pub struct Config<'a> {
14 pub dry_run: bool,
15 pub force_author: bool,
16 pub force_detach: bool,
17 pub base: Option<&'a str>,
18 pub and_rebase: bool,
19 pub rebase_options: &'a Vec<&'a str>,
20 pub whole_file: bool,
21 pub one_fixup_per_commit: bool,
22}
23
24pub fn run(logger: &slog::Logger, config: &Config) -> Result<()> {
25 let repo = git2::Repository::open_from_env()?;
26 debug!(logger, "repository found"; "path" => repo.path().to_str());
27
28 run_with_repo(&logger, &config, &repo)
29}
30
31fn run_with_repo(logger: &slog::Logger, config: &Config, repo: &git2::Repository) -> Result<()> {
32 if !config.rebase_options.is_empty() && !config.and_rebase {
33 return Err(anyhow!(
34 "REBASE_OPTIONS were specified without --and-rebase flag"
35 ));
36 }
37
38 let config = config::unify(&config, repo);
39 let stack = stack::working_stack(
40 repo,
41 config.base,
42 config.force_author,
43 config.force_detach,
44 logger,
45 )?;
46 if stack.is_empty() {
47 crit!(logger, "No commits available to fix up, exiting");
48 return Ok(());
49 }
50
51 let autostage_enabled = config::auto_stage_if_nothing_staged(repo);
52 let index_was_empty = nothing_left_in_index(repo)?;
53 let mut we_added_everything_to_index = false;
54 if autostage_enabled && index_was_empty {
55 we_added_everything_to_index = true;
56
57 let pathspec = ["."];
60 let mut index = repo.index()?;
61 index.add_all(pathspec.iter(), git2::IndexAddOption::DEFAULT, None)?;
62 index.write()?;
63 }
64
65 let mut diff_options = Some({
66 let mut ret = git2::DiffOptions::new();
67 ret.context_lines(0)
68 .id_abbrev(40)
69 .ignore_filemode(true)
70 .ignore_submodules(true);
71 ret
72 });
73
74 let (stack, summary_counts): (Vec<_>, _) = {
75 let mut diffs = Vec::with_capacity(stack.len());
76 for commit in &stack {
77 let diff = owned::Diff::new(
78 &repo.diff_tree_to_tree(
79 if commit.parents().len() == 0 {
80 None
81 } else {
82 Some(commit.parent(0)?.tree()?)
83 }
84 .as_ref(),
85 Some(&commit.tree()?),
86 diff_options.as_mut(),
87 )?,
88 )?;
89 trace!(logger, "parsed commit diff";
90 "commit" => commit.id().to_string(),
91 "diff" => format!("{:?}", diff),
92 );
93 diffs.push(diff);
94 }
95
96 let summary_counts = stack::summary_counts(&stack);
97 (stack.into_iter().zip(diffs).collect(), summary_counts)
98 };
99
100 let mut head_tree = repo.head()?.peel_to_tree()?;
101 let index = owned::Diff::new(&repo.diff_tree_to_index(
102 Some(&head_tree),
103 None,
104 diff_options.as_mut(),
105 )?)?;
106 trace!(logger, "parsed index";
107 "index" => format!("{:?}", index),
108 );
109
110 let signature = repo
111 .signature()
112 .or_else(|_| git2::Signature::now("nobody", "nobody@example.com"))?;
113 let mut head_commit = repo.head()?.peel_to_commit()?;
114
115 let mut hunks_with_commit = vec![];
116
117 let mut patches_considered = 0usize;
118 'patch: for index_patch in index.iter() {
119 let old_path = index_patch.new_path.as_slice();
120 if index_patch.status != git2::Delta::Modified {
121 debug!(logger, "skipped non-modified hunk";
122 "path" => String::from_utf8_lossy(old_path).into_owned(),
123 "status" => format!("{:?}", index_patch.status),
124 );
125 continue 'patch;
126 }
127
128 patches_considered += 1;
129
130 let mut preceding_hunks_offset = 0isize;
131 let mut applied_hunks_offset = 0isize;
132 'hunk: for index_hunk in &index_patch.hunks {
133 debug!(logger, "next hunk";
134 "header" => index_hunk.header(),
135 "path" => String::from_utf8_lossy(old_path).into_owned(),
136 );
137
138 let isolated_hunk = index_hunk
144 .clone()
145 .shift_added_block(-preceding_hunks_offset);
146
147 let hunk_to_apply = isolated_hunk
151 .clone()
152 .shift_both_blocks(applied_hunks_offset);
153
154 let hunk_offset = index_hunk.changed_offset();
156
157 debug!(logger, "";
179 "to apply" => hunk_to_apply.header(),
180 "to commute" => isolated_hunk.header(),
181 "preceding hunks" => format!("{}/{}", applied_hunks_offset, preceding_hunks_offset),
182 );
183
184 preceding_hunks_offset += hunk_offset;
185
186 let mut dest_commit = None;
188 let mut commuted_old_path = old_path;
189 let mut commuted_index_hunk = isolated_hunk;
190
191 'commit: for (commit, diff) in &stack {
192 let c_logger = logger.new(o!(
193 "commit" => commit.id().to_string(),
194 ));
195 let next_patch = match diff.by_new(commuted_old_path) {
196 Some(patch) => patch,
197 None => {
201 debug!(c_logger, "skipped commit with no path");
202 continue 'commit;
203 }
204 };
205
206 if config.whole_file {
211 debug!(
212 c_logger,
213 "Commit touches the hunk file and match whole file is enabled"
214 );
215 dest_commit = Some(commit);
216 break 'commit;
217 }
218
219 if next_patch.status == git2::Delta::Added {
220 debug!(c_logger, "found noncommutative commit by add");
221 dest_commit = Some(commit);
222 break 'commit;
223 }
224 if commuted_old_path != next_patch.old_path.as_slice() {
225 debug!(c_logger, "changed commute path";
226 "path" => String::from_utf8_lossy(&next_patch.old_path).into_owned(),
227 );
228 commuted_old_path = next_patch.old_path.as_slice();
229 }
230 commuted_index_hunk = match commute::commute_diff_before(
231 &commuted_index_hunk,
232 &next_patch.hunks,
233 ) {
234 Some(hunk) => {
235 debug!(c_logger, "commuted hunk with commit";
236 "offset" => (hunk.added.start as i64) - (commuted_index_hunk.added.start as i64),
237 );
238 hunk
239 }
240 None => {
243 debug!(c_logger, "found noncommutative commit by conflict");
244 dest_commit = Some(commit);
245 break 'commit;
246 }
247 };
248 }
249 let dest_commit = match dest_commit {
250 Some(commit) => commit,
251 None => {
254 warn!(
255 logger,
256 "Could not find a commit to fix up, use \
257 --base to increase the search range."
258 );
259 continue 'hunk;
260 }
261 };
262
263 let hunk_with_commit = HunkWithCommit {
264 hunk_to_apply,
265 dest_commit,
266 index_patch,
267 };
268 hunks_with_commit.push(hunk_with_commit);
269
270 applied_hunks_offset += hunk_offset;
271 }
272 }
273
274 let target_always_sha: bool = config::fixup_target_always_sha(repo);
275
276 for (current, next) in hunks_with_commit
284 .iter()
285 .zip(hunks_with_commit.iter().skip(1).map(Some).chain([None]))
286 {
287 let new_head_tree = apply_hunk_to_tree(
288 repo,
289 &head_tree,
290 ¤t.hunk_to_apply,
291 ¤t.index_patch.old_path,
292 )?;
293
294 let commit_fixup = next.map_or(true, |next| {
296 !config.one_fixup_per_commit || next.dest_commit.id() != current.dest_commit.id()
298 });
299 if commit_fixup {
300 let dest_commit_id = current.dest_commit.id().to_string();
305 let dest_commit_locator = match target_always_sha {
306 true => &dest_commit_id,
307 false => current
308 .dest_commit
309 .summary()
310 .filter(|&msg| summary_counts[msg] == 1)
311 .unwrap_or(&dest_commit_id),
312 };
313 let diff = repo
314 .diff_tree_to_tree(Some(&head_commit.tree()?), Some(&new_head_tree), None)?
315 .stats()?;
316 if !config.dry_run {
317 head_tree = new_head_tree;
318 head_commit = repo.find_commit(repo.commit(
319 Some("HEAD"),
320 &signature,
321 &signature,
322 &format!("fixup! {}\n", dest_commit_locator),
323 &head_tree,
324 &[&head_commit],
325 )?)?;
326 info!(logger, "committed";
327 "commit" => head_commit.id().to_string(),
328 "header" => format!("+{},-{}", diff.insertions(), diff.deletions()),
329 );
330 } else {
331 info!(logger, "would have committed";
332 "fixup" => dest_commit_locator,
333 "header" => format!("+{},-{}", diff.insertions(), diff.deletions()),
334 );
335 }
336 } else {
337 head_tree = new_head_tree;
339 }
340 }
341
342 if autostage_enabled && we_added_everything_to_index {
343 let mut index = repo.index()?;
347 index.read_tree(&head_tree)?;
348 index.write()?;
349 }
350
351 if patches_considered == 0 {
352 if index_was_empty && !we_added_everything_to_index {
353 warn!(
354 logger,
355 "No changes staged, try adding something \
356 to the index or set {} = true",
357 config::AUTO_STAGE_IF_NOTHING_STAGED_CONFIG_NAME
358 );
359 } else {
360 warn!(
361 logger,
362 "Could not find a commit to fix up, use \
363 --base to increase the search range."
364 )
365 }
366 } else if config.and_rebase {
367 use std::process::Command;
368 let last_commit_in_stack = &stack.last().unwrap().0;
370 let number_of_parents = last_commit_in_stack.parents().len();
372 assert!(number_of_parents <= 1);
373
374 let mut command = Command::new("git");
375
376 let repo_path = repo.path().parent().map(Path::to_str).flatten();
382 match repo_path {
383 Some(path) => {
384 command.args(["-C", path]);
385 }
386 _ => {
387 warn!(
388 logger,
389 "Could not determine repository path for rebase. Running in current directory."
390 );
391 }
392 }
393
394 command.args(["rebase", "--interactive", "--autosquash", "--autostash"]);
395
396 for arg in config.rebase_options {
397 command.arg(arg);
398 }
399
400 if number_of_parents == 0 {
401 command.arg("--root");
402 } else {
403 let base_commit_sha = last_commit_in_stack.parent(0)?.id().to_string();
406 command.arg(&base_commit_sha);
407 }
408
409 if config.dry_run {
410 info!(logger, "would have run git rebase"; "command" => format!("{:?}", command));
411 } else {
412 debug!(logger, "running git rebase"; "command" => format!("{:?}", command));
413 command.status().expect("could not run git rebase");
416 }
417 }
418
419 Ok(())
420}
421
422struct HunkWithCommit<'c, 'r, 'p> {
423 hunk_to_apply: owned::Hunk,
424 dest_commit: &'c git2::Commit<'r>,
425 index_patch: &'p owned::Patch,
426}
427
428fn apply_hunk_to_tree<'repo>(
429 repo: &'repo git2::Repository,
430 base: &git2::Tree,
431 hunk: &owned::Hunk,
432 path: &[u8],
433) -> Result<git2::Tree<'repo>> {
434 let mut treebuilder = repo.treebuilder(Some(base))?;
435
436 if let Some(slash) = path.iter().position(|&x| x == b'/') {
438 let (first, rest) = path.split_at(slash);
439 let rest = &rest[1..];
440
441 let (subtree, submode) = {
442 let entry = treebuilder
443 .get(first)?
444 .ok_or_else(|| anyhow!("couldn't find tree entry in tree for path"))?;
445 (repo.find_tree(entry.id())?, entry.filemode())
446 };
447 let result_subtree = apply_hunk_to_tree(repo, &subtree, hunk, rest)?;
449
450 treebuilder.insert(first, result_subtree.id(), submode)?;
451 return Ok(repo.find_tree(treebuilder.write()?)?);
452 }
453
454 let (blob, mode) = {
455 let entry = treebuilder
456 .get(path)?
457 .ok_or_else(|| anyhow!("couldn't find blob entry in tree for path"))?;
458 (repo.find_blob(entry.id())?, entry.filemode())
459 };
460
461 let mut blobwriter = repo.blob_writer(None)?;
465 let old_content = blob.content();
466 let (old_start, _, _, _) = hunk.anchors();
467
468 let old_content = {
471 let (pre, post) = split_lines_after(old_content, old_start);
472 blobwriter.write_all(pre)?;
473 post
474 };
475 for line in &*hunk.added.lines {
477 blobwriter.write_all(line)?;
478 }
479 let (_, old_content) = split_lines_after(old_content, hunk.removed.lines.len());
482 blobwriter.write_all(old_content)?;
484
485 treebuilder.insert(path, blobwriter.commit()?, mode)?;
486 Ok(repo.find_tree(treebuilder.write()?)?)
487}
488
489fn split_lines_after(content: &[u8], n: usize) -> (&[u8], &[u8]) {
491 let split_index = if n > 0 {
492 memchr::Memchr::new(b'\n', content)
493 .fuse() .nth(n - 1) .map(|x| x + 1)
496 .unwrap_or_else(|| content.len())
497 } else {
498 0
499 };
500 content.split_at(split_index)
501}
502
503fn nothing_left_in_index(repo: &git2::Repository) -> Result<bool> {
504 let stats = index_stats(repo)?;
505 let nothing = stats.files_changed() == 0 && stats.insertions() == 0 && stats.deletions() == 0;
506 Ok(nothing)
507}
508
509fn index_stats(repo: &git2::Repository) -> Result<git2::DiffStats> {
510 let head = repo.head()?.peel_to_tree()?;
511 let diff = repo.diff_tree_to_index(Some(&head), Some(&repo.index()?), None)?;
512 let stats = diff.stats()?;
513 Ok(stats)
514}
515
516#[cfg(test)]
517mod tests {
518 use git2::message_trailers_strs;
519 use std::path::PathBuf;
520
521 use super::*;
522 mod repo_utils;
523
524 #[test]
525 fn multiple_fixups_per_commit() {
526 let ctx = repo_utils::prepare_and_stage();
527
528 let drain = slog::Discard;
530 let logger = slog::Logger::root(drain, o!());
531 run_with_repo(&logger, &DEFAULT_CONFIG, &ctx.repo).unwrap();
532
533 let mut revwalk = ctx.repo.revwalk().unwrap();
534 revwalk.push_head().unwrap();
535 assert_eq!(revwalk.count(), 3);
536
537 assert!(nothing_left_in_index(&ctx.repo).unwrap());
538 }
539
540 #[test]
541 fn one_fixup_per_commit() {
542 let ctx = repo_utils::prepare_and_stage();
543
544 let drain = slog::Discard;
546 let logger = slog::Logger::root(drain, o!());
547 let config = Config {
548 one_fixup_per_commit: true,
549 ..DEFAULT_CONFIG
550 };
551 run_with_repo(&logger, &config, &ctx.repo).unwrap();
552
553 let mut revwalk = ctx.repo.revwalk().unwrap();
554 revwalk.push_head().unwrap();
555 assert_eq!(revwalk.count(), 2);
556
557 assert!(nothing_left_in_index(&ctx.repo).unwrap());
558 }
559
560 #[test]
561 fn foreign_author() {
562 let ctx = repo_utils::prepare_and_stage();
563
564 repo_utils::become_author(&ctx.repo, "nobody2", "nobody2@example.com");
565
566 let drain = slog::Discard;
568 let logger = slog::Logger::root(drain, o!());
569 run_with_repo(&logger, &DEFAULT_CONFIG, &ctx.repo).unwrap();
570
571 let mut revwalk = ctx.repo.revwalk().unwrap();
572 revwalk.push_head().unwrap();
573 assert_eq!(revwalk.count(), 1);
574 let is_something_in_index = !nothing_left_in_index(&ctx.repo).unwrap();
575 assert!(is_something_in_index);
576 }
577
578 #[test]
579 fn foreign_author_with_force_author_flag() {
580 let ctx = repo_utils::prepare_and_stage();
581
582 repo_utils::become_author(&ctx.repo, "nobody2", "nobody2@example.com");
583
584 let drain = slog::Discard;
586 let logger = slog::Logger::root(drain, o!());
587 let config = Config {
588 force_author: true,
589 ..DEFAULT_CONFIG
590 };
591 run_with_repo(&logger, &config, &ctx.repo).unwrap();
592
593 let mut revwalk = ctx.repo.revwalk().unwrap();
594 revwalk.push_head().unwrap();
595 assert_eq!(revwalk.count(), 3);
596
597 assert!(nothing_left_in_index(&ctx.repo).unwrap());
598 }
599
600 #[test]
601 fn foreign_author_with_force_author_config() {
602 let ctx = repo_utils::prepare_and_stage();
603
604 repo_utils::become_author(&ctx.repo, "nobody2", "nobody2@example.com");
605
606 repo_utils::set_config_flag(&ctx.repo, "absorb.forceAuthor");
607
608 let drain = slog::Discard;
610 let logger = slog::Logger::root(drain, o!());
611 run_with_repo(&logger, &DEFAULT_CONFIG, &ctx.repo).unwrap();
612
613 let mut revwalk = ctx.repo.revwalk().unwrap();
614 revwalk.push_head().unwrap();
615 assert_eq!(revwalk.count(), 3);
616
617 assert!(nothing_left_in_index(&ctx.repo).unwrap());
618 }
619
620 #[test]
621 fn detached_head() {
622 let ctx = repo_utils::prepare_and_stage();
623 repo_utils::detach_head(&ctx.repo);
624
625 let drain = slog::Discard;
627 let logger = slog::Logger::root(drain, o!());
628 let result = run_with_repo(&logger, &DEFAULT_CONFIG, &ctx.repo);
629 assert_eq!(
630 result.err().unwrap().to_string(),
631 "HEAD is not a branch, use --force-detach to override"
632 );
633
634 let mut revwalk = ctx.repo.revwalk().unwrap();
635 revwalk.push_head().unwrap();
636 assert_eq!(revwalk.count(), 1);
637 let is_something_in_index = !nothing_left_in_index(&ctx.repo).unwrap();
638 assert!(is_something_in_index);
639 }
640
641 #[test]
642 fn detached_head_pointing_at_branch_with_force_detach_flag() {
643 let ctx = repo_utils::prepare_and_stage();
644 repo_utils::detach_head(&ctx.repo);
645
646 let drain = slog::Discard;
648 let logger = slog::Logger::root(drain, o!());
649 let config = Config {
650 force_detach: true,
651 ..DEFAULT_CONFIG
652 };
653 run_with_repo(&logger, &config, &ctx.repo).unwrap();
654 let mut revwalk = ctx.repo.revwalk().unwrap();
655 revwalk.push_head().unwrap();
656
657 assert_eq!(revwalk.count(), 1); let is_something_in_index = !nothing_left_in_index(&ctx.repo).unwrap();
659 assert!(is_something_in_index);
660 }
661
662 #[test]
663 fn detached_head_with_force_detach_flag() {
664 let ctx = repo_utils::prepare_and_stage();
665 repo_utils::detach_head(&ctx.repo);
666 repo_utils::delete_branch(&ctx.repo, "master");
667
668 let drain = slog::Discard;
670 let logger = slog::Logger::root(drain, o!());
671 let config = Config {
672 force_detach: true,
673 ..DEFAULT_CONFIG
674 };
675 run_with_repo(&logger, &config, &ctx.repo).unwrap();
676 let mut revwalk = ctx.repo.revwalk().unwrap();
677 revwalk.push_head().unwrap();
678
679 assert_eq!(revwalk.count(), 3);
680 assert!(nothing_left_in_index(&ctx.repo).unwrap());
681 }
682
683 #[test]
684 fn detached_head_with_force_detach_config() {
685 let ctx = repo_utils::prepare_and_stage();
686 repo_utils::detach_head(&ctx.repo);
687 repo_utils::delete_branch(&ctx.repo, "master");
688
689 repo_utils::set_config_flag(&ctx.repo, "absorb.forceDetach");
690
691 let drain = slog::Discard;
693 let logger = slog::Logger::root(drain, o!());
694 run_with_repo(&logger, &DEFAULT_CONFIG, &ctx.repo).unwrap();
695 let mut revwalk = ctx.repo.revwalk().unwrap();
696 revwalk.push_head().unwrap();
697
698 assert_eq!(revwalk.count(), 3);
699 assert!(nothing_left_in_index(&ctx.repo).unwrap());
700 }
701
702 #[test]
703 fn and_rebase_flag() {
704 let ctx = repo_utils::prepare_and_stage();
705 repo_utils::set_config_option(&ctx.repo, "core.editor", "true");
706
707 let drain = slog::Discard;
709 let logger = slog::Logger::root(drain, o!());
710 let config = Config {
711 and_rebase: true,
712 ..DEFAULT_CONFIG
713 };
714 run_with_repo(&logger, &config, &ctx.repo).unwrap();
715
716 let mut revwalk = ctx.repo.revwalk().unwrap();
717 revwalk.push_head().unwrap();
718
719 assert_eq!(revwalk.count(), 1);
720 assert!(nothing_left_in_index(&ctx.repo).unwrap());
721 }
722
723 #[test]
724 fn and_rebase_flag_with_rebase_options() {
725 let ctx = repo_utils::prepare_and_stage();
726 repo_utils::set_config_option(&ctx.repo, "core.editor", "true");
727
728 let drain = slog::Discard;
730 let logger = slog::Logger::root(drain, o!());
731 let config = Config {
732 and_rebase: true,
733 rebase_options: &vec!["--signoff"],
734 ..DEFAULT_CONFIG
735 };
736 run_with_repo(&logger, &config, &ctx.repo).unwrap();
737
738 let mut revwalk = ctx.repo.revwalk().unwrap();
739 revwalk.push_head().unwrap();
740 assert_eq!(revwalk.count(), 1);
741
742 let trailers = message_trailers_strs(
743 ctx.repo
744 .head()
745 .unwrap()
746 .peel_to_commit()
747 .unwrap()
748 .message()
749 .unwrap(),
750 )
751 .unwrap();
752 assert_eq!(
753 trailers
754 .iter()
755 .filter(|trailer| trailer.0 == "Signed-off-by")
756 .count(),
757 1
758 );
759
760 assert!(nothing_left_in_index(&ctx.repo).unwrap());
761 }
762
763 #[test]
764 fn rebase_options_without_and_rebase_flag() {
765 let ctx = repo_utils::prepare_and_stage();
766
767 let drain = slog::Discard;
769 let logger = slog::Logger::root(drain, o!());
770 let config = Config {
771 rebase_options: &vec!["--some-option"],
772 ..DEFAULT_CONFIG
773 };
774 let result = run_with_repo(&logger, &config, &ctx.repo);
775
776 assert_eq!(
777 result.err().unwrap().to_string(),
778 "REBASE_OPTIONS were specified without --and-rebase flag"
779 );
780
781 let mut revwalk = ctx.repo.revwalk().unwrap();
782 revwalk.push_head().unwrap();
783 assert_eq!(revwalk.count(), 1);
784 let is_something_in_index = !nothing_left_in_index(&ctx.repo).unwrap();
785 assert!(is_something_in_index);
786 }
787
788 #[test]
789 fn dry_run_flag() {
790 let ctx = repo_utils::prepare_and_stage();
791
792 let drain = slog::Discard;
794 let logger = slog::Logger::root(drain, o!());
795 let config = Config {
796 dry_run: true,
797 ..DEFAULT_CONFIG
798 };
799 run_with_repo(&logger, &config, &ctx.repo).unwrap();
800
801 let mut revwalk = ctx.repo.revwalk().unwrap();
802 revwalk.push_head().unwrap();
803 assert_eq!(revwalk.count(), 1);
804 let is_something_in_index = !nothing_left_in_index(&ctx.repo).unwrap();
805 assert!(is_something_in_index);
806 }
807
808 #[test]
809 fn dry_run_flag_with_and_rebase_flag() {
810 let (ctx, path) = repo_utils::prepare_repo();
811 repo_utils::set_config_option(&ctx.repo, "core.editor", "true");
812
813 let tree = repo_utils::stage_file_changes(&ctx, &path);
815 let signature = ctx.repo.signature().unwrap();
816 let head_commit = ctx.repo.head().unwrap().peel_to_commit().unwrap();
817 ctx.repo
818 .commit(
819 Some("HEAD"),
820 &signature,
821 &signature,
822 &format!("fixup! {}\n", head_commit.id()),
823 &tree,
824 &[&head_commit],
825 )
826 .unwrap();
827
828 repo_utils::stage_file_changes(&ctx, &path);
830
831 let drain = slog::Discard;
833 let logger = slog::Logger::root(drain, o!());
834 let config = Config {
835 and_rebase: true,
836 dry_run: true,
837 ..DEFAULT_CONFIG
838 };
839 run_with_repo(&logger, &config, &ctx.repo).unwrap();
840
841 let mut revwalk = ctx.repo.revwalk().unwrap();
842 revwalk.push_head().unwrap();
843 assert_eq!(revwalk.count(), 2); let is_something_in_index = !nothing_left_in_index(&ctx.repo).unwrap();
845 assert!(is_something_in_index);
846 }
847
848 fn autostage_common(ctx: &repo_utils::Context, file_path: &PathBuf) -> (PathBuf, PathBuf) {
849 let path = ctx.join(&file_path);
851 let contents = std::fs::read_to_string(&path).unwrap();
852 let modifications = format!("{contents}\nnew_line2");
853 std::fs::write(&path, &modifications).unwrap();
854
855 let fp2 = PathBuf::from("unrel.txt");
857 std::fs::write(ctx.join(&fp2), "foo").unwrap();
858
859 (path, fp2)
860 }
861
862 #[test]
863 fn autostage_if_index_was_empty() {
864 let (ctx, file_path) = repo_utils::prepare_repo();
865
866 ctx.repo
868 .config()
869 .unwrap()
870 .set_bool(config::AUTO_STAGE_IF_NOTHING_STAGED_CONFIG_NAME, true)
871 .unwrap();
872
873 autostage_common(&ctx, &file_path);
874
875 let drain = slog::Discard;
877 let logger = slog::Logger::root(drain, o!());
878 run_with_repo(&logger, &DEFAULT_CONFIG, &ctx.repo).unwrap();
879
880 let mut revwalk = ctx.repo.revwalk().unwrap();
881 revwalk.push_head().unwrap();
882 assert_eq!(revwalk.count(), 2);
883
884 assert!(nothing_left_in_index(&ctx.repo).unwrap());
885 }
886
887 #[test]
888 fn do_not_autostage_if_index_was_not_empty() {
889 let (ctx, file_path) = repo_utils::prepare_repo();
890
891 ctx.repo
893 .config()
894 .unwrap()
895 .set_bool(config::AUTO_STAGE_IF_NOTHING_STAGED_CONFIG_NAME, true)
896 .unwrap();
897
898 let (_, fp2) = autostage_common(&ctx, &file_path);
899 repo_utils::add(&ctx.repo, &fp2);
901
902 let drain = slog::Discard;
904 let logger = slog::Logger::root(drain, o!());
905 run_with_repo(&logger, &DEFAULT_CONFIG, &ctx.repo).unwrap();
906
907 let mut revwalk = ctx.repo.revwalk().unwrap();
908 revwalk.push_head().unwrap();
909 assert_eq!(revwalk.count(), 1);
910
911 assert_eq!(index_stats(&ctx.repo).unwrap().files_changed(), 1);
912 }
913
914 #[test]
915 fn do_not_autostage_if_not_enabled_by_config_var() {
916 let (ctx, file_path) = repo_utils::prepare_repo();
917
918 ctx.repo
920 .config()
921 .unwrap()
922 .set_bool(config::AUTO_STAGE_IF_NOTHING_STAGED_CONFIG_NAME, false)
923 .unwrap();
924
925 autostage_common(&ctx, &file_path);
926
927 let drain = slog::Discard;
929 let logger = slog::Logger::root(drain, o!());
930 run_with_repo(&logger, &DEFAULT_CONFIG, &ctx.repo).unwrap();
931
932 let mut revwalk = ctx.repo.revwalk().unwrap();
933 revwalk.push_head().unwrap();
934 assert_eq!(revwalk.count(), 1);
935
936 assert!(nothing_left_in_index(&ctx.repo).unwrap());
937 }
938
939 #[test]
940 fn fixup_message_always_commit_sha_if_configured() {
941 let ctx = repo_utils::prepare_and_stage();
942
943 ctx.repo
944 .config()
945 .unwrap()
946 .set_bool(config::FIXUP_TARGET_ALWAYS_SHA_CONFIG_NAME, true)
947 .unwrap();
948
949 let drain = slog::Discard;
951 let logger = slog::Logger::root(drain, o!());
952 run_with_repo(&logger, &DEFAULT_CONFIG, &ctx.repo).unwrap();
953 assert!(nothing_left_in_index(&ctx.repo).unwrap());
954
955 let mut revwalk = ctx.repo.revwalk().unwrap();
956 revwalk.push_head().unwrap();
957
958 let oids: Vec<git2::Oid> = revwalk.by_ref().collect::<Result<Vec<_>, _>>().unwrap();
959 assert_eq!(oids.len(), 3);
960
961 let commit = ctx.repo.find_commit(oids[0]).unwrap();
962 let actual_msg = commit.summary().unwrap();
963 let expected_msg = format!("fixup! {}", oids.last().unwrap());
964 assert_eq!(actual_msg, expected_msg);
965 }
966
967 const DEFAULT_CONFIG: Config = Config {
968 dry_run: false,
969 force_author: false,
970 force_detach: false,
971 base: None,
972 and_rebase: false,
973 rebase_options: &Vec::new(),
974 whole_file: false,
975 one_fixup_per_commit: false,
976 };
977}