git-gui / lib / checkout_op.tclon commit sequencer: fix "rebase -i --root" corrupting author header timestamp (67f16e3)
   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
  12field old_hash   ; # commit SHA-1 that was checked out when we started
  13
  14field 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
  21
  22field reset_ok      0; # did the user agree to reset?
  23field fetch_ok      0; # did the fetch succeed?
  24
  25field 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
  28
  29constructor new {expr hash {ref {}}} {
  30        set new_expr $expr
  31        set new_hash $hash
  32        set new_ref  $ref
  33
  34        return $this
  35}
  36
  37method parent {path} {
  38        set parent_w [winfo toplevel $path]
  39}
  40
  41method enable_merge {type} {
  42        set merge_type $type
  43}
  44
  45method enable_fetch {spec} {
  46        set fetch_spec $spec
  47}
  48
  49method remote_source {spec} {
  50        set remote_source $spec
  51}
  52
  53method enable_checkout {co} {
  54        set checkout $co
  55}
  56
  57method enable_create {co} {
  58        set create $co
  59}
  60
  61method run {} {
  62        if {$fetch_spec ne {}} {
  63                global M1B
  64
  65                # 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
  75                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
  82                _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
  89                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
  98                if {!$fetch_ok} {
  99                        delete_this
 100                        return 0
 101                }
 102        }
 103
 104        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
 114        if {$checkout} {
 115                _checkout $this
 116                return 1
 117        }
 118
 119        delete_this
 120        return 1
 121}
 122
 123method _noop {} {}
 124
 125method _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
 138        $w_cons done $ok
 139        set w_cons {}
 140        wm protocol $w WM_DELETE_WINDOW {}
 141
 142        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
 150        set fetch_ok $ok
 151}
 152
 153method _update_ref {} {
 154        global null_sha1 current_branch repo_config
 155
 156        set ref $new_ref
 157        set new $new_hash
 158
 159        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
 171        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
 179                set reflog_msg "branch: Created from $new_expr"
 180                set cur $null_sha1
 181
 182                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
 187                        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
 250        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
 259                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
 267        return 1
 268}
 269
 270method _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}
 278
 279method _start_checkout {} {
 280        global HEAD commit_type
 281
 282        # -- 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.
 290
 291Another Git program has modified this repository since the last scan.  A rescan must be performed before the current branch can be changed.
 292
 293The rescan will be automatically started now.
 294"]
 295                unlock_index
 296                rescan ui_ready
 297                delete_this
 298                return
 299        }
 300
 301        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}
 317
 318method _refresh_wait {fd} {
 319        read $fd
 320        if {[eof $fd]} {
 321                close $fd
 322                _readtree $this
 323        }
 324}
 325
 326method _name {} {
 327        if {$new_ref eq {}} {
 328                return [string range $new_hash 0 7]
 329        }
 330
 331        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}
 339
 340method _readtree {} {
 341        global HEAD
 342
 343        set readtree_d {}
 344        $::main_status start \
 345                [mc "Updating working directory to '%s'..." [_name $this]] \
 346                [mc "files checked out"]
 347
 348        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}
 359
 360method _readtree_wait {fd} {
 361        global current_branch
 362
 363        set buf [read $fd]
 364        $::main_status update_meter $buf
 365        append readtree_d $buf
 366
 367        fconfigure $fd -blocking 1
 368        if {![eof $fd]} {
 369                fconfigure $fd -blocking 0
 370                return
 371        }
 372
 373        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
 379$err
 380
 381" [mc "Staying on branch '%s'." $current_branch]]
 382                unlock_index
 383                delete_this
 384                return
 385        }
 386
 387        $::main_status stop
 388        _after_readtree $this
 389}
 390
 391method _after_readtree {} {
 392        global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
 393        global current_branch is_detached
 394        global ui_comm
 395
 396        set name [_name $this]
 397        set log "checkout: moving"
 398        if {!$is_detached} {
 399                append log " from $current_branch"
 400        }
 401
 402        # -- 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
 434        # -- 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
 451        if {$is_detached} {
 452                info_popup [mc "You are no longer on a local branch.
 453
 454If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."]
 455        }
 456
 457        # -- 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}
 469
 470method _postcheckout_wait {fd_ph} {
 471        global pch_error
 472
 473        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}
 485
 486method _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
 496        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}
 511
 512git-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}
 525
 526method _confirm_reset {cur} {
 527        set reset_ok 0
 528        set name [_name $this]
 529        set gitk [list do_gitk [list $cur ^$new_hash]]
 530
 531        _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
 538        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
 554        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
 565        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
 584        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
 593        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}
 603
 604method _error {msg} {
 605        if {[winfo ismapped $parent_w]} {
 606                set p $parent_w
 607        } else {
 608                set p .
 609        }
 610
 611        tk_messageBox \
 612                -icon error \
 613                -type ok \
 614                -title [wm title $p] \
 615                -parent $p \
 616                -message $msg
 617}
 618
 619method _toplevel {title} {
 620        regsub -all {::} $this {__} w
 621        set w .$w
 622
 623        if {[winfo ismapped $parent_w]} {
 624                set p $parent_w
 625        } else {
 626                set p .
 627        }
 628
 629        toplevel $w
 630        wm title $w $title
 631        wm geometry $w "+[winfo rootx $p]+[winfo rooty $p]"
 632}
 633
 634method _fatal {err} {
 635        error_popup [strcat [mc "Failed to set current branch.
 636
 637This working directory is only partially switched.  We successfully updated your files, but failed to update an internal Git file.
 638
 639This should not have occurred.  %s will now close and give up." [appname]] "
 640
 641$err"]
 642        exit 1
 643}
 644
 645}