git-gui: Don't crash in ask_popup if we haven't mapped main window yet
[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 fetch_spec   {}; # refetch tracking branch if used?
16 field checkout      1; # actually checkout the branch?
17 field create        0; # create the branch if it doesn't exist?
18
19 field reset_ok      0; # did the user agree to reset?
20 field fetch_ok      0; # did the fetch succeed?
21
22 field readtree_d   {}; # buffered output from read-tree
23 field update_old   {}; # was the update-ref call deferred?
24 field reflog_msg   {}; # log message for the update-ref call
25
26 constructor new {expr hash {ref {}}} {
27         set new_expr $expr
28         set new_hash $hash
29         set new_ref  $ref
30
31         return $this
32 }
33
34 method parent {path} {
35         set parent_w [winfo toplevel $path]
36 }
37
38 method enable_merge {type} {
39         set merge_type $type
40 }
41
42 method enable_fetch {spec} {
43         set fetch_spec $spec
44 }
45
46 method enable_checkout {co} {
47         set checkout $co
48 }
49
50 method enable_create {co} {
51         set create $co
52 }
53
54 method run {} {
55         if {$fetch_spec ne {}} {
56                 global M1B
57
58                 # We were asked to refresh a single tracking branch
59                 # before we get to work.  We should do that before we
60                 # consider any ref updating.
61                 #
62                 set fetch_ok 0
63                 set l_trck [lindex $fetch_spec 0]
64                 set remote [lindex $fetch_spec 1]
65                 set r_head [lindex $fetch_spec 2]
66                 regsub ^refs/heads/ $r_head {} r_name
67
68                 _toplevel $this {Refreshing Tracking Branch}
69                 set w_cons [::console::embed \
70                         $w.console \
71                         "Fetching $r_name from $remote"]
72                 pack $w.console -fill both -expand 1
73                 $w_cons exec \
74                         [list git fetch $remote +$r_head:$l_trck] \
75                         [cb _finish_fetch]
76
77                 bind $w <$M1B-Key-w> break
78                 bind $w <$M1B-Key-W> break
79                 bind $w <Visibility> "
80                         [list grab $w]
81                         [list focus $w]
82                 "
83                 wm protocol $w WM_DELETE_WINDOW [cb _noop]
84                 tkwait window $w
85
86                 if {!$fetch_ok} {
87                         delete_this
88                         return 0
89                 }
90         }
91
92         if {$new_ref ne {}} {
93                 # If we have a ref we need to update it before we can
94                 # proceed with a checkout (if one was enabled).
95                 #
96                 if {![_update_ref $this]} {
97                         delete_this
98                         return 0
99                 }
100         }
101
102         if {$checkout} {
103                 _checkout $this
104                 return 1
105         }
106
107         delete_this
108         return 1
109 }
110
111 method _noop {} {}
112
113 method _finish_fetch {ok} {
114         if {$ok} {
115                 set l_trck [lindex $fetch_spec 0]
116                 if {[catch {set new_hash [git rev-parse --verify "$l_trck^0"]} err]} {
117                         set ok 0
118                         $w_cons insert "fatal: Cannot resolve $l_trck"
119                         $w_cons insert $err
120                 }
121         }
122
123         $w_cons done $ok
124         set w_cons {}
125         wm protocol $w WM_DELETE_WINDOW {}
126
127         if {$ok} {
128                 destroy $w
129                 set w {}
130         } else {
131                 button $w.close -text Close -command [list destroy $w]
132                 pack $w.close -side bottom -anchor e -padx 10 -pady 10
133         }
134
135         set fetch_ok $ok
136 }
137
138 method _update_ref {} {
139         global null_sha1 current_branch
140
141         set ref $new_ref
142         set new $new_hash
143
144         set is_current 0
145         set rh refs/heads/
146         set rn [string length $rh]
147         if {[string equal -length $rn $rh $ref]} {
148                 set newbranch [string range $ref $rn end]
149                 if {$current_branch eq $newbranch} {
150                         set is_current 1
151                 }
152         } else {
153                 set newbranch $ref
154         }
155
156         if {[catch {set cur [git rev-parse --verify "$ref^0"]}]} {
157                 # Assume it does not exist, and that is what the error was.
158                 #
159                 if {!$create} {
160                         _error $this "Branch '$newbranch' does not exist."
161                         return 0
162                 }
163
164                 set reflog_msg "branch: Created from $new_expr"
165                 set cur $null_sha1
166         } elseif {$create && $merge_type eq {none}} {
167                 # We were told to create it, but not do a merge.
168                 # Bad.  Name shouldn't have existed.
169                 #
170                 _error $this "Branch '$newbranch' already exists."
171                 return 0
172         } elseif {!$create && $merge_type eq {none}} {
173                 # We aren't creating, it exists and we don't merge.
174                 # We are probably just a simple branch switch.
175                 # Use whatever value we just read.
176                 #
177                 set new      $cur
178                 set new_hash $cur
179         } elseif {$new eq $cur} {
180                 # No merge would be required, don't compute anything.
181                 #
182         } else {
183                 set mrb {}
184                 catch {set mrb [git merge-base $new $cur]}
185                 switch -- $merge_type {
186                 ff {
187                         if {$mrb eq $new} {
188                                 # The current branch is actually newer.
189                                 #
190                                 set new $cur
191                         } elseif {$mrb eq $cur} {
192                                 # The current branch is older.
193                                 #
194                                 set reflog_msg "merge $new_expr: Fast-forward"
195                         } else {
196                                 _error $this "Branch '$newbranch' already exists.\n\nIt cannot fast-forward to $new_expr.\nA merge is required."
197                                 return 0
198                         }
199                 }
200                 reset {
201                         if {$mrb eq $cur} {
202                                 # The current branch is older.
203                                 #
204                                 set reflog_msg "merge $new_expr: Fast-forward"
205                         } else {
206                                 # The current branch will lose things.
207                                 #
208                                 if {[_confirm_reset $this $cur]} {
209                                         set reflog_msg "reset $new_expr"
210                                 } else {
211                                         return 0
212                                 }
213                         }
214                 }
215                 default {
216                         _error $this "Only 'ff' and 'reset' merge is currently supported."
217                         return 0
218                 }
219                 }
220         }
221
222         if {$new ne $cur} {
223                 if {$is_current} {
224                         # No so fast.  We should defer this in case
225                         # we cannot update the working directory.
226                         #
227                         set update_old $cur
228                         return 1
229                 }
230
231                 if {[catch {
232                                 git update-ref -m $reflog_msg $ref $new $cur
233                         } err]} {
234                         _error $this "Failed to update '$newbranch'.\n\n$err"
235                         return 0
236                 }
237         }
238
239         return 1
240 }
241
242 method _checkout {} {
243         if {[lock_index checkout_op]} {
244                 after idle [cb _start_checkout]
245         } else {
246                 _error $this "Index is already locked."
247                 delete_this
248         }
249 }
250
251 method _start_checkout {} {
252         global HEAD commit_type
253
254         # -- Our in memory state should match the repository.
255         #
256         repository_state curType curHEAD curMERGE_HEAD
257         if {[string match amend* $commit_type]
258                 && $curType eq {normal}
259                 && $curHEAD eq $HEAD} {
260         } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
261                 info_popup {Last scanned state does not match repository state.
262
263 Another Git program has modified this repository since the last scan.  A rescan must be performed before the current branch can be changed.
264
265 The rescan will be automatically started now.
266 }
267                 unlock_index
268                 rescan ui_ready
269                 delete_this
270                 return
271         }
272
273         if {[is_config_true gui.trustmtime]} {
274                 _readtree $this
275         } else {
276                 ui_status {Refreshing file status...}
277                 set fd [git_read update-index \
278                         -q \
279                         --unmerged \
280                         --ignore-missing \
281                         --refresh \
282                         ]
283                 fconfigure $fd -blocking 0 -translation binary
284                 fileevent $fd readable [cb _refresh_wait $fd]
285         }
286 }
287
288 method _refresh_wait {fd} {
289         read $fd
290         if {[eof $fd]} {
291                 close $fd
292                 _readtree $this
293         }
294 }
295
296 method _name {} {
297         if {$new_ref eq {}} {
298                 return [string range $new_hash 0 7]
299         }
300
301         set rh refs/heads/
302         set rn [string length $rh]
303         if {[string equal -length $rn $rh $new_ref]} {
304                 return [string range $new_ref $rn end]
305         } else {
306                 return $new_ref
307         }
308 }
309
310 method _readtree {} {
311         global HEAD
312
313         set readtree_d {}
314         $::main_status start \
315                 "Updating working directory to '[_name $this]'..." \
316                 {files checked out}
317
318         set fd [git_read --stderr read-tree \
319                 -m \
320                 -u \
321                 -v \
322                 --exclude-per-directory=.gitignore \
323                 $HEAD \
324                 $new_hash \
325                 ]
326         fconfigure $fd -blocking 0 -translation binary
327         fileevent $fd readable [cb _readtree_wait $fd]
328 }
329
330 method _readtree_wait {fd} {
331         global current_branch
332
333         set buf [read $fd]
334         $::main_status update_meter $buf
335         append readtree_d $buf
336
337         fconfigure $fd -blocking 1
338         if {![eof $fd]} {
339                 fconfigure $fd -blocking 0
340                 return
341         }
342
343         if {[catch {close $fd}]} {
344                 set err $readtree_d
345                 regsub {^fatal: } $err {} err
346                 $::main_status stop "Aborted checkout of '[_name $this]' (file level merging is required)."
347                 warn_popup "File level merge required.
348
349 $err
350
351 Staying on branch '$current_branch'."
352                 unlock_index
353                 delete_this
354                 return
355         }
356
357         $::main_status stop
358         _after_readtree $this
359 }
360
361 method _after_readtree {} {
362         global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
363         global current_branch is_detached
364         global ui_comm
365
366         set name [_name $this]
367         set log "checkout: moving"
368         if {!$is_detached} {
369                 append log " from $current_branch"
370         }
371
372         # -- Move/create HEAD as a symbolic ref.  Core git does not
373         #    even check for failure here, it Just Works(tm).  If it
374         #    doesn't we are in some really ugly state that is difficult
375         #    to recover from within git-gui.
376         #
377         set rh refs/heads/
378         set rn [string length $rh]
379         if {[string equal -length $rn $rh $new_ref]} {
380                 set new_branch [string range $new_ref $rn end]
381                 append log " to $new_branch"
382
383                 if {[catch {
384                                 git symbolic-ref -m $log HEAD $new_ref
385                         } err]} {
386                         _fatal $this $err
387                 }
388                 set current_branch $new_branch
389                 set is_detached 0
390         } else {
391                 append log " to $new_expr"
392
393                 if {[catch {
394                                 _detach_HEAD $log $new_hash
395                         } err]} {
396                         _fatal $this $err
397                 }
398                 set current_branch HEAD
399                 set is_detached 1
400         }
401
402         # -- We had to defer updating the branch itself until we
403         #    knew the working directory would update.  So now we
404         #    need to finish that work.  If it fails we're in big
405         #    trouble.
406         #
407         if {$update_old ne {}} {
408                 if {[catch {
409                                 git update-ref \
410                                         -m $reflog_msg \
411                                         $new_ref \
412                                         $new_hash \
413                                         $update_old
414                         } err]} {
415                         _fatal $this $err
416                 }
417         }
418
419         if {$is_detached} {
420                 info_popup "You are no longer on a local branch.
421
422 If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."
423         }
424
425         # -- Update our repository state.  If we were previously in
426         #    amend mode we need to toss the current buffer and do a
427         #    full rescan to update our file lists.  If we weren't in
428         #    amend mode our file lists are accurate and we can avoid
429         #    the rescan.
430         #
431         unlock_index
432         set selected_commit_type new
433         if {[string match amend* $commit_type]} {
434                 $ui_comm delete 0.0 end
435                 $ui_comm edit reset
436                 $ui_comm edit modified false
437                 rescan [list ui_status "Checked out '$name'."]
438         } else {
439                 repository_state commit_type HEAD MERGE_HEAD
440                 set PARENT $HEAD
441                 ui_status "Checked out '$name'."
442         }
443         delete_this
444 }
445
446 git-version proc _detach_HEAD {log new} {
447         >= 1.5.3 {
448                 git update-ref --no-deref -m $log HEAD $new
449         }
450         default {
451                 set p [gitdir HEAD]
452                 file delete $p
453                 set fd [open $p w]
454                 fconfigure $fd -translation lf -encoding utf-8
455                 puts $fd $new
456                 close $fd
457         }
458 }
459
460 method _confirm_reset {cur} {
461         set reset_ok 0
462         set name [_name $this]
463         set gitk [list do_gitk [list $cur ^$new_hash]]
464
465         _toplevel $this {Confirm Branch Reset}
466         pack [label $w.msg1 \
467                 -anchor w \
468                 -justify left \
469                 -text "Resetting '$name' to $new_expr will lose the following commits:" \
470                 ] -anchor w
471
472         set list $w.list.l
473         frame $w.list
474         text $list \
475                 -font font_diff \
476                 -width 80 \
477                 -height 10 \
478                 -wrap none \
479                 -xscrollcommand [list $w.list.sbx set] \
480                 -yscrollcommand [list $w.list.sby set]
481         scrollbar $w.list.sbx -orient h -command [list $list xview]
482         scrollbar $w.list.sby -orient v -command [list $list yview]
483         pack $w.list.sbx -fill x -side bottom
484         pack $w.list.sby -fill y -side right
485         pack $list -fill both -expand 1
486         pack $w.list -fill both -expand 1 -padx 5 -pady 5
487
488         pack [label $w.msg2 \
489                 -anchor w \
490                 -justify left \
491                 -text {Recovering lost commits may not be easy.} \
492                 ]
493         pack [label $w.msg3 \
494                 -anchor w \
495                 -justify left \
496                 -text "Reset '$name'?" \
497                 ]
498
499         frame $w.buttons
500         button $w.buttons.visualize \
501                 -text Visualize \
502                 -command $gitk
503         pack $w.buttons.visualize -side left
504         button $w.buttons.reset \
505                 -text Reset \
506                 -command "
507                         set @reset_ok 1
508                         destroy $w
509                 "
510         pack $w.buttons.reset -side right
511         button $w.buttons.cancel \
512                 -default active \
513                 -text Cancel \
514                 -command [list destroy $w]
515         pack $w.buttons.cancel -side right -padx 5
516         pack $w.buttons -side bottom -fill x -pady 10 -padx 10
517
518         set fd [git_read rev-list --pretty=oneline $cur ^$new_hash]
519         while {[gets $fd line] > 0} {
520                 set abbr [string range $line 0 7]
521                 set subj [string range $line 41 end]
522                 $list insert end "$abbr  $subj\n"
523         }
524         close $fd
525         $list configure -state disabled
526
527         bind $w    <Key-v> $gitk
528         bind $w <Visibility> "
529                 grab $w
530                 focus $w.buttons.cancel
531         "
532         bind $w <Key-Return> [list destroy $w]
533         bind $w <Key-Escape> [list destroy $w]
534         tkwait window $w
535         return $reset_ok
536 }
537
538 method _error {msg} {
539         if {[winfo ismapped $parent_w]} {
540                 set p $parent_w
541         } else {
542                 set p .
543         }
544
545         tk_messageBox \
546                 -icon error \
547                 -type ok \
548                 -title [wm title $p] \
549                 -parent $p \
550                 -message $msg
551 }
552
553 method _toplevel {title} {
554         regsub -all {::} $this {__} w
555         set w .$w
556
557         if {[winfo ismapped $parent_w]} {
558                 set p $parent_w
559         } else {
560                 set p .
561         }
562
563         toplevel $w
564         wm title $w $title
565         wm geometry $w "+[winfo rootx $p]+[winfo rooty $p]"
566 }
567
568 method _fatal {err} {
569         error_popup "Failed to set current branch.
570
571 This working directory is only partially switched.  We successfully updated your files, but failed to update an internal Git file.
572
573 This should not have occurred.  [appname] will now close and give up.
574
575 $err"
576         exit 1
577 }
578
579 }