1# git-gui misc. commit reading/writing support
   2# Copyright (C) 2006, 2007 Shawn Pearce
   3proc load_last_commit {} {
   5        global HEAD PARENT MERGE_HEAD commit_type ui_comm
   6        global repo_config
   7        if {[llength $PARENT] == 0} {
   9                error_popup [mc "There is nothing to amend.
  10You are about to create the initial commit.  There is no commit before this to amend.
  12"]
  13                return
  14        }
  15        repository_state curType curHEAD curMERGE_HEAD
  17        if {$curType eq {merge}} {
  18                error_popup [mc "Cannot amend while merging.
  19You 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        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                                }
  38                        }
  39                        set msg [read $fd]
  40                        close $fd
  41                        set enc [tcl_encoding $enc]
  43                        if {$enc ne {}} {
  44                                set msg [encoding convertfrom $enc $msg]
  45                        }
  46                        set msg [string trim $msg]
  47                } err]} {
  48                error_popup [strcat [mc "Error loading commit data for amend:"] "\n\n$err"]
  49                return
  50        }
  51        set HEAD $curHEAD
  53        set PARENT $parents
  54        set MERGE_HEAD [list]
  55        switch -- [llength $parents] {
  56        0       {set commit_type amend-initial}
  57        1       {set commit_type amend}
  58        default {set commit_type amend-merge}
  59        }
  60        $ui_comm delete 0.0 end
  62        $ui_comm insert end $msg
  63        $ui_comm edit reset
  64        $ui_comm edit modified false
  65        rescan ui_ready
  66}
  67set GIT_COMMITTER_IDENT {}
  69proc committer_ident {} {
  71        global GIT_COMMITTER_IDENT
  72        if {$GIT_COMMITTER_IDENT eq {}} {
  74                if {[catch {set me [git var GIT_COMMITTER_IDENT]} err]} {
  75                        error_popup [strcat [mc "Unable to obtain your identity:"] "\n\n$err"]
  76                        return {}
  77                }
  78                if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \
  79                        $me me GIT_COMMITTER_IDENT]} {
  80                        error_popup [strcat [mc "Invalid GIT_COMMITTER_IDENT:"] "\n\n$me"]
  81                        return {}
  82                }
  83        }
  84        return $GIT_COMMITTER_IDENT
  86}
  87proc do_signoff {} {
  89        global ui_comm
  90        set me [committer_ident]
  92        if {$me eq {}} return
  93        set sob "Signed-off-by: $me"
  95        set last [$ui_comm get {end -1c linestart} {end -1c}]
  96        if {$last ne $sob} {
  97                $ui_comm edit separator
  98                if {$last ne {}
  99                        && ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} {
 100                        $ui_comm insert end "\n"
 101                }
 102                $ui_comm insert end "\n$sob"
 103                $ui_comm edit separator
 104                $ui_comm see end
 105        }
 106}
 107proc create_new_commit {} {
 109        global commit_type ui_comm
 110        set commit_type normal
 112        $ui_comm delete 0.0 end
 113        $ui_comm edit reset
 114        $ui_comm edit modified false
 115        rescan ui_ready
 116}
 117proc setup_commit_encoding {msg_wt {quiet 0}} {
 119        global repo_config
 120        if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
 122                set enc utf-8
 123        }
 124        set use_enc [tcl_encoding $enc]
 125        if {$use_enc ne {}} {
 126                fconfigure $msg_wt -encoding $use_enc
 127        } else {
 128                if {!$quiet} {
 129                        error_popup [mc "warning: Tcl does not support encoding '%s'." $enc]
 130                }
 131                fconfigure $msg_wt -encoding utf-8
 132        }
 133}
 134proc commit_tree {} {
 136        global HEAD commit_type file_states ui_comm repo_config
 137        global pch_error
 138        if {[committer_ident] eq {}} return
 140        if {![lock_index update]} return
 141        # -- Our in memory state should match the repository.
 143        #
 144        repository_state curType curHEAD curMERGE_HEAD
 145        if {[string match amend* $commit_type]
 146                && $curType eq {normal}
 147                && $curHEAD eq $HEAD} {
 148        } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
 149                info_popup [mc "Last scanned state does not match repository state.
 150Another Git program has modified this repository since the last scan.  A rescan must be performed before another commit can be created.
 152The rescan will be automatically started now.
 154"]
 155                unlock_index
 156                rescan ui_ready
 157                return
 158        }
 159        # -- At least one file should differ in the index.
 161        #
 162        set files_ready 0
 163        foreach path [array names file_states] {
 164                set s $file_states($path)
 165                switch -glob -- [lindex $s 0] {
 166                _? {continue}
 167                A? -
 168                D? -
 169                T? -
 170                M? {set files_ready 1}
 171                _U -
 172                U? {
 173                        error_popup [mc "Unmerged files cannot be committed.
 174File %s has merge conflicts.  You must resolve them and stage the file before committing.
 176" [short_path $path]]
 177                        unlock_index
 178                        return
 179                }
 180                default {
 181                        error_popup [mc "Unknown file state %s detected.
 182File %s cannot be committed by this program.
 184" [lindex $s 0] [short_path $path]]
 185                }
 186                }
 187        }
 188        if {!$files_ready && ![string match *merge $curType] && ![is_enabled nocommit]} {
 189                info_popup [mc "No changes to commit.
 190You must stage at least 1 file before you can commit.
 192"]
 193                unlock_index
 194                return
 195        }
 196        if {[is_enabled nocommitmsg]} { do_quit 0 }
 198        # -- A message is required.
 200        #
 201        set msg [string trim [$ui_comm get 1.0 end]]
 202        regsub -all -line {[ \t\r]+$} $msg {} msg
 203        if {$msg eq {}} {
 204                error_popup [mc "Please supply a commit message.
 205A good commit message has the following format:
 207- First line: Describe in one sentence what you did.
 209- Second line: Blank
 210- Remaining lines: Describe why this change is good.
 211"]
 212                unlock_index
 213                return
 214        }
 215        # -- Build the message file.
 217        #
 218        set msg_p [gitdir GITGUI_EDITMSG]
 219        set msg_wt [open $msg_p w]
 220        fconfigure $msg_wt -translation lf
 221        setup_commit_encoding $msg_wt
 222        puts $msg_wt $msg
 223        close $msg_wt
 224        if {[is_enabled nocommit]} { do_quit 0 }
 226        # -- Run the pre-commit hook.
 228        #
 229        set fd_ph [githook_read pre-commit]
 230        if {$fd_ph eq {}} {
 231                commit_commitmsg $curHEAD $msg_p
 232                return
 233        }
 234        ui_status [mc "Calling pre-commit hook..."]
 236        set pch_error {}
 237        fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
 238        fileevent $fd_ph readable \
 239                [list commit_prehook_wait $fd_ph $curHEAD $msg_p]
 240}
 241proc commit_prehook_wait {fd_ph curHEAD msg_p} {
 243        global pch_error
 244        append pch_error [read $fd_ph]
 246        fconfigure $fd_ph -blocking 1
 247        if {[eof $fd_ph]} {
 248                if {[catch {close $fd_ph}]} {
 249                        catch {file delete $msg_p}
 250                        ui_status [mc "Commit declined by pre-commit hook."]
 251                        hook_failed_popup pre-commit $pch_error
 252                        unlock_index
 253                } else {
 254                        commit_commitmsg $curHEAD $msg_p
 255                }
 256                set pch_error {}
 257                return
 258        }
 259        fconfigure $fd_ph -blocking 0
 260}
 261proc commit_commitmsg {curHEAD msg_p} {
 263        global is_detached repo_config
 264        global pch_error
 265        if {$is_detached
 267            && ![file exists [gitdir rebase-merge head-name]]
 268            &&  [is_config_true gui.warndetachedcommit]} {
 269                set msg [mc "You are about to commit on a detached head.\
 270This is a potentially dangerous thing to do because if you switch\
 271to another branch you will loose your changes and it can be difficult\
 272to retrieve them later from the reflog. You should probably cancel this\
 273commit and create a new branch to continue.\n\
 274\n\
 275Do you really want to proceed with your Commit?"]
 276                if {[ask_popup $msg] ne yes} {
 277                        unlock_index
 278                        return
 279                }
 280        }
 281        # -- Run the commit-msg hook.
 283        #
 284        set fd_ph [githook_read commit-msg $msg_p]
 285        if {$fd_ph eq {}} {
 286                commit_writetree $curHEAD $msg_p
 287                return
 288        }
 289        ui_status [mc "Calling commit-msg hook..."]
 291        set pch_error {}
 292        fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
 293        fileevent $fd_ph readable \
 294                [list commit_commitmsg_wait $fd_ph $curHEAD $msg_p]
 295}
 296proc commit_commitmsg_wait {fd_ph curHEAD msg_p} {
 298        global pch_error
 299        append pch_error [read $fd_ph]
 301        fconfigure $fd_ph -blocking 1
 302        if {[eof $fd_ph]} {
 303                if {[catch {close $fd_ph}]} {
 304                        catch {file delete $msg_p}
 305                        ui_status [mc "Commit declined by commit-msg hook."]
 306                        hook_failed_popup commit-msg $pch_error
 307                        unlock_index
 308                } else {
 309                        commit_writetree $curHEAD $msg_p
 310                }
 311                set pch_error {}
 312                return
 313        }
 314        fconfigure $fd_ph -blocking 0
 315}
 316proc commit_writetree {curHEAD msg_p} {
 318        ui_status [mc "Committing changes..."]
 319        set fd_wt [git_read write-tree]
 320        fileevent $fd_wt readable \
 321                [list commit_committree $fd_wt $curHEAD $msg_p]
 322}
 323proc commit_committree {fd_wt curHEAD msg_p} {
 325        global HEAD PARENT MERGE_HEAD commit_type
 326        global current_branch
 327        global ui_comm selected_commit_type
 328        global file_states selected_paths rescan_active
 329        global repo_config
 330        gets $fd_wt tree_id
 332        if {[catch {close $fd_wt} err]} {
 333                catch {file delete $msg_p}
 334                error_popup [strcat [mc "write-tree failed:"] "\n\n$err"]
 335                ui_status [mc "Commit failed."]
 336                unlock_index
 337                return
 338        }
 339        # -- Verify this wasn't an empty change.
 341        #
 342        if {$commit_type eq {normal}} {
 343                set fd_ot [git_read cat-file commit $PARENT]
 344                fconfigure $fd_ot -encoding binary -translation lf
 345                set old_tree [gets $fd_ot]
 346                close $fd_ot
 347                if {[string equal -length 5 {tree } $old_tree]
 349                        && [string length $old_tree] == 45} {
 350                        set old_tree [string range $old_tree 5 end]
 351                } else {
 352                        error [mc "Commit %s appears to be corrupt" $PARENT]
 353                }
 354                if {$tree_id eq $old_tree} {
 356                        catch {file delete $msg_p}
 357                        info_popup [mc "No changes to commit.
 358No files were modified by this commit and it was not a merge commit.
 360A rescan will be automatically started now.
 362"]
 363                        unlock_index
 364                        rescan {ui_status [mc "No changes to commit."]}
 365                        return
 366                }
 367        }
 368        # -- Create the commit.
 370        #
 371        set cmd [list commit-tree $tree_id]
 372        foreach p [concat $PARENT $MERGE_HEAD] {
 373                lappend cmd -p $p
 374        }
 375        lappend cmd <$msg_p
 376        if {[catch {set cmt_id [eval git $cmd]} err]} {
 377                catch {file delete $msg_p}
 378                error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
 379                ui_status [mc "Commit failed."]
 380                unlock_index
 381                return
 382        }
 383        # -- Update the HEAD ref.
 385        #
 386        set reflogm commit
 387        if {$commit_type ne {normal}} {
 388                append reflogm " ($commit_type)"
 389        }
 390        set msg_fd [open $msg_p r]
 391        setup_commit_encoding $msg_fd 1
 392        gets $msg_fd subject
 393        close $msg_fd
 394        append reflogm {: } $subject
 395        if {[catch {
 396                        git update-ref -m $reflogm HEAD $cmt_id $curHEAD
 397                } err]} {
 398                catch {file delete $msg_p}
 399                error_popup [strcat [mc "update-ref failed:"] "\n\n$err"]
 400                ui_status [mc "Commit failed."]
 401                unlock_index
 402                return
 403        }
 404        # -- Cleanup after ourselves.
 406        #
 407        catch {file delete $msg_p}
 408        catch {file delete [gitdir MERGE_HEAD]}
 409        catch {file delete [gitdir MERGE_MSG]}
 410        catch {file delete [gitdir SQUASH_MSG]}
 411        catch {file delete [gitdir GITGUI_MSG]}
 412        # -- Let rerere do its thing.
 414        #
 415        if {[get_config rerere.enabled] eq {}} {
 416                set rerere [file isdirectory [gitdir rr-cache]]
 417        } else {
 418                set rerere [is_config_true rerere.enabled]
 419        }
 420        if {$rerere} {
 421                catch {git rerere}
 422        }
 423        # -- Run the post-commit hook.
 425        #
 426        set fd_ph [githook_read post-commit]
 427        if {$fd_ph ne {}} {
 428                global pch_error
 429                set pch_error {}
 430                fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
 431                fileevent $fd_ph readable \
 432                        [list commit_postcommit_wait $fd_ph $cmt_id]
 433        }
 434        $ui_comm delete 0.0 end
 436        $ui_comm edit reset
 437        $ui_comm edit modified false
 438        if {$::GITGUI_BCK_exists} {
 439                catch {file delete [gitdir GITGUI_BCK]}
 440                set ::GITGUI_BCK_exists 0
 441        }
 442        if {[is_enabled singlecommit]} { do_quit 0 }
 444        # -- Update in memory status
 446        #
 447        set selected_commit_type new
 448        set commit_type normal
 449        set HEAD $cmt_id
 450        set PARENT $cmt_id
 451        set MERGE_HEAD [list]
 452        foreach path [array names file_states] {
 454                set s $file_states($path)
 455                set m [lindex $s 0]
 456                switch -glob -- $m {
 457                _O -
 458                _M -
 459                _D {continue}
 460                __ -
 461                A_ -
 462                M_ -
 463                T_ -
 464                D_ {
 465                        unset file_states($path)
 466                        catch {unset selected_paths($path)}
 467                }
 468                DO {
 469                        set file_states($path) [list _O [lindex $s 1] {} {}]
 470                }
 471                AM -
 472                AD -
 473                AT -
 474                TM -
 475                TD -
 476                MM -
 477                MT -
 478                MD {
 479                        set file_states($path) [list \
 480                                _[string index $m 1] \
 481                                [lindex $s 1] \
 482                                [lindex $s 3] \
 483                                {}]
 484                }
 485                }
 486        }
 487        display_all_files
 489        unlock_index
 490        reshow_diff
 491        ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
 492}
 493proc commit_postcommit_wait {fd_ph cmt_id} {
 495        global pch_error
 496        append pch_error [read $fd_ph]
 498        fconfigure $fd_ph -blocking 1
 499        if {[eof $fd_ph]} {
 500                if {[catch {close $fd_ph}]} {
 501                        hook_failed_popup post-commit $pch_error 0
 502                }
 503                unset pch_error
 504                return
 505        }
 506        fconfigure $fd_ph -blocking 0
 507}