lib / commit.tclon commit git-gui: avoid persisting modified author identity (cfe616b)
   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 commit_author
   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                                } elseif {[regexp "author (.*)\\s<(.*)>\\s(\\d.*$)" $line all name email time]} {
  38                                        set commit_author [list name $name email $email date $time]
  39                                }
  40                        }
  41                        set msg [read $fd]
  42                        close $fd
  43
  44                        set enc [tcl_encoding $enc]
  45                        if {$enc ne {}} {
  46                                set msg [encoding convertfrom $enc $msg]
  47                        }
  48                        set msg [string trim $msg]
  49                } err]} {
  50                error_popup [strcat [mc "Error loading commit data for amend:"] "\n\n$err"]
  51                return
  52        }
  53
  54        set HEAD $curHEAD
  55        set PARENT $parents
  56        set MERGE_HEAD [list]
  57        switch -- [llength $parents] {
  58        0       {set commit_type amend-initial}
  59        1       {set commit_type amend}
  60        default {set commit_type amend-merge}
  61        }
  62
  63        $ui_comm delete 0.0 end
  64        $ui_comm insert end $msg
  65        $ui_comm edit reset
  66        $ui_comm edit modified false
  67        rescan ui_ready
  68}
  69
  70set GIT_COMMITTER_IDENT {}
  71
  72proc committer_ident {} {
  73        global GIT_COMMITTER_IDENT
  74
  75        if {$GIT_COMMITTER_IDENT eq {}} {
  76                if {[catch {set me [git var GIT_COMMITTER_IDENT]} err]} {
  77                        error_popup [strcat [mc "Unable to obtain your identity:"] "\n\n$err"]
  78                        return {}
  79                }
  80                if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \
  81                        $me me GIT_COMMITTER_IDENT]} {
  82                        error_popup [strcat [mc "Invalid GIT_COMMITTER_IDENT:"] "\n\n$me"]
  83                        return {}
  84                }
  85        }
  86
  87        return $GIT_COMMITTER_IDENT
  88}
  89
  90proc do_signoff {} {
  91        global ui_comm
  92
  93        set me [committer_ident]
  94        if {$me eq {}} return
  95
  96        set sob "Signed-off-by: $me"
  97        set last [$ui_comm get {end -1c linestart} {end -1c}]
  98        if {$last ne $sob} {
  99                $ui_comm edit separator
 100                if {$last ne {}
 101                        && ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} {
 102                        $ui_comm insert end "\n"
 103                }
 104                $ui_comm insert end "\n$sob"
 105                $ui_comm edit separator
 106                $ui_comm see end
 107        }
 108}
 109
 110proc create_new_commit {} {
 111        global commit_type ui_comm commit_author
 112
 113        set commit_type normal
 114        unset -nocomplain commit_author
 115        $ui_comm delete 0.0 end
 116        $ui_comm edit reset
 117        $ui_comm edit modified false
 118        rescan ui_ready
 119}
 120
 121proc setup_commit_encoding {msg_wt {quiet 0}} {
 122        global repo_config
 123
 124        if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
 125                set enc utf-8
 126        }
 127        set use_enc [tcl_encoding $enc]
 128        if {$use_enc ne {}} {
 129                fconfigure $msg_wt -encoding $use_enc
 130        } else {
 131                if {!$quiet} {
 132                        error_popup [mc "warning: Tcl does not support encoding '%s'." $enc]
 133                }
 134                fconfigure $msg_wt -encoding utf-8
 135        }
 136}
 137
 138proc commit_tree {} {
 139        global HEAD commit_type file_states ui_comm repo_config
 140        global pch_error
 141
 142        if {[committer_ident] eq {}} return
 143        if {![lock_index update]} return
 144
 145        # -- Our in memory state should match the repository.
 146        #
 147        repository_state curType curHEAD curMERGE_HEAD
 148        if {[string match amend* $commit_type]
 149                && $curType eq {normal}
 150                && $curHEAD eq $HEAD} {
 151        } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
 152                info_popup [mc "Last scanned state does not match repository state.
 153
 154Another Git program has modified this repository since the last scan.  A rescan must be performed before another commit can be created.
 155
 156The rescan will be automatically started now.
 157"]
 158                unlock_index
 159                rescan ui_ready
 160                return
 161        }
 162
 163        # -- At least one file should differ in the index.
 164        #
 165        set files_ready 0
 166        foreach path [array names file_states] {
 167                set s $file_states($path)
 168                switch -glob -- [lindex $s 0] {
 169                _? {continue}
 170                A? -
 171                D? -
 172                T? -
 173                M? {set files_ready 1}
 174                _U -
 175                U? {
 176                        error_popup [mc "Unmerged files cannot be committed.
 177
 178File %s has merge conflicts.  You must resolve them and stage the file before committing.
 179" [short_path $path]]
 180                        unlock_index
 181                        return
 182                }
 183                default {
 184                        error_popup [mc "Unknown file state %s detected.
 185
 186File %s cannot be committed by this program.
 187" [lindex $s 0] [short_path $path]]
 188                }
 189                }
 190        }
 191        if {!$files_ready && ![string match *merge $curType] && ![is_enabled nocommit]} {
 192                info_popup [mc "No changes to commit.
 193
 194You must stage at least 1 file before you can commit.
 195"]
 196                unlock_index
 197                return
 198        }
 199
 200        if {[is_enabled nocommitmsg]} { do_quit 0 }
 201
 202        # -- A message is required.
 203        #
 204        set msg [string trim [$ui_comm get 1.0 end]]
 205        regsub -all -line {[ \t\r]+$} $msg {} msg
 206        if {$msg eq {}} {
 207                error_popup [mc "Please supply a commit message.
 208
 209A good commit message has the following format:
 210
 211- First line: Describe in one sentence what you did.
 212- Second line: Blank
 213- Remaining lines: Describe why this change is good.
 214"]
 215                unlock_index
 216                return
 217        }
 218
 219        # -- Build the message file.
 220        #
 221        set msg_p [gitdir GITGUI_EDITMSG]
 222        set msg_wt [open $msg_p w]
 223        fconfigure $msg_wt -translation lf
 224        setup_commit_encoding $msg_wt
 225        puts $msg_wt $msg
 226        close $msg_wt
 227
 228        if {[is_enabled nocommit]} { do_quit 0 }
 229
 230        # -- Run the pre-commit hook.
 231        #
 232        set fd_ph [githook_read pre-commit]
 233        if {$fd_ph eq {}} {
 234                commit_commitmsg $curHEAD $msg_p
 235                return
 236        }
 237
 238        ui_status [mc "Calling pre-commit hook..."]
 239        set pch_error {}
 240        fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
 241        fileevent $fd_ph readable \
 242                [list commit_prehook_wait $fd_ph $curHEAD $msg_p]
 243}
 244
 245proc commit_prehook_wait {fd_ph curHEAD msg_p} {
 246        global pch_error
 247
 248        append pch_error [read $fd_ph]
 249        fconfigure $fd_ph -blocking 1
 250        if {[eof $fd_ph]} {
 251                if {[catch {close $fd_ph}]} {
 252                        catch {file delete $msg_p}
 253                        ui_status [mc "Commit declined by pre-commit hook."]
 254                        hook_failed_popup pre-commit $pch_error
 255                        unlock_index
 256                } else {
 257                        commit_commitmsg $curHEAD $msg_p
 258                }
 259                set pch_error {}
 260                return
 261        }
 262        fconfigure $fd_ph -blocking 0
 263}
 264
 265proc commit_commitmsg {curHEAD msg_p} {
 266        global is_detached repo_config
 267        global pch_error
 268
 269        if {$is_detached
 270            && ![file exists [gitdir rebase-merge head-name]]
 271            &&  [is_config_true gui.warndetachedcommit]} {
 272                set msg [mc "You are about to commit on a detached head.\
 273This is a potentially dangerous thing to do because if you switch\
 274to another branch you will lose your changes and it can be difficult\
 275to retrieve them later from the reflog. You should probably cancel this\
 276commit and create a new branch to continue.\n\
 277\n\
 278Do you really want to proceed with your Commit?"]
 279                if {[ask_popup $msg] ne yes} {
 280                        unlock_index
 281                        return
 282                }
 283        }
 284
 285        # -- Run the commit-msg hook.
 286        #
 287        set fd_ph [githook_read commit-msg $msg_p]
 288        if {$fd_ph eq {}} {
 289                commit_writetree $curHEAD $msg_p
 290                return
 291        }
 292
 293        ui_status [mc "Calling commit-msg hook..."]
 294        set pch_error {}
 295        fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
 296        fileevent $fd_ph readable \
 297                [list commit_commitmsg_wait $fd_ph $curHEAD $msg_p]
 298}
 299
 300proc commit_commitmsg_wait {fd_ph curHEAD msg_p} {
 301        global pch_error
 302
 303        append pch_error [read $fd_ph]
 304        fconfigure $fd_ph -blocking 1
 305        if {[eof $fd_ph]} {
 306                if {[catch {close $fd_ph}]} {
 307                        catch {file delete $msg_p}
 308                        ui_status [mc "Commit declined by commit-msg hook."]
 309                        hook_failed_popup commit-msg $pch_error
 310                        unlock_index
 311                } else {
 312                        commit_writetree $curHEAD $msg_p
 313                }
 314                set pch_error {}
 315                return
 316        }
 317        fconfigure $fd_ph -blocking 0
 318}
 319
 320proc commit_writetree {curHEAD msg_p} {
 321        ui_status [mc "Committing changes..."]
 322        set fd_wt [git_read write-tree]
 323        fileevent $fd_wt readable \
 324                [list commit_committree $fd_wt $curHEAD $msg_p]
 325}
 326
 327proc commit_committree {fd_wt curHEAD msg_p} {
 328        global HEAD PARENT MERGE_HEAD commit_type commit_author
 329        global current_branch
 330        global ui_comm selected_commit_type
 331        global file_states selected_paths rescan_active
 332        global repo_config
 333        global env
 334
 335        gets $fd_wt tree_id
 336        if {[catch {close $fd_wt} err]} {
 337                catch {file delete $msg_p}
 338                error_popup [strcat [mc "write-tree failed:"] "\n\n$err"]
 339                ui_status [mc "Commit failed."]
 340                unlock_index
 341                return
 342        }
 343
 344        # -- Verify this wasn't an empty change.
 345        #
 346        if {$commit_type eq {normal}} {
 347                set fd_ot [git_read cat-file commit $PARENT]
 348                fconfigure $fd_ot -encoding binary -translation lf
 349                set old_tree [gets $fd_ot]
 350                close $fd_ot
 351
 352                if {[string equal -length 5 {tree } $old_tree]
 353                        && [string length $old_tree] == 45} {
 354                        set old_tree [string range $old_tree 5 end]
 355                } else {
 356                        error [mc "Commit %s appears to be corrupt" $PARENT]
 357                }
 358
 359                if {$tree_id eq $old_tree} {
 360                        catch {file delete $msg_p}
 361                        info_popup [mc "No changes to commit.
 362
 363No files were modified by this commit and it was not a merge commit.
 364
 365A rescan will be automatically started now.
 366"]
 367                        unlock_index
 368                        rescan {ui_status [mc "No changes to commit."]}
 369                        return
 370                }
 371        }
 372
 373        if {[info exists commit_author]} {
 374                set old_author [commit_author_ident $commit_author]
 375        }
 376        # -- Create the commit.
 377        #
 378        set cmd [list commit-tree $tree_id]
 379        foreach p [concat $PARENT $MERGE_HEAD] {
 380                lappend cmd -p $p
 381        }
 382        lappend cmd <$msg_p
 383        if {[catch {set cmt_id [eval git $cmd]} err]} {
 384                catch {file delete $msg_p}
 385                error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
 386                ui_status [mc "Commit failed."]
 387                unlock_index
 388                unset -nocomplain commit_author
 389                commit_author_reset $old_author
 390                return
 391        }
 392        if {[info exists commit_author]} {
 393                unset -nocomplain commit_author
 394                commit_author_reset $old_author
 395        }
 396
 397        # -- Update the HEAD ref.
 398        #
 399        set reflogm commit
 400        if {$commit_type ne {normal}} {
 401                append reflogm " ($commit_type)"
 402        }
 403        set msg_fd [open $msg_p r]
 404        setup_commit_encoding $msg_fd 1
 405        gets $msg_fd subject
 406        close $msg_fd
 407        append reflogm {: } $subject
 408        if {[catch {
 409                        git update-ref -m $reflogm HEAD $cmt_id $curHEAD
 410                } err]} {
 411                catch {file delete $msg_p}
 412                error_popup [strcat [mc "update-ref failed:"] "\n\n$err"]
 413                ui_status [mc "Commit failed."]
 414                unlock_index
 415                return
 416        }
 417
 418        # -- Cleanup after ourselves.
 419        #
 420        catch {file delete $msg_p}
 421        catch {file delete [gitdir MERGE_HEAD]}
 422        catch {file delete [gitdir MERGE_MSG]}
 423        catch {file delete [gitdir SQUASH_MSG]}
 424        catch {file delete [gitdir GITGUI_MSG]}
 425        catch {file delete [gitdir CHERRY_PICK_HEAD]}
 426
 427        # -- Let rerere do its thing.
 428        #
 429        if {[get_config rerere.enabled] eq {}} {
 430                set rerere [file isdirectory [gitdir rr-cache]]
 431        } else {
 432                set rerere [is_config_true rerere.enabled]
 433        }
 434        if {$rerere} {
 435                catch {git rerere}
 436        }
 437
 438        # -- Run the post-commit hook.
 439        #
 440        set fd_ph [githook_read post-commit]
 441        if {$fd_ph ne {}} {
 442                global pch_error
 443                set pch_error {}
 444                fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
 445                fileevent $fd_ph readable \
 446                        [list commit_postcommit_wait $fd_ph $cmt_id]
 447        }
 448
 449        $ui_comm delete 0.0 end
 450        $ui_comm edit reset
 451        $ui_comm edit modified false
 452        if {$::GITGUI_BCK_exists} {
 453                catch {file delete [gitdir GITGUI_BCK]}
 454                set ::GITGUI_BCK_exists 0
 455        }
 456
 457        if {[is_enabled singlecommit]} { do_quit 0 }
 458
 459        # -- Update in memory status
 460        #
 461        set selected_commit_type new
 462        set commit_type normal
 463        set HEAD $cmt_id
 464        set PARENT $cmt_id
 465        set MERGE_HEAD [list]
 466
 467        foreach path [array names file_states] {
 468                set s $file_states($path)
 469                set m [lindex $s 0]
 470                switch -glob -- $m {
 471                _O -
 472                _M -
 473                _D {continue}
 474                __ -
 475                A_ -
 476                M_ -
 477                T_ -
 478                D_ {
 479                        unset file_states($path)
 480                        catch {unset selected_paths($path)}
 481                }
 482                DO {
 483                        set file_states($path) [list _O [lindex $s 1] {} {}]
 484                }
 485                AM -
 486                AD -
 487                AT -
 488                TM -
 489                TD -
 490                MM -
 491                MT -
 492                MD {
 493                        set file_states($path) [list \
 494                                _[string index $m 1] \
 495                                [lindex $s 1] \
 496                                [lindex $s 3] \
 497                                {}]
 498                }
 499                }
 500        }
 501
 502        display_all_files
 503        unlock_index
 504        reshow_diff
 505        ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
 506}
 507
 508proc commit_postcommit_wait {fd_ph cmt_id} {
 509        global pch_error
 510
 511        append pch_error [read $fd_ph]
 512        fconfigure $fd_ph -blocking 1
 513        if {[eof $fd_ph]} {
 514                if {[catch {close $fd_ph}]} {
 515                        hook_failed_popup post-commit $pch_error 0
 516                }
 517                unset pch_error
 518                return
 519        }
 520        fconfigure $fd_ph -blocking 0
 521}
 522
 523proc commit_author_ident {details} {
 524        global env
 525        array set author $details
 526        set old [array get env GIT_AUTHOR_*]
 527        set env(GIT_AUTHOR_NAME) $author(name)
 528        set env(GIT_AUTHOR_EMAIL) $author(email)
 529        set env(GIT_AUTHOR_DATE) $author(date)
 530        return $old
 531}
 532proc commit_author_reset {details} {
 533        global env
 534        unset env(GIT_AUTHOR_NAME) env(GIT_AUTHOR_EMAIL) env(GIT_AUTHOR_DATE)
 535        if {$details ne {}} {
 536                array set env $details
 537        }
 538}