]> Pileus Git - ~andy/git/blob - lib/checkout_op.tcl
git-gui: Skip unnecessary read-tree work during checkout
[~andy/git] / lib / checkout_op.tcl
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
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?
19
20 field reset_ok      0; # did the user agree to reset?
21 field fetch_ok      0; # did the fetch succeed?
22
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
26
27 constructor 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
35 method parent {path} {
36         set parent_w [winfo toplevel $path]
37 }
38
39 method enable_merge {type} {
40         set merge_type $type
41 }
42
43 method enable_fetch {spec} {
44         set fetch_spec $spec
45 }
46
47 method enable_checkout {co} {
48         set checkout $co
49 }
50
51 method enable_create {co} {
52         set create $co
53 }
54
55 method 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
112 method _noop {} {}
113
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
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
139 method _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 {
184                 catch {set merge_base [git merge-base $new $cur]}
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                                 }
200                         }
201                         reset {
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                         }
210                         default {
211                                 _error $this "Merge strategy '$merge_type' not supported."
212                                 return 0
213                         }
214                         }
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
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
244         }
245 }
246
247 method _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
259 Another Git program has modified this repository since the last scan.  A rescan must be performed before the current branch can be changed.
260
261 The rescan will be automatically started now.
262 }
263                 unlock_index
264                 rescan ui_ready
265                 delete_this
266                 return
267         }
268
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 \
280                         ]
281                 fconfigure $fd -blocking 0 -translation binary
282                 fileevent $fd readable [cb _refresh_wait $fd]
283         }
284 }
285
286 method _refresh_wait {fd} {
287         read $fd
288         if {[eof $fd]} {
289                 close $fd
290                 _readtree $this
291         }
292 }
293
294 method _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
308 method _readtree {} {
309         global HEAD
310
311         set readtree_d {}
312         $::main_status start \
313                 "Updating working directory to '[_name $this]'..." \
314                 {files checked out}
315
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                 ]
324         fconfigure $fd -blocking 0 -translation binary
325         fileevent $fd readable [cb _readtree_wait $fd]
326 }
327
328 method _readtree_wait {fd} {
329         global current_branch
330
331         set buf [read $fd]
332         $::main_status update_meter $buf
333         append readtree_d $buf
334
335         fconfigure $fd -blocking 1
336         if {![eof $fd]} {
337                 fconfigure $fd -blocking 0
338                 return
339         }
340
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.
346
347 $err
348
349 Staying on branch '$current_branch'."
350                 unlock_index
351                 delete_this
352                 return
353         }
354
355         $::main_status stop
356         _after_readtree $this
357 }
358
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
363
364         set name [_name $this]
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
420 If 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
444 git-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
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]]
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
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"
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
536 method _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
551 method _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
566 method _fatal {err} {
567         error_popup "Failed to set current branch.
568
569 This working directory is only partially switched.  We successfully updated your files, but failed to update an internal Git file.
570
571 This should not have occurred.  [appname] will now close and give up.
572
573 $err"
574         exit 1
575 }
576
577 }