lib / commit.tclon commit git-gui: Fix applying a line when all following lines are deletions (390425b)
   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                switch -glob -- [lindex $file_states($path) 0] {
 165                _? {continue}
 166                A? -
 167                D? -
 168                T_ -
 169                M? {set files_ready 1}
 170                _U -
 171                U? {
 172                        error_popup [mc "Unmerged files cannot be committed.
 173
 174File %s has merge conflicts.  You must resolve them and stage the file before committing.
 175" [short_path $path]]
 176                        unlock_index
 177                        return
 178                }
 179                default {
 180                        error_popup [mc "Unknown file state %s detected.
 181
 182File %s cannot be committed by this program.
 183" [lindex $s 0] [short_path $path]]
 184                }
 185                }
 186        }
 187        if {!$files_ready && ![string match *merge $curType] && ![is_enabled nocommit]} {
 188                info_popup [mc "No changes to commit.
 189
 190You must stage at least 1 file before you can commit.
 191"]
 192                unlock_index
 193                return
 194        }
 195
 196        if {[is_enabled nocommitmsg]} { do_quit 0 }
 197
 198        # -- A message is required.
 199        #
 200        set msg [string trim [$ui_comm get 1.0 end]]
 201        regsub -all -line {[ \t\r]+$} $msg {} msg
 202        if {$msg eq {}} {
 203                error_popup [mc "Please supply a commit message.
 204
 205A good commit message has the following format:
 206
 207- First line: Describe in one sentence what you did.
 208- Second line: Blank
 209- Remaining lines: Describe why this change is good.
 210"]
 211                unlock_index
 212                return
 213        }
 214
 215        # -- Build the message file.
 216        #
 217        set msg_p [gitdir GITGUI_EDITMSG]
 218        set msg_wt [open $msg_p w]
 219        fconfigure $msg_wt -translation lf
 220        setup_commit_encoding $msg_wt
 221        puts $msg_wt $msg
 222        close $msg_wt
 223
 224        if {[is_enabled nocommit]} { do_quit 0 }
 225
 226        # -- Run the pre-commit hook.
 227        #
 228        set fd_ph [githook_read pre-commit]
 229        if {$fd_ph eq {}} {
 230                commit_commitmsg $curHEAD $msg_p
 231                return
 232        }
 233
 234        ui_status [mc "Calling pre-commit hook..."]
 235        set pch_error {}
 236        fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
 237        fileevent $fd_ph readable \
 238                [list commit_prehook_wait $fd_ph $curHEAD $msg_p]
 239}
 240
 241proc commit_prehook_wait {fd_ph curHEAD msg_p} {
 242        global pch_error
 243
 244        append pch_error [read $fd_ph]
 245        fconfigure $fd_ph -blocking 1
 246        if {[eof $fd_ph]} {
 247                if {[catch {close $fd_ph}]} {
 248                        catch {file delete $msg_p}
 249                        ui_status [mc "Commit declined by pre-commit hook."]
 250                        hook_failed_popup pre-commit $pch_error
 251                        unlock_index
 252                } else {
 253                        commit_commitmsg $curHEAD $msg_p
 254                }
 255                set pch_error {}
 256                return
 257        }
 258        fconfigure $fd_ph -blocking 0
 259}
 260
 261proc commit_commitmsg {curHEAD msg_p} {
 262        global pch_error
 263
 264        # -- Run the commit-msg hook.
 265        #
 266        set fd_ph [githook_read commit-msg $msg_p]
 267        if {$fd_ph eq {}} {
 268                commit_writetree $curHEAD $msg_p
 269                return
 270        }
 271
 272        ui_status [mc "Calling commit-msg hook..."]
 273        set pch_error {}
 274        fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
 275        fileevent $fd_ph readable \
 276                [list commit_commitmsg_wait $fd_ph $curHEAD $msg_p]
 277}
 278
 279proc commit_commitmsg_wait {fd_ph curHEAD msg_p} {
 280        global pch_error
 281
 282        append pch_error [read $fd_ph]
 283        fconfigure $fd_ph -blocking 1
 284        if {[eof $fd_ph]} {
 285                if {[catch {close $fd_ph}]} {
 286                        catch {file delete $msg_p}
 287                        ui_status [mc "Commit declined by commit-msg hook."]
 288                        hook_failed_popup commit-msg $pch_error
 289                        unlock_index
 290                } else {
 291                        commit_writetree $curHEAD $msg_p
 292                }
 293                set pch_error {}
 294                return
 295        }
 296        fconfigure $fd_ph -blocking 0
 297}
 298
 299proc commit_writetree {curHEAD msg_p} {
 300        ui_status [mc "Committing changes..."]
 301        set fd_wt [git_read write-tree]
 302        fileevent $fd_wt readable \
 303                [list commit_committree $fd_wt $curHEAD $msg_p]
 304}
 305
 306proc commit_committree {fd_wt curHEAD msg_p} {
 307        global HEAD PARENT MERGE_HEAD commit_type
 308        global current_branch
 309        global ui_comm selected_commit_type
 310        global file_states selected_paths rescan_active
 311        global repo_config
 312
 313        gets $fd_wt tree_id
 314        if {[catch {close $fd_wt} err]} {
 315                catch {file delete $msg_p}
 316                error_popup [strcat [mc "write-tree failed:"] "\n\n$err"]
 317                ui_status [mc "Commit failed."]
 318                unlock_index
 319                return
 320        }
 321
 322        # -- Verify this wasn't an empty change.
 323        #
 324        if {$commit_type eq {normal}} {
 325                set fd_ot [git_read cat-file commit $PARENT]
 326                fconfigure $fd_ot -encoding binary -translation lf
 327                set old_tree [gets $fd_ot]
 328                close $fd_ot
 329
 330                if {[string equal -length 5 {tree } $old_tree]
 331                        && [string length $old_tree] == 45} {
 332                        set old_tree [string range $old_tree 5 end]
 333                } else {
 334                        error [mc "Commit %s appears to be corrupt" $PARENT]
 335                }
 336
 337                if {$tree_id eq $old_tree} {
 338                        catch {file delete $msg_p}
 339                        info_popup [mc "No changes to commit.
 340
 341No files were modified by this commit and it was not a merge commit.
 342
 343A rescan will be automatically started now.
 344"]
 345                        unlock_index
 346                        rescan {ui_status [mc "No changes to commit."]}
 347                        return
 348                }
 349        }
 350
 351        # -- Create the commit.
 352        #
 353        set cmd [list commit-tree $tree_id]
 354        foreach p [concat $PARENT $MERGE_HEAD] {
 355                lappend cmd -p $p
 356        }
 357        lappend cmd <$msg_p
 358        if {[catch {set cmt_id [eval git $cmd]} err]} {
 359                catch {file delete $msg_p}
 360                error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
 361                ui_status [mc "Commit failed."]
 362                unlock_index
 363                return
 364        }
 365
 366        # -- Update the HEAD ref.
 367        #
 368        set reflogm commit
 369        if {$commit_type ne {normal}} {
 370                append reflogm " ($commit_type)"
 371        }
 372        set msg_fd [open $msg_p r]
 373        setup_commit_encoding $msg_fd 1
 374        gets $msg_fd subject
 375        close $msg_fd
 376        append reflogm {: } $subject
 377        if {[catch {
 378                        git update-ref -m $reflogm HEAD $cmt_id $curHEAD
 379                } err]} {
 380                catch {file delete $msg_p}
 381                error_popup [strcat [mc "update-ref failed:"] "\n\n$err"]
 382                ui_status [mc "Commit failed."]
 383                unlock_index
 384                return
 385        }
 386
 387        # -- Cleanup after ourselves.
 388        #
 389        catch {file delete $msg_p}
 390        catch {file delete [gitdir MERGE_HEAD]}
 391        catch {file delete [gitdir MERGE_MSG]}
 392        catch {file delete [gitdir SQUASH_MSG]}
 393        catch {file delete [gitdir GITGUI_MSG]}
 394
 395        # -- Let rerere do its thing.
 396        #
 397        if {[get_config rerere.enabled] eq {}} {
 398                set rerere [file isdirectory [gitdir rr-cache]]
 399        } else {
 400                set rerere [is_config_true rerere.enabled]
 401        }
 402        if {$rerere} {
 403                catch {git rerere}
 404        }
 405
 406        # -- Run the post-commit hook.
 407        #
 408        set fd_ph [githook_read post-commit]
 409        if {$fd_ph ne {}} {
 410                global pch_error
 411                set pch_error {}
 412                fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
 413                fileevent $fd_ph readable \
 414                        [list commit_postcommit_wait $fd_ph $cmt_id]
 415        }
 416
 417        $ui_comm delete 0.0 end
 418        $ui_comm edit reset
 419        $ui_comm edit modified false
 420        if {$::GITGUI_BCK_exists} {
 421                catch {file delete [gitdir GITGUI_BCK]}
 422                set ::GITGUI_BCK_exists 0
 423        }
 424
 425        if {[is_enabled singlecommit]} { do_quit 0 }
 426
 427        # -- Update in memory status
 428        #
 429        set selected_commit_type new
 430        set commit_type normal
 431        set HEAD $cmt_id
 432        set PARENT $cmt_id
 433        set MERGE_HEAD [list]
 434
 435        foreach path [array names file_states] {
 436                set s $file_states($path)
 437                set m [lindex $s 0]
 438                switch -glob -- $m {
 439                _O -
 440                _M -
 441                _D {continue}
 442                __ -
 443                A_ -
 444                M_ -
 445                T_ -
 446                D_ {
 447                        unset file_states($path)
 448                        catch {unset selected_paths($path)}
 449                }
 450                DO {
 451                        set file_states($path) [list _O [lindex $s 1] {} {}]
 452                }
 453                AM -
 454                AD -
 455                MM -
 456                MD {
 457                        set file_states($path) [list \
 458                                _[string index $m 1] \
 459                                [lindex $s 1] \
 460                                [lindex $s 3] \
 461                                {}]
 462                }
 463                }
 464        }
 465
 466        display_all_files
 467        unlock_index
 468        reshow_diff
 469        ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
 470}
 471
 472proc commit_postcommit_wait {fd_ph cmt_id} {
 473        global pch_error
 474
 475        append pch_error [read $fd_ph]
 476        fconfigure $fd_ph -blocking 1
 477        if {[eof $fd_ph]} {
 478                if {[catch {close $fd_ph}]} {
 479                        hook_failed_popup post-commit $pch_error 0
 480                }
 481                unset pch_error
 482                return
 483        }
 484        fconfigure $fd_ph -blocking 0
 485}