git-gui: avoid persisting modified author identity
[git-gui.git] / lib / commit.tcl
blob1623897e0d4da4ed4ec84036b55d69202faf3a11
1 # git-gui misc. commit reading/writing support
2 # Copyright (C) 2006, 2007 Shawn Pearce
4 proc load_last_commit {} {
5 global HEAD PARENT MERGE_HEAD commit_type ui_comm commit_author
6 global repo_config
8 if {[llength $PARENT] == 0} {
9 error_popup [mc "There is nothing to amend.
11 You are about to create the initial commit. There is no commit before this to amend.
13 return
16 repository_state curType curHEAD curMERGE_HEAD
17 if {$curType eq {merge}} {
18 error_popup [mc "Cannot amend while merging.
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.
22 return
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]
41 set msg [read $fd]
42 close $fd
44 set enc [tcl_encoding $enc]
45 if {$enc ne {}} {
46 set msg [encoding convertfrom $enc $msg]
48 set msg [string trim $msg]
49 } err]} {
50 error_popup [strcat [mc "Error loading commit data for amend:"] "\n\n$err"]
51 return
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}
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
70 set GIT_COMMITTER_IDENT {}
72 proc committer_ident {} {
73 global GIT_COMMITTER_IDENT
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 {}
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 {}
87 return $GIT_COMMITTER_IDENT
90 proc do_signoff {} {
91 global ui_comm
93 set me [committer_ident]
94 if {$me eq {}} return
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"
104 $ui_comm insert end "\n$sob"
105 $ui_comm edit separator
106 $ui_comm see end
110 proc create_new_commit {} {
111 global commit_type ui_comm commit_author
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
121 proc setup_commit_encoding {msg_wt {quiet 0}} {
122 global repo_config
124 if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
125 set enc utf-8
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]
134 fconfigure $msg_wt -encoding utf-8
138 proc commit_tree {} {
139 global HEAD commit_type file_states ui_comm repo_config
140 global pch_error
142 if {[committer_ident] eq {}} return
143 if {![lock_index update]} return
145 # -- Our in memory state should match the repository.
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.
154 Another Git program has modified this repository since the last scan. A rescan must be performed before another commit can be created.
156 The rescan will be automatically started now.
158 unlock_index
159 rescan ui_ready
160 return
163 # -- At least one file should differ in the index.
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.
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
183 default {
184 error_popup [mc "Unknown file state %s detected.
186 File %s cannot be committed by this program.
187 " [lindex $s 0] [short_path $path]]
191 if {!$files_ready && ![string match *merge $curType] && ![is_enabled nocommit]} {
192 info_popup [mc "No changes to commit.
194 You must stage at least 1 file before you can commit.
196 unlock_index
197 return
200 if {[is_enabled nocommitmsg]} { do_quit 0 }
202 # -- A message is required.
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.
209 A good commit message has the following format:
211 - First line: Describe in one sentence what you did.
212 - Second line: Blank
213 - Remaining lines: Describe why this change is good.
215 unlock_index
216 return
219 # -- Build the message file.
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
228 if {[is_enabled nocommit]} { do_quit 0 }
230 # -- Run the pre-commit hook.
232 set fd_ph [githook_read pre-commit]
233 if {$fd_ph eq {}} {
234 commit_commitmsg $curHEAD $msg_p
235 return
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]
245 proc commit_prehook_wait {fd_ph curHEAD msg_p} {
246 global pch_error
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
259 set pch_error {}
260 return
262 fconfigure $fd_ph -blocking 0
265 proc commit_commitmsg {curHEAD msg_p} {
266 global is_detached repo_config
267 global pch_error
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\
278 Do you really want to proceed with your Commit?"]
279 if {[ask_popup $msg] ne yes} {
280 unlock_index
281 return
285 # -- Run the commit-msg hook.
287 set fd_ph [githook_read commit-msg $msg_p]
288 if {$fd_ph eq {}} {
289 commit_writetree $curHEAD $msg_p
290 return
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]
300 proc commit_commitmsg_wait {fd_ph curHEAD msg_p} {
301 global pch_error
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
314 set pch_error {}
315 return
317 fconfigure $fd_ph -blocking 0
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]
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
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
344 # -- Verify this wasn't an empty change.
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
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]
359 if {$tree_id eq $old_tree} {
360 catch {file delete $msg_p}
361 info_popup [mc "No changes to commit.
363 No files were modified by this commit and it was not a merge commit.
365 A rescan will be automatically started now.
367 unlock_index
368 rescan {ui_status [mc "No changes to commit."]}
369 return
373 if {[info exists commit_author]} {
374 set old_author [commit_author_ident $commit_author]
376 # -- Create the commit.
378 set cmd [list commit-tree $tree_id]
379 foreach p [concat $PARENT $MERGE_HEAD] {
380 lappend cmd -p $p
382 lappend cmd <$msg_p
383 if {[catch {set cmt_id [eval git $cmd]} err]} {
384 catch {file delete $msg_p}
385 error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
386 ui_status [mc "Commit failed."]
387 unlock_index
388 unset -nocomplain commit_author
389 commit_author_reset $old_author
390 return
392 if {[info exists commit_author]} {
393 unset -nocomplain commit_author
394 commit_author_reset $old_author
397 # -- Update the HEAD ref.
399 set reflogm commit
400 if {$commit_type ne {normal}} {
401 append reflogm " ($commit_type)"
403 set msg_fd [open $msg_p r]
404 setup_commit_encoding $msg_fd 1
405 gets $msg_fd subject
406 close $msg_fd
407 append reflogm {: } $subject
408 if {[catch {
409 git update-ref -m $reflogm HEAD $cmt_id $curHEAD
410 } err]} {
411 catch {file delete $msg_p}
412 error_popup [strcat [mc "update-ref failed:"] "\n\n$err"]
413 ui_status [mc "Commit failed."]
414 unlock_index
415 return
418 # -- Cleanup after ourselves.
420 catch {file delete $msg_p}
421 catch {file delete [gitdir MERGE_HEAD]}
422 catch {file delete [gitdir MERGE_MSG]}
423 catch {file delete [gitdir SQUASH_MSG]}
424 catch {file delete [gitdir GITGUI_MSG]}
425 catch {file delete [gitdir CHERRY_PICK_HEAD]}
427 # -- Let rerere do its thing.
429 if {[get_config rerere.enabled] eq {}} {
430 set rerere [file isdirectory [gitdir rr-cache]]
431 } else {
432 set rerere [is_config_true rerere.enabled]
434 if {$rerere} {
435 catch {git rerere}
438 # -- Run the post-commit hook.
440 set fd_ph [githook_read post-commit]
441 if {$fd_ph ne {}} {
442 global pch_error
443 set pch_error {}
444 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
445 fileevent $fd_ph readable \
446 [list commit_postcommit_wait $fd_ph $cmt_id]
449 $ui_comm delete 0.0 end
450 $ui_comm edit reset
451 $ui_comm edit modified false
452 if {$::GITGUI_BCK_exists} {
453 catch {file delete [gitdir GITGUI_BCK]}
454 set ::GITGUI_BCK_exists 0
457 if {[is_enabled singlecommit]} { do_quit 0 }
459 # -- Update in memory status
461 set selected_commit_type new
462 set commit_type normal
463 set HEAD $cmt_id
464 set PARENT $cmt_id
465 set MERGE_HEAD [list]
467 foreach path [array names file_states] {
468 set s $file_states($path)
469 set m [lindex $s 0]
470 switch -glob -- $m {
471 _O -
472 _M -
473 _D {continue}
474 __ -
475 A_ -
476 M_ -
477 T_ -
478 D_ {
479 unset file_states($path)
480 catch {unset selected_paths($path)}
482 DO {
483 set file_states($path) [list _O [lindex $s 1] {} {}]
485 AM -
486 AD -
487 AT -
488 TM -
489 TD -
490 MM -
491 MT -
492 MD {
493 set file_states($path) [list \
494 _[string index $m 1] \
495 [lindex $s 1] \
496 [lindex $s 3] \
502 display_all_files
503 unlock_index
504 reshow_diff
505 ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
508 proc commit_postcommit_wait {fd_ph cmt_id} {
509 global pch_error
511 append pch_error [read $fd_ph]
512 fconfigure $fd_ph -blocking 1
513 if {[eof $fd_ph]} {
514 if {[catch {close $fd_ph}]} {
515 hook_failed_popup post-commit $pch_error 0
517 unset pch_error
518 return
520 fconfigure $fd_ph -blocking 0
523 proc commit_author_ident {details} {
524 global env
525 array set author $details
526 set old [array get env GIT_AUTHOR_*]
527 set env(GIT_AUTHOR_NAME) $author(name)
528 set env(GIT_AUTHOR_EMAIL) $author(email)
529 set env(GIT_AUTHOR_DATE) $author(date)
530 return $old
532 proc commit_author_reset {details} {
533 global env
534 unset env(GIT_AUTHOR_NAME) env(GIT_AUTHOR_EMAIL) env(GIT_AUTHOR_DATE)
535 if {$details ne {}} {
536 array set env $details