Raw
1 # git-gui commit checkout support
2 # Copyright (C) 2007 Shawn Pearce
3
4 class checkout_op {
5
6 field w {}; # our window (if we have one)
7 field w_cons {}; # embedded console window object
8
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
13
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
21
22 field reset_ok 0; # did the user agree to reset?
23 field fetch_ok 0; # did the fetch succeed?
24
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
28
29 constructor 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
37 method parent {path} {
38 set parent_w [winfo toplevel $path]
39 }
40
41 method enable_merge {type} {
42 set merge_type $type
43 }
44
45 method enable_fetch {spec} {
46 set fetch_spec $spec
47 }
48
49 method remote_source {spec} {
50 set remote_source $spec
51 }
52
53 method enable_checkout {co} {
54 set checkout $co
55 }
56
57 method enable_create {co} {
58 set create $co
59 }
60
61 method 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
123 method _noop {} {}
124
125 method _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
153 method _update_ref {} {
154 global nullid 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 $nullid
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
270 method _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
279 method _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
291 Another Git program has modified this repository since the last scan. A rescan must be performed before the current branch can be changed.
292
293 The 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 [list 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
318 method _refresh_wait {fd} {
319 read $fd
320 if {[eof $fd]} {
321 close $fd
322 _readtree $this
323 }
324 }
325
326 method _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
340 method _readtree {} {
341 global HEAD
342
343 set readtree_d {}
344 set status_bar_operation [$::main_status start \
345 [mc "Updating working directory to '%s'..." [_name $this]] \
346 [mc "files checked out"]]
347
348 set fd [git_read [list read-tree \
349 -m \
350 -u \
351 -v \
352 --exclude-per-directory=.gitignore \
353 $HEAD \
354 $new_hash \
355 ] \
356 [list 2>@1]]
357 fconfigure $fd -blocking 0 -translation binary
358 fileevent $fd readable [cb _readtree_wait $fd $status_bar_operation]
359 }
360
361 method _readtree_wait {fd status_bar_operation} {
362 global current_branch
363
364 set buf [read $fd]
365 $status_bar_operation update_meter $buf
366 append readtree_d $buf
367
368 fconfigure $fd -blocking 1
369 if {![eof $fd]} {
370 fconfigure $fd -blocking 0
371 $status_bar_operation stop
372 return
373 }
374
375 if {[catch {close $fd}]} {
376 set err $readtree_d
377 regsub {^fatal: } $err {} err
378 $status_bar_operation stop [mc "Aborted checkout of '%s' (file level merging is required)." [_name $this]]
379 warn_popup [strcat [mc "File level merge required."] "
380
381 $err
382
383 " [mc "Staying on branch '%s'." $current_branch]]
384 unlock_index
385 delete_this
386 return
387 }
388
389 $status_bar_operation stop
390 _after_readtree $this
391 }
392
393 method _after_readtree {} {
394 global commit_type HEAD MERGE_HEAD PARENT
395 global current_branch is_detached
396 global ui_comm
397
398 set name [_name $this]
399 set log "checkout: moving"
400 if {!$is_detached} {
401 append log " from $current_branch"
402 }
403
404 # -- Move/create HEAD as a symbolic ref. Core git does not
405 # even check for failure here, it Just Works(tm). If it
406 # doesn't we are in some really ugly state that is difficult
407 # to recover from within git-gui.
408 #
409 set rh refs/heads/
410 set rn [string length $rh]
411 if {[string equal -length $rn $rh $new_ref]} {
412 set new_branch [string range $new_ref $rn end]
413 if {$is_detached || $current_branch ne $new_branch} {
414 append log " to $new_branch"
415 if {[catch {
416 git symbolic-ref -m $log HEAD $new_ref
417 } err]} {
418 _fatal $this $err
419 }
420 set current_branch $new_branch
421 set is_detached 0
422 }
423 } else {
424 if {!$is_detached || $new_hash ne $HEAD} {
425 append log " to $new_expr"
426 if {[catch {
427 _detach_HEAD $log $new_hash
428 } err]} {
429 _fatal $this $err
430 }
431 }
432 set current_branch HEAD
433 set is_detached 1
434 }
435
436 # -- We had to defer updating the branch itself until we
437 # knew the working directory would update. So now we
438 # need to finish that work. If it fails we're in big
439 # trouble.
440 #
441 if {$update_old ne {}} {
442 if {[catch {
443 git update-ref \
444 -m $reflog_msg \
445 $new_ref \
446 $new_hash \
447 $update_old
448 } err]} {
449 _fatal $this $err
450 }
451 }
452
453 if {$is_detached} {
454 info_popup [mc "You are no longer on a local branch.
455
456 If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."]
457 }
458
459 # -- Run the post-checkout hook.
460 #
461 set fd_ph [githook_read post-checkout $old_hash $new_hash 1]
462 if {$fd_ph ne {}} {
463 global pch_error
464 set pch_error {}
465 fconfigure $fd_ph -blocking 0 -translation binary
466 fileevent $fd_ph readable [cb _postcheckout_wait $fd_ph]
467 } else {
468 _update_repo_state $this
469 }
470 }
471
472 method _postcheckout_wait {fd_ph} {
473 global pch_error
474
475 append pch_error [read $fd_ph]
476 fconfigure $fd_ph -blocking 1
477 if {[eof $fd_ph]} {
478 if {[catch {close $fd_ph}]} {
479 hook_failed_popup post-checkout $pch_error 0
480 }
481 unset pch_error
482 _update_repo_state $this
483 return
484 }
485 fconfigure $fd_ph -blocking 0
486 }
487
488 method _update_repo_state {} {
489 # -- Update our repository state. If we were previously in
490 # amend mode we need to toss the current buffer and do a
491 # full rescan to update our file lists. If we weren't in
492 # amend mode our file lists are accurate and we can avoid
493 # the rescan.
494 #
495 global commit_type_is_amend commit_type HEAD MERGE_HEAD PARENT
496 global ui_comm
497
498 unlock_index
499 set name [_name $this]
500 set commit_type_is_amend 0
501 if {[string match amend* $commit_type]} {
502 $ui_comm delete 0.0 end
503 $ui_comm edit reset
504 $ui_comm edit modified false
505 rescan [list ui_status [mc "Checked out '%s'." $name]]
506 } else {
507 repository_state commit_type HEAD MERGE_HEAD
508 set PARENT $HEAD
509 ui_status [mc "Checked out '%s'." $name]
510 }
511 delete_this
512 }
513
514 proc _detach_HEAD {log new} {
515 git update-ref --no-deref -m $log HEAD $new
516 }
517
518 method _confirm_reset {cur} {
519 set reset_ok 0
520 set name [_name $this]
521 set gitk [list do_gitk [list $cur ^$new_hash]]
522
523 _toplevel $this {Confirm Branch Reset}
524 pack [label $w.msg1 \
525 -anchor w \
526 -justify left \
527 -text [mc "Resetting '%s' to '%s' will lose the following commits:" $name $new_expr]\
528 ] -anchor w
529
530 set list $w.list.l
531 frame $w.list
532 text $list \
533 -font font_diff \
534 -width 80 \
535 -height 10 \
536 -wrap none \
537 -xscrollcommand [list $w.list.sbx set] \
538 -yscrollcommand [list $w.list.sby set]
539 scrollbar $w.list.sbx -orient h -command [list $list xview]
540 scrollbar $w.list.sby -orient v -command [list $list yview]
541 pack $w.list.sbx -fill x -side bottom
542 pack $w.list.sby -fill y -side right
543 pack $list -fill both -expand 1
544 pack $w.list -fill both -expand 1 -padx 5 -pady 5
545
546 pack [label $w.msg2 \
547 -anchor w \
548 -justify left \
549 -text [mc "Recovering lost commits may not be easy."] \
550 ]
551 pack [label $w.msg3 \
552 -anchor w \
553 -justify left \
554 -text [mc "Reset '%s'?" $name] \
555 ]
556
557 frame $w.buttons
558 button $w.buttons.visualize \
559 -text [mc Visualize] \
560 -command $gitk
561 pack $w.buttons.visualize -side left
562 button $w.buttons.reset \
563 -text [mc Reset] \
564 -command "
565 set @reset_ok 1
566 destroy $w
567 "
568 pack $w.buttons.reset -side right
569 button $w.buttons.cancel \
570 -default active \
571 -text [mc Cancel] \
572 -command [list destroy $w]
573 pack $w.buttons.cancel -side right -padx 5
574 pack $w.buttons -side bottom -fill x -pady 10 -padx 10
575
576 set fd [git_read [list rev-list --pretty=oneline $cur ^$new_hash]]
577 while {[gets $fd line] > 0} {
578 set abbr [string range $line 0 7]
579 set subj [string range $line 41 end]
580 $list insert end "$abbr $subj\n"
581 }
582 close $fd
583 $list configure -state disabled
584
585 bind $w <Key-v> $gitk
586 bind $w <Visibility> "
587 grab $w
588 focus $w.buttons.cancel
589 "
590 bind $w <Key-Return> [list destroy $w]
591 bind $w <Key-Escape> [list destroy $w]
592 tkwait window $w
593 return $reset_ok
594 }
595
596 method _error {msg} {
597 if {[winfo ismapped $parent_w]} {
598 set p $parent_w
599 } else {
600 set p .
601 }
602
603 tk_messageBox \
604 -icon error \
605 -type ok \
606 -title [wm title $p] \
607 -parent $p \
608 -message $msg
609 }
610
611 method _toplevel {title} {
612 regsub -all {::} $this {__} w
613 set w .$w
614
615 if {[winfo ismapped $parent_w]} {
616 set p $parent_w
617 } else {
618 set p .
619 }
620
621 toplevel $w
622 wm title $w $title
623 wm geometry $w "+[winfo rootx $p]+[winfo rooty $p]"
624 }
625
626 method _fatal {err} {
627 error_popup [strcat [mc "Failed to set current branch.
628
629 This working directory is only partially switched. We successfully updated your files, but failed to update an internal Git file.
630
631 This should not have occurred. %s will now close and give up." [appname]] "
632
633 $err"]
634 exit 1
635 }
636
637 }