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