gnostr_asyncgit/sync/
reword.rs

1use git2::{Oid, RebaseOptions, Repository};
2
3use super::{
4    commit::signature_allow_undefined_name,
5    repo,
6    utils::{bytes2string, get_head_refname, get_head_repo},
7    CommitId, RepoPath,
8};
9use crate::error::{Error, Result};
10
11/// This is the same as reword, but will abort and fix the repo if
12/// something goes wrong
13pub fn reword(repo_path: &RepoPath, commit: CommitId, message: &str) -> Result<CommitId> {
14    let repo = repo(repo_path)?;
15    let config = repo.config()?;
16
17    if config.get_bool("commit.gpgsign").unwrap_or(false) {
18        // HACK: we undo the last commit and create a new one
19        use crate::sync::utils::undo_last_commit;
20
21        let head = get_head_repo(&repo)?;
22        if head == commit {
23            // Check if there are any staged changes
24            let parent = repo.find_commit(head.into())?;
25            let tree = parent.tree()?;
26            if repo
27                .diff_tree_to_index(Some(&tree), None, None)?
28                .deltas()
29                .len()
30                == 0
31            {
32                undo_last_commit(repo_path)?;
33                return super::commit(repo_path, message);
34            }
35
36            return Err(Error::SignRewordLastCommitStaged);
37        }
38
39        return Err(Error::SignRewordNonLastCommit);
40    }
41
42    let cur_branch_ref = get_head_refname(&repo)?;
43
44    match reword_internal(&repo, commit.get_oid(), message) {
45        Ok(id) => Ok(id.into()),
46        // Something went wrong, checkout the previous branch then
47        // error
48        Err(e) => {
49            if let Ok(mut rebase) = repo.open_rebase(None) {
50                rebase.abort()?;
51                repo.set_head(&cur_branch_ref)?;
52                repo.checkout_head(None)?;
53            }
54            Err(e)
55        }
56    }
57}
58
59/// Gets the current branch the user is on.
60/// Returns none if they are not on a branch
61/// and Err if there was a problem finding the branch
62fn get_current_branch(repo: &Repository) -> Result<Option<git2::Branch<'_>>> {
63    for b in repo.branches(None)? {
64        let branch = b?.0;
65        if branch.is_head() {
66            return Ok(Some(branch));
67        }
68    }
69    Ok(None)
70}
71
72/// Changes the commit message of a commit with a specified oid
73///
74/// While this function is most commonly associated with doing a
75/// reword operation in an interactive rebase, that is not how it
76/// is implemented in git2rs
77///
78/// This is dangerous if it errors, as the head will be detached so
79/// this should always be wrapped by another function which aborts the
80/// rebase if something goes wrong
81fn reword_internal(repo: &Repository, commit: Oid, message: &str) -> Result<Oid> {
82    let sig = signature_allow_undefined_name(repo)?;
83
84    let parent_commit_oid = repo
85        .find_commit(commit)?
86        .parent(0)
87        .map_or(None, |parent_commit| Some(parent_commit.id()));
88
89    let commit_to_change = if let Some(pc_oid) = parent_commit_oid {
90        // Need to start at one previous to the commit, so
91        // first rebase.next() points to the actual commit we want to
92        // change
93        repo.find_annotated_commit(pc_oid)?
94    } else {
95        return Err(Error::NoParent);
96    };
97
98    // If we are on a branch
99    if let Ok(Some(branch)) = get_current_branch(repo) {
100        let cur_branch_ref = bytes2string(branch.get().name_bytes())?;
101        let cur_branch_name = bytes2string(branch.name_bytes()?)?;
102        let top_branch_commit = repo.find_annotated_commit(branch.get().peel_to_commit()?.id())?;
103
104        let mut rebase = repo.rebase(
105            Some(&top_branch_commit),
106            Some(&commit_to_change),
107            None,
108            Some(&mut RebaseOptions::default()),
109        )?;
110
111        let mut target;
112
113        rebase.next();
114        if parent_commit_oid.is_none() {
115            return Err(Error::NoParent);
116        }
117        target = rebase.commit(None, &sig, Some(message))?;
118        let reworded_commit = target;
119
120        // Set target to top commit, don't know when the rebase will
121        // end so have to loop till end
122        while rebase.next().is_some() {
123            target = rebase.commit(None, &sig, None)?;
124        }
125        rebase.finish(None)?;
126
127        // Now override the previous branch
128        repo.branch(&cur_branch_name, &repo.find_commit(target)?, true)?;
129
130        // Reset the head back to the branch then checkout head
131        repo.set_head(&cur_branch_ref)?;
132        repo.checkout_head(None)?;
133        return Ok(reworded_commit);
134    }
135    // Repo is not on a branch, possibly detached head
136    Err(Error::NoBranch)
137}
138
139#[cfg(test)]
140mod tests {
141    use pretty_assertions::assert_eq;
142
143    use super::*;
144    use crate::sync::{
145        get_commit_info,
146        tests::{repo_init_empty, write_commit_file},
147    };
148
149    #[test]
150    fn test_reword() {
151        let (_td, repo) = repo_init_empty().unwrap();
152        let root = repo.path().parent().unwrap();
153
154        let repo_path: &RepoPath = &root.as_os_str().to_str().unwrap().into();
155
156        write_commit_file(&repo, "foo", "a", "commit1");
157
158        let oid2 = write_commit_file(&repo, "foo", "ab", "commit2");
159
160        let branch = repo.branches(None).unwrap().next().unwrap().unwrap().0;
161        let branch_ref = branch.get();
162        let commit_ref = branch_ref.peel_to_commit().unwrap();
163        let message = commit_ref.message().unwrap();
164
165        assert_eq!(message, "commit2");
166
167        let reworded = reword(repo_path, oid2.into(), "NewCommitMessage").unwrap();
168
169        // Need to get the branch again as top oid has changed
170        let branch = repo.branches(None).unwrap().next().unwrap().unwrap().0;
171        let branch_ref = branch.get();
172        let commit_ref_new = branch_ref.peel_to_commit().unwrap();
173        let message_new = commit_ref_new.message().unwrap();
174        assert_eq!(message_new, "NewCommitMessage");
175
176        assert_eq!(
177            message_new,
178            get_commit_info(repo_path, &reworded).unwrap().message
179        );
180    }
181}