1use eyre::{eyre, Result};
2use std::process;
3
4use git2::{self, Repository, RepositoryState, ResetType};
5use process::Command;
6
7use color_eyre::{eyre::Report, Section};
8
9extern crate log;
10
11pub fn wrapper_pick_and_clean(
17 repo: &Repository,
18 target_branch: &str,
19 onto_branch: &str,
20 force_new_branch: bool,
21) -> Result<()> {
22 assure_workspace_is_clean(repo)
23 .suggestion("Consider auto-stashing your changes with --autostash.")
24 .suggestion("Running this again with RUST_LOG=debug provides more details.")?;
25 cherrypick_commit_onto_new_branch(repo, target_branch, onto_branch, force_new_branch)?;
26 remove_commit_from_head(repo)?;
27 Ok(())
28}
29
30pub fn cherrypick_commit_onto_new_branch(
34 repo: &Repository,
35 target_branch: &str,
36 onto_branch: &str,
37 force_new_branch: bool,
38) -> Result<(), Report> {
39 let main_commit = repo
40 .revparse(onto_branch)?
41 .from()
42 .unwrap()
43 .peel_to_commit()?;
44
45 let new_branch = repo
47 .branch(target_branch, &main_commit, force_new_branch)
48 .suggestion("Consider using --force to overwrite the existing branch")?;
49
50 let fix_commit = repo.head()?.peel_to_commit()?;
53 if fix_commit.parent_count() != 1 {
54 return Err(eyre!("Only works with non-merge commits"))
55 .suggestion("Quickfixing a merge commit is not supported. If you meant to do this please file a ticket with your use case.");
56 };
57
58 let mut index = repo.cherrypick_commit(&fix_commit, &main_commit, 0, None)?;
60 let tree_oid = index.write_tree_to(repo)?;
61 let tree = repo.find_tree(tree_oid)?;
62
63 let signature = repo.signature()?;
65 let message = fix_commit
66 .message_raw()
67 .ok_or_else(|| eyre!("Could not read the commit message."))
68 .suggestion("Make sure the commit message contains only UTF-8 characters or try to manually cherry-pick the commit.")?;
69
70 let commit_oid = repo
71 .commit(
72 new_branch.get().name(),
73 &fix_commit.author(),
74 &signature,
75 message,
76 &tree,
77 &[&main_commit],
78 )
79 .suggestion(
80 "You cannot provide an existing branch name. Choose a new branch name or run with '--force'.",
81 )?; log::debug!(
83 "Wrote quickfixed changes to new commit {} and new branch {}",
84 commit_oid,
85 target_branch
86 );
87
88 Ok(())
89}
90
91fn remove_commit_from_head(repo: &Repository) -> Result<(), Report> {
93 let head_1 = repo.head()?.peel_to_commit()?.parent(0)?;
95 repo.reset(head_1.as_object(), ResetType::Hard, None)?;
96
97 Ok(())
98}
99
100pub fn push_new_commit(repo: &Repository, branch: &str) -> Result<(), Report> {
103 let workdir = repo
105 .workdir()
106 .ok_or_else(|| eyre!("Could not get workdir"))?;
107
108 log::info!("Pushing new branch to origin.");
109 let status = Command::new("git")
110 .args(&["push", "--set-upstream", "origin", branch])
111 .current_dir(workdir)
112 .status()?;
113 if !status.success() {
114 Err(eyre!("Failed to run git push. {}", status))
115 } else {
116 log::info!("Git push succeeded");
117 Ok(())
118 }
119}
120
121pub fn assure_repo_in_normal_state(repo: &Repository) -> Result<()> {
126 let state = repo.state();
127 if state != RepositoryState::Clean {
128 return Err(eyre!(
129 "The repository is currently not in a clean state ({:?}).",
130 state
131 ));
132 }
133
134 Ok(())
135}
136
137fn assure_workspace_is_clean(repo: &Repository) -> Result<()> {
139 let mut options = git2::StatusOptions::new();
140 options.include_ignored(false);
141 let statuses = repo.statuses(Some(&mut options))?;
142 for s in statuses.iter() {
143 log::warn!("Dirty: {:?} -- {:?}", s.path(), s.status());
144 }
145 let is_dirty = !statuses.is_empty();
146 if is_dirty {
147 Err(eyre!("The repository is dirty."))
148 } else {
149 Ok(())
150 }
151}
152
153fn get_default_branch_from_head(repo: &Repository) -> Result<String> {
156 let workdir = repo
157 .workdir()
158 .ok_or_else(|| eyre!("Could not get workdir"))?;
159 let output = Command::new("git")
160 .args(&["branch", "-rl", "*/HEAD"])
161 .current_dir(workdir)
162 .output()?;
163 let status = output.status;
164 if !status.success() {
165 return Err(eyre!("Failed to run git branch -rl. {}", status));
166 }
167
168 let output = String::from_utf8_lossy(&output.stdout);
169 let head_reference = output.split("->").nth(1);
170 if let Some(head_reference) = head_reference {
171 let head_reference = head_reference.trim();
172 Ok(head_reference.to_string())
173 } else {
174 Err(eyre!("Could not find a default branch."))
175 }
176}
177
178pub fn get_default_branch(repo: &Repository) -> Result<String, Report> {
182 match get_default_branch_from_head(repo) {
183 Ok(branch) => return Ok(branch),
184 Err(e) => {
185 log::debug!(
186 "Failed to get default branch from HEAD: {}. Using hardcoded branches",
187 e
188 );
189 }
190 }
191
192 for name in [
195 "origin/main",
196 "origin/master",
197 "origin/devel",
198 "origin/develop",
199 ]
200 .iter()
201 {
202 match repo.resolve_reference_from_short_name(name) {
203 Ok(_) => {
204 log::debug!("Found {} as the default remote branch. A bit hacky -- wrong results certainly possible.", name);
205 return Ok(name.to_string());
206 }
207 Err(_) => continue,
208 }
209 }
210 Err(eyre!("Could not find remote default branch."))
211}
212
213pub fn stash(repo: &mut Repository) -> Result<bool> {
215 let signature = repo.signature()?;
216 let stashed = match repo.stash_save(&signature, "quickfix: auto-stash", None) {
218 Ok(stash) => {
219 log::debug!("Stashed to object {}", stash);
220 true
221 }
222 Err(e) => {
223 if e.code() == git2::ErrorCode::NotFound && e.class() == git2::ErrorClass::Stash {
225 log::debug!("Nothing to stash.");
226 false
227 } else {
228 return Err(eyre!("{}", e.message()));
229 }
230 }
231 };
232
233 Ok(stashed)
234}
235
236#[cfg(test)]
237mod test {
238 use super::*;
239
240 #[test]
241 fn test_default_branch() {
242 let repo = Repository::open(".").unwrap();
243 let branch = get_default_branch(&repo).unwrap();
244 assert_eq!(branch, "origin/main");
245 }
246}