]> asedeno.scripts.mit.edu Git - git.git/blob - lib/checkout_op.tcl
git-gui: Factor out common fast-forward merge case
[git.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 "Only 'ff' and 'reset' merge is currently 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 {[is_config_true gui.trustmtime]} {
270                 _readtree $this
271         } else {
272                 ui_status {Refreshing file status...}
273                 set fd [git_read update-index \
274                         -q \
275                         --unmerged \
276                         --ignore-missing \
277                         --refresh \
278                         ]
279                 fconfigure $fd -blocking 0 -translation binary
280                 fileevent $fd readable [cb _refresh_wait $fd]
281         }
282 }
283
284 method _refresh_wait {fd} {
285         read $fd
286         if {[eof $fd]} {
287                 close $fd
288                 _readtree $this
289         }
290 }
291
292 method _name {} {
293         if {$new_ref eq {}} {
294                 return [string range $new_hash 0 7]
295         }
296
297         set rh refs/heads/
298         set rn [string length $rh]
299         if {[string equal -length $rn $rh $new_ref]} {
300                 return [string range $new_ref $rn end]
301         } else {
302                 return $new_ref
303         }
304 }
305
306 method _readtree {} {
307         global HEAD
308
309         set readtree_d {}
310         $::main_status start \
311                 "Updating working directory to '[_name $this]'..." \
312                 {files checked out}
313
314         set fd [git_read --stderr read-tree \
315                 -m \
316                 -u \
317                 -v \
318                 --exclude-per-directory=.gitignore \
319                 $HEAD \
320                 $new_hash \
321                 ]
322         fconfigure $fd -blocking 0 -translation binary
323         fileevent $fd readable [cb _readtree_wait $fd]
324 }
325
326 method _readtree_wait {fd} {
327         global current_branch
328
329         set buf [read $fd]
330         $::main_status update_meter $buf
331         append readtree_d $buf
332
333         fconfigure $fd -blocking 1
334         if {![eof $fd]} {
335                 fconfigure $fd -blocking 0
336                 return
337         }
338
339         if {[catch {close $fd}]} {
340                 set err $readtree_d
341                 regsub {^fatal: } $err {} err
342                 $::main_status stop "Aborted checkout of '[_name $this]' (file level merging is required)."
343                 warn_popup "File level merge required.
344
345 $err
346
347 Staying on branch '$current_branch'."
348                 unlock_index
349                 delete_this
350                 return
351         }
352
353         $::main_status stop
354         _after_readtree $this
355 }
356
357 method _after_readtree {} {
358         global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
359         global current_branch is_detached
360         global ui_comm
361
362         set name [_name $this]
363         set log "checkout: moving"
364         if {!$is_detached} {
365                 append log " from $current_branch"
366         }
367
368         # -- Move/create HEAD as a symbolic ref.  Core git does not
369         #    even check for failure here, it Just Works(tm).  If it
370         #    doesn't we are in some really ugly state that is difficult
371         #    to recover from within git-gui.
372         #
373         set rh refs/heads/
374         set rn [string length $rh]
375         if {[string equal -length $rn $rh $new_ref]} {
376                 set new_branch [string range $new_ref $rn end]
377                 append log " to $new_branch"
378
379                 if {[catch {
380                                 git symbolic-ref -m $log HEAD $new_ref
381                         } err]} {
382                         _fatal $this $err
383                 }
384                 set current_branch $new_branch
385                 set is_detached 0
386         } else {
387                 append log " to $new_expr"
388
389                 if {[catch {
390                                 _detach_HEAD $log $new_hash
391                         } err]} {
392                         _fatal $this $err
393                 }
394                 set current_branch HEAD
395                 set is_detached 1
396         }
397
398         # -- We had to defer updating the branch itself until we
399         #    knew the working directory would update.  So now we
400         #    need to finish that work.  If it fails we're in big
401         #    trouble.
402         #
403         if {$update_old ne {}} {
404                 if {[catch {
405                                 git update-ref \
406                                         -m $reflog_msg \
407                                         $new_ref \
408                                         $new_hash \
409                                         $update_old
410                         } err]} {
411                         _fatal $this $err
412                 }
413         }
414
415         if {$is_detached} {
416                 info_popup "You are no longer on a local branch.
417
418 If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."
419         }
420
421         # -- Update our repository state.  If we were previously in
422         #    amend mode we need to toss the current buffer and do a
423         #    full rescan to update our file lists.  If we weren't in
424         #    amend mode our file lists are accurate and we can avoid
425         #    the rescan.
426         #
427         unlock_index
428         set selected_commit_type new
429         if {[string match amend* $commit_type]} {
430                 $ui_comm delete 0.0 end
431                 $ui_comm edit reset
432                 $ui_comm edit modified false
433                 rescan [list ui_status "Checked out '$name'."]
434         } else {
435                 repository_state commit_type HEAD MERGE_HEAD
436                 set PARENT $HEAD
437                 ui_status "Checked out '$name'."
438         }
439         delete_this
440 }
441
442 git-version proc _detach_HEAD {log new} {
443         >= 1.5.3 {
444                 git update-ref --no-deref -m $log HEAD $new
445         }
446         default {
447                 set p [gitdir HEAD]
448                 file delete $p
449                 set fd [open $p w]
450                 fconfigure $fd -translation lf -encoding utf-8
451                 puts $fd $new
452                 close $fd
453         }
454 }
455
456 method _confirm_reset {cur} {
457         set reset_ok 0
458         set name [_name $this]
459         set gitk [list do_gitk [list $cur ^$new_hash]]
460
461         _toplevel $this {Confirm Branch Reset}
462         pack [label $w.msg1 \
463                 -anchor w \
464                 -justify left \
465                 -text "Resetting '$name' to $new_expr will lose the following commits:" \
466                 ] -anchor w
467
468         set list $w.list.l
469         frame $w.list
470         text $list \
471                 -font font_diff \
472                 -width 80 \
473                 -height 10 \
474                 -wrap none \
475                 -xscrollcommand [list $w.list.sbx set] \
476                 -yscrollcommand [list $w.list.sby set]
477         scrollbar $w.list.sbx -orient h -command [list $list xview]
478         scrollbar $w.list.sby -orient v -command [list $list yview]
479         pack $w.list.sbx -fill x -side bottom
480         pack $w.list.sby -fill y -side right
481         pack $list -fill both -expand 1
482         pack $w.list -fill both -expand 1 -padx 5 -pady 5
483
484         pack [label $w.msg2 \
485                 -anchor w \
486                 -justify left \
487                 -text {Recovering lost commits may not be easy.} \
488                 ]
489         pack [label $w.msg3 \
490                 -anchor w \
491                 -justify left \
492                 -text "Reset '$name'?" \
493                 ]
494
495         frame $w.buttons
496         button $w.buttons.visualize \
497                 -text Visualize \
498                 -command $gitk
499         pack $w.buttons.visualize -side left
500         button $w.buttons.reset \
501                 -text Reset \
502                 -command "
503                         set @reset_ok 1
504                         destroy $w
505                 "
506         pack $w.buttons.reset -side right
507         button $w.buttons.cancel \
508                 -default active \
509                 -text Cancel \
510                 -command [list destroy $w]
511         pack $w.buttons.cancel -side right -padx 5
512         pack $w.buttons -side bottom -fill x -pady 10 -padx 10
513
514         set fd [git_read rev-list --pretty=oneline $cur ^$new_hash]
515         while {[gets $fd line] > 0} {
516                 set abbr [string range $line 0 7]
517                 set subj [string range $line 41 end]
518                 $list insert end "$abbr  $subj\n"
519         }
520         close $fd
521         $list configure -state disabled
522
523         bind $w    <Key-v> $gitk
524         bind $w <Visibility> "
525                 grab $w
526                 focus $w.buttons.cancel
527         "
528         bind $w <Key-Return> [list destroy $w]
529         bind $w <Key-Escape> [list destroy $w]
530         tkwait window $w
531         return $reset_ok
532 }
533
534 method _error {msg} {
535         if {[winfo ismapped $parent_w]} {
536                 set p $parent_w
537         } else {
538                 set p .
539         }
540
541         tk_messageBox \
542                 -icon error \
543                 -type ok \
544                 -title [wm title $p] \
545                 -parent $p \
546                 -message $msg
547 }
548
549 method _toplevel {title} {
550         regsub -all {::} $this {__} w
551         set w .$w
552
553         if {[winfo ismapped $parent_w]} {
554                 set p $parent_w
555         } else {
556                 set p .
557         }
558
559         toplevel $w
560         wm title $w $title
561         wm geometry $w "+[winfo rootx $p]+[winfo rooty $p]"
562 }
563
564 method _fatal {err} {
565         error_popup "Failed to set current branch.
566
567 This working directory is only partially switched.  We successfully updated your files, but failed to update an internal Git file.
568
569 This should not have occurred.  [appname] will now close and give up.
570
571 $err"
572         exit 1
573 }
574
575 }