git-gui: Skip unnecessary read-tree work during checkout
[git-gui/me-and.git] / lib / checkout_op.tcl
blobcb04d1e57e254858e1ceca36158e40aec570034f
1 # git-gui commit checkout support
2 # Copyright (C) 2007 Shawn Pearce
4 class checkout_op {
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
13 field parent_w .; # window that started us
14 field merge_type none; # type of merge to apply to existing branch
15 field merge_base {}; # merge base if we have another ref involved
16 field fetch_spec {}; # refetch tracking branch if used?
17 field checkout 1; # actually checkout the branch?
18 field create 0; # create the branch if it doesn't exist?
20 field reset_ok 0; # did the user agree to reset?
21 field fetch_ok 0; # did the fetch succeed?
23 field readtree_d {}; # buffered output from read-tree
24 field update_old {}; # was the update-ref call deferred?
25 field reflog_msg {}; # log message for the update-ref call
27 constructor new {expr hash {ref {}}} {
28 set new_expr $expr
29 set new_hash $hash
30 set new_ref $ref
32 return $this
35 method parent {path} {
36 set parent_w [winfo toplevel $path]
39 method enable_merge {type} {
40 set merge_type $type
43 method enable_fetch {spec} {
44 set fetch_spec $spec
47 method enable_checkout {co} {
48 set checkout $co
51 method enable_create {co} {
52 set create $co
55 method run {} {
56 if {$fetch_spec ne {}} {
57 global M1B
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.
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
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]
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]
84 wm protocol $w WM_DELETE_WINDOW [cb _noop]
85 tkwait window $w
87 if {!$fetch_ok} {
88 delete_this
89 return 0
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).
97 if {![_update_ref $this]} {
98 delete_this
99 return 0
103 if {$checkout} {
104 _checkout $this
105 return 1
108 delete_this
109 return 1
112 method _noop {} {}
114 method _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
124 $w_cons done $ok
125 set w_cons {}
126 wm protocol $w WM_DELETE_WINDOW {}
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
136 set fetch_ok $ok
139 method _update_ref {} {
140 global null_sha1 current_branch
142 set ref $new_ref
143 set new $new_hash
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
153 } else {
154 set newbranch $ref
157 if {[catch {set cur [git rev-parse --verify "$ref^0"]}]} {
158 # Assume it does not exist, and that is what the error was.
160 if {!$create} {
161 _error $this "Branch '$newbranch' does not exist."
162 return 0
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.
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.
178 set new $cur
179 set new_hash $cur
180 } elseif {$new eq $cur} {
181 # No merge would be required, don't compute anything.
183 } else {
184 catch {set merge_base [git merge-base $new $cur]}
185 if {$merge_base eq $cur} {
186 # The current branch is older.
188 set reflog_msg "merge $new_expr: Fast-forward"
189 } else {
190 switch -- $merge_type {
191 ff {
192 if {$merge_base eq $new} {
193 # The current branch is actually newer.
195 set new $cur
196 } else {
197 _error $this "Branch '$newbranch' already exists.\n\nIt cannot fast-forward to $new_expr.\nA merge is required."
198 return 0
201 reset {
202 # The current branch will lose things.
204 if {[_confirm_reset $this $cur]} {
205 set reflog_msg "reset $new_expr"
206 } else {
207 return 0
210 default {
211 _error $this "Merge strategy '$merge_type' not supported."
212 return 0
218 if {$new ne $cur} {
219 if {$is_current} {
220 # No so fast. We should defer this in case
221 # we cannot update the working directory.
223 set update_old $cur
224 return 1
227 if {[catch {
228 git update-ref -m $reflog_msg $ref $new $cur
229 } err]} {
230 _error $this "Failed to update '$newbranch'.\n\n$err"
231 return 0
235 return 1
238 method _checkout {} {
239 if {[lock_index checkout_op]} {
240 after idle [cb _start_checkout]
241 } else {
242 _error $this "Index is already locked."
243 delete_this
247 method _start_checkout {} {
248 global HEAD commit_type
250 # -- Our in memory state should match the repository.
252 repository_state curType curHEAD curMERGE_HEAD
253 if {[string match amend* $commit_type]
254 && $curType eq {normal}
255 && $curHEAD eq $HEAD} {
256 } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
257 info_popup {Last scanned state does not match repository state.
259 Another Git program has modified this repository since the last scan. A rescan must be performed before the current branch can be changed.
261 The rescan will be automatically started now.
263 unlock_index
264 rescan ui_ready
265 delete_this
266 return
269 if {$curHEAD eq $new_hash} {
270 _after_readtree $this
271 } elseif {[is_config_true gui.trustmtime]} {
272 _readtree $this
273 } else {
274 ui_status {Refreshing file status...}
275 set fd [git_read update-index \
276 -q \
277 --unmerged \
278 --ignore-missing \
279 --refresh \
281 fconfigure $fd -blocking 0 -translation binary
282 fileevent $fd readable [cb _refresh_wait $fd]
286 method _refresh_wait {fd} {
287 read $fd
288 if {[eof $fd]} {
289 close $fd
290 _readtree $this
294 method _name {} {
295 if {$new_ref eq {}} {
296 return [string range $new_hash 0 7]
299 set rh refs/heads/
300 set rn [string length $rh]
301 if {[string equal -length $rn $rh $new_ref]} {
302 return [string range $new_ref $rn end]
303 } else {
304 return $new_ref
308 method _readtree {} {
309 global HEAD
311 set readtree_d {}
312 $::main_status start \
313 "Updating working directory to '[_name $this]'..." \
314 {files checked out}
316 set fd [git_read --stderr read-tree \
317 -m \
318 -u \
319 -v \
320 --exclude-per-directory=.gitignore \
321 $HEAD \
322 $new_hash \
324 fconfigure $fd -blocking 0 -translation binary
325 fileevent $fd readable [cb _readtree_wait $fd]
328 method _readtree_wait {fd} {
329 global current_branch
331 set buf [read $fd]
332 $::main_status update_meter $buf
333 append readtree_d $buf
335 fconfigure $fd -blocking 1
336 if {![eof $fd]} {
337 fconfigure $fd -blocking 0
338 return
341 if {[catch {close $fd}]} {
342 set err $readtree_d
343 regsub {^fatal: } $err {} err
344 $::main_status stop "Aborted checkout of '[_name $this]' (file level merging is required)."
345 warn_popup "File level merge required.
347 $err
349 Staying on branch '$current_branch'."
350 unlock_index
351 delete_this
352 return
355 $::main_status stop
356 _after_readtree $this
359 method _after_readtree {} {
360 global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
361 global current_branch is_detached
362 global ui_comm
364 set name [_name $this]
365 set log "checkout: moving"
366 if {!$is_detached} {
367 append log " from $current_branch"
370 # -- Move/create HEAD as a symbolic ref. Core git does not
371 # even check for failure here, it Just Works(tm). If it
372 # doesn't we are in some really ugly state that is difficult
373 # to recover from within git-gui.
375 set rh refs/heads/
376 set rn [string length $rh]
377 if {[string equal -length $rn $rh $new_ref]} {
378 set new_branch [string range $new_ref $rn end]
379 append log " to $new_branch"
381 if {[catch {
382 git symbolic-ref -m $log HEAD $new_ref
383 } err]} {
384 _fatal $this $err
386 set current_branch $new_branch
387 set is_detached 0
388 } else {
389 append log " to $new_expr"
391 if {[catch {
392 _detach_HEAD $log $new_hash
393 } err]} {
394 _fatal $this $err
396 set current_branch HEAD
397 set is_detached 1
400 # -- We had to defer updating the branch itself until we
401 # knew the working directory would update. So now we
402 # need to finish that work. If it fails we're in big
403 # trouble.
405 if {$update_old ne {}} {
406 if {[catch {
407 git update-ref \
408 -m $reflog_msg \
409 $new_ref \
410 $new_hash \
411 $update_old
412 } err]} {
413 _fatal $this $err
417 if {$is_detached} {
418 info_popup "You are no longer on a local branch.
420 If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."
423 # -- Update our repository state. If we were previously in
424 # amend mode we need to toss the current buffer and do a
425 # full rescan to update our file lists. If we weren't in
426 # amend mode our file lists are accurate and we can avoid
427 # the rescan.
429 unlock_index
430 set selected_commit_type new
431 if {[string match amend* $commit_type]} {
432 $ui_comm delete 0.0 end
433 $ui_comm edit reset
434 $ui_comm edit modified false
435 rescan [list ui_status "Checked out '$name'."]
436 } else {
437 repository_state commit_type HEAD MERGE_HEAD
438 set PARENT $HEAD
439 ui_status "Checked out '$name'."
441 delete_this
444 git-version proc _detach_HEAD {log new} {
445 >= 1.5.3 {
446 git update-ref --no-deref -m $log HEAD $new
448 default {
449 set p [gitdir HEAD]
450 file delete $p
451 set fd [open $p w]
452 fconfigure $fd -translation lf -encoding utf-8
453 puts $fd $new
454 close $fd
458 method _confirm_reset {cur} {
459 set reset_ok 0
460 set name [_name $this]
461 set gitk [list do_gitk [list $cur ^$new_hash]]
463 _toplevel $this {Confirm Branch Reset}
464 pack [label $w.msg1 \
465 -anchor w \
466 -justify left \
467 -text "Resetting '$name' to $new_expr will lose the following commits:" \
468 ] -anchor w
470 set list $w.list.l
471 frame $w.list
472 text $list \
473 -font font_diff \
474 -width 80 \
475 -height 10 \
476 -wrap none \
477 -xscrollcommand [list $w.list.sbx set] \
478 -yscrollcommand [list $w.list.sby set]
479 scrollbar $w.list.sbx -orient h -command [list $list xview]
480 scrollbar $w.list.sby -orient v -command [list $list yview]
481 pack $w.list.sbx -fill x -side bottom
482 pack $w.list.sby -fill y -side right
483 pack $list -fill both -expand 1
484 pack $w.list -fill both -expand 1 -padx 5 -pady 5
486 pack [label $w.msg2 \
487 -anchor w \
488 -justify left \
489 -text {Recovering lost commits may not be easy.} \
491 pack [label $w.msg3 \
492 -anchor w \
493 -justify left \
494 -text "Reset '$name'?" \
497 frame $w.buttons
498 button $w.buttons.visualize \
499 -text Visualize \
500 -command $gitk
501 pack $w.buttons.visualize -side left
502 button $w.buttons.reset \
503 -text Reset \
504 -command "
505 set @reset_ok 1
506 destroy $w
508 pack $w.buttons.reset -side right
509 button $w.buttons.cancel \
510 -default active \
511 -text Cancel \
512 -command [list destroy $w]
513 pack $w.buttons.cancel -side right -padx 5
514 pack $w.buttons -side bottom -fill x -pady 10 -padx 10
516 set fd [git_read rev-list --pretty=oneline $cur ^$new_hash]
517 while {[gets $fd line] > 0} {
518 set abbr [string range $line 0 7]
519 set subj [string range $line 41 end]
520 $list insert end "$abbr $subj\n"
522 close $fd
523 $list configure -state disabled
525 bind $w <Key-v> $gitk
526 bind $w <Visibility> "
527 grab $w
528 focus $w.buttons.cancel
530 bind $w <Key-Return> [list destroy $w]
531 bind $w <Key-Escape> [list destroy $w]
532 tkwait window $w
533 return $reset_ok
536 method _error {msg} {
537 if {[winfo ismapped $parent_w]} {
538 set p $parent_w
539 } else {
540 set p .
543 tk_messageBox \
544 -icon error \
545 -type ok \
546 -title [wm title $p] \
547 -parent $p \
548 -message $msg
551 method _toplevel {title} {
552 regsub -all {::} $this {__} w
553 set w .$w
555 if {[winfo ismapped $parent_w]} {
556 set p $parent_w
557 } else {
558 set p .
561 toplevel $w
562 wm title $w $title
563 wm geometry $w "+[winfo rootx $p]+[winfo rooty $p]"
566 method _fatal {err} {
567 error_popup "Failed to set current branch.
569 This working directory is only partially switched. We successfully updated your files, but failed to update an internal Git file.
571 This should not have occurred. [appname] will now close and give up.
573 $err"
574 exit 1