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