git-gui / lib / commit.tclon commit completion: simplify tests using test_completion_long() (a1be444)
   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        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        catch {file delete [gitdir CHERRY_PICK_HEAD]}
 413
 414        # -- Let rerere do its thing.
 415        #
 416        if {[get_config rerere.enabled] eq {}} {
 417                set rerere [file isdirectory [gitdir rr-cache]]
 418        } else {
 419                set rerere [is_config_true rerere.enabled]
 420        }
 421        if {$rerere} {
 422                catch {git rerere}
 423        }
 424
 425        # -- Run the post-commit hook.
 426        #
 427        set fd_ph [githook_read post-commit]
 428        if {$fd_ph ne {}} {
 429                global pch_error
 430                set pch_error {}
 431                fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
 432                fileevent $fd_ph readable \
 433                        [list commit_postcommit_wait $fd_ph $cmt_id]
 434        }
 435
 436        $ui_comm delete 0.0 end
 437        $ui_comm edit reset
 438        $ui_comm edit modified false
 439        if {$::GITGUI_BCK_exists} {
 440                catch {file delete [gitdir GITGUI_BCK]}
 441                set ::GITGUI_BCK_exists 0
 442        }
 443
 444        if {[is_enabled singlecommit]} { do_quit 0 }
 445
 446        # -- Update in memory status
 447        #
 448        set selected_commit_type new
 449        set commit_type normal
 450        set HEAD $cmt_id
 451        set PARENT $cmt_id
 452        set MERGE_HEAD [list]
 453
 454        foreach path [array names file_states] {
 455                set s $file_states($path)
 456                set m [lindex $s 0]
 457                switch -glob -- $m {
 458                _O -
 459                _M -
 460                _D {continue}
 461                __ -
 462                A_ -
 463                M_ -
 464                T_ -
 465                D_ {
 466                        unset file_states($path)
 467                        catch {unset selected_paths($path)}
 468                }
 469                DO {
 470                        set file_states($path) [list _O [lindex $s 1] {} {}]
 471                }
 472                AM -
 473                AD -
 474                AT -
 475                TM -
 476                TD -
 477                MM -
 478                MT -
 479                MD {
 480                        set file_states($path) [list \
 481                                _[string index $m 1] \
 482                                [lindex $s 1] \
 483                                [lindex $s 3] \
 484                                {}]
 485                }
 486                }
 487        }
 488
 489        display_all_files
 490        unlock_index
 491        reshow_diff
 492        ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
 493}
 494
 495proc commit_postcommit_wait {fd_ph cmt_id} {
 496        global pch_error
 497
 498        append pch_error [read $fd_ph]
 499        fconfigure $fd_ph -blocking 1
 500        if {[eof $fd_ph]} {
 501                if {[catch {close $fd_ph}]} {
 502                        hook_failed_popup post-commit $pch_error 0
 503                }
 504                unset pch_error
 505                return
 506        }
 507        fconfigure $fd_ph -blocking 0
 508}