git-gui: allow Ctrl+T to toggle multiple paths
[git/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
c9498339 12field old_hash ; # commit SHA-1 that was checked out when we started
d41b43eb
SP
13
14field parent_w .; # window that started us
15field merge_type none; # type of merge to apply to existing branch
60f7352f 16field merge_base {}; # merge base if we have another ref involved
d41b43eb
SP
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?
fe70225d 20field remote_source {}; # same as fetch_spec, to setup tracking
d41b43eb
SP
21
22field reset_ok 0; # did the user agree to reset?
23field fetch_ok 0; # did the fetch succeed?
24
b7922306 25field readtree_d {}; # buffered output from read-tree
d41b43eb
SP
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
fe70225d
SP
49method remote_source {spec} {
50 set remote_source $spec
51}
52
d41b43eb
SP
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
54febd4f
SP
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
d41b43eb
SP
82 _toplevel $this {Refreshing Tracking Branch}
83 set w_cons [::console::embed \
84 $w.console \
1ac17950 85 [mc "Fetching %s from %s" $r_name $remote]]
d41b43eb 86 pack $w.console -fill both -expand 1
54febd4f 87 $w_cons exec $cmd [cb _finish_fetch]
d41b43eb
SP
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]
54febd4f
SP
128 if {$l_trck eq {}} {
129 set l_trck FETCH_HEAD
130 }
d41b43eb
SP
131 if {[catch {set new_hash [git rev-parse --verify "$l_trck^0"]} err]} {
132 set ok 0
c8c4854b 133 $w_cons insert [mc "fatal: Cannot resolve %s" $l_trck]
d41b43eb
SP
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 {
1ac17950 146 button $w.close -text [mc Close] -command [list destroy $w]
d41b43eb
SP
147 pack $w.close -side bottom -anchor e -padx 10 -pady 10
148 }
149
150 set fetch_ok $ok
151}
152
153method _update_ref {} {
fe70225d 154 global null_sha1 current_branch repo_config
d41b43eb
SP
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} {
1ac17950 175 _error $this [mc "Branch '%s' does not exist." $newbranch]
d41b43eb
SP
176 return 0
177 }
178
179 set reflog_msg "branch: Created from $new_expr"
180 set cur $null_sha1
fe70225d
SP
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 }
d41b43eb
SP
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 #
1ac17950 202 _error $this [mc "Branch '%s' already exists." $newbranch]
d41b43eb
SP
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 {
60f7352f 215 catch {set merge_base [git merge-base $new $cur]}
f66b8a68
SP
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
7bd197c7 227 set new_hash $cur
f66b8a68 228 } else {
1ac17950 229 _error $this [mc "Branch '%s' already exists.\n\nIt cannot fast-forward to %s.\nA merge is required." $newbranch $new_expr]
f66b8a68
SP
230 return 0
231 }
d41b43eb 232 }
f66b8a68 233 reset {
d41b43eb
SP
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 }
f66b8a68 242 default {
1ac17950 243 _error $this [mc "Merge strategy '%s' not supported." $merge_type]
f66b8a68
SP
244 return 0
245 }
246 }
d41b43eb
SP
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]} {
31bb1d1b 262 _error $this [strcat [mc "Failed to update '%s'." $newbranch] "\n\n$err"]
d41b43eb
SP
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 {
1ac17950 274 _error $this [mc "Staging area (index) is already locked."]
d41b43eb
SP
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 #
c9498339 284 repository_state curType old_hash curMERGE_HEAD
d41b43eb
SP
285 if {[string match amend* $commit_type]
286 && $curType eq {normal}
c9498339
JL
287 && $old_hash eq $HEAD} {
288 } elseif {$commit_type ne $curType || $HEAD ne $old_hash} {
1ac17950 289 info_popup [mc "Last scanned state does not match repository state.
d41b43eb
SP
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.
1ac17950 294"]
d41b43eb
SP
295 unlock_index
296 rescan ui_ready
297 delete_this
298 return
299 }
300
c9498339 301 if {$old_hash eq $new_hash} {
dba07411
SP
302 _after_readtree $this
303 } elseif {[is_config_true gui.trustmtime]} {
d41b43eb
SP
304 _readtree $this
305 } else {
5e6d7768 306 ui_status [mc "Refreshing file status..."]
0b812616
SP
307 set fd [git_read update-index \
308 -q \
309 --unmerged \
310 --ignore-missing \
311 --refresh \
312 ]
d41b43eb
SP
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
b7922306
SP
343 set readtree_d {}
344 $::main_status start \
c8c4854b 345 [mc "Updating working directory to '%s'..." [_name $this]] \
5e6d7768 346 [mc "files checked out"]
b7922306 347
0b812616
SP
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 ]
d41b43eb
SP
356 fconfigure $fd -blocking 0 -translation binary
357 fileevent $fd readable [cb _readtree_wait $fd]
358}
359
360method _readtree_wait {fd} {
b7922306
SP
361 global current_branch
362
363 set buf [read $fd]
364 $::main_status update_meter $buf
365 append readtree_d $buf
d41b43eb 366
d41b43eb
SP
367 fconfigure $fd -blocking 1
368 if {![eof $fd]} {
369 fconfigure $fd -blocking 0
370 return
371 }
372
b7922306
SP
373 if {[catch {close $fd}]} {
374 set err $readtree_d
d41b43eb 375 regsub {^fatal: } $err {} err
1ac17950 376 $::main_status stop [mc "Aborted checkout of '%s' (file level merging is required)." [_name $this]]
31bb1d1b 377 warn_popup [strcat [mc "File level merge required."] "
d41b43eb
SP
378
379$err
380
1ac17950 381" [mc "Staying on branch '%s'." $current_branch]]
d41b43eb
SP
382 unlock_index
383 delete_this
384 return
385 }
386
b7922306
SP
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]
d41b43eb
SP
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]
7d5266a7
SP
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
d41b43eb 420 }
d41b43eb 421 } else {
881d8f24 422 if {!$is_detached || $new_hash ne $HEAD} {
7d5266a7
SP
423 append log " to $new_expr"
424 if {[catch {
425 _detach_HEAD $log $new_hash
426 } err]} {
427 _fatal $this $err
428 }
d41b43eb
SP
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} {
1ac17950 452 info_popup [mc "You are no longer on a local branch.
d41b43eb 453
1ac17950 454If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."]
d41b43eb
SP
455 }
456
c9498339
JL
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 {} {
d41b43eb
SP
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 #
c9498339
JL
493 global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
494 global ui_comm
495
d41b43eb 496 unlock_index
c9498339 497 set name [_name $this]
d41b43eb
SP
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
c8c4854b 503 rescan [list ui_status [mc "Checked out '%s'." $name]]
d41b43eb
SP
504 } else {
505 repository_state commit_type HEAD MERGE_HEAD
506 set PARENT $HEAD
5e6d7768 507 ui_status [mc "Checked out '%s'." $name]
d41b43eb
SP
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 \
1ac17950 535 -text [mc "Resetting '%s' to '%s' will lose the following commits:" $name $new_expr]\
d41b43eb
SP
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 \
1ac17950 557 -text [mc "Recovering lost commits may not be easy."] \
d41b43eb
SP
558 ]
559 pack [label $w.msg3 \
560 -anchor w \
561 -justify left \
1ac17950 562 -text [mc "Reset '%s'?" $name] \
d41b43eb
SP
563 ]
564
565 frame $w.buttons
566 button $w.buttons.visualize \
1ac17950 567 -text [mc Visualize] \
d41b43eb
SP
568 -command $gitk
569 pack $w.buttons.visualize -side left
570 button $w.buttons.reset \
1ac17950 571 -text [mc Reset] \
d41b43eb
SP
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 \
1ac17950 579 -text [mc Cancel] \
d41b43eb
SP
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
0b812616 584 set fd [git_read rev-list --pretty=oneline $cur ^$new_hash]
d41b43eb
SP
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} {
31bb1d1b 635 error_popup [strcat [mc "Failed to set current branch.
d41b43eb
SP
636
637This working directory is only partially switched. We successfully updated your files, but failed to update an internal Git file.
638
1ac17950 639This should not have occurred. %s will now close and give up." [appname]] "
d41b43eb 640
1ac17950 641$err"]
d41b43eb
SP
642 exit 1
643}
644
645}