]> git.ipfire.org Git - thirdparty/git.git/blame - lib/checkout_op.tcl
git-gui: Skip unnecessary read-tree work during checkout
[thirdparty/git.git] / lib / checkout_op.tcl
CommitLineData
d41b43eb
SP
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
60f7352f 15field merge_base {}; # merge base if we have another ref involved
d41b43eb
SP
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
b7922306 23field readtree_d {}; # buffered output from read-tree
d41b43eb
SP
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 _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]
77
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]
83 "
84 wm protocol $w WM_DELETE_WINDOW [cb _noop]
85 tkwait window $w
86
87 if {!$fetch_ok} {
88 delete_this
89 return 0
90 }
91 }
92
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).
96 #
97 if {![_update_ref $this]} {
98 delete_this
99 return 0
100 }
101 }
102
103 if {$checkout} {
104 _checkout $this
105 return 1
106 }
107
108 delete_this
109 return 1
110}
111
112method _noop {} {}
113
114method _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
121 }
122 }
123
124 $w_cons done $ok
125 set w_cons {}
126 wm protocol $w WM_DELETE_WINDOW {}
127
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
134 }
135
136 set fetch_ok $ok
137}
138
139method _update_ref {} {
140 global null_sha1 current_branch
141
142 set ref $new_ref
143 set new $new_hash
144
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
152 }
153 } else {
154 set newbranch $ref
155 }
156
157 if {[catch {set cur [git rev-parse --verify "$ref^0"]}]} {
158 # Assume it does not exist, and that is what the error was.
159 #
160 if {!$create} {
161 _error $this "Branch '$newbranch' does not exist."
162 return 0
163 }
164
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.
170 #
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.
177 #
178 set new $cur
179 set new_hash $cur
180 } elseif {$new eq $cur} {
181 # No merge would be required, don't compute anything.
182 #
183 } else {
60f7352f 184 catch {set merge_base [git merge-base $new $cur]}
f66b8a68
SP
185 if {$merge_base eq $cur} {
186 # The current branch is older.
187 #
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.
194 #
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
199 }
d41b43eb 200 }
f66b8a68 201 reset {
d41b43eb
SP
202 # The current branch will lose things.
203 #
204 if {[_confirm_reset $this $cur]} {
205 set reflog_msg "reset $new_expr"
206 } else {
207 return 0
208 }
209 }
f66b8a68 210 default {
eea1ab6e 211 _error $this "Merge strategy '$merge_type' not supported."
f66b8a68
SP
212 return 0
213 }
214 }
d41b43eb
SP
215 }
216 }
217
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.
222 #
223 set update_old $cur
224 return 1
225 }
226
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
232 }
233 }
234
235 return 1
236}
237
238method _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
244 }
245}
246
247method _start_checkout {} {
248 global HEAD commit_type
249
250 # -- Our in memory state should match the repository.
251 #
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.
258
259Another Git program has modified this repository since the last scan. A rescan must be performed before the current branch can be changed.
260
261The rescan will be automatically started now.
262}
263 unlock_index
264 rescan ui_ready
265 delete_this
266 return
267 }
268
dba07411
SP
269 if {$curHEAD eq $new_hash} {
270 _after_readtree $this
271 } elseif {[is_config_true gui.trustmtime]} {
d41b43eb
SP
272 _readtree $this
273 } else {
274 ui_status {Refreshing file status...}
0b812616
SP
275 set fd [git_read update-index \
276 -q \
277 --unmerged \
278 --ignore-missing \
279 --refresh \
280 ]
d41b43eb
SP
281 fconfigure $fd -blocking 0 -translation binary
282 fileevent $fd readable [cb _refresh_wait $fd]
283 }
284}
285
286method _refresh_wait {fd} {
287 read $fd
288 if {[eof $fd]} {
289 close $fd
290 _readtree $this
291 }
292}
293
294method _name {} {
295 if {$new_ref eq {}} {
296 return [string range $new_hash 0 7]
297 }
298
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
305 }
306}
307
308method _readtree {} {
309 global HEAD
310
b7922306
SP
311 set readtree_d {}
312 $::main_status start \
313 "Updating working directory to '[_name $this]'..." \
314 {files checked out}
315
0b812616
SP
316 set fd [git_read --stderr read-tree \
317 -m \
318 -u \
319 -v \
320 --exclude-per-directory=.gitignore \
321 $HEAD \
322 $new_hash \
323 ]
d41b43eb
SP
324 fconfigure $fd -blocking 0 -translation binary
325 fileevent $fd readable [cb _readtree_wait $fd]
326}
327
328method _readtree_wait {fd} {
b7922306
SP
329 global current_branch
330
331 set buf [read $fd]
332 $::main_status update_meter $buf
333 append readtree_d $buf
d41b43eb 334
d41b43eb
SP
335 fconfigure $fd -blocking 1
336 if {![eof $fd]} {
337 fconfigure $fd -blocking 0
338 return
339 }
340
b7922306
SP
341 if {[catch {close $fd}]} {
342 set err $readtree_d
d41b43eb 343 regsub {^fatal: } $err {} err
b7922306 344 $::main_status stop "Aborted checkout of '[_name $this]' (file level merging is required)."
d41b43eb
SP
345 warn_popup "File level merge required.
346
347$err
348
349Staying on branch '$current_branch'."
d41b43eb
SP
350 unlock_index
351 delete_this
352 return
353 }
354
b7922306
SP
355 $::main_status stop
356 _after_readtree $this
357}
358
359method _after_readtree {} {
360 global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
361 global current_branch is_detached
362 global ui_comm
363
364 set name [_name $this]
d41b43eb
SP
365 set log "checkout: moving"
366 if {!$is_detached} {
367 append log " from $current_branch"
368 }
369
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.
374 #
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"
380
381 if {[catch {
382 git symbolic-ref -m $log HEAD $new_ref
383 } err]} {
384 _fatal $this $err
385 }
386 set current_branch $new_branch
387 set is_detached 0
388 } else {
389 append log " to $new_expr"
390
391 if {[catch {
392 _detach_HEAD $log $new_hash
393 } err]} {
394 _fatal $this $err
395 }
396 set current_branch HEAD
397 set is_detached 1
398 }
399
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.
404 #
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
414 }
415 }
416
417 if {$is_detached} {
418 info_popup "You are no longer on a local branch.
419
420If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."
421 }
422
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.
428 #
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'."
440 }
441 delete_this
442}
443
444git-version proc _detach_HEAD {log new} {
445 >= 1.5.3 {
446 git update-ref --no-deref -m $log HEAD $new
447 }
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
455 }
456}
457
458method _confirm_reset {cur} {
459 set reset_ok 0
460 set name [_name $this]
461 set gitk [list do_gitk [list $cur ^$new_hash]]
462
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
469
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
485
486 pack [label $w.msg2 \
487 -anchor w \
488 -justify left \
489 -text {Recovering lost commits may not be easy.} \
490 ]
491 pack [label $w.msg3 \
492 -anchor w \
493 -justify left \
494 -text "Reset '$name'?" \
495 ]
496
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
507 "
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
515
0b812616 516 set fd [git_read rev-list --pretty=oneline $cur ^$new_hash]
d41b43eb
SP
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"
521 }
522 close $fd
523 $list configure -state disabled
524
525 bind $w <Key-v> $gitk
526 bind $w <Visibility> "
527 grab $w
528 focus $w.buttons.cancel
529 "
530 bind $w <Key-Return> [list destroy $w]
531 bind $w <Key-Escape> [list destroy $w]
532 tkwait window $w
533 return $reset_ok
534}
535
536method _error {msg} {
537 if {[winfo ismapped $parent_w]} {
538 set p $parent_w
539 } else {
540 set p .
541 }
542
543 tk_messageBox \
544 -icon error \
545 -type ok \
546 -title [wm title $p] \
547 -parent $p \
548 -message $msg
549}
550
551method _toplevel {title} {
552 regsub -all {::} $this {__} w
553 set w .$w
554
555 if {[winfo ismapped $parent_w]} {
556 set p $parent_w
557 } else {
558 set p .
559 }
560
561 toplevel $w
562 wm title $w $title
563 wm geometry $w "+[winfo rootx $p]+[winfo rooty $p]"
564}
565
566method _fatal {err} {
567 error_popup "Failed to set current branch.
568
569This working directory is only partially switched. We successfully updated your files, but failed to update an internal Git file.
570
571This should not have occurred. [appname] will now close and give up.
572
573$err"
574 exit 1
575}
576
577}