git-gui / lib / commit.tclon commit Merge branch 'ef/checkout-empty' (a735b79)
   1# git-gui misc. commit reading/writing support
   2# Copyright (C) 2006, 2007 Shawn Pearce
   3
   4proc load_last_commit {} {
   5        global HEAD PARENT MERGE_HEAD commit_type ui_comm
   6        global repo_config
   7
   8        if {[llength $PARENT] == 0} {
   9                error_popup [mc "There is nothing to amend.
  10
  11You 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
  20You 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                                }
  38                        }
  39                        set msg [read $fd]
  40                        close $fd
  41
  42                        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
  52        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
  61        $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}
  67
  68set GIT_COMMITTER_IDENT {}
  69
  70proc committer_ident {} {
  71        global GIT_COMMITTER_IDENT
  72
  73        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
  85        return $GIT_COMMITTER_IDENT
  86}
  87
  88proc do_signoff {} {
  89        global ui_comm
  90
  91        set me [committer_ident]
  92        if {$me eq {}} return
  93
  94        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}
 107
 108proc create_new_commit {} {
 109        global commit_type ui_comm
 110
 111        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}
 117
 118proc setup_commit_encoding {msg_wt {quiet 0}} {
 119        global repo_config
 120
 121        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}
 134
 135proc commit_tree {} {
 136        global HEAD commit_type file_states ui_comm repo_config
 137        global pch_error
 138
 139        if {[committer_ident] eq {}} return
 140        if {![lock_index update]} return
 141
 142        # -- 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.
 150
 151Another Git program has modified this repository since the last scan.  A rescan must be performed before another commit can be created.
 152
 153The rescan will be automatically started now.
 154"]
 155                unlock_index
 156                rescan ui_ready
 157                return
 158        }
 159
 160        # -- 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.
 174
 175File %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.
 182
 183File %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.
 190
 191You must stage at least 1 file before you can commit.
 192"]
 193                unlock_index
 194                return
 195        }
 196
 197        if {[is_enabled nocommitmsg]} { do_quit 0 }
 198
 199        # -- 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.
 205
 206A good commit message has the following format:
 207
 208- 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
 216        # -- 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
 225        if {[is_enabled nocommit]} { do_quit 0 }
 226
 227        # -- 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
 235        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}
 241
 242proc commit_prehook_wait {fd_ph curHEAD msg_p} {
 243        global pch_error
 244
 245        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}
 261
 262proc commit_commitmsg {curHEAD msg_p} {
 263        global is_detached repo_config
 264        global pch_error
 265
 266        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
 282        # -- 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
 290        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}
 296
 297proc commit_commitmsg_wait {fd_ph curHEAD msg_p} {
 298        global pch_error
 299
 300        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}
 316
 317proc 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}
 323
 324proc 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
 331        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
 340        # -- 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
 348                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
 355                if {$tree_id eq $old_tree} {
 356                        catch {file delete $msg_p}
 357                        info_popup [mc "No changes to commit.
 358
 359No files were modified by this commit and it was not a merge commit.
 360
 361A rescan will be automatically started now.
 362"]
 363                        unlock_index
 364                        rescan {ui_status [mc "No changes to commit."]}
 365                        return
 366                }
 367        }
 368
 369        # -- 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
 384        # -- 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
 405        # -- 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
 413        # -- 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
 424        # -- 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
 435        $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
 443        if {[is_enabled singlecommit]} { do_quit 0 }
 444
 445        # -- 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
 453        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
 488        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}
 493
 494proc commit_postcommit_wait {fd_ph cmt_id} {
 495        global pch_error
 496
 497        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}