lib / checkout_op.tclon commit git-gui: Skip unnecessary read-tree work during checkout (dba0741)
   1# git-gui commit checkout support
   2# Copyright (C) 2007 Shawn Pearce
   3
   4class checkout_op {
   5
   6field w        {}; # our window (if we have one)
   7field w_cons   {}; # embedded console window object
   8
   9field new_expr   ; # expression the user saw/thinks this is
  10field new_hash   ; # commit SHA-1 we are switching to
  11field new_ref    ; # ref we are updating/creating
  12
  13field parent_w      .; # window that started us
  14field merge_type none; # type of merge to apply to existing branch
  15field merge_base   {}; # merge base if we have another ref involved
  16field fetch_spec   {}; # refetch tracking branch if used?
  17field checkout      1; # actually checkout the branch?
  18field create        0; # create the branch if it doesn't exist?
  19
  20field reset_ok      0; # did the user agree to reset?
  21field fetch_ok      0; # did the fetch succeed?
  22
  23field readtree_d   {}; # buffered output from read-tree
  24field update_old   {}; # was the update-ref call deferred?
  25field reflog_msg   {}; # log message for the update-ref call
  26
  27constructor new {expr hash {ref {}}} {
  28        set new_expr $expr
  29        set new_hash $hash
  30        set new_ref  $ref
  31
  32        return $this
  33}
  34
  35method parent {path} {
  36        set parent_w [winfo toplevel $path]
  37}
  38
  39method enable_merge {type} {
  40        set merge_type $type
  41}
  42
  43method enable_fetch {spec} {
  44        set fetch_spec $spec
  45}
  46
  47method enable_checkout {co} {
  48        set checkout $co
  49}
  50
  51method enable_create {co} {
  52        set create $co
  53}
  54
  55method run {} {
  56        if {$fetch_spec ne {}} {
  57                global M1B
  58
  59                # We were asked to refresh a single tracking branch
  60                # before we get to work.  We should do that before we
  61                # consider any ref updating.
  62                #
  63                set fetch_ok 0
  64                set l_trck [lindex $fetch_spec 0]
  65                set remote [lindex $fetch_spec 1]
  66                set r_head [lindex $fetch_spec 2]
  67                regsub ^refs/heads/ $r_head {} r_name
  68
  69                _toplevel $this {Refreshing Tracking Branch}
  70                set w_cons [::console::embed \
  71                        $w.console \
  72                        "Fetching $r_name from $remote"]
  73                pack $w.console -fill both -expand 1
  74                $w_cons exec \
  75                        [list git fetch $remote +$r_head:$l_trck] \
  76                        [cb _finish_fetch]
  77
  78                bind $w <$M1B-Key-w> break
  79                bind $w <$M1B-Key-W> break
  80                bind $w <Visibility> "
  81                        [list grab $w]
  82                        [list focus $w]
  83                "
  84                wm protocol $w WM_DELETE_WINDOW [cb _noop]
  85                tkwait window $w
  86
  87                if {!$fetch_ok} {
  88                        delete_this
  89                        return 0
  90                }
  91        }
  92
  93        if {$new_ref ne {}} {
  94                # If we have a ref we need to update it before we can
  95                # proceed with a checkout (if one was enabled).
  96                #
  97                if {![_update_ref $this]} {
  98                        delete_this
  99                        return 0
 100                }
 101        }
 102
 103        if {$checkout} {
 104                _checkout $this
 105                return 1
 106        }
 107
 108        delete_this
 109        return 1
 110}
 111
 112method _noop {} {}
 113
 114method _finish_fetch {ok} {
 115        if {$ok} {
 116                set l_trck [lindex $fetch_spec 0]
 117                if {[catch {set new_hash [git rev-parse --verify "$l_trck^0"]} err]} {
 118                        set ok 0
 119                        $w_cons insert "fatal: Cannot resolve $l_trck"
 120                        $w_cons insert $err
 121                }
 122        }
 123
 124        $w_cons done $ok
 125        set w_cons {}
 126        wm protocol $w WM_DELETE_WINDOW {}
 127
 128        if {$ok} {
 129                destroy $w
 130                set w {}
 131        } else {
 132                button $w.close -text Close -command [list destroy $w]
 133                pack $w.close -side bottom -anchor e -padx 10 -pady 10
 134        }
 135
 136        set fetch_ok $ok
 137}
 138
 139method _update_ref {} {
 140        global null_sha1 current_branch
 141
 142        set ref $new_ref
 143        set new $new_hash
 144
 145        set is_current 0
 146        set rh refs/heads/
 147        set rn [string length $rh]
 148        if {[string equal -length $rn $rh $ref]} {
 149                set newbranch [string range $ref $rn end]
 150                if {$current_branch eq $newbranch} {
 151                        set is_current 1
 152                }
 153        } else {
 154                set newbranch $ref
 155        }
 156
 157        if {[catch {set cur [git rev-parse --verify "$ref^0"]}]} {
 158                # Assume it does not exist, and that is what the error was.
 159                #
 160                if {!$create} {
 161                        _error $this "Branch '$newbranch' does not exist."
 162                        return 0
 163                }
 164
 165                set reflog_msg "branch: Created from $new_expr"
 166                set cur $null_sha1
 167        } elseif {$create && $merge_type eq {none}} {
 168                # We were told to create it, but not do a merge.
 169                # Bad.  Name shouldn't have existed.
 170                #
 171                _error $this "Branch '$newbranch' already exists."
 172                return 0
 173        } elseif {!$create && $merge_type eq {none}} {
 174                # We aren't creating, it exists and we don't merge.
 175                # We are probably just a simple branch switch.
 176                # Use whatever value we just read.
 177                #
 178                set new      $cur
 179                set new_hash $cur
 180        } elseif {$new eq $cur} {
 181                # No merge would be required, don't compute anything.
 182                #
 183        } else {
 184                catch {set merge_base [git merge-base $new $cur]}
 185                if {$merge_base eq $cur} {
 186                        # The current branch is older.
 187                        #
 188                        set reflog_msg "merge $new_expr: Fast-forward"
 189                } else {
 190                        switch -- $merge_type {
 191                        ff {
 192                                if {$merge_base eq $new} {
 193                                        # The current branch is actually newer.
 194                                        #
 195                                        set new $cur
 196                                } else {
 197                                        _error $this "Branch '$newbranch' already exists.\n\nIt cannot fast-forward to $new_expr.\nA merge is required."
 198                                        return 0
 199                                }
 200                        }
 201                        reset {
 202                                # The current branch will lose things.
 203                                #
 204                                if {[_confirm_reset $this $cur]} {
 205                                        set reflog_msg "reset $new_expr"
 206                                } else {
 207                                        return 0
 208                                }
 209                        }
 210                        default {
 211                                _error $this "Merge strategy '$merge_type' not supported."
 212                                return 0
 213                        }
 214                        }
 215                }
 216        }
 217
 218        if {$new ne $cur} {
 219                if {$is_current} {
 220                        # No so fast.  We should defer this in case
 221                        # we cannot update the working directory.
 222                        #
 223                        set update_old $cur
 224                        return 1
 225                }
 226
 227                if {[catch {
 228                                git update-ref -m $reflog_msg $ref $new $cur
 229                        } err]} {
 230                        _error $this "Failed to update '$newbranch'.\n\n$err"
 231                        return 0
 232                }
 233        }
 234
 235        return 1
 236}
 237
 238method _checkout {} {
 239        if {[lock_index checkout_op]} {
 240                after idle [cb _start_checkout]
 241        } else {
 242                _error $this "Index is already locked."
 243                delete_this
 244        }
 245}
 246
 247method _start_checkout {} {
 248        global HEAD commit_type
 249
 250        # -- Our in memory state should match the repository.
 251        #
 252        repository_state curType curHEAD curMERGE_HEAD
 253        if {[string match amend* $commit_type]
 254                && $curType eq {normal}
 255                && $curHEAD eq $HEAD} {
 256        } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
 257                info_popup {Last scanned state does not match repository state.
 258
 259Another Git program has modified this repository since the last scan.  A rescan must be performed before the current branch can be changed.
 260
 261The rescan will be automatically started now.
 262}
 263                unlock_index
 264                rescan ui_ready
 265                delete_this
 266                return
 267        }
 268
 269        if {$curHEAD eq $new_hash} {
 270                _after_readtree $this
 271        } elseif {[is_config_true gui.trustmtime]} {
 272                _readtree $this
 273        } else {
 274                ui_status {Refreshing file status...}
 275                set fd [git_read update-index \
 276                        -q \
 277                        --unmerged \
 278                        --ignore-missing \
 279                        --refresh \
 280                        ]
 281                fconfigure $fd -blocking 0 -translation binary
 282                fileevent $fd readable [cb _refresh_wait $fd]
 283        }
 284}
 285
 286method _refresh_wait {fd} {
 287        read $fd
 288        if {[eof $fd]} {
 289                close $fd
 290                _readtree $this
 291        }
 292}
 293
 294method _name {} {
 295        if {$new_ref eq {}} {
 296                return [string range $new_hash 0 7]
 297        }
 298
 299        set rh refs/heads/
 300        set rn [string length $rh]
 301        if {[string equal -length $rn $rh $new_ref]} {
 302                return [string range $new_ref $rn end]
 303        } else {
 304                return $new_ref
 305        }
 306}
 307
 308method _readtree {} {
 309        global HEAD
 310
 311        set readtree_d {}
 312        $::main_status start \
 313                "Updating working directory to '[_name $this]'..." \
 314                {files checked out}
 315
 316        set fd [git_read --stderr read-tree \
 317                -m \
 318                -u \
 319                -v \
 320                --exclude-per-directory=.gitignore \
 321                $HEAD \
 322                $new_hash \
 323                ]
 324        fconfigure $fd -blocking 0 -translation binary
 325        fileevent $fd readable [cb _readtree_wait $fd]
 326}
 327
 328method _readtree_wait {fd} {
 329        global current_branch
 330
 331        set buf [read $fd]
 332        $::main_status update_meter $buf
 333        append readtree_d $buf
 334
 335        fconfigure $fd -blocking 1
 336        if {![eof $fd]} {
 337                fconfigure $fd -blocking 0
 338                return
 339        }
 340
 341        if {[catch {close $fd}]} {
 342                set err $readtree_d
 343                regsub {^fatal: } $err {} err
 344                $::main_status stop "Aborted checkout of '[_name $this]' (file level merging is required)."
 345                warn_popup "File level merge required.
 346
 347$err
 348
 349Staying on branch '$current_branch'."
 350                unlock_index
 351                delete_this
 352                return
 353        }
 354
 355        $::main_status stop
 356        _after_readtree $this
 357}
 358
 359method _after_readtree {} {
 360        global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
 361        global current_branch is_detached
 362        global ui_comm
 363
 364        set name [_name $this]
 365        set log "checkout: moving"
 366        if {!$is_detached} {
 367                append log " from $current_branch"
 368        }
 369
 370        # -- Move/create HEAD as a symbolic ref.  Core git does not
 371        #    even check for failure here, it Just Works(tm).  If it
 372        #    doesn't we are in some really ugly state that is difficult
 373        #    to recover from within git-gui.
 374        #
 375        set rh refs/heads/
 376        set rn [string length $rh]
 377        if {[string equal -length $rn $rh $new_ref]} {
 378                set new_branch [string range $new_ref $rn end]
 379                append log " to $new_branch"
 380
 381                if {[catch {
 382                                git symbolic-ref -m $log HEAD $new_ref
 383                        } err]} {
 384                        _fatal $this $err
 385                }
 386                set current_branch $new_branch
 387                set is_detached 0
 388        } else {
 389                append log " to $new_expr"
 390
 391                if {[catch {
 392                                _detach_HEAD $log $new_hash
 393                        } err]} {
 394                        _fatal $this $err
 395                }
 396                set current_branch HEAD
 397                set is_detached 1
 398        }
 399
 400        # -- We had to defer updating the branch itself until we
 401        #    knew the working directory would update.  So now we
 402        #    need to finish that work.  If it fails we're in big
 403        #    trouble.
 404        #
 405        if {$update_old ne {}} {
 406                if {[catch {
 407                                git update-ref \
 408                                        -m $reflog_msg \
 409                                        $new_ref \
 410                                        $new_hash \
 411                                        $update_old
 412                        } err]} {
 413                        _fatal $this $err
 414                }
 415        }
 416
 417        if {$is_detached} {
 418                info_popup "You are no longer on a local branch.
 419
 420If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."
 421        }
 422
 423        # -- Update our repository state.  If we were previously in
 424        #    amend mode we need to toss the current buffer and do a
 425        #    full rescan to update our file lists.  If we weren't in
 426        #    amend mode our file lists are accurate and we can avoid
 427        #    the rescan.
 428        #
 429        unlock_index
 430        set selected_commit_type new
 431        if {[string match amend* $commit_type]} {
 432                $ui_comm delete 0.0 end
 433                $ui_comm edit reset
 434                $ui_comm edit modified false
 435                rescan [list ui_status "Checked out '$name'."]
 436        } else {
 437                repository_state commit_type HEAD MERGE_HEAD
 438                set PARENT $HEAD
 439                ui_status "Checked out '$name'."
 440        }
 441        delete_this
 442}
 443
 444git-version proc _detach_HEAD {log new} {
 445        >= 1.5.3 {
 446                git update-ref --no-deref -m $log HEAD $new
 447        }
 448        default {
 449                set p [gitdir HEAD]
 450                file delete $p
 451                set fd [open $p w]
 452                fconfigure $fd -translation lf -encoding utf-8
 453                puts $fd $new
 454                close $fd
 455        }
 456}
 457
 458method _confirm_reset {cur} {
 459        set reset_ok 0
 460        set name [_name $this]
 461        set gitk [list do_gitk [list $cur ^$new_hash]]
 462
 463        _toplevel $this {Confirm Branch Reset}
 464        pack [label $w.msg1 \
 465                -anchor w \
 466                -justify left \
 467                -text "Resetting '$name' to $new_expr will lose the following commits:" \
 468                ] -anchor w
 469
 470        set list $w.list.l
 471        frame $w.list
 472        text $list \
 473                -font font_diff \
 474                -width 80 \
 475                -height 10 \
 476                -wrap none \
 477                -xscrollcommand [list $w.list.sbx set] \
 478                -yscrollcommand [list $w.list.sby set]
 479        scrollbar $w.list.sbx -orient h -command [list $list xview]
 480        scrollbar $w.list.sby -orient v -command [list $list yview]
 481        pack $w.list.sbx -fill x -side bottom
 482        pack $w.list.sby -fill y -side right
 483        pack $list -fill both -expand 1
 484        pack $w.list -fill both -expand 1 -padx 5 -pady 5
 485
 486        pack [label $w.msg2 \
 487                -anchor w \
 488                -justify left \
 489                -text {Recovering lost commits may not be easy.} \
 490                ]
 491        pack [label $w.msg3 \
 492                -anchor w \
 493                -justify left \
 494                -text "Reset '$name'?" \
 495                ]
 496
 497        frame $w.buttons
 498        button $w.buttons.visualize \
 499                -text Visualize \
 500                -command $gitk
 501        pack $w.buttons.visualize -side left
 502        button $w.buttons.reset \
 503                -text Reset \
 504                -command "
 505                        set @reset_ok 1
 506                        destroy $w
 507                "
 508        pack $w.buttons.reset -side right
 509        button $w.buttons.cancel \
 510                -default active \
 511                -text Cancel \
 512                -command [list destroy $w]
 513        pack $w.buttons.cancel -side right -padx 5
 514        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
 515
 516        set fd [git_read rev-list --pretty=oneline $cur ^$new_hash]
 517        while {[gets $fd line] > 0} {
 518                set abbr [string range $line 0 7]
 519                set subj [string range $line 41 end]
 520                $list insert end "$abbr  $subj\n"
 521        }
 522        close $fd
 523        $list configure -state disabled
 524
 525        bind $w    <Key-v> $gitk
 526        bind $w <Visibility> "
 527                grab $w
 528                focus $w.buttons.cancel
 529        "
 530        bind $w <Key-Return> [list destroy $w]
 531        bind $w <Key-Escape> [list destroy $w]
 532        tkwait window $w
 533        return $reset_ok
 534}
 535
 536method _error {msg} {
 537        if {[winfo ismapped $parent_w]} {
 538                set p $parent_w
 539        } else {
 540                set p .
 541        }
 542
 543        tk_messageBox \
 544                -icon error \
 545                -type ok \
 546                -title [wm title $p] \
 547                -parent $p \
 548                -message $msg
 549}
 550
 551method _toplevel {title} {
 552        regsub -all {::} $this {__} w
 553        set w .$w
 554
 555        if {[winfo ismapped $parent_w]} {
 556                set p $parent_w
 557        } else {
 558                set p .
 559        }
 560
 561        toplevel $w
 562        wm title $w $title
 563        wm geometry $w "+[winfo rootx $p]+[winfo rooty $p]"
 564}
 565
 566method _fatal {err} {
 567        error_popup "Failed to set current branch.
 568
 569This working directory is only partially switched.  We successfully updated your files, but failed to update an internal Git file.
 570
 571This should not have occurred.  [appname] will now close and give up.
 572
 573$err"
 574        exit 1
 575}
 576
 577}