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
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.
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.
30 set fd [git_read cat-file commit $curHEAD]
31 fconfigure $fd -encoding binary -translation lf
32 # By default commits are assumed to be in utf-8
34 while {[gets $fd line] > 0} {
35 if {[string match {parent *} $line]} {
36 lappend parents [string range $line 7 end]
37 } elseif {[string match {encoding *} $line]} {
38 set enc [string tolower [string range $line 9 end]]
39 } elseif {[regexp "author (.*)\\s<(.*)>\\s(\\d.*$)" $line all name email time]} { }
44 set enc [tcl_encoding $enc]
46 set msg [encoding convertfrom $enc $msg]
47 set name [encoding convertfrom $enc $name]
48 set email [encoding convertfrom $enc $email]
50 if {$name ne {} && $email ne {}} {
51 set commit_author [list name $name email $email date $time]
54 set msg [string trim $msg]
56 error_popup [strcat [mc "Error loading commit data for amend:"] "\n\n$err"]
63 switch -- [llength $parents] {
64 0 {set commit_type amend-initial}
65 1 {set commit_type amend}
66 default {set commit_type amend-merge}
69 $ui_comm delete 0.0 end
70 $ui_comm insert end $msg
72 $ui_comm edit modified false
76 set GIT_COMMITTER_IDENT {}
78 proc committer_ident {} {
79 global GIT_COMMITTER_IDENT
81 if {$GIT_COMMITTER_IDENT eq {}} {
82 if {[catch {set me [git var GIT_COMMITTER_IDENT]} err]} {
83 error_popup [strcat [mc "Unable to obtain your identity:"] "\n\n$err"]
86 if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \
87 $me me GIT_COMMITTER_IDENT]} {
88 error_popup [strcat [mc "Invalid GIT_COMMITTER_IDENT:"] "\n\n$me"]
93 return $GIT_COMMITTER_IDENT
99 set me [committer_ident]
100 if {$me eq {}} return
102 set sob "Signed-off-by: $me"
103 set last [$ui_comm get {end -1c linestart} {end -1c}]
105 $ui_comm edit separator
107 && ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} {
108 $ui_comm insert end "\n"
110 $ui_comm insert end "\n$sob"
111 $ui_comm edit separator
116 proc create_new_commit {} {
117 global commit_type ui_comm commit_author
119 set commit_type normal
120 unset -nocomplain commit_author
121 $ui_comm delete 0.0 end
123 $ui_comm edit modified false
127 proc setup_commit_encoding {msg_wt {quiet 0}} {
130 if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
133 set use_enc [tcl_encoding $enc]
134 if {$use_enc ne {}} {
135 fconfigure $msg_wt -encoding $use_enc
138 error_popup [mc "warning: Tcl does not support encoding '%s'." $enc]
140 fconfigure $msg_wt -encoding utf-8
144 proc strip_msg {msg} {
145 set cmd [concat [list | ] [_git_cmd stripspace] --strip-comments]
147 set fd [open $cmd r+]
148 fconfigure $fd -translation binary -encoding utf-8
150 puts -nonewline $fd $msg
152 set result [read $fd]
158 proc commit_tree {} {
159 global HEAD commit_type file_states ui_comm repo_config
162 if {[committer_ident] eq {}} return
163 if {![lock_index update]} return
165 # -- Our in memory state should match the repository.
167 repository_state curType curHEAD curMERGE_HEAD
168 if {[string match amend* $commit_type]
169 && $curType eq {normal}
170 && $curHEAD eq $HEAD} {
171 } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
172 info_popup [mc "Last scanned state does not match repository state.
174 Another Git program has modified this repository since the last scan. A rescan must be performed before another commit can be created.
176 The rescan will be automatically started now.
183 # -- At least one file should differ in the index.
186 foreach path [array names file_states] {
187 set s $file_states($path)
188 switch -glob -- [lindex $s 0] {
193 M? {set files_ready 1}
196 error_popup [mc "Unmerged files cannot be committed.
198 File %s has merge conflicts. You must resolve them and stage the file before committing.
199 " [short_path $path]]
204 error_popup [mc "Unknown file state %s detected.
206 File %s cannot be committed by this program.
207 " [lindex $s 0] [short_path $path]]
211 if {!$files_ready && ![string match *merge $curType] && ![is_enabled nocommit]} {
212 info_popup [mc "No changes to commit.
214 You must stage at least 1 file before you can commit.
220 if {[is_enabled nocommitmsg]} { do_quit 0 }
222 # -- A message is required.
224 set msg [strip_msg [$ui_comm get 1.0 end]]
227 error_popup [mc "Please supply a commit message.
229 A good commit message has the following format:
231 - First line: Describe in one sentence what you did.
233 - Remaining lines: Describe why this change is good.
239 # -- Build the message file.
241 set msg_p [gitdir GITGUI_EDITMSG]
242 set msg_wt [open $msg_p w]
243 fconfigure $msg_wt -translation lf
244 setup_commit_encoding $msg_wt
248 if {[is_enabled nocommit]} { do_quit 0 }
250 # -- Run the pre-commit hook.
252 set fd_ph [githook_read pre-commit]
254 commit_commitmsg $curHEAD $msg_p
258 ui_status [mc "Calling pre-commit hook..."]
260 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
261 fileevent $fd_ph readable \
262 [list commit_prehook_wait $fd_ph $curHEAD $msg_p]
265 proc commit_prehook_wait {fd_ph curHEAD msg_p} {
268 append pch_error [read $fd_ph]
269 fconfigure $fd_ph -blocking 1
271 if {[catch {close $fd_ph}]} {
272 catch {file delete $msg_p}
273 ui_status [mc "Commit declined by pre-commit hook."]
274 hook_failed_popup pre-commit $pch_error
277 commit_commitmsg $curHEAD $msg_p
282 fconfigure $fd_ph -blocking 0
285 proc commit_commitmsg {curHEAD msg_p} {
286 global is_detached repo_config
290 && ![file exists [gitdir rebase-merge head-name]]
291 && [is_config_true gui.warndetachedcommit]} {
292 set msg [mc "You are about to commit on a detached head.\
293 This is a potentially dangerous thing to do because if you switch\
294 to another branch you will lose your changes and it can be difficult\
295 to retrieve them later from the reflog. You should probably cancel this\
296 commit and create a new branch to continue.\n\
298 Do you really want to proceed with your Commit?"]
299 if {[ask_popup $msg] ne yes} {
305 # -- Run the commit-msg hook.
307 set fd_ph [githook_read commit-msg $msg_p]
309 commit_writetree $curHEAD $msg_p
313 ui_status [mc "Calling commit-msg hook..."]
315 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
316 fileevent $fd_ph readable \
317 [list commit_commitmsg_wait $fd_ph $curHEAD $msg_p]
320 proc commit_commitmsg_wait {fd_ph curHEAD msg_p} {
323 append pch_error [read $fd_ph]
324 fconfigure $fd_ph -blocking 1
326 if {[catch {close $fd_ph}]} {
327 catch {file delete $msg_p}
328 ui_status [mc "Commit declined by commit-msg hook."]
329 hook_failed_popup commit-msg $pch_error
332 commit_writetree $curHEAD $msg_p
337 fconfigure $fd_ph -blocking 0
340 proc commit_writetree {curHEAD msg_p} {
341 ui_status [mc "Committing changes..."]
342 set fd_wt [git_read write-tree]
343 fileevent $fd_wt readable \
344 [list commit_committree $fd_wt $curHEAD $msg_p]
347 proc commit_committree {fd_wt curHEAD msg_p} {
348 global HEAD PARENT MERGE_HEAD commit_type commit_author
349 global current_branch
350 global ui_comm commit_type_is_amend
351 global file_states selected_paths rescan_active
356 if {[catch {close $fd_wt} err]} {
357 catch {file delete $msg_p}
358 error_popup [strcat [mc "write-tree failed:"] "\n\n$err"]
359 ui_status [mc "Commit failed."]
364 # -- Verify this wasn't an empty change.
366 if {$commit_type eq {normal}} {
367 set fd_ot [git_read cat-file commit $PARENT]
368 fconfigure $fd_ot -encoding binary -translation lf
369 set old_tree [gets $fd_ot]
372 if {[string equal -length 5 {tree } $old_tree]
373 && [string length $old_tree] == 45} {
374 set old_tree [string range $old_tree 5 end]
376 error [mc "Commit %s appears to be corrupt" $PARENT]
379 if {$tree_id eq $old_tree} {
380 catch {file delete $msg_p}
381 info_popup [mc "No changes to commit.
383 No files were modified by this commit and it was not a merge commit.
385 A rescan will be automatically started now.
388 rescan {ui_status [mc "No changes to commit."]}
393 if {[info exists commit_author]} {
394 set old_author [commit_author_ident $commit_author]
396 # -- Create the commit.
398 set cmd [list commit-tree $tree_id]
399 if {[is_config_true commit.gpgsign]} {
402 foreach p [concat $PARENT $MERGE_HEAD] {
406 if {[catch {set cmt_id [eval git $cmd]} err]} {
407 catch {file delete $msg_p}
408 error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
409 ui_status [mc "Commit failed."]
411 unset -nocomplain commit_author
412 commit_author_reset $old_author
415 if {[info exists commit_author]} {
416 unset -nocomplain commit_author
417 commit_author_reset $old_author
420 # -- Update the HEAD ref.
423 if {$commit_type ne {normal}} {
424 append reflogm " ($commit_type)"
426 set msg_fd [open $msg_p r]
427 setup_commit_encoding $msg_fd 1
430 append reflogm {: } $subject
432 git update-ref -m $reflogm HEAD $cmt_id $curHEAD
434 catch {file delete $msg_p}
435 error_popup [strcat [mc "update-ref failed:"] "\n\n$err"]
436 ui_status [mc "Commit failed."]
441 # -- Cleanup after ourselves.
443 catch {file delete $msg_p}
444 catch {file delete [gitdir MERGE_HEAD]}
445 catch {file delete [gitdir MERGE_MSG]}
446 catch {file delete [gitdir SQUASH_MSG]}
447 catch {file delete [gitdir GITGUI_MSG]}
448 catch {file delete [gitdir CHERRY_PICK_HEAD]}
450 # -- Let rerere do its thing.
452 if {[get_config rerere.enabled] eq {}} {
453 set rerere [file isdirectory [gitdir rr-cache]]
455 set rerere [is_config_true rerere.enabled]
461 # -- Run the post-commit hook.
463 set fd_ph [githook_read post-commit]
467 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
468 fileevent $fd_ph readable \
469 [list commit_postcommit_wait $fd_ph $cmt_id]
472 $ui_comm delete 0.0 end
473 load_message [get_config commit.template]
475 $ui_comm edit modified false
476 if {$::GITGUI_BCK_exists} {
477 catch {file delete [gitdir GITGUI_BCK]}
478 set ::GITGUI_BCK_exists 0
481 if {[is_enabled singlecommit]} { do_quit 0 }
483 # -- Update in memory status
485 set commit_type normal
486 set commit_type_is_amend 0
489 set MERGE_HEAD [list]
491 foreach path [array names file_states] {
492 set s $file_states($path)
503 unset file_states($path)
504 catch {unset selected_paths($path)}
507 set file_states($path) [list _O [lindex $s 1] {} {}]
517 set file_states($path) [list \
518 _[string index $m 1] \
529 ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
532 proc commit_postcommit_wait {fd_ph cmt_id} {
535 append pch_error [read $fd_ph]
536 fconfigure $fd_ph -blocking 1
538 if {[catch {close $fd_ph}]} {
539 hook_failed_popup post-commit $pch_error 0
544 fconfigure $fd_ph -blocking 0
547 proc commit_author_ident {details} {
549 array set author $details
550 set old [array get env GIT_AUTHOR_*]
551 set env(GIT_AUTHOR_NAME) $author(name)
552 set env(GIT_AUTHOR_EMAIL) $author(email)
553 set env(GIT_AUTHOR_DATE) $author(date)
556 proc commit_author_reset {details} {
558 unset env(GIT_AUTHOR_NAME) env(GIT_AUTHOR_EMAIL) env(GIT_AUTHOR_DATE)
559 if {$details ne {}} {
560 array set env $details