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