554c107032458f3b9585b58d7920924acf0ecb69
   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 {[is_config_true gui.trustmtime]} {
 270                _readtree $this
 271        } else {
 272                ui_status {Refreshing file status...}
 273                set fd [git_read update-index \
 274                        -q \
 275                        --unmerged \
 276                        --ignore-missing \
 277                        --refresh \
 278                        ]
 279                fconfigure $fd -blocking 0 -translation binary
 280                fileevent $fd readable [cb _refresh_wait $fd]
 281        }
 282}
 283
 284method _refresh_wait {fd} {
 285        read $fd
 286        if {[eof $fd]} {
 287                close $fd
 288                _readtree $this
 289        }
 290}
 291
 292method _name {} {
 293        if {$new_ref eq {}} {
 294                return [string range $new_hash 0 7]
 295        }
 296
 297        set rh refs/heads/
 298        set rn [string length $rh]
 299        if {[string equal -length $rn $rh $new_ref]} {
 300                return [string range $new_ref $rn end]
 301        } else {
 302                return $new_ref
 303        }
 304}
 305
 306method _readtree {} {
 307        global HEAD
 308
 309        set readtree_d {}
 310        $::main_status start \
 311                "Updating working directory to '[_name $this]'..." \
 312                {files checked out}
 313
 314        set fd [git_read --stderr read-tree \
 315                -m \
 316                -u \
 317                -v \
 318                --exclude-per-directory=.gitignore \
 319                $HEAD \
 320                $new_hash \
 321                ]
 322        fconfigure $fd -blocking 0 -translation binary
 323        fileevent $fd readable [cb _readtree_wait $fd]
 324}
 325
 326method _readtree_wait {fd} {
 327        global current_branch
 328
 329        set buf [read $fd]
 330        $::main_status update_meter $buf
 331        append readtree_d $buf
 332
 333        fconfigure $fd -blocking 1
 334        if {![eof $fd]} {
 335                fconfigure $fd -blocking 0
 336                return
 337        }
 338
 339        if {[catch {close $fd}]} {
 340                set err $readtree_d
 341                regsub {^fatal: } $err {} err
 342                $::main_status stop "Aborted checkout of '[_name $this]' (file level merging is required)."
 343                warn_popup "File level merge required.
 344
 345$err
 346
 347Staying on branch '$current_branch'."
 348                unlock_index
 349                delete_this
 350                return
 351        }
 352
 353        $::main_status stop
 354        _after_readtree $this
 355}
 356
 357method _after_readtree {} {
 358        global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
 359        global current_branch is_detached
 360        global ui_comm
 361
 362        set name [_name $this]
 363        set log "checkout: moving"
 364        if {!$is_detached} {
 365                append log " from $current_branch"
 366        }
 367
 368        # -- Move/create HEAD as a symbolic ref.  Core git does not
 369        #    even check for failure here, it Just Works(tm).  If it
 370        #    doesn't we are in some really ugly state that is difficult
 371        #    to recover from within git-gui.
 372        #
 373        set rh refs/heads/
 374        set rn [string length $rh]
 375        if {[string equal -length $rn $rh $new_ref]} {
 376                set new_branch [string range $new_ref $rn end]
 377                append log " to $new_branch"
 378
 379                if {[catch {
 380                                git symbolic-ref -m $log HEAD $new_ref
 381                        } err]} {
 382                        _fatal $this $err
 383                }
 384                set current_branch $new_branch
 385                set is_detached 0
 386        } else {
 387                append log " to $new_expr"
 388
 389                if {[catch {
 390                                _detach_HEAD $log $new_hash
 391                        } err]} {
 392                        _fatal $this $err
 393                }
 394                set current_branch HEAD
 395                set is_detached 1
 396        }
 397
 398        # -- We had to defer updating the branch itself until we
 399        #    knew the working directory would update.  So now we
 400        #    need to finish that work.  If it fails we're in big
 401        #    trouble.
 402        #
 403        if {$update_old ne {}} {
 404                if {[catch {
 405                                git update-ref \
 406                                        -m $reflog_msg \
 407                                        $new_ref \
 408                                        $new_hash \
 409                                        $update_old
 410                        } err]} {
 411                        _fatal $this $err
 412                }
 413        }
 414
 415        if {$is_detached} {
 416                info_popup "You are no longer on a local branch.
 417
 418If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."
 419        }
 420
 421        # -- Update our repository state.  If we were previously in
 422        #    amend mode we need to toss the current buffer and do a
 423        #    full rescan to update our file lists.  If we weren't in
 424        #    amend mode our file lists are accurate and we can avoid
 425        #    the rescan.
 426        #
 427        unlock_index
 428        set selected_commit_type new
 429        if {[string match amend* $commit_type]} {
 430                $ui_comm delete 0.0 end
 431                $ui_comm edit reset
 432                $ui_comm edit modified false
 433                rescan [list ui_status "Checked out '$name'."]
 434        } else {
 435                repository_state commit_type HEAD MERGE_HEAD
 436                set PARENT $HEAD
 437                ui_status "Checked out '$name'."
 438        }
 439        delete_this
 440}
 441
 442git-version proc _detach_HEAD {log new} {
 443        >= 1.5.3 {
 444                git update-ref --no-deref -m $log HEAD $new
 445        }
 446        default {
 447                set p [gitdir HEAD]
 448                file delete $p
 449                set fd [open $p w]
 450                fconfigure $fd -translation lf -encoding utf-8
 451                puts $fd $new
 452                close $fd
 453        }
 454}
 455
 456method _confirm_reset {cur} {
 457        set reset_ok 0
 458        set name [_name $this]
 459        set gitk [list do_gitk [list $cur ^$new_hash]]
 460
 461        _toplevel $this {Confirm Branch Reset}
 462        pack [label $w.msg1 \
 463                -anchor w \
 464                -justify left \
 465                -text "Resetting '$name' to $new_expr will lose the following commits:" \
 466                ] -anchor w
 467
 468        set list $w.list.l
 469        frame $w.list
 470        text $list \
 471                -font font_diff \
 472                -width 80 \
 473                -height 10 \
 474                -wrap none \
 475                -xscrollcommand [list $w.list.sbx set] \
 476                -yscrollcommand [list $w.list.sby set]
 477        scrollbar $w.list.sbx -orient h -command [list $list xview]
 478        scrollbar $w.list.sby -orient v -command [list $list yview]
 479        pack $w.list.sbx -fill x -side bottom
 480        pack $w.list.sby -fill y -side right
 481        pack $list -fill both -expand 1
 482        pack $w.list -fill both -expand 1 -padx 5 -pady 5
 483
 484        pack [label $w.msg2 \
 485                -anchor w \
 486                -justify left \
 487                -text {Recovering lost commits may not be easy.} \
 488                ]
 489        pack [label $w.msg3 \
 490                -anchor w \
 491                -justify left \
 492                -text "Reset '$name'?" \
 493                ]
 494
 495        frame $w.buttons
 496        button $w.buttons.visualize \
 497                -text Visualize \
 498                -command $gitk
 499        pack $w.buttons.visualize -side left
 500        button $w.buttons.reset \
 501                -text Reset \
 502                -command "
 503                        set @reset_ok 1
 504                        destroy $w
 505                "
 506        pack $w.buttons.reset -side right
 507        button $w.buttons.cancel \
 508                -default active \
 509                -text Cancel \
 510                -command [list destroy $w]
 511        pack $w.buttons.cancel -side right -padx 5
 512        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
 513
 514        set fd [git_read rev-list --pretty=oneline $cur ^$new_hash]
 515        while {[gets $fd line] > 0} {
 516                set abbr [string range $line 0 7]
 517                set subj [string range $line 41 end]
 518                $list insert end "$abbr  $subj\n"
 519        }
 520        close $fd
 521        $list configure -state disabled
 522
 523        bind $w    <Key-v> $gitk
 524        bind $w <Visibility> "
 525                grab $w
 526                focus $w.buttons.cancel
 527        "
 528        bind $w <Key-Return> [list destroy $w]
 529        bind $w <Key-Escape> [list destroy $w]
 530        tkwait window $w
 531        return $reset_ok
 532}
 533
 534method _error {msg} {
 535        if {[winfo ismapped $parent_w]} {
 536                set p $parent_w
 537        } else {
 538                set p .
 539        }
 540
 541        tk_messageBox \
 542                -icon error \
 543                -type ok \
 544                -title [wm title $p] \
 545                -parent $p \
 546                -message $msg
 547}
 548
 549method _toplevel {title} {
 550        regsub -all {::} $this {__} w
 551        set w .$w
 552
 553        if {[winfo ismapped $parent_w]} {
 554                set p $parent_w
 555        } else {
 556                set p .
 557        }
 558
 559        toplevel $w
 560        wm title $w $title
 561        wm geometry $w "+[winfo rootx $p]+[winfo rooty $p]"
 562}
 563
 564method _fatal {err} {
 565        error_popup "Failed to set current branch.
 566
 567This working directory is only partially switched.  We successfully updated your files, but failed to update an internal Git file.
 568
 569This should not have occurred.  [appname] will now close and give up.
 570
 571$err"
 572        exit 1
 573}
 574
 575}