1 # git-gui commit checkout support
2 # Copyright (C) 2007 Shawn Pearce
6 field w {}; # our window (if we have one)
7 field w_cons {}; # embedded console window object
9 field new_expr ; # expression the user saw/thinks this is
10 field new_hash ; # commit SHA-1 we are switching to
11 field new_ref ; # ref we are updating/creating
12 field old_hash ; # commit SHA-1 that was checked out when we started
14 field parent_w .; # window that started us
15 field merge_type none; # type of merge to apply to existing branch
16 field merge_base {}; # merge base if we have another ref involved
17 field fetch_spec {}; # refetch tracking branch if used?
18 field checkout 1; # actually checkout the branch?
19 field create 0; # create the branch if it doesn't exist?
20 field remote_source {}; # same as fetch_spec, to setup tracking
22 field reset_ok 0; # did the user agree to reset?
23 field fetch_ok 0; # did the fetch succeed?
25 field readtree_d {}; # buffered output from read-tree
26 field update_old {}; # was the update-ref call deferred?
27 field reflog_msg {}; # log message for the update-ref call
29 constructor new {expr hash {ref {}}} {
37 method parent {path} {
38 set parent_w [winfo toplevel $path]
41 method enable_merge {type} {
45 method enable_fetch {spec} {
49 method remote_source {spec} {
50 set remote_source $spec
53 method enable_checkout {co} {
57 method enable_create {co} {
62 if {$fetch_spec ne {}} {
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.
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
75 set cmd [list git fetch $remote]
77 lappend cmd +$r_head:$l_trck
82 _toplevel $this {Refreshing Tracking Branch}
83 set w_cons [::console::embed \
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]
89 bind $w <$M1B-Key-w> break
90 bind $w <$M1B-Key-W> break
91 bind $w <Visibility> "
95 wm protocol $w WM_DELETE_WINDOW [cb _noop]
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).
108 if {![_update_ref $this]} {
125 method _finish_fetch {ok} {
127 set l_trck [lindex $fetch_spec 0]
129 set l_trck FETCH_HEAD
131 if {[catch {set new_hash [git rev-parse --verify "$l_trck^0"]} err]} {
133 $w_cons insert [mc "fatal: Cannot resolve %s" $l_trck]
140 wm protocol $w WM_DELETE_WINDOW {}
146 button $w.close -text [mc Close] -command [list destroy $w]
147 pack $w.close -side bottom -anchor e -padx 10 -pady 10
153 method _update_ref {} {
154 global null_sha1 current_branch repo_config
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} {
171 if {[catch {set cur [git rev-parse --verify "$ref^0"]}]} {
172 # Assume it does not exist, and that is what the error was.
175 _error $this [mc "Branch '%s' does not exist." $newbranch]
179 set reflog_msg "branch: Created from $new_expr"
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} {
187 set c_remote [lindex $remote_source 1]
188 set c_merge [lindex $remote_source 2]
190 git config branch.$newbranch.remote $c_remote
191 git config branch.$newbranch.merge $c_merge
193 _error $this [strcat \
194 [mc "Failed to configure simplified git-pull for '%s'." $newbranch] \
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.
202 _error $this [mc "Branch '%s' already exists." $newbranch]
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.
211 } elseif {$new eq $cur} {
212 # No merge would be required, don't compute anything.
215 catch {set merge_base [git merge-base $new $cur]}
216 if {$merge_base eq $cur} {
217 # The current branch is older.
219 set reflog_msg "merge $new_expr: Fast-forward"
221 switch -- $merge_type {
223 if {$merge_base eq $new} {
224 # The current branch is actually newer.
229 _error $this [mc "Branch '%s' already exists.\n\nIt cannot fast-forward to %s.\nA merge is required." $newbranch $new_expr]
234 # The current branch will lose things.
236 if {[_confirm_reset $this $cur]} {
237 set reflog_msg "reset $new_expr"
243 _error $this [mc "Merge strategy '%s' not supported." $merge_type]
252 # No so fast. We should defer this in case
253 # we cannot update the working directory.
260 git update-ref -m $reflog_msg $ref $new $cur
262 _error $this [strcat [mc "Failed to update '%s'." $newbranch] "\n\n$err"]
270 method _checkout {} {
271 if {[lock_index checkout_op]} {
272 after idle [cb _start_checkout]
274 _error $this [mc "Staging area (index) is already locked."]
279 method _start_checkout {} {
280 global HEAD commit_type
282 # -- Our in memory state should match the repository.
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.
291 Another Git program has modified this repository since the last scan. A rescan must be performed before the current branch can be changed.
293 The rescan will be automatically started now.
301 if {$old_hash eq $new_hash} {
302 _after_readtree $this
303 } elseif {[is_config_true gui.trustmtime]} {
306 ui_status [mc "Refreshing file status..."]
307 set fd [git_read update-index \
313 fconfigure $fd -blocking 0 -translation binary
314 fileevent $fd readable [cb _refresh_wait $fd]
318 method _refresh_wait {fd} {
327 if {$new_ref eq {}} {
328 return [string range $new_hash 0 7]
332 set rn [string length $rh]
333 if {[string equal -length $rn $rh $new_ref]} {
334 return [string range $new_ref $rn end]
340 method _readtree {} {
344 set status_bar_operation [$::main_status start \
345 [mc "Updating working directory to '%s'..." [_name $this]] \
346 [mc "files checked out"]]
348 set fd [git_read --stderr read-tree \
352 --exclude-per-directory=.gitignore \
356 fconfigure $fd -blocking 0 -translation binary
357 fileevent $fd readable [cb _readtree_wait $fd $status_bar_operation]
360 method _readtree_wait {fd status_bar_operation} {
361 global current_branch
364 $status_bar_operation update_meter $buf
365 append readtree_d $buf
367 fconfigure $fd -blocking 1
369 fconfigure $fd -blocking 0
370 $status_bar_operation stop
374 if {[catch {close $fd}]} {
376 regsub {^fatal: } $err {} err
377 $status_bar_operation stop [mc "Aborted checkout of '%s' (file level merging is required)." [_name $this]]
378 warn_popup [strcat [mc "File level merge required."] "
382 " [mc "Staying on branch '%s'." $current_branch]]
388 $status_bar_operation stop
389 _after_readtree $this
392 method _after_readtree {} {
393 global commit_type HEAD MERGE_HEAD PARENT
394 global current_branch is_detached
397 set name [_name $this]
398 set log "checkout: moving"
400 append log " from $current_branch"
403 # -- Move/create HEAD as a symbolic ref. Core git does not
404 # even check for failure here, it Just Works(tm). If it
405 # doesn't we are in some really ugly state that is difficult
406 # to recover from within git-gui.
409 set rn [string length $rh]
410 if {[string equal -length $rn $rh $new_ref]} {
411 set new_branch [string range $new_ref $rn end]
412 if {$is_detached || $current_branch ne $new_branch} {
413 append log " to $new_branch"
415 git symbolic-ref -m $log HEAD $new_ref
419 set current_branch $new_branch
423 if {!$is_detached || $new_hash ne $HEAD} {
424 append log " to $new_expr"
426 _detach_HEAD $log $new_hash
431 set current_branch HEAD
435 # -- We had to defer updating the branch itself until we
436 # knew the working directory would update. So now we
437 # need to finish that work. If it fails we're in big
440 if {$update_old ne {}} {
453 info_popup [mc "You are no longer on a local branch.
455 If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."]
458 # -- Run the post-checkout hook.
460 set fd_ph [githook_read post-checkout $old_hash $new_hash 1]
464 fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
465 fileevent $fd_ph readable [cb _postcheckout_wait $fd_ph]
467 _update_repo_state $this
471 method _postcheckout_wait {fd_ph} {
474 append pch_error [read $fd_ph]
475 fconfigure $fd_ph -blocking 1
477 if {[catch {close $fd_ph}]} {
478 hook_failed_popup post-checkout $pch_error 0
481 _update_repo_state $this
484 fconfigure $fd_ph -blocking 0
487 method _update_repo_state {} {
488 # -- Update our repository state. If we were previously in
489 # amend mode we need to toss the current buffer and do a
490 # full rescan to update our file lists. If we weren't in
491 # amend mode our file lists are accurate and we can avoid
494 global commit_type_is_amend commit_type HEAD MERGE_HEAD PARENT
498 set name [_name $this]
499 set commit_type_is_amend 0
500 if {[string match amend* $commit_type]} {
501 $ui_comm delete 0.0 end
503 $ui_comm edit modified false
504 rescan [list ui_status [mc "Checked out '%s'." $name]]
506 repository_state commit_type HEAD MERGE_HEAD
508 ui_status [mc "Checked out '%s'." $name]
513 git-version proc _detach_HEAD {log new} {
515 git update-ref --no-deref -m $log HEAD $new
521 fconfigure $fd -translation lf -encoding utf-8
527 method _confirm_reset {cur} {
529 set name [_name $this]
530 set gitk [list do_gitk [list $cur ^$new_hash]]
532 _toplevel $this {Confirm Branch Reset}
533 pack [label $w.msg1 \
536 -text [mc "Resetting '%s' to '%s' will lose the following commits:" $name $new_expr]\
546 -xscrollcommand [list $w.list.sbx set] \
547 -yscrollcommand [list $w.list.sby set]
548 scrollbar $w.list.sbx -orient h -command [list $list xview]
549 scrollbar $w.list.sby -orient v -command [list $list yview]
550 pack $w.list.sbx -fill x -side bottom
551 pack $w.list.sby -fill y -side right
552 pack $list -fill both -expand 1
553 pack $w.list -fill both -expand 1 -padx 5 -pady 5
555 pack [label $w.msg2 \
558 -text [mc "Recovering lost commits may not be easy."] \
560 pack [label $w.msg3 \
563 -text [mc "Reset '%s'?" $name] \
567 button $w.buttons.visualize \
568 -text [mc Visualize] \
570 pack $w.buttons.visualize -side left
571 button $w.buttons.reset \
577 pack $w.buttons.reset -side right
578 button $w.buttons.cancel \
581 -command [list destroy $w]
582 pack $w.buttons.cancel -side right -padx 5
583 pack $w.buttons -side bottom -fill x -pady 10 -padx 10
585 set fd [git_read rev-list --pretty=oneline $cur ^$new_hash]
586 while {[gets $fd line] > 0} {
587 set abbr [string range $line 0 7]
588 set subj [string range $line 41 end]
589 $list insert end "$abbr $subj\n"
592 $list configure -state disabled
594 bind $w <Key-v> $gitk
595 bind $w <Visibility> "
597 focus $w.buttons.cancel
599 bind $w <Key-Return> [list destroy $w]
600 bind $w <Key-Escape> [list destroy $w]
605 method _error {msg} {
606 if {[winfo ismapped $parent_w]} {
615 -title [wm title $p] \
620 method _toplevel {title} {
621 regsub -all {::} $this {__} w
624 if {[winfo ismapped $parent_w]} {
632 wm geometry $w "+[winfo rootx $p]+[winfo rooty $p]"
635 method _fatal {err} {
636 error_popup [strcat [mc "Failed to set current branch.
638 This working directory is only partially switched. We successfully updated your files, but failed to update an internal Git file.
640 This should not have occurred. %s will now close and give up." [appname]] "