1use crate::GitTimeTravel;
2use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime};
3use git2::{Error, Oid, RebaseOptions, Repository, Signature, Time};
4use git_utils::{count_commits_from, find_closest_git_repo};
5use rand::Rng;
6use std::collections::BTreeSet;
7
8use rand::thread_rng;
9
10impl GitTimeTravel {
11 pub fn run(&self) -> Result<(), Error> {
12 let repo = find_closest_git_repo()?;
13 let oid = repo.revparse_single(&self.commit)?.id();
14 let commits = count_commits_from(oid, &repo)?;
15 let start_time = self.start_time()?;
16 let end_time = self.end_time(commits)?;
17 let mut dates = BTreeSet::new();
18 let mut rng = thread_rng();
19 for _ in 0..commits {
20 let random_time = rng.gen_range(start_time.timestamp()..end_time.timestamp());
21 dates.insert(Time::new(random_time, 0));
22 }
23 rebase_to_branch(&self.branch_name(), oid, &repo, &dates.into_iter().collect::<Vec<_>>())?;
24 Ok(())
25 }
26 fn start_time(&self) -> Result<NaiveDateTime, Error> {
27 let date = match NaiveDate::parse_from_str(&self.start_date, "%Y-%m-%d") {
28 Ok(o) => o,
29 Err(_) => Err(Error::from_str("date parse failed"))?,
30 };
31 Ok(date.and_time(NaiveTime::MIN))
32 }
33 fn end_time(&self, days: usize) -> Result<NaiveDateTime, Error> {
34 match &self.end_date {
35 Some(s) => {
36 let date = match NaiveDate::parse_from_str(&s, "%Y-%m-%d") {
37 Ok(o) => o,
38 Err(_) => Err(Error::from_str("date parse failed"))?,
39 };
40 Ok(date.and_time(NaiveTime::MIN))
41 }
42 None => {
43 let start_time = self.start_time()?;
44 Ok(start_time + Duration::days(days as i64))
45 }
46 }
47 }
48 fn branch_name(&self) -> String {
49 match &self.branch {
50 Some(s) => s.to_string(),
51 None => "time-travel".to_string(),
52 }
53 }
54}
55
56fn rebase_to_branch(name: &str, id: Oid, repo: &Repository, dates: &[Time]) -> Result<(), Error> {
58 let annotated = repo.find_annotated_commit(id)?;
59 let mut rebase_options = RebaseOptions::new();
60 rebase_options.inmemory(true);
61 let mut last = id;
62 let mut rebase = repo.rebase(None, Some(&annotated), None, Some(&mut rebase_options))?;
63 let mut index = 0;
64 while let Some(operation) = rebase.next() {
65 let commit = repo.find_commit(operation?.id())?;
66 let mut author = commit.author();
67 let mut committer = commit.committer();
68
69 match dates.get(index) {
71 Some(s) => {
72 author = new_sign(author, *s)?;
73 committer = new_sign(committer, *s)?;
74 }
75 None => {
76 println!("Not enough dates provided, need {} but only got {}", index, dates.len());
77 }
78 }
79 index += 1;
80 last = rebase.commit(Some(&author), &committer, commit.message())?;
81 }
82 rebase.finish(None)?;
83 let target = repo.find_commit(last)?;
85 repo.branch(name, &target, true)?;
86 Ok(())
87}
88
89fn new_sign(old: Signature, date: Time) -> Result<Signature, Error> {
90 let name = String::from_utf8_lossy(old.name_bytes());
91 let email = String::from_utf8_lossy(old.email_bytes());
92 Signature::new(name.as_ref(), email.as_ref(), &date)
93}