git-gui / lib / commit.tclon commit Merge branch 'jc/cvsserver' into maint-2.10 (31add46)
   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 lose 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        if {[is_config_true commit.gpgsign]} {
 373                lappend cmd -S
 374        }
 375        foreach p [concat $PARENT $MERGE_HEAD] {
 376                lappend cmd -p $p
 377        }
 378        lappend cmd <$msg_p
 379        if {[catch {set cmt_id [eval git $cmd]} err]} {
 380                catch {file delete $msg_p}
 381                error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
 382                ui_status [mc "Commit failed."]
 383                unlock_index
 384                return
 385        }
 386
 387        # -- Update the HEAD ref.
 388        #
 389        set reflogm commit
 390        if {$commit_type ne {normal}} {
 391                append reflogm " ($commit_type)"
 392        }
 393        set msg_fd [open $msg_p r]
 394        setup_commit_encoding $msg_fd 1
 395        gets $msg_fd subject
 396        close $msg_fd
 397        append reflogm {: } $subject
 398        if {[catch {
 399                        git update-ref -m $reflogm HEAD $cmt_id $curHEAD
 400                } err]} {
 401                catch {file delete $msg_p}
 402                error_popup [strcat [mc "update-ref failed:"] "\n\n$err"]
 403                ui_status [mc "Commit failed."]
 404                unlock_index
 405                return
 406        }
 407
 408        # -- Cleanup after ourselves.
 409        #
 410        catch {file delete $msg_p}
 411        catch {file delete [gitdir MERGE_HEAD]}
 412        catch {file delete [gitdir MERGE_MSG]}
 413        catch {file delete [gitdir SQUASH_MSG]}
 414        catch {file delete [gitdir GITGUI_MSG]}
 415        catch {file delete [gitdir CHERRY_PICK_HEAD]}
 416
 417        # -- Let rerere do its thing.
 418        #
 419        if {[get_config rerere.enabled] eq {}} {
 420                set rerere [file isdirectory [gitdir rr-cache]]
 421        } else {
 422                set rerere [is_config_true rerere.enabled]
 423        }
 424        if {$rerere} {
 425                catch {git rerere}
 426        }
 427
 428        # -- Run the post-commit hook.
 429        #
 430        set fd_ph [githook_read post-commit]
 431        if {$fd_ph ne {}} {
 432                global pch_error
 433                set pch_error {}
 434                fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
 435                fileevent $fd_ph readable \
 436                        [list commit_postcommit_wait $fd_ph $cmt_id]
 437        }
 438
 439        $ui_comm delete 0.0 end
 440        $ui_comm edit reset
 441        $ui_comm edit modified false
 442        if {$::GITGUI_BCK_exists} {
 443                catch {file delete [gitdir GITGUI_BCK]}
 444                set ::GITGUI_BCK_exists 0
 445        }
 446
 447        if {[is_enabled singlecommit]} { do_quit 0 }
 448
 449        # -- Update in memory status
 450        #
 451        set selected_commit_type new
 452        set commit_type normal
 453        set HEAD $cmt_id
 454        set PARENT $cmt_id
 455        set MERGE_HEAD [list]
 456
 457        foreach path [array names file_states] {
 458                set s $file_states($path)
 459                set m [lindex $s 0]
 460                switch -glob -- $m {
 461                _O -
 462                _M -
 463                _D {continue}
 464                __ -
 465                A_ -
 466                M_ -
 467                T_ -
 468                D_ {
 469                        unset file_states($path)
 470                        catch {unset selected_paths($path)}
 471                }
 472                DO {
 473                        set file_states($path) [list _O [lindex $s 1] {} {}]
 474                }
 475                AM -
 476                AD -
 477                AT -
 478                TM -
 479                TD -
 480                MM -
 481                MT -
 482                MD {
 483                        set file_states($path) [list \
 484                                _[string index $m 1] \
 485                                [lindex $s 1] \
 486                                [lindex $s 3] \
 487                                {}]
 488                }
 489                }
 490        }
 491
 492        display_all_files
 493        unlock_index
 494        reshow_diff
 495        ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
 496}
 497
 498proc commit_postcommit_wait {fd_ph cmt_id} {
 499        global pch_error
 500
 501        append pch_error [read $fd_ph]
 502        fconfigure $fd_ph -blocking 1
 503        if {[eof $fd_ph]} {
 504                if {[catch {close $fd_ph}]} {
 505                        hook_failed_popup post-commit $pch_error 0
 506                }
 507                unset pch_error
 508                return
 509        }
 510        fconfigure $fd_ph -blocking 0
 511}