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