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