git-gui / lib / commit.tclon commit grep: recurse in-process using 'struct repository' (f9ee2fc)
   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        if {[is_config_true commit.gpgsign]} {
 380                lappend cmd -S
 381        }
 382        foreach p [concat $PARENT $MERGE_HEAD] {
 383                lappend cmd -p $p
 384        }
 385        lappend cmd <$msg_p
 386        if {[catch {set cmt_id [eval git $cmd]} err]} {
 387                catch {file delete $msg_p}
 388                error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
 389                ui_status [mc "Commit failed."]
 390                unlock_index
 391                unset -nocomplain commit_author
 392                commit_author_reset $old_author
 393                return
 394        }
 395        if {[info exists commit_author]} {
 396                unset -nocomplain commit_author
 397                commit_author_reset $old_author
 398        }
 399
 400        # -- Update the HEAD ref.
 401        #
 402        set reflogm commit
 403        if {$commit_type ne {normal}} {
 404                append reflogm " ($commit_type)"
 405        }
 406        set msg_fd [open $msg_p r]
 407        setup_commit_encoding $msg_fd 1
 408        gets $msg_fd subject
 409        close $msg_fd
 410        append reflogm {: } $subject
 411        if {[catch {
 412                        git update-ref -m $reflogm HEAD $cmt_id $curHEAD
 413                } err]} {
 414                catch {file delete $msg_p}
 415                error_popup [strcat [mc "update-ref failed:"] "\n\n$err"]
 416                ui_status [mc "Commit failed."]
 417                unlock_index
 418                return
 419        }
 420
 421        # -- Cleanup after ourselves.
 422        #
 423        catch {file delete $msg_p}
 424        catch {file delete [gitdir MERGE_HEAD]}
 425        catch {file delete [gitdir MERGE_MSG]}
 426        catch {file delete [gitdir SQUASH_MSG]}
 427        catch {file delete [gitdir GITGUI_MSG]}
 428        catch {file delete [gitdir CHERRY_PICK_HEAD]}
 429
 430        # -- Let rerere do its thing.
 431        #
 432        if {[get_config rerere.enabled] eq {}} {
 433                set rerere [file isdirectory [gitdir rr-cache]]
 434        } else {
 435                set rerere [is_config_true rerere.enabled]
 436        }
 437        if {$rerere} {
 438                catch {git rerere}
 439        }
 440
 441        # -- Run the post-commit hook.
 442        #
 443        set fd_ph [githook_read post-commit]
 444        if {$fd_ph ne {}} {
 445                global pch_error
 446                set pch_error {}
 447                fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
 448                fileevent $fd_ph readable \
 449                        [list commit_postcommit_wait $fd_ph $cmt_id]
 450        }
 451
 452        $ui_comm delete 0.0 end
 453        $ui_comm edit reset
 454        $ui_comm edit modified false
 455        if {$::GITGUI_BCK_exists} {
 456                catch {file delete [gitdir GITGUI_BCK]}
 457                set ::GITGUI_BCK_exists 0
 458        }
 459
 460        if {[is_enabled singlecommit]} { do_quit 0 }
 461
 462        # -- Update in memory status
 463        #
 464        set selected_commit_type new
 465        set commit_type normal
 466        set HEAD $cmt_id
 467        set PARENT $cmt_id
 468        set MERGE_HEAD [list]
 469
 470        foreach path [array names file_states] {
 471                set s $file_states($path)
 472                set m [lindex $s 0]
 473                switch -glob -- $m {
 474                _O -
 475                _M -
 476                _D {continue}
 477                __ -
 478                A_ -
 479                M_ -
 480                T_ -
 481                D_ {
 482                        unset file_states($path)
 483                        catch {unset selected_paths($path)}
 484                }
 485                DO {
 486                        set file_states($path) [list _O [lindex $s 1] {} {}]
 487                }
 488                AM -
 489                AD -
 490                AT -
 491                TM -
 492                TD -
 493                MM -
 494                MT -
 495                MD {
 496                        set file_states($path) [list \
 497                                _[string index $m 1] \
 498                                [lindex $s 1] \
 499                                [lindex $s 3] \
 500                                {}]
 501                }
 502                }
 503        }
 504
 505        display_all_files
 506        unlock_index
 507        reshow_diff
 508        ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject]
 509}
 510
 511proc commit_postcommit_wait {fd_ph cmt_id} {
 512        global pch_error
 513
 514        append pch_error [read $fd_ph]
 515        fconfigure $fd_ph -blocking 1
 516        if {[eof $fd_ph]} {
 517                if {[catch {close $fd_ph}]} {
 518                        hook_failed_popup post-commit $pch_error 0
 519                }
 520                unset pch_error
 521                return
 522        }
 523        fconfigure $fd_ph -blocking 0
 524}
 525
 526proc commit_author_ident {details} {
 527        global env
 528        array set author $details
 529        set old [array get env GIT_AUTHOR_*]
 530        set env(GIT_AUTHOR_NAME) $author(name)
 531        set env(GIT_AUTHOR_EMAIL) $author(email)
 532        set env(GIT_AUTHOR_DATE) $author(date)
 533        return $old
 534}
 535proc commit_author_reset {details} {
 536        global env
 537        unset env(GIT_AUTHOR_NAME) env(GIT_AUTHOR_EMAIL) env(GIT_AUTHOR_DATE)
 538        if {$details ne {}} {
 539                array set env $details
 540        }
 541}