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