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