git-gui: allow Ctrl+T to toggle multiple paths
[git/git.git] / lib / commit.tcl
CommitLineData
f522c9b5
SP
1# git-gui misc. commit reading/writing support
2# Copyright (C) 2006, 2007 Shawn Pearce
3
4proc load_last_commit {} {
cfe616bc 5 global HEAD PARENT MERGE_HEAD commit_type ui_comm commit_author
f522c9b5
SP
6 global repo_config
7
8 if {[llength $PARENT] == 0} {
1ac17950 9 error_popup [mc "There is nothing to amend.
f522c9b5
SP
10
11You are about to create the initial commit. There is no commit before this to amend.
1ac17950 12"]
f522c9b5
SP
13 return
14 }
15
16 repository_state curType curHEAD curMERGE_HEAD
17 if {$curType eq {merge}} {
1ac17950 18 error_popup [mc "Cannot amend while merging.
f522c9b5
SP
19
20You are currently in the middle of a merge that has not been fully completed. You cannot amend the prior commit unless you first abort the current merge activity.
1ac17950 21"]
f522c9b5
SP
22 return
23 }
24
25 set msg {}
26 set parents [list]
27 if {[catch {
0b812616 28 set fd [git_read cat-file commit $curHEAD]
f522c9b5 29 fconfigure $fd -encoding binary -translation lf
3ac31e44
AG
30 # By default commits are assumed to be in utf-8
31 set enc utf-8
f522c9b5
SP
32 while {[gets $fd line] > 0} {
33 if {[string match {parent *} $line]} {
34 lappend parents [string range $line 7 end]
35 } elseif {[string match {encoding *} $line]} {
36 set enc [string tolower [string range $line 9 end]]
7e71adc7 37 } elseif {[regexp "author (.*)\\s<(.*)>\\s(\\d.*$)" $line all name email time]} {
cfe616bc 38 set commit_author [list name $name email $email date $time]
f522c9b5
SP
39 }
40 }
c4638f66 41 set msg [read $fd]
f522c9b5 42 close $fd
c4638f66
SP
43
44 set enc [tcl_encoding $enc]
45 if {$enc ne {}} {
46 set msg [encoding convertfrom $enc $msg]
47 }
48 set msg [string trim $msg]
f522c9b5 49 } err]} {
31bb1d1b 50 error_popup [strcat [mc "Error loading commit data for amend:"] "\n\n$err"]
f522c9b5
SP
51 return
52 }
53
54 set HEAD $curHEAD
55 set PARENT $parents
56 set MERGE_HEAD [list]
57 switch -- [llength $parents] {
58 0 {set commit_type amend-initial}
59 1 {set commit_type amend}
60 default {set commit_type amend-merge}
61 }
62
63 $ui_comm delete 0.0 end
64 $ui_comm insert end $msg
65 $ui_comm edit reset
66 $ui_comm edit modified false
699d5601 67 rescan ui_ready
f522c9b5
SP
68}
69
70set GIT_COMMITTER_IDENT {}
71
72proc committer_ident {} {
73 global GIT_COMMITTER_IDENT
74
75 if {$GIT_COMMITTER_IDENT eq {}} {
76 if {[catch {set me [git var GIT_COMMITTER_IDENT]} err]} {
31bb1d1b 77 error_popup [strcat [mc "Unable to obtain your identity:"] "\n\n$err"]
f522c9b5
SP
78 return {}
79 }
80 if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \
81 $me me GIT_COMMITTER_IDENT]} {
31bb1d1b 82 error_popup [strcat [mc "Invalid GIT_COMMITTER_IDENT:"] "\n\n$me"]
f522c9b5
SP
83 return {}
84 }
85 }
86
87 return $GIT_COMMITTER_IDENT
88}
89
90proc do_signoff {} {
91 global ui_comm
92
93 set me [committer_ident]
94 if {$me eq {}} return
95
96 set sob "Signed-off-by: $me"
97 set last [$ui_comm get {end -1c linestart} {end -1c}]
98 if {$last ne $sob} {
99 $ui_comm edit separator
100 if {$last ne {}
101 && ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} {
102 $ui_comm insert end "\n"
103 }
104 $ui_comm insert end "\n$sob"
105 $ui_comm edit separator
106 $ui_comm see end
107 }
108}
109
110proc create_new_commit {} {
cfe616bc 111 global commit_type ui_comm commit_author
f522c9b5
SP
112
113 set commit_type normal
cfe616bc 114 unset -nocomplain commit_author
f522c9b5
SP
115 $ui_comm delete 0.0 end
116 $ui_comm edit reset
117 $ui_comm edit modified false
699d5601 118 rescan ui_ready
f522c9b5
SP
119}
120
06569cd5
AG
121proc setup_commit_encoding {msg_wt {quiet 0}} {
122 global repo_config
123
124 if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
125 set enc utf-8
126 }
127 set use_enc [tcl_encoding $enc]
128 if {$use_enc ne {}} {
129 fconfigure $msg_wt -encoding $use_enc
130 } else {
131 if {!$quiet} {
132 error_popup [mc "warning: Tcl does not support encoding '%s'." $enc]
133 }
134 fconfigure $msg_wt -encoding utf-8
135 }
136}
137
f522c9b5
SP
138proc commit_tree {} {
139 global HEAD commit_type file_states ui_comm repo_config
699d5601 140 global pch_error
f522c9b5
SP
141
142 if {[committer_ident] eq {}} return
143 if {![lock_index update]} return
144
145 # -- Our in memory state should match the repository.
146 #
147 repository_state curType curHEAD curMERGE_HEAD
148 if {[string match amend* $commit_type]
149 && $curType eq {normal}
150 && $curHEAD eq $HEAD} {
151 } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
1ac17950 152 info_popup [mc "Last scanned state does not match repository state.
f522c9b5
SP
153
154Another Git program has modified this repository since the last scan. A rescan must be performed before another commit can be created.
155
156The rescan will be automatically started now.
1ac17950 157"]
f522c9b5 158 unlock_index
699d5601 159 rescan ui_ready
f522c9b5
SP
160 return
161 }
162
163 # -- At least one file should differ in the index.
164 #
165 set files_ready 0
166 foreach path [array names file_states] {
4a065c8a
BW
167 set s $file_states($path)
168 switch -glob -- [lindex $s 0] {
f522c9b5
SP
169 _? {continue}
170 A? -
171 D? -
7587f4d3 172 T? -
f522c9b5 173 M? {set files_ready 1}
ff515d81 174 _U -
f522c9b5 175 U? {
1ac17950 176 error_popup [mc "Unmerged files cannot be committed.
f522c9b5 177
1ac17950
CS
178File %s has merge conflicts. You must resolve them and stage the file before committing.
179" [short_path $path]]
f522c9b5
SP
180 unlock_index
181 return
182 }
183 default {
1ac17950 184 error_popup [mc "Unknown file state %s detected.
f522c9b5 185
1ac17950
CS
186File %s cannot be committed by this program.
187" [lindex $s 0] [short_path $path]]
f522c9b5
SP
188 }
189 }
190 }
1e65c622 191 if {!$files_ready && ![string match *merge $curType] && ![is_enabled nocommit]} {
1ac17950 192 info_popup [mc "No changes to commit.
f522c9b5 193
360cc106 194You must stage at least 1 file before you can commit.
1ac17950 195"]
f522c9b5
SP
196 unlock_index
197 return
198 }
199
1e65c622
AG
200 if {[is_enabled nocommitmsg]} { do_quit 0 }
201
f522c9b5
SP
202 # -- A message is required.
203 #
204 set msg [string trim [$ui_comm get 1.0 end]]
205 regsub -all -line {[ \t\r]+$} $msg {} msg
206 if {$msg eq {}} {
1ac17950 207 error_popup [mc "Please supply a commit message.
f522c9b5
SP
208
209A good commit message has the following format:
210
208320de 211- First line: Describe in one sentence what you did.
f522c9b5
SP
212- Second line: Blank
213- Remaining lines: Describe why this change is good.
1ac17950 214"]
f522c9b5
SP
215 unlock_index
216 return
217 }
218
fb0ca475
SP
219 # -- Build the message file.
220 #
221 set msg_p [gitdir GITGUI_EDITMSG]
222 set msg_wt [open $msg_p w]
223 fconfigure $msg_wt -translation lf
06569cd5 224 setup_commit_encoding $msg_wt
fb0ca475
SP
225 puts $msg_wt $msg
226 close $msg_wt
227
1e65c622
AG
228 if {[is_enabled nocommit]} { do_quit 0 }
229
f522c9b5
SP
230 # -- Run the pre-commit hook.
231 #
ed76cb70
SP
232 set fd_ph [githook_read pre-commit]
233 if {$fd_ph eq {}} {
fb0ca475 234 commit_commitmsg $curHEAD $msg_p
f522c9b5
SP
235 return
236 }
237
5e6d7768 238 ui_status [mc "Calling pre-commit hook..."]
f522c9b5 239 set pch_error {}
6eb420ef 240 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
f522c9b5 241 fileevent $fd_ph readable \
fb0ca475 242 [list commit_prehook_wait $fd_ph $curHEAD $msg_p]
f522c9b5
SP
243}
244
fb0ca475 245proc commit_prehook_wait {fd_ph curHEAD msg_p} {
699d5601 246 global pch_error
f522c9b5
SP
247
248 append pch_error [read $fd_ph]
249 fconfigure $fd_ph -blocking 1
250 if {[eof $fd_ph]} {
251 if {[catch {close $fd_ph}]} {
fb0ca475 252 catch {file delete $msg_p}
5e6d7768 253 ui_status [mc "Commit declined by pre-commit hook."]
f522c9b5
SP
254 hook_failed_popup pre-commit $pch_error
255 unlock_index
256 } else {
fb0ca475 257 commit_commitmsg $curHEAD $msg_p
f522c9b5
SP
258 }
259 set pch_error {}
260 return
261 }
262 fconfigure $fd_ph -blocking 0
263}
264
fb0ca475 265proc commit_commitmsg {curHEAD msg_p} {
e34789cc 266 global is_detached repo_config
fb0ca475
SP
267 global pch_error
268
d8d166bf
BW
269 if {$is_detached
270 && ![file exists [gitdir rebase-merge head-name]]
271 && [is_config_true gui.warndetachedcommit]} {
e34789cc
HV
272 set msg [mc "You are about to commit on a detached head.\
273This is a potentially dangerous thing to do because if you switch\
9ef75087 274to another branch you will lose your changes and it can be difficult\
e34789cc
HV
275to retrieve them later from the reflog. You should probably cancel this\
276commit and create a new branch to continue.\n\
277\n\
278Do you really want to proceed with your Commit?"]
279 if {[ask_popup $msg] ne yes} {
280 unlock_index
281 return
282 }
283 }
284
fb0ca475
SP
285 # -- Run the commit-msg hook.
286 #
ed76cb70
SP
287 set fd_ph [githook_read commit-msg $msg_p]
288 if {$fd_ph eq {}} {
fb0ca475
SP
289 commit_writetree $curHEAD $msg_p
290 return
291 }
292
5e6d7768 293 ui_status [mc "Calling commit-msg hook..."]
fb0ca475 294 set pch_error {}
fb0ca475
SP
295 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
296 fileevent $fd_ph readable \
297 [list commit_commitmsg_wait $fd_ph $curHEAD $msg_p]
298}
299
300proc commit_commitmsg_wait {fd_ph curHEAD msg_p} {
301 global pch_error
302
303 append pch_error [read $fd_ph]
304 fconfigure $fd_ph -blocking 1
305 if {[eof $fd_ph]} {
306 if {[catch {close $fd_ph}]} {
307 catch {file delete $msg_p}
5e6d7768 308 ui_status [mc "Commit declined by commit-msg hook."]
fb0ca475
SP
309 hook_failed_popup commit-msg $pch_error
310 unlock_index
311 } else {
312 commit_writetree $curHEAD $msg_p
313 }
314 set pch_error {}
315 return
316 }
317 fconfigure $fd_ph -blocking 0
318}
319
320proc commit_writetree {curHEAD msg_p} {
5e6d7768 321 ui_status [mc "Committing changes..."]
0b812616 322 set fd_wt [git_read write-tree]
f522c9b5 323 fileevent $fd_wt readable \
fb0ca475 324 [list commit_committree $fd_wt $curHEAD $msg_p]
f522c9b5
SP
325}
326
fb0ca475 327proc commit_committree {fd_wt curHEAD msg_p} {
cfe616bc 328 global HEAD PARENT MERGE_HEAD commit_type commit_author
699d5601
SP
329 global current_branch
330 global ui_comm selected_commit_type
f522c9b5
SP
331 global file_states selected_paths rescan_active
332 global repo_config
cfe616bc 333 global env
f522c9b5
SP
334
335 gets $fd_wt tree_id
8af52d7a 336 if {[catch {close $fd_wt} err]} {
fb0ca475 337 catch {file delete $msg_p}
31bb1d1b 338 error_popup [strcat [mc "write-tree failed:"] "\n\n$err"]
5e6d7768 339 ui_status [mc "Commit failed."]
f522c9b5
SP
340 unlock_index
341 return
342 }
343
344 # -- Verify this wasn't an empty change.
345 #
346 if {$commit_type eq {normal}} {
b215883d 347 set fd_ot [git_read cat-file commit $PARENT]
20f1a10b
SP
348 fconfigure $fd_ot -encoding binary -translation lf
349 set old_tree [gets $fd_ot]
350 close $fd_ot
351
352 if {[string equal -length 5 {tree } $old_tree]
353 && [string length $old_tree] == 45} {
354 set old_tree [string range $old_tree 5 end]
355 } else {
c8c4854b 356 error [mc "Commit %s appears to be corrupt" $PARENT]
20f1a10b
SP
357 }
358
f522c9b5 359 if {$tree_id eq $old_tree} {
fb0ca475 360 catch {file delete $msg_p}
1ac17950 361 info_popup [mc "No changes to commit.
f522c9b5
SP
362
363No files were modified by this commit and it was not a merge commit.
364
365A rescan will be automatically started now.
1ac17950 366"]
f522c9b5 367 unlock_index
1ac17950 368 rescan {ui_status [mc "No changes to commit."]}
f522c9b5
SP
369 return
370 }
371 }
372
cfe616bc
PT
373 if {[info exists commit_author]} {
374 set old_author [commit_author_ident $commit_author]
7e71adc7 375 }
f522c9b5
SP
376 # -- Create the commit.
377 #
378 set cmd [list commit-tree $tree_id]
2afe6b73
JS
379 if {[is_config_true commit.gpgsign]} {
380 lappend cmd -S
381 }
f522c9b5
SP
382 foreach p [concat $PARENT $MERGE_HEAD] {
383 lappend cmd -p $p
384 }
385 lappend cmd <$msg_p
386 if {[catch {set cmt_id [eval git $cmd]} err]} {
fb0ca475 387 catch {file delete $msg_p}
31bb1d1b 388 error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
5e6d7768 389 ui_status [mc "Commit failed."]
f522c9b5 390 unlock_index
cfe616bc
PT
391 unset -nocomplain commit_author
392 commit_author_reset $old_author
f522c9b5
SP
393 return
394 }
cfe616bc
PT
395 if {[info exists commit_author]} {
396 unset -nocomplain commit_author
397 commit_author_reset $old_author
398 }
f522c9b5
SP
399
400 # -- Update the HEAD ref.
401 #
402 set reflogm commit
403 if {$commit_type ne {normal}} {
404 append reflogm " ($commit_type)"
405 }
fb0ca475 406 set msg_fd [open $msg_p r]
06569cd5 407 setup_commit_encoding $msg_fd 1
fb0ca475
SP
408 gets $msg_fd subject
409 close $msg_fd
f522c9b5
SP
410 append reflogm {: } $subject
411 if {[catch {
412 git update-ref -m $reflogm HEAD $cmt_id $curHEAD
413 } err]} {
fb0ca475 414 catch {file delete $msg_p}
31bb1d1b 415 error_popup [strcat [mc "update-ref failed:"] "\n\n$err"]
5e6d7768 416 ui_status [mc "Commit failed."]
f522c9b5
SP
417 unlock_index
418 return
419 }
420
421 # -- Cleanup after ourselves.
422 #
423 catch {file delete $msg_p}
424 catch {file delete [gitdir MERGE_HEAD]}
425 catch {file delete [gitdir MERGE_MSG]}
426 catch {file delete [gitdir SQUASH_MSG]}
427 catch {file delete [gitdir GITGUI_MSG]}
5a5e4d25 428 catch {file delete [gitdir CHERRY_PICK_HEAD]}
f522c9b5
SP
429
430 # -- Let rerere do its thing.
431 #
d4c53077
SP
432 if {[get_config rerere.enabled] eq {}} {
433 set rerere [file isdirectory [gitdir rr-cache]]
434 } else {
435 set rerere [is_config_true rerere.enabled]
436 }
437 if {$rerere} {
f522c9b5
SP
438 catch {git rerere}
439 }
440
441 # -- Run the post-commit hook.
442 #
ed76cb70
SP
443 set fd_ph [githook_read post-commit]
444 if {$fd_ph ne {}} {
f0d4eec9
JL
445 global pch_error
446 set pch_error {}
ed76cb70
SP
447 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
448 fileevent $fd_ph readable \
449 [list commit_postcommit_wait $fd_ph $cmt_id]
f522c9b5
SP
450 }
451
452 $ui_comm delete 0.0 end
453 $ui_comm edit reset
454 $ui_comm edit modified false
4578c5cb
SP
455 if {$::GITGUI_BCK_exists} {
456 catch {file delete [gitdir GITGUI_BCK]}
9c5a3c77 457 set ::GITGUI_BCK_exists 0
4578c5cb 458 }
f522c9b5 459
1e65c622 460 if {[is_enabled singlecommit]} { do_quit 0 }
f522c9b5 461
f522c9b5
SP
462 # -- Update in memory status
463 #
464 set selected_commit_type new
465 set commit_type normal
466 set HEAD $cmt_id
467 set PARENT $cmt_id
468 set MERGE_HEAD [list]
469
470 foreach path [array names file_states] {
471 set s $file_states($path)
472 set m [lindex $s 0]
473 switch -glob -- $m {
474 _O -
475 _M -
476 _D {continue}
477 __ -
478 A_ -
479 M_ -
e681cb7d 480 T_ -
f522c9b5
SP
481 D_ {
482 unset file_states($path)
483 catch {unset selected_paths($path)}
484 }
485 DO {
486 set file_states($path) [list _O [lindex $s 1] {} {}]
487 }
488 AM -
489 AD -
7587f4d3
BW
490 AT -
491 TM -
492 TD -
f522c9b5 493 MM -
7587f4d3 494 MT -
f522c9b5
SP
495 MD {
496 set file_states($path) [list \
497 _[string index $m 1] \
498 [lindex $s 1] \
499 [lindex $s 3] \
500 {}]
501 }
502 }
503 }
504
505 display_all_files
506 unlock_index
507 reshow_diff
1ac17950 508 ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
f522c9b5 509}
ed76cb70
SP
510
511proc commit_postcommit_wait {fd_ph cmt_id} {
f0d4eec9 512 global pch_error
ed76cb70
SP
513
514 append pch_error [read $fd_ph]
515 fconfigure $fd_ph -blocking 1
516 if {[eof $fd_ph]} {
517 if {[catch {close $fd_ph}]} {
518 hook_failed_popup post-commit $pch_error 0
519 }
520 unset pch_error
521 return
522 }
523 fconfigure $fd_ph -blocking 0
524}
cfe616bc
PT
525
526proc commit_author_ident {details} {
527 global env
528 array set author $details
529 set old [array get env GIT_AUTHOR_*]
530 set env(GIT_AUTHOR_NAME) $author(name)
531 set env(GIT_AUTHOR_EMAIL) $author(email)
532 set env(GIT_AUTHOR_DATE) $author(date)
533 return $old
534}
535proc commit_author_reset {details} {
536 global env
537 unset env(GIT_AUTHOR_NAME) env(GIT_AUTHOR_EMAIL) env(GIT_AUTHOR_DATE)
538 if {$details ne {}} {
539 array set env $details
540 }
541}