14
14
15
15
use std:: collections:: HashMap ;
16
16
use std:: env;
17
- use std:: error:: Error ;
18
17
use std:: process:: Command ;
19
18
20
- use anyhow:: bail;
19
+ use anyhow:: { anyhow , bail} ;
21
20
use console:: style;
22
21
use dialoguer:: { Confirm , Select } ;
23
- use git2:: { Branch , Commit , Diff , Object , ObjectType , Oid , Repository } ;
22
+ use git2:: { Branch , Commit , Diff , Object , ObjectType , Oid , Rebase , Repository } ;
24
23
use structopt:: StructOpt ;
25
24
26
25
const UPSTREAM_VAR : & str = "GIT_INSTAFIX_UPSTREAM" ;
@@ -72,53 +71,138 @@ fn main() {
72
71
// An empty message means don't display any error message
73
72
let msg = e. to_string ( ) ;
74
73
if !msg. is_empty ( ) {
75
- println ! ( "Error: {}" , e) ;
74
+ println ! ( "Error: {:# }" , e) ;
76
75
}
77
76
std:: process:: exit ( 1 ) ;
78
77
}
79
78
}
80
79
81
80
fn run (
82
- squash : bool ,
81
+ _squash : bool ,
83
82
max_commits : usize ,
84
83
message_pattern : Option < String > ,
85
- ) -> Result < ( ) , Box < dyn Error > > {
84
+ ) -> Result < ( ) , anyhow :: Error > {
86
85
let repo = Repository :: open ( "." ) ?;
86
+ let diff = create_diff ( & repo) ?;
87
87
let head = repo
88
88
. head ( )
89
- . map_err ( |e| format ! ( "HEAD is not pointing at a valid branch: {}" , e) ) ?;
90
- let head_tree = head. peel_to_tree ( ) ?;
89
+ . map_err ( |e| anyhow ! ( "HEAD is not pointing at a valid branch: {}" , e) ) ?;
91
90
let head_branch = Branch :: wrap ( head) ;
92
- let diff = repo . diff_tree_to_index ( Some ( & head_tree ) , None , None ) ? ;
91
+ println ! ( "head_branch: {:?}" , head_branch . name ( ) . unwrap ( ) . unwrap ( ) ) ;
93
92
let upstream = get_upstream ( & repo, & head_branch) ?;
94
- let commit_to_amend = create_fixup_commit (
95
- & repo,
96
- & head_branch,
97
- upstream,
98
- & diff,
99
- squash,
100
- max_commits,
101
- & message_pattern,
102
- ) ?;
103
- println ! (
104
- "selected: {} {}" ,
105
- & commit_to_amend. id( ) . to_string( ) [ 0 ..10 ] ,
106
- commit_to_amend. summary( ) . unwrap_or( "" )
107
- ) ;
108
- // do the rebase
109
- let target_id = format ! ( "{}~" , commit_to_amend. id( ) ) ;
110
- Command :: new ( "git" )
111
- . args ( & [ "rebase" , "--interactive" , "--autosquash" , & target_id] )
112
- . env ( "GIT_SEQUENCE_EDITOR" , "true" )
113
- . spawn ( ) ?
114
- . wait ( ) ?;
93
+ let commit_to_amend = select_commit_to_amend ( & repo, upstream, max_commits, & message_pattern) ?;
94
+ do_fixup_commit ( & repo, & head_branch, & commit_to_amend, false ) ?;
95
+ println ! ( "selected: {}" , disp( & commit_to_amend) ) ;
96
+ let current_branch = Branch :: wrap ( repo. head ( ) ?) ;
97
+ do_rebase ( & repo, & current_branch, & commit_to_amend, & diff) ?;
98
+
99
+ Ok ( ( ) )
100
+ }
101
+
102
+ fn do_rebase (
103
+ repo : & Repository ,
104
+ branch : & Branch ,
105
+ commit_to_amend : & Commit ,
106
+ diff : & Diff ,
107
+ ) -> Result < ( ) , anyhow:: Error > {
108
+ let first_parent = repo. find_annotated_commit ( commit_parent ( commit_to_amend) ?. id ( ) ) ?;
109
+ let branch_commit = repo. reference_to_annotated_commit ( branch. get ( ) ) ?;
110
+ let fixup_commit = branch. get ( ) . peel_to_commit ( ) ?;
111
+ let fixup_message = fixup_commit. message ( ) ;
112
+
113
+ let rebase = & mut repo
114
+ . rebase ( Some ( & branch_commit) , Some ( & first_parent) , None , None )
115
+ . map_err ( |e| anyhow ! ( "Error starting rebase: {}" , e) ) ?;
116
+ match do_rebase_inner ( repo, rebase, diff, fixup_message) {
117
+ Ok ( _) => {
118
+ rebase. finish ( None ) ?;
119
+ Ok ( ( ) )
120
+ }
121
+ Err ( e) => {
122
+ eprintln ! ( "Aborting rebase, please apply it manualy via" ) ;
123
+ eprintln ! (
124
+ " git rebase --interactive --autosquash {}~" ,
125
+ first_parent. id( )
126
+ ) ;
127
+ rebase. abort ( ) ?;
128
+ Err ( e)
129
+ }
130
+ }
131
+ }
132
+
133
+ fn do_rebase_inner (
134
+ repo : & Repository ,
135
+ rebase : & mut Rebase ,
136
+ diff : & Diff ,
137
+ fixup_message : Option < & str > ,
138
+ ) -> Result < ( ) , anyhow:: Error > {
139
+ let sig = repo. signature ( ) ?;
140
+
141
+ match rebase. next ( ) {
142
+ Some ( ref res) => {
143
+ let op = res. as_ref ( ) . map_err ( |e| anyhow ! ( "No commit: {}" , e) ) ?;
144
+ let target_commit = repo. find_commit ( op. id ( ) ) ?;
145
+ repo. apply ( diff, git2:: ApplyLocation :: Both , None ) ?;
146
+ let mut idx = repo. index ( ) ?;
147
+ let oid = idx. write_tree ( ) ?;
148
+ let tree = repo. find_tree ( oid) ?;
149
+
150
+ // TODO: Support squash amends
151
+
152
+ let rewrit_id = target_commit. amend ( None , None , None , None , None , Some ( & tree) ) ?;
153
+ repo. reset (
154
+ & repo. find_object ( rewrit_id, None ) ?,
155
+ git2:: ResetType :: Soft ,
156
+ None ,
157
+ ) ?;
158
+
159
+ rewrit_id
160
+ }
161
+ None => bail ! ( "Unable to start rebase: no first step in rebase" ) ,
162
+ } ;
163
+
164
+ while let Some ( ref res) = rebase. next ( ) {
165
+ use git2:: RebaseOperationType :: * ;
166
+
167
+ let op = res. as_ref ( ) . map_err ( |e| anyhow ! ( "Err: {}" , e) ) ?;
168
+ match op. kind ( ) {
169
+ Some ( Pick ) => {
170
+ let commit = repo. find_commit ( op. id ( ) ) ?;
171
+ if commit. message ( ) != fixup_message {
172
+ rebase. commit ( None , & sig, None ) ?;
173
+ }
174
+ }
175
+ Some ( Fixup ) | Some ( Squash ) | Some ( Exec ) | Some ( Edit ) | Some ( Reword ) => {
176
+ // None of this should happen, we'd need to manually create the commits
177
+ bail ! ( "Unable to handle {:?} rebase operation" , op. kind( ) . unwrap( ) )
178
+ }
179
+ None => { }
180
+ }
181
+ }
182
+
115
183
Ok ( ( ) )
116
184
}
117
185
186
+ fn commit_parent < ' a > ( commit : & ' a Commit ) -> Result < Commit < ' a > , anyhow:: Error > {
187
+ match commit. parents ( ) . next ( ) {
188
+ Some ( c) => Ok ( c) ,
189
+ None => bail ! ( "Commit '{}' has no parents" , disp( & commit) ) ,
190
+ }
191
+ }
192
+
193
+ /// Display a commit as "short_hash summary"
194
+ fn disp ( commit : & Commit ) -> String {
195
+ format ! (
196
+ "{} {}" ,
197
+ & commit. id( ) . to_string( ) [ 0 ..10 ] ,
198
+ commit. summary( ) . unwrap_or( "<no summary>" ) ,
199
+ )
200
+ }
201
+
118
202
fn get_upstream < ' a > (
119
203
repo : & ' a Repository ,
120
204
head_branch : & ' a Branch ,
121
- ) -> Result < Option < Object < ' a > > , Box < dyn Error > > {
205
+ ) -> Result < Option < Object < ' a > > , anyhow :: Error > {
122
206
let upstream = if let Ok ( upstream_name) = env:: var ( UPSTREAM_VAR ) {
123
207
let branch = repo
124
208
. branches ( None ) ?
@@ -128,7 +212,7 @@ fn get_upstream<'a>(
128
212
. map ( |n| n. expect ( "valid utf8 branchname" ) == & upstream_name)
129
213
. unwrap_or ( false )
130
214
} )
131
- . ok_or_else ( || format ! ( "cannot find branch with name {:?}" , upstream_name) ) ?;
215
+ . ok_or_else ( || anyhow ! ( "cannot find branch with name {:?}" , upstream_name) ) ?;
132
216
branch. into_reference ( ) . peel ( ObjectType :: Commit ) ?
133
217
} else {
134
218
if let Ok ( upstream) = head_branch. upstream ( ) {
@@ -150,48 +234,44 @@ fn get_upstream<'a>(
150
234
Ok ( Some ( commit) )
151
235
}
152
236
153
- fn create_fixup_commit < ' a > (
154
- repo : & ' a Repository ,
155
- head_branch : & ' a Branch ,
156
- upstream : Option < Object < ' a > > ,
157
- diff : & ' a Diff ,
158
- squash : bool ,
159
- max_commits : usize ,
160
- message_pattern : & Option < String > ,
161
- ) -> Result < Commit < ' a > , Box < dyn Error > > {
162
- let diffstat = diff. stats ( ) ?;
163
- if diffstat. files_changed ( ) == 0 {
164
- let dirty_workdir_stats = repo. diff_index_to_workdir ( None , None ) ?. stats ( ) ?;
237
+ /// Get a diff either from the index or the diff from the index to the working tree
238
+ fn create_diff ( repo : & Repository ) -> Result < Diff , anyhow:: Error > {
239
+ let head = repo. head ( ) ?;
240
+ let head_tree = head. peel_to_tree ( ) ?;
241
+ let staged_diff = repo. diff_tree_to_index ( Some ( & head_tree) , None , None ) ?;
242
+ let diffstat = staged_diff. stats ( ) ?;
243
+ let diff = if diffstat. files_changed ( ) == 0 {
244
+ let diff = repo. diff_index_to_workdir ( None , None ) ?;
245
+ let dirty_workdir_stats = diff. stats ( ) ?;
165
246
if dirty_workdir_stats. files_changed ( ) > 0 {
166
247
print_diff ( Changes :: Unstaged ) ?;
167
248
if !Confirm :: new ( )
168
249
. with_prompt ( "Nothing staged, stage and commit everything?" )
169
250
. interact ( ) ?
170
251
{
171
- return Err ( "" . into ( ) ) ;
252
+ bail ! ( "" ) ;
172
253
}
173
254
} else {
174
- return Err ( "Nothing staged and no tracked files have any changes" . into ( ) ) ;
255
+ bail ! ( "Nothing staged and no tracked files have any changes" ) ;
175
256
}
176
- let pathspecs: Vec < & str > = vec ! [ ] ;
177
- let mut idx = repo. index ( ) ?;
178
- idx. update_all ( & pathspecs, None ) ?;
179
- idx. write ( ) ?;
257
+ repo. apply ( & diff, git2:: ApplyLocation :: Index , None ) ?;
258
+ diff
180
259
} else {
181
260
println ! ( "Staged changes:" ) ;
182
261
print_diff ( Changes :: Staged ) ?;
183
- }
184
- let commit_to_amend = select_commit_to_amend ( & repo , upstream , max_commits , message_pattern ) ? ;
185
- do_fixup_commit ( & repo , & head_branch , & commit_to_amend , squash ) ? ;
186
- Ok ( commit_to_amend )
262
+ staged_diff
263
+ } ;
264
+
265
+ Ok ( diff )
187
266
}
188
267
268
+ /// Commit the current index as a fixup or squash commit
189
269
fn do_fixup_commit < ' a > (
190
270
repo : & ' a Repository ,
191
271
head_branch : & ' a Branch ,
192
272
commit_to_amend : & ' a Commit ,
193
273
squash : bool ,
194
- ) -> Result < ( ) , Box < dyn Error > > {
274
+ ) -> Result < ( ) , anyhow :: Error > {
195
275
let msg = if squash {
196
276
format ! ( "squash! {}" , commit_to_amend. id( ) )
197
277
} else {
@@ -278,9 +358,9 @@ fn select_commit_to_amend<'a>(
278
358
} )
279
359
. collect :: < Vec < _ > > ( ) ;
280
360
if upstream. is_none ( ) {
281
- eprintln ! ( "Select a commit to amend (no upstream for HEAD):" ) ;
361
+ println ! ( "Select a commit to amend (no upstream for HEAD):" ) ;
282
362
} else {
283
- eprintln ! ( "Select a commit to amend:" ) ;
363
+ println ! ( "Select a commit to amend:" ) ;
284
364
}
285
365
let selected = Select :: new ( ) . items ( & rev_aliases) . default ( 0 ) . interact ( ) ;
286
366
Ok ( repo. find_commit ( commits[ selected?] . id ( ) ) ?)
@@ -293,7 +373,7 @@ fn format_ref(rf: &git2::Reference<'_>) -> Result<String, anyhow::Error> {
293
373
Ok ( format ! ( "{} ({})" , shorthand, & sha[ ..10 ] ) )
294
374
}
295
375
296
- fn print_diff ( kind : Changes ) -> Result < ( ) , Box < dyn Error > > {
376
+ fn print_diff ( kind : Changes ) -> Result < ( ) , anyhow :: Error > {
297
377
let mut args = vec ! [ "diff" , "--stat" ] ;
298
378
if kind == Changes :: Staged {
299
379
args. push ( "--cached" ) ;
@@ -302,6 +382,6 @@ fn print_diff(kind: Changes) -> Result<(), Box<dyn Error>> {
302
382
if status. success ( ) {
303
383
Ok ( ( ) )
304
384
} else {
305
- Err ( "git diff failed" . into ( ) )
385
+ bail ! ( "git diff failed" )
306
386
}
307
387
}
0 commit comments