diff options
Diffstat (limited to 'git-gui/lib/commit.tcl')
-rw-r--r-- | git-gui/lib/commit.tcl | 547 |
1 files changed, 547 insertions, 0 deletions
diff --git a/git-gui/lib/commit.tcl b/git-gui/lib/commit.tcl new file mode 100644 index 000000000000..75ea965dacd7 --- /dev/null +++ b/git-gui/lib/commit.tcl @@ -0,0 +1,547 @@ +# git-gui misc. commit reading/writing support +# Copyright (C) 2006, 2007 Shawn Pearce + +proc load_last_commit {} { + global HEAD PARENT MERGE_HEAD commit_type ui_comm commit_author + global repo_config + + if {[llength $PARENT] == 0} { + error_popup [mc "There is nothing to amend. + +You are about to create the initial commit. There is no commit before this to amend. +"] + return + } + + repository_state curType curHEAD curMERGE_HEAD + if {$curType eq {merge}} { + error_popup [mc "Cannot amend while merging. + +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. +"] + return + } + + set msg {} + set parents [list] + if {[catch { + set name "" + set email "" + set fd [git_read cat-file commit $curHEAD] + fconfigure $fd -encoding binary -translation lf + # By default commits are assumed to be in utf-8 + set enc utf-8 + while {[gets $fd line] > 0} { + if {[string match {parent *} $line]} { + lappend parents [string range $line 7 end] + } elseif {[string match {encoding *} $line]} { + set enc [string tolower [string range $line 9 end]] + } elseif {[regexp "author (.*)\\s<(.*)>\\s(\\d.*$)" $line all name email time]} { } + } + set msg [read $fd] + close $fd + + set enc [tcl_encoding $enc] + if {$enc ne {}} { + set msg [encoding convertfrom $enc $msg] + set name [encoding convertfrom $enc $name] + set email [encoding convertfrom $enc $email] + } + if {$name ne {} && $email ne {}} { + set commit_author [list name $name email $email date $time] + } + + set msg [string trim $msg] + } err]} { + error_popup [strcat [mc "Error loading commit data for amend:"] "\n\n$err"] + return + } + + set HEAD $curHEAD + set PARENT $parents + set MERGE_HEAD [list] + switch -- [llength $parents] { + 0 {set commit_type amend-initial} + 1 {set commit_type amend} + default {set commit_type amend-merge} + } + + $ui_comm delete 0.0 end + $ui_comm insert end $msg + $ui_comm edit reset + $ui_comm edit modified false + rescan ui_ready +} + +set GIT_COMMITTER_IDENT {} + +proc committer_ident {} { + global GIT_COMMITTER_IDENT + + if {$GIT_COMMITTER_IDENT eq {}} { + if {[catch {set me [git var GIT_COMMITTER_IDENT]} err]} { + error_popup [strcat [mc "Unable to obtain your identity:"] "\n\n$err"] + return {} + } + if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \ + $me me GIT_COMMITTER_IDENT]} { + error_popup [strcat [mc "Invalid GIT_COMMITTER_IDENT:"] "\n\n$me"] + return {} + } + } + + return $GIT_COMMITTER_IDENT +} + +proc do_signoff {} { + global ui_comm + + set me [committer_ident] + if {$me eq {}} return + + set sob "Signed-off-by: $me" + set last [$ui_comm get {end -1c linestart} {end -1c}] + if {$last ne $sob} { + $ui_comm edit separator + if {$last ne {} + && ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} { + $ui_comm insert end "\n" + } + $ui_comm insert end "\n$sob" + $ui_comm edit separator + $ui_comm see end + } +} + +proc create_new_commit {} { + global commit_type ui_comm commit_author + + set commit_type normal + unset -nocomplain commit_author + $ui_comm delete 0.0 end + $ui_comm edit reset + $ui_comm edit modified false + rescan ui_ready +} + +proc setup_commit_encoding {msg_wt {quiet 0}} { + global repo_config + + if {[catch {set enc $repo_config(i18n.commitencoding)}]} { + set enc utf-8 + } + set use_enc [tcl_encoding $enc] + if {$use_enc ne {}} { + fconfigure $msg_wt -encoding $use_enc + } else { + if {!$quiet} { + error_popup [mc "warning: Tcl does not support encoding '%s'." $enc] + } + fconfigure $msg_wt -encoding utf-8 + } +} + +proc commit_tree {} { + global HEAD commit_type file_states ui_comm repo_config + global pch_error + + if {[committer_ident] eq {}} return + if {![lock_index update]} return + + # -- Our in memory state should match the repository. + # + repository_state curType curHEAD curMERGE_HEAD + if {[string match amend* $commit_type] + && $curType eq {normal} + && $curHEAD eq $HEAD} { + } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} { + info_popup [mc "Last scanned state does not match repository state. + +Another Git program has modified this repository since the last scan. A rescan must be performed before another commit can be created. + +The rescan will be automatically started now. +"] + unlock_index + rescan ui_ready + return + } + + # -- At least one file should differ in the index. + # + set files_ready 0 + foreach path [array names file_states] { + set s $file_states($path) + switch -glob -- [lindex $s 0] { + _? {continue} + A? - + D? - + T? - + M? {set files_ready 1} + _U - + U? { + error_popup [mc "Unmerged files cannot be committed. + +File %s has merge conflicts. You must resolve them and stage the file before committing. +" [short_path $path]] + unlock_index + return + } + default { + error_popup [mc "Unknown file state %s detected. + +File %s cannot be committed by this program. +" [lindex $s 0] [short_path $path]] + } + } + } + if {!$files_ready && ![string match *merge $curType] && ![is_enabled nocommit]} { + info_popup [mc "No changes to commit. + +You must stage at least 1 file before you can commit. +"] + unlock_index + return + } + + if {[is_enabled nocommitmsg]} { do_quit 0 } + + # -- A message is required. + # + set msg [string trim [$ui_comm get 1.0 end]] + regsub -all -line {[ \t\r]+$} $msg {} msg + if {$msg eq {}} { + error_popup [mc "Please supply a commit message. + +A good commit message has the following format: + +- First line: Describe in one sentence what you did. +- Second line: Blank +- Remaining lines: Describe why this change is good. +"] + unlock_index + return + } + + # -- Build the message file. + # + set msg_p [gitdir GITGUI_EDITMSG] + set msg_wt [open $msg_p w] + fconfigure $msg_wt -translation lf + setup_commit_encoding $msg_wt + puts $msg_wt $msg + close $msg_wt + + if {[is_enabled nocommit]} { do_quit 0 } + + # -- Run the pre-commit hook. + # + set fd_ph [githook_read pre-commit] + if {$fd_ph eq {}} { + commit_commitmsg $curHEAD $msg_p + return + } + + ui_status [mc "Calling pre-commit hook..."] + set pch_error {} + fconfigure $fd_ph -blocking 0 -translation binary -eofchar {} + fileevent $fd_ph readable \ + [list commit_prehook_wait $fd_ph $curHEAD $msg_p] +} + +proc commit_prehook_wait {fd_ph curHEAD msg_p} { + global pch_error + + append pch_error [read $fd_ph] + fconfigure $fd_ph -blocking 1 + if {[eof $fd_ph]} { + if {[catch {close $fd_ph}]} { + catch {file delete $msg_p} + ui_status [mc "Commit declined by pre-commit hook."] + hook_failed_popup pre-commit $pch_error + unlock_index + } else { + commit_commitmsg $curHEAD $msg_p + } + set pch_error {} + return + } + fconfigure $fd_ph -blocking 0 +} + +proc commit_commitmsg {curHEAD msg_p} { + global is_detached repo_config + global pch_error + + if {$is_detached + && ![file exists [gitdir rebase-merge head-name]] + && [is_config_true gui.warndetachedcommit]} { + set msg [mc "You are about to commit on a detached head.\ +This is a potentially dangerous thing to do because if you switch\ +to another branch you will lose your changes and it can be difficult\ +to retrieve them later from the reflog. You should probably cancel this\ +commit and create a new branch to continue.\n\ +\n\ +Do you really want to proceed with your Commit?"] + if {[ask_popup $msg] ne yes} { + unlock_index + return + } + } + + # -- Run the commit-msg hook. + # + set fd_ph [githook_read commit-msg $msg_p] + if {$fd_ph eq {}} { + commit_writetree $curHEAD $msg_p + return + } + + ui_status [mc "Calling commit-msg hook..."] + set pch_error {} + fconfigure $fd_ph -blocking 0 -translation binary -eofchar {} + fileevent $fd_ph readable \ + [list commit_commitmsg_wait $fd_ph $curHEAD $msg_p] +} + +proc commit_commitmsg_wait {fd_ph curHEAD msg_p} { + global pch_error + + append pch_error [read $fd_ph] + fconfigure $fd_ph -blocking 1 + if {[eof $fd_ph]} { + if {[catch {close $fd_ph}]} { + catch {file delete $msg_p} + ui_status [mc "Commit declined by commit-msg hook."] + hook_failed_popup commit-msg $pch_error + unlock_index + } else { + commit_writetree $curHEAD $msg_p + } + set pch_error {} + return + } + fconfigure $fd_ph -blocking 0 +} + +proc commit_writetree {curHEAD msg_p} { + ui_status [mc "Committing changes..."] + set fd_wt [git_read write-tree] + fileevent $fd_wt readable \ + [list commit_committree $fd_wt $curHEAD $msg_p] +} + +proc commit_committree {fd_wt curHEAD msg_p} { + global HEAD PARENT MERGE_HEAD commit_type commit_author + global current_branch + global ui_comm selected_commit_type + global file_states selected_paths rescan_active + global repo_config + global env + + gets $fd_wt tree_id + if {[catch {close $fd_wt} err]} { + catch {file delete $msg_p} + error_popup [strcat [mc "write-tree failed:"] "\n\n$err"] + ui_status [mc "Commit failed."] + unlock_index + return + } + + # -- Verify this wasn't an empty change. + # + if {$commit_type eq {normal}} { + set fd_ot [git_read cat-file commit $PARENT] + fconfigure $fd_ot -encoding binary -translation lf + set old_tree [gets $fd_ot] + close $fd_ot + + if {[string equal -length 5 {tree } $old_tree] + && [string length $old_tree] == 45} { + set old_tree [string range $old_tree 5 end] + } else { + error [mc "Commit %s appears to be corrupt" $PARENT] + } + + if {$tree_id eq $old_tree} { + catch {file delete $msg_p} + info_popup [mc "No changes to commit. + +No files were modified by this commit and it was not a merge commit. + +A rescan will be automatically started now. +"] + unlock_index + rescan {ui_status [mc "No changes to commit."]} + return + } + } + + if {[info exists commit_author]} { + set old_author [commit_author_ident $commit_author] + } + # -- Create the commit. + # + set cmd [list commit-tree $tree_id] + if {[is_config_true commit.gpgsign]} { + lappend cmd -S + } + foreach p [concat $PARENT $MERGE_HEAD] { + lappend cmd -p $p + } + lappend cmd <$msg_p + if {[catch {set cmt_id [eval git $cmd]} err]} { + catch {file delete $msg_p} + error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"] + ui_status [mc "Commit failed."] + unlock_index + unset -nocomplain commit_author + commit_author_reset $old_author + return + } + if {[info exists commit_author]} { + unset -nocomplain commit_author + commit_author_reset $old_author + } + + # -- Update the HEAD ref. + # + set reflogm commit + if {$commit_type ne {normal}} { + append reflogm " ($commit_type)" + } + set msg_fd [open $msg_p r] + setup_commit_encoding $msg_fd 1 + gets $msg_fd subject + close $msg_fd + append reflogm {: } $subject + if {[catch { + git update-ref -m $reflogm HEAD $cmt_id $curHEAD + } err]} { + catch {file delete $msg_p} + error_popup [strcat [mc "update-ref failed:"] "\n\n$err"] + ui_status [mc "Commit failed."] + unlock_index + return + } + + # -- Cleanup after ourselves. + # + catch {file delete $msg_p} + catch {file delete [gitdir MERGE_HEAD]} + catch {file delete [gitdir MERGE_MSG]} + catch {file delete [gitdir SQUASH_MSG]} + catch {file delete [gitdir GITGUI_MSG]} + catch {file delete [gitdir CHERRY_PICK_HEAD]} + + # -- Let rerere do its thing. + # + if {[get_config rerere.enabled] eq {}} { + set rerere [file isdirectory [gitdir rr-cache]] + } else { + set rerere [is_config_true rerere.enabled] + } + if {$rerere} { + catch {git rerere} + } + + # -- Run the post-commit hook. + # + set fd_ph [githook_read post-commit] + if {$fd_ph ne {}} { + global pch_error + set pch_error {} + fconfigure $fd_ph -blocking 0 -translation binary -eofchar {} + fileevent $fd_ph readable \ + [list commit_postcommit_wait $fd_ph $cmt_id] + } + + $ui_comm delete 0.0 end + $ui_comm edit reset + $ui_comm edit modified false + if {$::GITGUI_BCK_exists} { + catch {file delete [gitdir GITGUI_BCK]} + set ::GITGUI_BCK_exists 0 + } + + if {[is_enabled singlecommit]} { do_quit 0 } + + # -- Update in memory status + # + set selected_commit_type new + set commit_type normal + set HEAD $cmt_id + set PARENT $cmt_id + set MERGE_HEAD [list] + + foreach path [array names file_states] { + set s $file_states($path) + set m [lindex $s 0] + switch -glob -- $m { + _O - + _M - + _D {continue} + __ - + A_ - + M_ - + T_ - + D_ { + unset file_states($path) + catch {unset selected_paths($path)} + } + DO { + set file_states($path) [list _O [lindex $s 1] {} {}] + } + AM - + AD - + AT - + TM - + TD - + MM - + MT - + MD { + set file_states($path) [list \ + _[string index $m 1] \ + [lindex $s 1] \ + [lindex $s 3] \ + {}] + } + } + } + + display_all_files + unlock_index + reshow_diff + ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject] +} + +proc commit_postcommit_wait {fd_ph cmt_id} { + global pch_error + + append pch_error [read $fd_ph] + fconfigure $fd_ph -blocking 1 + if {[eof $fd_ph]} { + if {[catch {close $fd_ph}]} { + hook_failed_popup post-commit $pch_error 0 + } + unset pch_error + return + } + fconfigure $fd_ph -blocking 0 +} + +proc commit_author_ident {details} { + global env + array set author $details + set old [array get env GIT_AUTHOR_*] + set env(GIT_AUTHOR_NAME) $author(name) + set env(GIT_AUTHOR_EMAIL) $author(email) + set env(GIT_AUTHOR_DATE) $author(date) + return $old +} +proc commit_author_reset {details} { + global env + unset env(GIT_AUTHOR_NAME) env(GIT_AUTHOR_EMAIL) env(GIT_AUTHOR_DATE) + if {$details ne {}} { + array set env $details + } +} |