git_retime/rebase/
mod.rs

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
56// modify all commits from hash to head's time to date and rebase into a new branch
57fn 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        // Update the author and committer dates
70        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    // Create a new branch with the rebased commits
84    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}