12
12
// See the License for the specific language governing permissions and
13
13
// limitations under the License.
14
14
15
+ use std:: collections:: HashMap ;
15
16
use std:: env;
16
17
use std:: error:: Error ;
17
18
use std:: process:: Command ;
18
19
19
20
use console:: style;
20
21
use dialoguer:: { Confirmation , Select } ;
21
- use git2:: { Branch , Commit , Diff , Repository } ;
22
+ use git2:: { Branch , Commit , Diff , Object , ObjectType , Oid , Repository } ;
22
23
use structopt:: StructOpt ;
23
24
25
+ const UPSTREAM_VAR : & str = "GIT_INSTAFIX_UPSTREAM" ;
26
+
24
27
#[ derive( StructOpt , Debug ) ]
25
28
#[ structopt(
26
29
about = "Fix a commit in your history with your currently-staged changes" ,
@@ -30,13 +33,15 @@ When run with no arguments this will:
30
33
31
34
* If you have no staged changes, ask if you'd like to stage all changes
32
35
* Print a `diff --stat` of your currently staged changes
33
- * Provide a list of commits from HEAD to HEAD's upstream,
34
- or --max-commits, whichever is lesser
36
+ * Provide a list of commits to fixup or amend going back to:
37
+ * The merge-base of HEAD and the environment var GIT_INSTAFIX_UPSTREAM
38
+ (if it is set)
39
+ * HEAD's upstream
35
40
* Fixup your selected commit with the staged changes
36
41
" ,
37
- raw ( max_term_width = " 100" ) ,
38
- raw ( setting = " structopt::clap::AppSettings::UnifiedHelpMessage" ) ,
39
- raw ( setting = " structopt::clap::AppSettings::ColoredHelp" )
42
+ max_term_width = 100 ,
43
+ setting = structopt:: clap:: AppSettings :: UnifiedHelpMessage ,
44
+ setting = structopt:: clap:: AppSettings :: ColoredHelp ,
40
45
) ]
41
46
struct Args {
42
47
/// Use `squash!`: change the commit message that you amend
@@ -69,34 +74,71 @@ fn main() {
69
74
70
75
fn run ( squash : bool , max_commits : usize ) -> Result < ( ) , Box < dyn Error > > {
71
76
let repo = Repository :: open ( "." ) ?;
72
- match repo. head ( ) {
73
- Ok ( head) => {
74
- let head_tree = head. peel_to_tree ( ) ?;
75
- let head_branch = Branch :: wrap ( head) ;
76
- let diff = repo. diff_tree_to_index ( Some ( & head_tree) , None , None ) ?;
77
- let commit_to_amend =
78
- create_fixup_commit ( & repo, & head_branch, & diff, squash, max_commits) ?;
79
- println ! (
80
- "selected: {} {}" ,
81
- & commit_to_amend. id( ) . to_string( ) [ 0 ..10 ] ,
82
- commit_to_amend. summary( ) . unwrap_or( "" )
83
- ) ;
84
- // do the rebase
85
- let target_id = format ! ( "{}~" , commit_to_amend. id( ) ) ;
86
- Command :: new ( "git" )
87
- . args ( & [ "rebase" , "--interactive" , "--autosquash" , & target_id] )
88
- . env ( "GIT_SEQUENCE_EDITOR" , "true" )
89
- . spawn ( ) ?
90
- . wait ( ) ?;
77
+ let head = repo
78
+ . head ( )
79
+ . map_err ( |e| format ! ( "HEAD is not pointing at a valid branch: {}" , e) ) ?;
80
+ let head_tree = head. peel_to_tree ( ) ?;
81
+ let head_branch = Branch :: wrap ( head) ;
82
+ let diff = repo. diff_tree_to_index ( Some ( & head_tree) , None , None ) ?;
83
+ let upstream = get_upstream ( & repo, & head_branch) ?;
84
+ let commit_to_amend =
85
+ create_fixup_commit ( & repo, & head_branch, upstream, & diff, squash, max_commits) ?;
86
+ println ! (
87
+ "selected: {} {}" ,
88
+ & commit_to_amend. id( ) . to_string( ) [ 0 ..10 ] ,
89
+ commit_to_amend. summary( ) . unwrap_or( "" )
90
+ ) ;
91
+ // do the rebase
92
+ let target_id = format ! ( "{}~" , commit_to_amend. id( ) ) ;
93
+ Command :: new ( "git" )
94
+ . args ( & [ "rebase" , "--interactive" , "--autosquash" , & target_id] )
95
+ . env ( "GIT_SEQUENCE_EDITOR" , "true" )
96
+ . spawn ( ) ?
97
+ . wait ( ) ?;
98
+ Ok ( ( ) )
99
+ }
100
+
101
+ fn get_upstream < ' a > (
102
+ repo : & ' a Repository ,
103
+ head_branch : & ' a Branch ,
104
+ ) -> Result < Option < Object < ' a > > , Box < dyn Error > > {
105
+ let upstream = if let Ok ( upstream_name) = env:: var ( UPSTREAM_VAR ) {
106
+ let branch = repo
107
+ . branches ( None ) ?
108
+ . filter_map ( |branch| branch. ok ( ) . map ( |( b, _type) | b) )
109
+ . find ( |b| {
110
+ b. name ( )
111
+ . map ( |n| n. expect ( "valid utf8 branchname" ) == & upstream_name)
112
+ . unwrap_or ( false )
113
+ } )
114
+ . ok_or_else ( || format ! ( "cannot find branch with name {:?}" , upstream_name) ) ?;
115
+ let result = Command :: new ( "git" )
116
+ . args ( & [
117
+ "merge-base" ,
118
+ head_branch. name ( ) . unwrap ( ) . unwrap ( ) ,
119
+ branch. name ( ) . unwrap ( ) . unwrap ( ) ,
120
+ ] )
121
+ . output ( ) ?
122
+ . stdout ;
123
+ let oid = Oid :: from_str ( std:: str:: from_utf8 ( & result) ?. trim ( ) ) ?;
124
+ let commit = repo. find_object ( oid, None ) . unwrap ( ) ;
125
+
126
+ commit
127
+ } else {
128
+ if let Ok ( upstream) = head_branch. upstream ( ) {
129
+ upstream. into_reference ( ) . peel ( ObjectType :: Commit ) ?
130
+ } else {
131
+ return Ok ( None ) ;
91
132
}
92
- Err ( e) => return Err ( format ! ( "head is not pointing at a valid branch: {}" , e) . into ( ) ) ,
93
133
} ;
94
- Ok ( ( ) )
134
+
135
+ Ok ( Some ( upstream) )
95
136
}
96
137
97
138
fn create_fixup_commit < ' a > (
98
139
repo : & ' a Repository ,
99
140
head_branch : & ' a Branch ,
141
+ upstream : Option < Object < ' a > > ,
100
142
diff : & ' a Diff ,
101
143
squash : bool ,
102
144
max_commits : usize ,
@@ -106,7 +148,10 @@ fn create_fixup_commit<'a>(
106
148
let dirty_workdir_stats = repo. diff_index_to_workdir ( None , None ) ?. stats ( ) ?;
107
149
if dirty_workdir_stats. files_changed ( ) > 0 {
108
150
print_diff ( Changes :: Unstaged ) ?;
109
- if !Confirmation :: new ( "Nothing staged, stage and commit everything?" ) . interact ( ) ? {
151
+ if !Confirmation :: new ( )
152
+ . with_text ( "Nothing staged, stage and commit everything?" )
153
+ . interact ( ) ?
154
+ {
110
155
return Err ( "" . into ( ) ) ;
111
156
}
112
157
} else {
@@ -116,18 +161,13 @@ fn create_fixup_commit<'a>(
116
161
let mut idx = repo. index ( ) ?;
117
162
idx. update_all ( & pathspecs, None ) ?;
118
163
idx. write ( ) ?;
119
- let commit_to_amend =
120
- select_commit_to_amend ( & repo, head_branch. upstream ( ) . ok ( ) , max_commits) ?;
121
- do_fixup_commit ( & repo, & head_branch, & commit_to_amend, squash) ?;
122
- Ok ( commit_to_amend)
123
164
} else {
124
165
println ! ( "Staged changes:" ) ;
125
166
print_diff ( Changes :: Staged ) ?;
126
- let commit_to_amend =
127
- select_commit_to_amend ( & repo, head_branch. upstream ( ) . ok ( ) , max_commits) ?;
128
- do_fixup_commit ( & repo, & head_branch, & commit_to_amend, squash) ?;
129
- Ok ( commit_to_amend)
130
167
}
168
+ let commit_to_amend = select_commit_to_amend ( & repo, upstream, max_commits) ?;
169
+ do_fixup_commit ( & repo, & head_branch, & commit_to_amend, squash) ?;
170
+ Ok ( commit_to_amend)
131
171
}
132
172
133
173
fn do_fixup_commit < ' a > (
@@ -152,13 +192,13 @@ fn do_fixup_commit<'a>(
152
192
153
193
fn select_commit_to_amend < ' a > (
154
194
repo : & ' a Repository ,
155
- upstream : Option < Branch < ' a > > ,
195
+ upstream : Option < Object < ' a > > ,
156
196
max_commits : usize ,
157
197
) -> Result < Commit < ' a > , Box < dyn Error > > {
158
198
let mut walker = repo. revwalk ( ) ?;
159
199
walker. push_head ( ) ?;
160
- let commits = if let Some ( upstream) = upstream {
161
- let upstream_oid = upstream. get ( ) . target ( ) . expect ( "No upstream target" ) ;
200
+ let commits = if let Some ( upstream) = upstream. as_ref ( ) {
201
+ let upstream_oid = upstream. id ( ) ;
162
202
walker
163
203
. flat_map ( |r| r)
164
204
. take_while ( |rev| * rev != upstream_oid)
@@ -172,19 +212,42 @@ fn select_commit_to_amend<'a>(
172
212
. map ( |rev| repo. find_commit ( rev) )
173
213
. collect :: < Result < Vec < _ > , _ > > ( ) ?
174
214
} ;
215
+ let branches: HashMap < Oid , String > = repo
216
+ . branches ( None ) ?
217
+ . filter_map ( |b| {
218
+ b. ok ( ) . and_then ( |( b, _type) | {
219
+ let name: Option < String > = b. name ( ) . ok ( ) . and_then ( |n| n. map ( |n| n. to_owned ( ) ) ) ;
220
+ let oid = b. into_reference ( ) . resolve ( ) . ok ( ) . and_then ( |r| r. target ( ) ) ;
221
+ name. and_then ( |name| oid. map ( |oid| ( oid, name) ) )
222
+ } )
223
+ } )
224
+ . collect ( ) ;
175
225
let rev_aliases = commits
176
226
. iter ( )
177
- . map ( |commit| {
227
+ . enumerate ( )
228
+ . map ( |( i, commit) | {
229
+ let bname = if i > 0 {
230
+ branches
231
+ . get ( & commit. id ( ) )
232
+ . map ( |n| format ! ( "({}) " , n) )
233
+ . unwrap_or_else ( String :: new)
234
+ } else {
235
+ String :: new ( )
236
+ } ;
178
237
format ! (
179
- "{} {}" ,
238
+ "{} {}{} " ,
180
239
& style( & commit. id( ) . to_string( ) [ 0 ..10 ] ) . blue( ) ,
240
+ style( bname) . green( ) ,
181
241
commit. summary( ) . unwrap_or( "no commit summary" )
182
242
)
183
243
} )
184
244
. collect :: < Vec < _ > > ( ) ;
185
- let commitmsgs = rev_aliases. iter ( ) . map ( |s| s. as_ref ( ) ) . collect :: < Vec < _ > > ( ) ;
186
- println ! ( "Select a commit to amend:" ) ;
187
- let selected = Select :: new ( ) . items ( & commitmsgs) . default ( 0 ) . interact ( ) ;
245
+ if upstream. is_none ( ) {
246
+ eprintln ! ( "Select a commit to amend (no upstream for HEAD):" ) ;
247
+ } else {
248
+ eprintln ! ( "Select a commit to amend:" ) ;
249
+ }
250
+ let selected = Select :: new ( ) . items ( & rev_aliases) . default ( 0 ) . interact ( ) ;
188
251
Ok ( repo. find_commit ( commits[ selected?] . id ( ) ) ?)
189
252
}
190
253
0 commit comments