git-gui / lib / checkout_op.tclon commit documentation: replace Discussion section by link to user-manual chapter (40dac51)
   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                set cmd [list git fetch $remote]
  70                if {$l_trck ne {}} {
  71                        lappend cmd +$r_head:$l_trck
  72                } else {
  73                        lappend cmd $r_head
  74                }
  75
  76                _toplevel $this {Refreshing Tracking Branch}
  77                set w_cons [::console::embed \
  78                        $w.console \
  79                        "Fetching $r_name from $remote"]
  80                pack $w.console -fill both -expand 1
  81                $w_cons exec $cmd [cb _finish_fetch]
  82
  83                bind $w <$M1B-Key-w> break
  84                bind $w <$M1B-Key-W> break
  85                bind $w <Visibility> "
  86                        [list grab $w]
  87                        [list focus $w]
  88                "
  89                wm protocol $w WM_DELETE_WINDOW [cb _noop]
  90                tkwait window $w
  91
  92                if {!$fetch_ok} {
  93                        delete_this
  94                        return 0
  95                }
  96        }
  97
  98        if {$new_ref ne {}} {
  99                # If we have a ref we need to update it before we can
 100                # proceed with a checkout (if one was enabled).
 101                #
 102                if {![_update_ref $this]} {
 103                        delete_this
 104                        return 0
 105                }
 106        }
 107
 108        if {$checkout} {
 109                _checkout $this
 110                return 1
 111        }
 112
 113        delete_this
 114        return 1
 115}
 116
 117method _noop {} {}
 118
 119method _finish_fetch {ok} {
 120        if {$ok} {
 121                set l_trck [lindex $fetch_spec 0]
 122                if {$l_trck eq {}} {
 123                        set l_trck FETCH_HEAD
 124                }
 125                if {[catch {set new_hash [git rev-parse --verify "$l_trck^0"]} err]} {
 126                        set ok 0
 127                        $w_cons insert "fatal: Cannot resolve $l_trck"
 128                        $w_cons insert $err
 129                }
 130        }
 131
 132        $w_cons done $ok
 133        set w_cons {}
 134        wm protocol $w WM_DELETE_WINDOW {}
 135
 136        if {$ok} {
 137                destroy $w
 138                set w {}
 139        } else {
 140                button $w.close -text Close -command [list destroy $w]
 141                pack $w.close -side bottom -anchor e -padx 10 -pady 10
 142        }
 143
 144        set fetch_ok $ok
 145}
 146
 147method _update_ref {} {
 148        global null_sha1 current_branch
 149
 150        set ref $new_ref
 151        set new $new_hash
 152
 153        set is_current 0
 154        set rh refs/heads/
 155        set rn [string length $rh]
 156        if {[string equal -length $rn $rh $ref]} {
 157                set newbranch [string range $ref $rn end]
 158                if {$current_branch eq $newbranch} {
 159                        set is_current 1
 160                }
 161        } else {
 162                set newbranch $ref
 163        }
 164
 165        if {[catch {set cur [git rev-parse --verify "$ref^0"]}]} {
 166                # Assume it does not exist, and that is what the error was.
 167                #
 168                if {!$create} {
 169                        _error $this "Branch '$newbranch' does not exist."
 170                        return 0
 171                }
 172
 173                set reflog_msg "branch: Created from $new_expr"
 174                set cur $null_sha1
 175        } elseif {$create && $merge_type eq {none}} {
 176                # We were told to create it, but not do a merge.
 177                # Bad.  Name shouldn't have existed.
 178                #
 179                _error $this "Branch '$newbranch' already exists."
 180                return 0
 181        } elseif {!$create && $merge_type eq {none}} {
 182                # We aren't creating, it exists and we don't merge.
 183                # We are probably just a simple branch switch.
 184                # Use whatever value we just read.
 185                #
 186                set new      $cur
 187                set new_hash $cur
 188        } elseif {$new eq $cur} {
 189                # No merge would be required, don't compute anything.
 190                #
 191        } else {
 192                catch {set merge_base [git merge-base $new $cur]}
 193                if {$merge_base eq $cur} {
 194                        # The current branch is older.
 195                        #
 196                        set reflog_msg "merge $new_expr: Fast-forward"
 197                } else {
 198                        switch -- $merge_type {
 199                        ff {
 200                                if {$merge_base eq $new} {
 201                                        # The current branch is actually newer.
 202                                        #
 203                                        set new $cur
 204                                        set new_hash $cur
 205                                } else {
 206                                        _error $this "Branch '$newbranch' already exists.\n\nIt cannot fast-forward to $new_expr.\nA merge is required."
 207                                        return 0
 208                                }
 209                        }
 210                        reset {
 211                                # The current branch will lose things.
 212                                #
 213                                if {[_confirm_reset $this $cur]} {
 214                                        set reflog_msg "reset $new_expr"
 215                                } else {
 216                                        return 0
 217                                }
 218                        }
 219                        default {
 220                                _error $this "Merge strategy '$merge_type' not supported."
 221                                return 0
 222                        }
 223                        }
 224                }
 225        }
 226
 227        if {$new ne $cur} {
 228                if {$is_current} {
 229                        # No so fast.  We should defer this in case
 230                        # we cannot update the working directory.
 231                        #
 232                        set update_old $cur
 233                        return 1
 234                }
 235
 236                if {[catch {
 237                                git update-ref -m $reflog_msg $ref $new $cur
 238                        } err]} {
 239                        _error $this "Failed to update '$newbranch'.\n\n$err"
 240                        return 0
 241                }
 242        }
 243
 244        return 1
 245}
 246
 247method _checkout {} {
 248        if {[lock_index checkout_op]} {
 249                after idle [cb _start_checkout]
 250        } else {
 251                _error $this "Staging area (index) is already locked."
 252                delete_this
 253        }
 254}
 255
 256method _start_checkout {} {
 257        global HEAD commit_type
 258
 259        # -- Our in memory state should match the repository.
 260        #
 261        repository_state curType curHEAD curMERGE_HEAD
 262        if {[string match amend* $commit_type]
 263                && $curType eq {normal}
 264                && $curHEAD eq $HEAD} {
 265        } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
 266                info_popup {Last scanned state does not match repository state.
 267
 268Another Git program has modified this repository since the last scan.  A rescan must be performed before the current branch can be changed.
 269
 270The rescan will be automatically started now.
 271}
 272                unlock_index
 273                rescan ui_ready
 274                delete_this
 275                return
 276        }
 277
 278        if {$curHEAD eq $new_hash} {
 279                _after_readtree $this
 280        } elseif {[is_config_true gui.trustmtime]} {
 281                _readtree $this
 282        } else {
 283                ui_status {Refreshing file status...}
 284                set fd [git_read update-index \
 285                        -q \
 286                        --unmerged \
 287                        --ignore-missing \
 288                        --refresh \
 289                        ]
 290                fconfigure $fd -blocking 0 -translation binary
 291                fileevent $fd readable [cb _refresh_wait $fd]
 292        }
 293}
 294
 295method _refresh_wait {fd} {
 296        read $fd
 297        if {[eof $fd]} {
 298                close $fd
 299                _readtree $this
 300        }
 301}
 302
 303method _name {} {
 304        if {$new_ref eq {}} {
 305                return [string range $new_hash 0 7]
 306        }
 307
 308        set rh refs/heads/
 309        set rn [string length $rh]
 310        if {[string equal -length $rn $rh $new_ref]} {
 311                return [string range $new_ref $rn end]
 312        } else {
 313                return $new_ref
 314        }
 315}
 316
 317method _readtree {} {
 318        global HEAD
 319
 320        set readtree_d {}
 321        $::main_status start \
 322                "Updating working directory to '[_name $this]'..." \
 323                {files checked out}
 324
 325        set fd [git_read --stderr read-tree \
 326                -m \
 327                -u \
 328                -v \
 329                --exclude-per-directory=.gitignore \
 330                $HEAD \
 331                $new_hash \
 332                ]
 333        fconfigure $fd -blocking 0 -translation binary
 334        fileevent $fd readable [cb _readtree_wait $fd]
 335}
 336
 337method _readtree_wait {fd} {
 338        global current_branch
 339
 340        set buf [read $fd]
 341        $::main_status update_meter $buf
 342        append readtree_d $buf
 343
 344        fconfigure $fd -blocking 1
 345        if {![eof $fd]} {
 346                fconfigure $fd -blocking 0
 347                return
 348        }
 349
 350        if {[catch {close $fd}]} {
 351                set err $readtree_d
 352                regsub {^fatal: } $err {} err
 353                $::main_status stop "Aborted checkout of '[_name $this]' (file level merging is required)."
 354                warn_popup "File level merge required.
 355
 356$err
 357
 358Staying on branch '$current_branch'."
 359                unlock_index
 360                delete_this
 361                return
 362        }
 363
 364        $::main_status stop
 365        _after_readtree $this
 366}
 367
 368method _after_readtree {} {
 369        global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
 370        global current_branch is_detached
 371        global ui_comm
 372
 373        set name [_name $this]
 374        set log "checkout: moving"
 375        if {!$is_detached} {
 376                append log " from $current_branch"
 377        }
 378
 379        # -- Move/create HEAD as a symbolic ref.  Core git does not
 380        #    even check for failure here, it Just Works(tm).  If it
 381        #    doesn't we are in some really ugly state that is difficult
 382        #    to recover from within git-gui.
 383        #
 384        set rh refs/heads/
 385        set rn [string length $rh]
 386        if {[string equal -length $rn $rh $new_ref]} {
 387                set new_branch [string range $new_ref $rn end]
 388                if {$is_detached || $current_branch ne $new_branch} {
 389                        append log " to $new_branch"
 390                        if {[catch {
 391                                        git symbolic-ref -m $log HEAD $new_ref
 392                                } err]} {
 393                                _fatal $this $err
 394                        }
 395                        set current_branch $new_branch
 396                        set is_detached 0
 397                }
 398        } else {
 399                if {$new_hash ne $HEAD} {
 400                        append log " to $new_expr"
 401                        if {[catch {
 402                                        _detach_HEAD $log $new_hash
 403                                } err]} {
 404                                _fatal $this $err
 405                        }
 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 [git_read rev-list --pretty=oneline $cur ^$new_hash]
 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}