lib / commit.tclon commit git-gui: deal with unknown files when pressing the "Stage Changed" button (856c2d7)
   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 && $repo_config(gui.warndetachedcommit)} {
 267                set msg [mc "You are about to commit on a detached head.\
 268This is a potentially dangerous thing to do because if you switch\
 269to another branch you will loose your changes and it can be difficult\
 270to retrieve them later from the reflog. You should probably cancel this\
 271commit and create a new branch to continue.\n\
 272\n\
 273Do you really want to proceed with your Commit?"]
 274                if {[ask_popup $msg] ne yes} {
 275                        unlock_index
 276                        return
 277                }
 278        }
 279
 280        # -- Run the commit-msg hook.
 281        #
 282        set fd_ph [githook_read commit-msg $msg_p]
 283        if {$fd_ph eq {}} {
 284                commit_writetree $curHEAD $msg_p
 285                return
 286        }
 287
 288        ui_status [mc "Calling commit-msg hook..."]
 289        set pch_error {}
 290        fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
 291        fileevent $fd_ph readable \
 292                [list commit_commitmsg_wait $fd_ph $curHEAD $msg_p]
 293}
 294
 295proc commit_commitmsg_wait {fd_ph curHEAD msg_p} {
 296        global pch_error
 297
 298        append pch_error [read $fd_ph]
 299        fconfigure $fd_ph -blocking 1
 300        if {[eof $fd_ph]} {
 301                if {[catch {close $fd_ph}]} {
 302                        catch {file delete $msg_p}
 303                        ui_status [mc "Commit declined by commit-msg hook."]
 304                        hook_failed_popup commit-msg $pch_error
 305                        unlock_index
 306                } else {
 307                        commit_writetree $curHEAD $msg_p
 308                }
 309                set pch_error {}
 310                return
 311        }
 312        fconfigure $fd_ph -blocking 0
 313}
 314
 315proc commit_writetree {curHEAD msg_p} {
 316        ui_status [mc "Committing changes..."]
 317        set fd_wt [git_read write-tree]
 318        fileevent $fd_wt readable \
 319                [list commit_committree $fd_wt $curHEAD $msg_p]
 320}
 321
 322proc commit_committree {fd_wt curHEAD msg_p} {
 323        global HEAD PARENT MERGE_HEAD commit_type
 324        global current_branch
 325        global ui_comm selected_commit_type
 326        global file_states selected_paths rescan_active
 327        global repo_config
 328
 329        gets $fd_wt tree_id
 330        if {[catch {close $fd_wt} err]} {
 331                catch {file delete $msg_p}
 332                error_popup [strcat [mc "write-tree failed:"] "\n\n$err"]
 333                ui_status [mc "Commit failed."]
 334                unlock_index
 335                return
 336        }
 337
 338        # -- Verify this wasn't an empty change.
 339        #
 340        if {$commit_type eq {normal}} {
 341                set fd_ot [git_read cat-file commit $PARENT]
 342                fconfigure $fd_ot -encoding binary -translation lf
 343                set old_tree [gets $fd_ot]
 344                close $fd_ot
 345
 346                if {[string equal -length 5 {tree } $old_tree]
 347                        && [string length $old_tree] == 45} {
 348                        set old_tree [string range $old_tree 5 end]
 349                } else {
 350                        error [mc "Commit %s appears to be corrupt" $PARENT]
 351                }
 352
 353                if {$tree_id eq $old_tree} {
 354                        catch {file delete $msg_p}
 355                        info_popup [mc "No changes to commit.
 356
 357No files were modified by this commit and it was not a merge commit.
 358
 359A rescan will be automatically started now.
 360"]
 361                        unlock_index
 362                        rescan {ui_status [mc "No changes to commit."]}
 363                        return
 364                }
 365        }
 366
 367        # -- Create the commit.
 368        #
 369        set cmd [list commit-tree $tree_id]
 370        foreach p [concat $PARENT $MERGE_HEAD] {
 371                lappend cmd -p $p
 372        }
 373        lappend cmd <$msg_p
 374        if {[catch {set cmt_id [eval git $cmd]} err]} {
 375                catch {file delete $msg_p}
 376                error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
 377                ui_status [mc "Commit failed."]
 378                unlock_index
 379                return
 380        }
 381
 382        # -- Update the HEAD ref.
 383        #
 384        set reflogm commit
 385        if {$commit_type ne {normal}} {
 386                append reflogm " ($commit_type)"
 387        }
 388        set msg_fd [open $msg_p r]
 389        setup_commit_encoding $msg_fd 1
 390        gets $msg_fd subject
 391        close $msg_fd
 392        append reflogm {: } $subject
 393        if {[catch {
 394                        git update-ref -m $reflogm HEAD $cmt_id $curHEAD
 395                } err]} {
 396                catch {file delete $msg_p}
 397                error_popup [strcat [mc "update-ref failed:"] "\n\n$err"]
 398                ui_status [mc "Commit failed."]
 399                unlock_index
 400                return
 401        }
 402
 403        # -- Cleanup after ourselves.
 404        #
 405        catch {file delete $msg_p}
 406        catch {file delete [gitdir MERGE_HEAD]}
 407        catch {file delete [gitdir MERGE_MSG]}
 408        catch {file delete [gitdir SQUASH_MSG]}
 409        catch {file delete [gitdir GITGUI_MSG]}
 410
 411        # -- Let rerere do its thing.
 412        #
 413        if {[get_config rerere.enabled] eq {}} {
 414                set rerere [file isdirectory [gitdir rr-cache]]
 415        } else {
 416                set rerere [is_config_true rerere.enabled]
 417        }
 418        if {$rerere} {
 419                catch {git rerere}
 420        }
 421
 422        # -- Run the post-commit hook.
 423        #
 424        set fd_ph [githook_read post-commit]
 425        if {$fd_ph ne {}} {
 426                global pch_error
 427                set pch_error {}
 428                fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
 429                fileevent $fd_ph readable \
 430                        [list commit_postcommit_wait $fd_ph $cmt_id]
 431        }
 432
 433        $ui_comm delete 0.0 end
 434        $ui_comm edit reset
 435        $ui_comm edit modified false
 436        if {$::GITGUI_BCK_exists} {
 437                catch {file delete [gitdir GITGUI_BCK]}
 438                set ::GITGUI_BCK_exists 0
 439        }
 440
 441        if {[is_enabled singlecommit]} { do_quit 0 }
 442
 443        # -- Update in memory status
 444        #
 445        set selected_commit_type new
 446        set commit_type normal
 447        set HEAD $cmt_id
 448        set PARENT $cmt_id
 449        set MERGE_HEAD [list]
 450
 451        foreach path [array names file_states] {
 452                set s $file_states($path)
 453                set m [lindex $s 0]
 454                switch -glob -- $m {
 455                _O -
 456                _M -
 457                _D {continue}
 458                __ -
 459                A_ -
 460                M_ -
 461                T_ -
 462                D_ {
 463                        unset file_states($path)
 464                        catch {unset selected_paths($path)}
 465                }
 466                DO {
 467                        set file_states($path) [list _O [lindex $s 1] {} {}]
 468                }
 469                AM -
 470                AD -
 471                AT -
 472                TM -
 473                TD -
 474                MM -
 475                MT -
 476                MD {
 477                        set file_states($path) [list \
 478                                _[string index $m 1] \
 479                                [lindex $s 1] \
 480                                [lindex $s 3] \
 481                                {}]
 482                }
 483                }
 484        }
 485
 486        display_all_files
 487        unlock_index
 488        reshow_diff
 489        ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
 490}
 491
 492proc commit_postcommit_wait {fd_ph cmt_id} {
 493        global pch_error
 494
 495        append pch_error [read $fd_ph]
 496        fconfigure $fd_ph -blocking 1
 497        if {[eof $fd_ph]} {
 498                if {[catch {close $fd_ph}]} {
 499                        hook_failed_popup post-commit $pch_error 0
 500                }
 501                unset pch_error
 502                return
 503        }
 504        fconfigure $fd_ph -blocking 0
 505}