Code

Merge branch 'maint'
[git.git] / lib / branch.tcl
1 # git-gui branch (create/delete) support
2 # Copyright (C) 2006, 2007 Shawn Pearce
4 proc load_all_heads {} {
5         global all_heads
7         set all_heads [list]
8         set fd [open "| git for-each-ref --format=%(refname) refs/heads" r]
9         while {[gets $fd line] > 0} {
10                 if {[is_tracking_branch $line]} continue
11                 if {![regsub ^refs/heads/ $line {} name]} continue
12                 lappend all_heads $name
13         }
14         close $fd
16         set all_heads [lsort $all_heads]
17 }
19 proc load_all_tags {} {
20         set all_tags [list]
21         set fd [open "| git for-each-ref --format=%(refname) refs/tags" r]
22         while {[gets $fd line] > 0} {
23                 if {![regsub ^refs/tags/ $line {} name]} continue
24                 lappend all_tags $name
25         }
26         close $fd
28         return [lsort $all_tags]
29 }
31 proc populate_branch_menu {} {
32         global all_heads disable_on_lock
34         set m .mbar.branch
35         set last [$m index last]
36         for {set i 0} {$i <= $last} {incr i} {
37                 if {[$m type $i] eq {separator}} {
38                         $m delete $i last
39                         set new_dol [list]
40                         foreach a $disable_on_lock {
41                                 if {[lindex $a 0] ne $m || [lindex $a 2] < $i} {
42                                         lappend new_dol $a
43                                 }
44                         }
45                         set disable_on_lock $new_dol
46                         break
47                 }
48         }
50         if {$all_heads ne {}} {
51                 $m add separator
52         }
53         foreach b $all_heads {
54                 $m add radiobutton \
55                         -label $b \
56                         -command [list switch_branch $b] \
57                         -variable current_branch \
58                         -value $b
59                 lappend disable_on_lock \
60                         [list $m entryconf [$m index last] -state]
61         }
62 }
64 proc do_create_branch_action {w} {
65         global all_heads null_sha1 repo_config
66         global create_branch_checkout create_branch_revtype
67         global create_branch_head create_branch_trackinghead
68         global create_branch_name create_branch_revexp
69         global create_branch_tag
71         set newbranch $create_branch_name
72         if {$newbranch eq {}
73                 || $newbranch eq $repo_config(gui.newbranchtemplate)} {
74                 tk_messageBox \
75                         -icon error \
76                         -type ok \
77                         -title [wm title $w] \
78                         -parent $w \
79                         -message "Please supply a branch name."
80                 focus $w.desc.name_t
81                 return
82         }
83         if {![catch {git show-ref --verify -- "refs/heads/$newbranch"}]} {
84                 tk_messageBox \
85                         -icon error \
86                         -type ok \
87                         -title [wm title $w] \
88                         -parent $w \
89                         -message "Branch '$newbranch' already exists."
90                 focus $w.desc.name_t
91                 return
92         }
93         if {[catch {git check-ref-format "heads/$newbranch"}]} {
94                 tk_messageBox \
95                         -icon error \
96                         -type ok \
97                         -title [wm title $w] \
98                         -parent $w \
99                         -message "We do not like '$newbranch' as a branch name."
100                 focus $w.desc.name_t
101                 return
102         }
104         set rev {}
105         switch -- $create_branch_revtype {
106         head {set rev $create_branch_head}
107         tracking {set rev $create_branch_trackinghead}
108         tag {set rev $create_branch_tag}
109         expression {set rev $create_branch_revexp}
110         }
111         if {[catch {set cmt [git rev-parse --verify "${rev}^0"]}]} {
112                 tk_messageBox \
113                         -icon error \
114                         -type ok \
115                         -title [wm title $w] \
116                         -parent $w \
117                         -message "Invalid starting revision: $rev"
118                 return
119         }
120         if {[catch {
121                         git update-ref \
122                                 -m "branch: Created from $rev" \
123                                 "refs/heads/$newbranch" \
124                                 $cmt \
125                                 $null_sha1
126                 } err]} {
127                 tk_messageBox \
128                         -icon error \
129                         -type ok \
130                         -title [wm title $w] \
131                         -parent $w \
132                         -message "Failed to create '$newbranch'.\n\n$err"
133                 return
134         }
136         lappend all_heads $newbranch
137         set all_heads [lsort $all_heads]
138         populate_branch_menu
139         destroy $w
140         if {$create_branch_checkout} {
141                 switch_branch $newbranch
142         }
145 proc radio_selector {varname value args} {
146         upvar #0 $varname var
147         set var $value
150 trace add variable create_branch_head write \
151         [list radio_selector create_branch_revtype head]
152 trace add variable create_branch_trackinghead write \
153         [list radio_selector create_branch_revtype tracking]
154 trace add variable create_branch_tag write \
155         [list radio_selector create_branch_revtype tag]
157 trace add variable delete_branch_head write \
158         [list radio_selector delete_branch_checktype head]
159 trace add variable delete_branch_trackinghead write \
160         [list radio_selector delete_branch_checktype tracking]
162 proc do_create_branch {} {
163         global all_heads current_branch repo_config
164         global create_branch_checkout create_branch_revtype
165         global create_branch_head create_branch_trackinghead
166         global create_branch_name create_branch_revexp
167         global create_branch_tag
169         set w .branch_editor
170         toplevel $w
171         wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
173         label $w.header -text {Create New Branch} \
174                 -font font_uibold
175         pack $w.header -side top -fill x
177         frame $w.buttons
178         button $w.buttons.create -text Create \
179                 -default active \
180                 -command [list do_create_branch_action $w]
181         pack $w.buttons.create -side right
182         button $w.buttons.cancel -text {Cancel} \
183                 -command [list destroy $w]
184         pack $w.buttons.cancel -side right -padx 5
185         pack $w.buttons -side bottom -fill x -pady 10 -padx 10
187         labelframe $w.desc -text {Branch Description}
188         label $w.desc.name_l -text {Name:}
189         entry $w.desc.name_t \
190                 -borderwidth 1 \
191                 -relief sunken \
192                 -width 40 \
193                 -textvariable create_branch_name \
194                 -validate key \
195                 -validatecommand {
196                         if {%d == 1 && [regexp {[~^:?*\[\0- ]} %S]} {return 0}
197                         return 1
198                 }
199         grid $w.desc.name_l $w.desc.name_t -sticky we -padx {0 5}
200         grid columnconfigure $w.desc 1 -weight 1
201         pack $w.desc -anchor nw -fill x -pady 5 -padx 5
203         labelframe $w.from -text {Starting Revision}
204         radiobutton $w.from.head_r \
205                 -text {Local Branch:} \
206                 -value head \
207                 -variable create_branch_revtype
208         eval tk_optionMenu $w.from.head_m create_branch_head $all_heads
209         grid $w.from.head_r $w.from.head_m -sticky w
210         set all_trackings [all_tracking_branches]
211         if {$all_trackings ne {}} {
212                 set create_branch_trackinghead [lindex $all_trackings 0]
213                 radiobutton $w.from.tracking_r \
214                         -text {Tracking Branch:} \
215                         -value tracking \
216                         -variable create_branch_revtype
217                 eval tk_optionMenu $w.from.tracking_m \
218                         create_branch_trackinghead \
219                         $all_trackings
220                 grid $w.from.tracking_r $w.from.tracking_m -sticky w
221         }
222         set all_tags [load_all_tags]
223         if {$all_tags ne {}} {
224                 set create_branch_tag [lindex $all_tags 0]
225                 radiobutton $w.from.tag_r \
226                         -text {Tag:} \
227                         -value tag \
228                         -variable create_branch_revtype
229                 eval tk_optionMenu $w.from.tag_m create_branch_tag $all_tags
230                 grid $w.from.tag_r $w.from.tag_m -sticky w
231         }
232         radiobutton $w.from.exp_r \
233                 -text {Revision Expression:} \
234                 -value expression \
235                 -variable create_branch_revtype
236         entry $w.from.exp_t \
237                 -borderwidth 1 \
238                 -relief sunken \
239                 -width 50 \
240                 -textvariable create_branch_revexp \
241                 -validate key \
242                 -validatecommand {
243                         if {%d == 1 && [regexp {\s} %S]} {return 0}
244                         if {%d == 1 && [string length %S] > 0} {
245                                 set create_branch_revtype expression
246                         }
247                         return 1
248                 }
249         grid $w.from.exp_r $w.from.exp_t -sticky we -padx {0 5}
250         grid columnconfigure $w.from 1 -weight 1
251         pack $w.from -anchor nw -fill x -pady 5 -padx 5
253         labelframe $w.postActions -text {Post Creation Actions}
254         checkbutton $w.postActions.checkout \
255                 -text {Checkout after creation} \
256                 -variable create_branch_checkout
257         pack $w.postActions.checkout -anchor nw
258         pack $w.postActions -anchor nw -fill x -pady 5 -padx 5
260         set create_branch_checkout 1
261         set create_branch_head $current_branch
262         set create_branch_revtype head
263         set create_branch_name $repo_config(gui.newbranchtemplate)
264         set create_branch_revexp {}
266         bind $w <Visibility> "
267                 grab $w
268                 $w.desc.name_t icursor end
269                 focus $w.desc.name_t
270         "
271         bind $w <Key-Escape> "destroy $w"
272         bind $w <Key-Return> "do_create_branch_action $w;break"
273         wm title $w "[appname] ([reponame]): Create Branch"
274         tkwait window $w
277 proc do_delete_branch_action {w} {
278         global all_heads
279         global delete_branch_checktype delete_branch_head delete_branch_trackinghead
281         set check_rev {}
282         switch -- $delete_branch_checktype {
283         head {set check_rev $delete_branch_head}
284         tracking {set check_rev $delete_branch_trackinghead}
285         always {set check_rev {:none}}
286         }
287         if {$check_rev eq {:none}} {
288                 set check_cmt {}
289         } elseif {[catch {set check_cmt [git rev-parse --verify "${check_rev}^0"]}]} {
290                 tk_messageBox \
291                         -icon error \
292                         -type ok \
293                         -title [wm title $w] \
294                         -parent $w \
295                         -message "Invalid check revision: $check_rev"
296                 return
297         }
299         set to_delete [list]
300         set not_merged [list]
301         foreach i [$w.list.l curselection] {
302                 set b [$w.list.l get $i]
303                 if {[catch {set o [git rev-parse --verify $b]}]} continue
304                 if {$check_cmt ne {}} {
305                         if {$b eq $check_rev} continue
306                         if {[catch {set m [git merge-base $o $check_cmt]}]} continue
307                         if {$o ne $m} {
308                                 lappend not_merged $b
309                                 continue
310                         }
311                 }
312                 lappend to_delete [list $b $o]
313         }
314         if {$not_merged ne {}} {
315                 set msg "The following branches are not completely merged into $check_rev:
317  - [join $not_merged "\n - "]"
318                 tk_messageBox \
319                         -icon info \
320                         -type ok \
321                         -title [wm title $w] \
322                         -parent $w \
323                         -message $msg
324         }
325         if {$to_delete eq {}} return
326         if {$delete_branch_checktype eq {always}} {
327                 set msg {Recovering deleted branches is difficult.
329 Delete the selected branches?}
330                 if {[tk_messageBox \
331                         -icon warning \
332                         -type yesno \
333                         -title [wm title $w] \
334                         -parent $w \
335                         -message $msg] ne yes} {
336                         return
337                 }
338         }
340         set failed {}
341         foreach i $to_delete {
342                 set b [lindex $i 0]
343                 set o [lindex $i 1]
344                 if {[catch {git update-ref -d "refs/heads/$b" $o} err]} {
345                         append failed " - $b: $err\n"
346                 } else {
347                         set x [lsearch -sorted -exact $all_heads $b]
348                         if {$x >= 0} {
349                                 set all_heads [lreplace $all_heads $x $x]
350                         }
351                 }
352         }
354         if {$failed ne {}} {
355                 tk_messageBox \
356                         -icon error \
357                         -type ok \
358                         -title [wm title $w] \
359                         -parent $w \
360                         -message "Failed to delete branches:\n$failed"
361         }
363         set all_heads [lsort $all_heads]
364         populate_branch_menu
365         destroy $w
368 proc do_delete_branch {} {
369         global all_heads tracking_branches current_branch
370         global delete_branch_checktype delete_branch_head delete_branch_trackinghead
372         set w .branch_editor
373         toplevel $w
374         wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
376         label $w.header -text {Delete Local Branch} \
377                 -font font_uibold
378         pack $w.header -side top -fill x
380         frame $w.buttons
381         button $w.buttons.create -text Delete \
382                 -command [list do_delete_branch_action $w]
383         pack $w.buttons.create -side right
384         button $w.buttons.cancel -text {Cancel} \
385                 -command [list destroy $w]
386         pack $w.buttons.cancel -side right -padx 5
387         pack $w.buttons -side bottom -fill x -pady 10 -padx 10
389         labelframe $w.list -text {Local Branches}
390         listbox $w.list.l \
391                 -height 10 \
392                 -width 70 \
393                 -selectmode extended \
394                 -yscrollcommand [list $w.list.sby set]
395         foreach h $all_heads {
396                 if {$h ne $current_branch} {
397                         $w.list.l insert end $h
398                 }
399         }
400         scrollbar $w.list.sby -command [list $w.list.l yview]
401         pack $w.list.sby -side right -fill y
402         pack $w.list.l -side left -fill both -expand 1
403         pack $w.list -fill both -expand 1 -pady 5 -padx 5
405         labelframe $w.validate -text {Delete Only If}
406         radiobutton $w.validate.head_r \
407                 -text {Merged Into Local Branch:} \
408                 -value head \
409                 -variable delete_branch_checktype
410         eval tk_optionMenu $w.validate.head_m delete_branch_head $all_heads
411         grid $w.validate.head_r $w.validate.head_m -sticky w
412         set all_trackings [all_tracking_branches]
413         if {$all_trackings ne {}} {
414                 set delete_branch_trackinghead [lindex $all_trackings 0]
415                 radiobutton $w.validate.tracking_r \
416                         -text {Merged Into Tracking Branch:} \
417                         -value tracking \
418                         -variable delete_branch_checktype
419                 eval tk_optionMenu $w.validate.tracking_m \
420                         delete_branch_trackinghead \
421                         $all_trackings
422                 grid $w.validate.tracking_r $w.validate.tracking_m -sticky w
423         }
424         radiobutton $w.validate.always_r \
425                 -text {Always (Do not perform merge checks)} \
426                 -value always \
427                 -variable delete_branch_checktype
428         grid $w.validate.always_r -columnspan 2 -sticky w
429         grid columnconfigure $w.validate 1 -weight 1
430         pack $w.validate -anchor nw -fill x -pady 5 -padx 5
432         set delete_branch_head $current_branch
433         set delete_branch_checktype head
435         bind $w <Visibility> "grab $w; focus $w"
436         bind $w <Key-Escape> "destroy $w"
437         wm title $w "[appname] ([reponame]): Delete Branch"
438         tkwait window $w
441 proc switch_branch {new_branch} {
442         global HEAD commit_type current_branch repo_config
444         if {![lock_index switch]} return
446         # -- Our in memory state should match the repository.
447         #
448         repository_state curType curHEAD curMERGE_HEAD
449         if {[string match amend* $commit_type]
450                 && $curType eq {normal}
451                 && $curHEAD eq $HEAD} {
452         } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
453                 info_popup {Last scanned state does not match repository state.
455 Another Git program has modified this repository since the last scan.  A rescan must be performed before the current branch can be changed.
457 The rescan will be automatically started now.
459                 unlock_index
460                 rescan {set ui_status_value {Ready.}}
461                 return
462         }
464         # -- Don't do a pointless switch.
465         #
466         if {$current_branch eq $new_branch} {
467                 unlock_index
468                 return
469         }
471         if {$repo_config(gui.trustmtime) eq {true}} {
472                 switch_branch_stage2 {} $new_branch
473         } else {
474                 set ui_status_value {Refreshing file status...}
475                 set cmd [list git update-index]
476                 lappend cmd -q
477                 lappend cmd --unmerged
478                 lappend cmd --ignore-missing
479                 lappend cmd --refresh
480                 set fd_rf [open "| $cmd" r]
481                 fconfigure $fd_rf -blocking 0 -translation binary
482                 fileevent $fd_rf readable \
483                         [list switch_branch_stage2 $fd_rf $new_branch]
484         }
487 proc switch_branch_stage2 {fd_rf new_branch} {
488         global ui_status_value HEAD
490         if {$fd_rf ne {}} {
491                 read $fd_rf
492                 if {![eof $fd_rf]} return
493                 close $fd_rf
494         }
496         set ui_status_value "Updating working directory to '$new_branch'..."
497         set cmd [list git read-tree]
498         lappend cmd -m
499         lappend cmd -u
500         lappend cmd --exclude-per-directory=.gitignore
501         lappend cmd $HEAD
502         lappend cmd $new_branch
503         set fd_rt [open "| $cmd" r]
504         fconfigure $fd_rt -blocking 0 -translation binary
505         fileevent $fd_rt readable \
506                 [list switch_branch_readtree_wait $fd_rt $new_branch]
509 proc switch_branch_readtree_wait {fd_rt new_branch} {
510         global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
511         global current_branch
512         global ui_comm ui_status_value
514         # -- We never get interesting output on stdout; only stderr.
515         #
516         read $fd_rt
517         fconfigure $fd_rt -blocking 1
518         if {![eof $fd_rt]} {
519                 fconfigure $fd_rt -blocking 0
520                 return
521         }
523         # -- The working directory wasn't in sync with the index and
524         #    we'd have to overwrite something to make the switch. A
525         #    merge is required.
526         #
527         if {[catch {close $fd_rt} err]} {
528                 regsub {^fatal: } $err {} err
529                 warn_popup "File level merge required.
531 $err
533 Staying on branch '$current_branch'."
534                 set ui_status_value "Aborted checkout of '$new_branch' (file level merging is required)."
535                 unlock_index
536                 return
537         }
539         # -- Update the symbolic ref.  Core git doesn't even check for failure
540         #    here, it Just Works(tm).  If it doesn't we are in some really ugly
541         #    state that is difficult to recover from within git-gui.
542         #
543         if {[catch {git symbolic-ref HEAD "refs/heads/$new_branch"} err]} {
544                 error_popup "Failed to set current branch.
546 This working directory is only partially switched.  We successfully updated your files, but failed to update an internal Git file.
548 This should not have occurred.  [appname] will now close and give up.
550 $err"
551                 do_quit
552                 return
553         }
555         # -- Update our repository state.  If we were previously in amend mode
556         #    we need to toss the current buffer and do a full rescan to update
557         #    our file lists.  If we weren't in amend mode our file lists are
558         #    accurate and we can avoid the rescan.
559         #
560         unlock_index
561         set selected_commit_type new
562         if {[string match amend* $commit_type]} {
563                 $ui_comm delete 0.0 end
564                 $ui_comm edit reset
565                 $ui_comm edit modified false
566                 rescan {set ui_status_value "Checked out branch '$current_branch'."}
567         } else {
568                 repository_state commit_type HEAD MERGE_HEAD
569                 set PARENT $HEAD
570                 set ui_status_value "Checked out branch '$current_branch'."
571         }