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