git-gui: allow Ctrl+T to toggle multiple paths
[git/git.git] / lib / commit.tcl
1 # git-gui misc. commit reading/writing support
2 # Copyright (C) 2006, 2007 Shawn Pearce
3
4 proc load_last_commit {} {
5 global HEAD PARENT MERGE_HEAD commit_type ui_comm commit_author
6 global repo_config
7
8 if {[llength $PARENT] == 0} {
9 error_popup [mc "There is nothing to amend.
10
11 You are about to create the initial commit. There is no commit before this to amend.
12 "]
13 return
14 }
15
16 repository_state curType curHEAD curMERGE_HEAD
17 if {$curType eq {merge}} {
18 error_popup [mc "Cannot amend while merging.
19
20 You 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.
21 "]
22 return
23 }
24
25 set msg {}
26 set parents [list]
27 if {[catch {
28 set fd [git_read cat-file commit $curHEAD]
29 fconfigure $fd -encoding binary -translation lf
30 # By default commits are assumed to be in utf-8
31 set enc utf-8
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]]
37 } elseif {[regexp "author (.*)\\s<(.*)>\\s(\\d.*$)" $line all name email time]} {
38 set commit_author [list name $name email $email date $time]
39 }
40 }
41 set msg [read $fd]
42 close $fd
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]
49 } err]} {
50 error_popup [strcat [mc "Error loading commit data for amend:"] "\n\n$err"]
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
67 rescan ui_ready
68 }
69
70 set GIT_COMMITTER_IDENT {}
71
72 proc 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]} {
77 error_popup [strcat [mc "Unable to obtain your identity:"] "\n\n$err"]
78 return {}
79 }
80 if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \
81 $me me GIT_COMMITTER_IDENT]} {
82 error_popup [strcat [mc "Invalid GIT_COMMITTER_IDENT:"] "\n\n$me"]
83 return {}
84 }
85 }
86
87 return $GIT_COMMITTER_IDENT
88 }
89
90 proc 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
110 proc create_new_commit {} {
111 global commit_type ui_comm commit_author
112
113 set commit_type normal
114 unset -nocomplain commit_author
115 $ui_comm delete 0.0 end
116 $ui_comm edit reset
117 $ui_comm edit modified false
118 rescan ui_ready
119 }
120
121 proc 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
138 proc commit_tree {} {
139 global HEAD commit_type file_states ui_comm repo_config
140 global pch_error
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} {
152 info_popup [mc "Last scanned state does not match repository state.
153
154 Another Git program has modified this repository since the last scan. A rescan must be performed before another commit can be created.
155
156 The rescan will be automatically started now.
157 "]
158 unlock_index
159 rescan ui_ready
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] {
167 set s $file_states($path)
168 switch -glob -- [lindex $s 0] {
169 _? {continue}
170 A? -
171 D? -
172 T? -
173 M? {set files_ready 1}
174 _U -
175 U? {
176 error_popup [mc "Unmerged files cannot be committed.
177
178 File %s has merge conflicts. You must resolve them and stage the file before committing.
179 " [short_path $path]]
180 unlock_index
181 return
182 }
183 default {
184 error_popup [mc "Unknown file state %s detected.
185
186 File %s cannot be committed by this program.
187 " [lindex $s 0] [short_path $path]]
188 }
189 }
190 }
191 if {!$files_ready && ![string match *merge $curType] && ![is_enabled nocommit]} {
192 info_popup [mc "No changes to commit.
193
194 You must stage at least 1 file before you can commit.
195 "]
196 unlock_index
197 return
198 }
199
200 if {[is_enabled nocommitmsg]} { do_quit 0 }
201
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 {}} {
207 error_popup [mc "Please supply a commit message.
208
209 A good commit message has the following format:
210
211 - First line: Describe in one sentence what you did.
212 - Second line: Blank
213 - Remaining lines: Describe why this change is good.
214 "]
215 unlock_index
216 return
217 }
218
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
224 setup_commit_encoding $msg_wt
225 puts $msg_wt $msg
226 close $msg_wt
227
228 if {[is_enabled nocommit]} { do_quit 0 }
229
230 # -- Run the pre-commit hook.
231 #
232 set fd_ph [githook_read pre-commit]
233 if {$fd_ph eq {}} {
234 commit_commitmsg $curHEAD $msg_p
235 return
236 }
237
238 ui_status [mc "Calling pre-commit hook..."]
239 set pch_error {}
240 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
241 fileevent $fd_ph readable \
242 [list commit_prehook_wait $fd_ph $curHEAD $msg_p]
243 }
244
245 proc commit_prehook_wait {fd_ph curHEAD msg_p} {
246 global pch_error
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}]} {
252 catch {file delete $msg_p}
253 ui_status [mc "Commit declined by pre-commit hook."]
254 hook_failed_popup pre-commit $pch_error
255 unlock_index
256 } else {
257 commit_commitmsg $curHEAD $msg_p
258 }
259 set pch_error {}
260 return
261 }
262 fconfigure $fd_ph -blocking 0
263 }
264
265 proc commit_commitmsg {curHEAD msg_p} {
266 global is_detached repo_config
267 global pch_error
268
269 if {$is_detached
270 && ![file exists [gitdir rebase-merge head-name]]
271 && [is_config_true gui.warndetachedcommit]} {
272 set msg [mc "You are about to commit on a detached head.\
273 This is a potentially dangerous thing to do because if you switch\
274 to another branch you will lose your changes and it can be difficult\
275 to retrieve them later from the reflog. You should probably cancel this\
276 commit and create a new branch to continue.\n\
277 \n\
278 Do you really want to proceed with your Commit?"]
279 if {[ask_popup $msg] ne yes} {
280 unlock_index
281 return
282 }
283 }
284
285 # -- Run the commit-msg hook.
286 #
287 set fd_ph [githook_read commit-msg $msg_p]
288 if {$fd_ph eq {}} {
289 commit_writetree $curHEAD $msg_p
290 return
291 }
292
293 ui_status [mc "Calling commit-msg hook..."]
294 set pch_error {}
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
300 proc 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}
308 ui_status [mc "Commit declined by commit-msg hook."]
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
320 proc commit_writetree {curHEAD msg_p} {
321 ui_status [mc "Committing changes..."]
322 set fd_wt [git_read write-tree]
323 fileevent $fd_wt readable \
324 [list commit_committree $fd_wt $curHEAD $msg_p]
325 }
326
327 proc commit_committree {fd_wt curHEAD msg_p} {
328 global HEAD PARENT MERGE_HEAD commit_type commit_author
329 global current_branch
330 global ui_comm selected_commit_type
331 global file_states selected_paths rescan_active
332 global repo_config
333 global env
334
335 gets $fd_wt tree_id
336 if {[catch {close $fd_wt} err]} {
337 catch {file delete $msg_p}
338 error_popup [strcat [mc "write-tree failed:"] "\n\n$err"]
339 ui_status [mc "Commit failed."]
340 unlock_index
341 return
342 }
343
344 # -- Verify this wasn't an empty change.
345 #
346 if {$commit_type eq {normal}} {
347 set fd_ot [git_read cat-file commit $PARENT]
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 {
356 error [mc "Commit %s appears to be corrupt" $PARENT]
357 }
358
359 if {$tree_id eq $old_tree} {
360 catch {file delete $msg_p}
361 info_popup [mc "No changes to commit.
362
363 No files were modified by this commit and it was not a merge commit.
364
365 A rescan will be automatically started now.
366 "]
367 unlock_index
368 rescan {ui_status [mc "No changes to commit."]}
369 return
370 }
371 }
372
373 if {[info exists commit_author]} {
374 set old_author [commit_author_ident $commit_author]
375 }
376 # -- Create the commit.
377 #
378 set cmd [list commit-tree $tree_id]
379 if {[is_config_true commit.gpgsign]} {
380 lappend cmd -S
381 }
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]} {
387 catch {file delete $msg_p}
388 error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
389 ui_status [mc "Commit failed."]
390 unlock_index
391 unset -nocomplain commit_author
392 commit_author_reset $old_author
393 return
394 }
395 if {[info exists commit_author]} {
396 unset -nocomplain commit_author
397 commit_author_reset $old_author
398 }
399
400 # -- Update the HEAD ref.
401 #
402 set reflogm commit
403 if {$commit_type ne {normal}} {
404 append reflogm " ($commit_type)"
405 }
406 set msg_fd [open $msg_p r]
407 setup_commit_encoding $msg_fd 1
408 gets $msg_fd subject
409 close $msg_fd
410 append reflogm {: } $subject
411 if {[catch {
412 git update-ref -m $reflogm HEAD $cmt_id $curHEAD
413 } err]} {
414 catch {file delete $msg_p}
415 error_popup [strcat [mc "update-ref failed:"] "\n\n$err"]
416 ui_status [mc "Commit failed."]
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]}
428 catch {file delete [gitdir CHERRY_PICK_HEAD]}
429
430 # -- Let rerere do its thing.
431 #
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} {
438 catch {git rerere}
439 }
440
441 # -- Run the post-commit hook.
442 #
443 set fd_ph [githook_read post-commit]
444 if {$fd_ph ne {}} {
445 global pch_error
446 set pch_error {}
447 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
448 fileevent $fd_ph readable \
449 [list commit_postcommit_wait $fd_ph $cmt_id]
450 }
451
452 $ui_comm delete 0.0 end
453 $ui_comm edit reset
454 $ui_comm edit modified false
455 if {$::GITGUI_BCK_exists} {
456 catch {file delete [gitdir GITGUI_BCK]}
457 set ::GITGUI_BCK_exists 0
458 }
459
460 if {[is_enabled singlecommit]} { do_quit 0 }
461
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_ -
480 T_ -
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 -
490 AT -
491 TM -
492 TD -
493 MM -
494 MT -
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
508 ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
509 }
510
511 proc commit_postcommit_wait {fd_ph cmt_id} {
512 global pch_error
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 }
525
526 proc 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 }
535 proc 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 }