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