1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
use eyre::{eyre, Result};
use std::process;
use git2::{self, Repository, RepositoryState, ResetType};
use process::Command;
use color_eyre::{eyre::Report, Section};
extern crate log;
pub fn wrapper_pick_and_clean(
repo: &Repository,
target_branch: &str,
onto_branch: &str,
force_new_branch: bool,
) -> Result<()> {
assure_workspace_is_clean(repo)
.suggestion("Consider auto-stashing your changes with --stash.")
.suggestion("Running this again with RUST_LOG=debug provides more details.")?;
cherrypick_commit_onto_new_branch(repo, target_branch, onto_branch, force_new_branch)?;
remove_commit_from_head(repo)?;
Ok(())
}
pub fn cherrypick_commit_onto_new_branch(
repo: &Repository,
target_branch: &str,
onto_branch: &str,
force_new_branch: bool,
) -> Result<(), Report> {
let main_commit = repo
.revparse(onto_branch)?
.from()
.unwrap()
.peel_to_commit()?;
let new_branch = repo
.branch(target_branch, &main_commit, force_new_branch)
.suggestion("Consider using --force to overwrite the existing branch")?;
let fix_commit = repo.head()?.peel_to_commit()?;
if fix_commit.parent_count() != 1 {
return Err(eyre!("Only works with non-merge commits"))
.suggestion("Quickfixing a merge commit is not supported. If you meant to do this please file a ticket with your use case.");
};
let mut index = repo.cherrypick_commit(&fix_commit, &main_commit, 0, None)?;
let tree_oid = index.write_tree_to(repo)?;
let tree = repo.find_tree(tree_oid)?;
let signature = repo.signature()?;
let message = fix_commit
.message_raw()
.ok_or_else(|| eyre!("Could not read the commit message."))
.suggestion("Make sure the commit message contains only UTF-8 characters or try to manually cherry-pick the commit.")?;
let commit_oid = repo
.commit(
new_branch.get().name(),
&fix_commit.author(),
&signature,
message,
&tree,
&[&main_commit],
)
.suggestion(
"You cannot provide an existing branch name. Choose a new branch name or run with '--force'.",
)?;
log::debug!(
"Wrote quickfixed changes to new commit {} and new branch {}",
commit_oid,
target_branch
);
Ok(())
}
fn remove_commit_from_head(repo: &Repository) -> Result<(), Report> {
let head_1 = repo.head()?.peel_to_commit()?.parent(0)?;
repo.reset(head_1.as_object(), ResetType::Hard, None)?;
Ok(())
}
pub fn push_new_commit(_repo: &Repository, branch: &str) -> Result<(), Report> {
log::info!("Pushing new branch to origin.");
let status = Command::new("git")
.args(&["push", "--set-upstream", "origin", branch])
.status()?;
if !status.success() {
eyre!("Failed to run git push. {}", status);
} else {
log::info!("Git push succeeded");
}
Ok(())
}
pub fn assure_repo_in_normal_state(repo: &Repository) -> Result<()> {
let state = repo.state();
if state != RepositoryState::Clean {
return Err(eyre!(
"The repository is currently not in a clean state ({:?}).",
state
));
}
Ok(())
}
fn assure_workspace_is_clean(repo: &Repository) -> Result<()> {
let mut options = git2::StatusOptions::new();
options.include_ignored(false);
let statuses = repo.statuses(Some(&mut options))?;
for s in statuses.iter() {
log::warn!("Dirty: {:?} -- {:?}", s.path(), s.status());
}
let is_dirty = !statuses.is_empty();
if is_dirty {
Err(eyre!("The repository is dirty."))
} else {
Ok(())
}
}
pub fn get_default_branch(repo: &Repository) -> Result<String, Report> {
for name in ["origin/main", "origin/master", "origin/devel"].iter() {
match repo.resolve_reference_from_short_name(name) {
Ok(_) => {
log::debug!("Found {} as the default remote branch. A bit hacky -- wrong results certainly possible.", name);
return Ok(name.to_string());
}
Err(_) => continue,
}
}
Err(eyre!("Could not find remote default branch."))
}
pub fn stash(repo: &mut Repository) -> Result<bool> {
let signature = repo.signature()?;
let stashed = match repo.stash_save(&signature, "quickfix: auto-stash", None) {
Ok(stash) => {
log::debug!("Stashed to object {}", stash);
true
}
Err(e) => {
if e.code() == git2::ErrorCode::NotFound && e.class() == git2::ErrorClass::Stash {
log::debug!("Nothing to stash.");
false
} else {
return Err(eyre!("{}", e.message()));
}
}
};
Ok(stashed)
}