Code

git-gui: Abstract the revision picker into a mega widget
[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 radio_selector {varname value args} {
65         upvar #0 $varname var
66         set var $value
67 }
69 proc do_delete_branch_action {w} {
70         global all_heads
71         global delete_branch_checktype delete_branch_head delete_branch_trackinghead
73         set check_rev {}
74         switch -- $delete_branch_checktype {
75         head {set check_rev $delete_branch_head}
76         tracking {set check_rev $delete_branch_trackinghead}
77         always {set check_rev {:none}}
78         }
79         if {$check_rev eq {:none}} {
80                 set check_cmt {}
81         } elseif {[catch {set check_cmt [git rev-parse --verify "${check_rev}^0"]}]} {
82                 tk_messageBox \
83                         -icon error \
84                         -type ok \
85                         -title [wm title $w] \
86                         -parent $w \
87                         -message "Invalid check revision: $check_rev"
88                 return
89         }
91         set to_delete [list]
92         set not_merged [list]
93         foreach i [$w.list.l curselection] {
94                 set b [$w.list.l get $i]
95                 if {[catch {set o [git rev-parse --verify $b]}]} continue
96                 if {$check_cmt ne {}} {
97                         if {$b eq $check_rev} continue
98                         if {[catch {set m [git merge-base $o $check_cmt]}]} continue
99                         if {$o ne $m} {
100                                 lappend not_merged $b
101                                 continue
102                         }
103                 }
104                 lappend to_delete [list $b $o]
105         }
106         if {$not_merged ne {}} {
107                 set msg "The following branches are not completely merged into $check_rev:
109  - [join $not_merged "\n - "]"
110                 tk_messageBox \
111                         -icon info \
112                         -type ok \
113                         -title [wm title $w] \
114                         -parent $w \
115                         -message $msg
116         }
117         if {$to_delete eq {}} return
118         if {$delete_branch_checktype eq {always}} {
119                 set msg {Recovering deleted branches is difficult.
121 Delete the selected branches?}
122                 if {[tk_messageBox \
123                         -icon warning \
124                         -type yesno \
125                         -title [wm title $w] \
126                         -parent $w \
127                         -message $msg] ne yes} {
128                         return
129                 }
130         }
132         set failed {}
133         foreach i $to_delete {
134                 set b [lindex $i 0]
135                 set o [lindex $i 1]
136                 if {[catch {git update-ref -d "refs/heads/$b" $o} err]} {
137                         append failed " - $b: $err\n"
138                 } else {
139                         set x [lsearch -sorted -exact $all_heads $b]
140                         if {$x >= 0} {
141                                 set all_heads [lreplace $all_heads $x $x]
142                         }
143                 }
144         }
146         if {$failed ne {}} {
147                 tk_messageBox \
148                         -icon error \
149                         -type ok \
150                         -title [wm title $w] \
151                         -parent $w \
152                         -message "Failed to delete branches:\n$failed"
153         }
155         set all_heads [lsort $all_heads]
156         populate_branch_menu
157         destroy $w
160 proc do_delete_branch {} {
161         global all_heads tracking_branches current_branch
162         global delete_branch_checktype delete_branch_head delete_branch_trackinghead
164         set w .branch_editor
165         toplevel $w
166         wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
168         label $w.header -text {Delete Local Branch} \
169                 -font font_uibold
170         pack $w.header -side top -fill x
172         frame $w.buttons
173         button $w.buttons.create -text Delete \
174                 -command [list do_delete_branch_action $w]
175         pack $w.buttons.create -side right
176         button $w.buttons.cancel -text {Cancel} \
177                 -command [list destroy $w]
178         pack $w.buttons.cancel -side right -padx 5
179         pack $w.buttons -side bottom -fill x -pady 10 -padx 10
181         labelframe $w.list -text {Local Branches}
182         listbox $w.list.l \
183                 -height 10 \
184                 -width 70 \
185                 -selectmode extended \
186                 -yscrollcommand [list $w.list.sby set]
187         foreach h $all_heads {
188                 if {$h ne $current_branch} {
189                         $w.list.l insert end $h
190                 }
191         }
192         scrollbar $w.list.sby -command [list $w.list.l yview]
193         pack $w.list.sby -side right -fill y
194         pack $w.list.l -side left -fill both -expand 1
195         pack $w.list -fill both -expand 1 -pady 5 -padx 5
197         labelframe $w.validate -text {Delete Only If}
198         radiobutton $w.validate.head_r \
199                 -text {Merged Into Local Branch:} \
200                 -value head \
201                 -variable delete_branch_checktype
202         eval tk_optionMenu $w.validate.head_m delete_branch_head $all_heads
203         grid $w.validate.head_r $w.validate.head_m -sticky w
204         set all_trackings [all_tracking_branches]
205         if {$all_trackings ne {}} {
206                 set delete_branch_trackinghead [lindex $all_trackings 0]
207                 radiobutton $w.validate.tracking_r \
208                         -text {Merged Into Tracking Branch:} \
209                         -value tracking \
210                         -variable delete_branch_checktype
211                 eval tk_optionMenu $w.validate.tracking_m \
212                         delete_branch_trackinghead \
213                         $all_trackings
214                 grid $w.validate.tracking_r $w.validate.tracking_m -sticky w
215         }
216         radiobutton $w.validate.always_r \
217                 -text {Always (Do not perform merge checks)} \
218                 -value always \
219                 -variable delete_branch_checktype
220         grid $w.validate.always_r -columnspan 2 -sticky w
221         grid columnconfigure $w.validate 1 -weight 1
222         pack $w.validate -anchor nw -fill x -pady 5 -padx 5
224         set delete_branch_head $current_branch
225         set delete_branch_checktype head
227         bind $w <Visibility> "grab $w; focus $w"
228         bind $w <Key-Escape> "destroy $w"
229         wm title $w "[appname] ([reponame]): Delete Branch"
230         tkwait window $w
233 proc switch_branch {new_branch} {
234         global HEAD commit_type current_branch repo_config
236         if {![lock_index switch]} return
238         # -- Our in memory state should match the repository.
239         #
240         repository_state curType curHEAD curMERGE_HEAD
241         if {[string match amend* $commit_type]
242                 && $curType eq {normal}
243                 && $curHEAD eq $HEAD} {
244         } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
245                 info_popup {Last scanned state does not match repository state.
247 Another Git program has modified this repository since the last scan.  A rescan must be performed before the current branch can be changed.
249 The rescan will be automatically started now.
251                 unlock_index
252                 rescan {set ui_status_value {Ready.}}
253                 return
254         }
256         # -- Don't do a pointless switch.
257         #
258         if {$current_branch eq $new_branch} {
259                 unlock_index
260                 return
261         }
263         if {$repo_config(gui.trustmtime) eq {true}} {
264                 switch_branch_stage2 {} $new_branch
265         } else {
266                 set ui_status_value {Refreshing file status...}
267                 set cmd [list git update-index]
268                 lappend cmd -q
269                 lappend cmd --unmerged
270                 lappend cmd --ignore-missing
271                 lappend cmd --refresh
272                 set fd_rf [open "| $cmd" r]
273                 fconfigure $fd_rf -blocking 0 -translation binary
274                 fileevent $fd_rf readable \
275                         [list switch_branch_stage2 $fd_rf $new_branch]
276         }
279 proc switch_branch_stage2 {fd_rf new_branch} {
280         global ui_status_value HEAD
282         if {$fd_rf ne {}} {
283                 read $fd_rf
284                 if {![eof $fd_rf]} return
285                 close $fd_rf
286         }
288         set ui_status_value "Updating working directory to '$new_branch'..."
289         set cmd [list git read-tree]
290         lappend cmd -m
291         lappend cmd -u
292         lappend cmd --exclude-per-directory=.gitignore
293         lappend cmd $HEAD
294         lappend cmd $new_branch
295         set fd_rt [open "| $cmd" r]
296         fconfigure $fd_rt -blocking 0 -translation binary
297         fileevent $fd_rt readable \
298                 [list switch_branch_readtree_wait $fd_rt $new_branch]
301 proc switch_branch_readtree_wait {fd_rt new_branch} {
302         global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
303         global current_branch
304         global ui_comm ui_status_value
306         # -- We never get interesting output on stdout; only stderr.
307         #
308         read $fd_rt
309         fconfigure $fd_rt -blocking 1
310         if {![eof $fd_rt]} {
311                 fconfigure $fd_rt -blocking 0
312                 return
313         }
315         # -- The working directory wasn't in sync with the index and
316         #    we'd have to overwrite something to make the switch. A
317         #    merge is required.
318         #
319         if {[catch {close $fd_rt} err]} {
320                 regsub {^fatal: } $err {} err
321                 warn_popup "File level merge required.
323 $err
325 Staying on branch '$current_branch'."
326                 set ui_status_value "Aborted checkout of '$new_branch' (file level merging is required)."
327                 unlock_index
328                 return
329         }
331         # -- Update the symbolic ref.  Core git doesn't even check for failure
332         #    here, it Just Works(tm).  If it doesn't we are in some really ugly
333         #    state that is difficult to recover from within git-gui.
334         #
335         if {[catch {git symbolic-ref HEAD "refs/heads/$new_branch"} err]} {
336                 error_popup "Failed to set current branch.
338 This working directory is only partially switched.  We successfully updated your files, but failed to update an internal Git file.
340 This should not have occurred.  [appname] will now close and give up.
342 $err"
343                 do_quit
344                 return
345         }
347         # -- Update our repository state.  If we were previously in amend mode
348         #    we need to toss the current buffer and do a full rescan to update
349         #    our file lists.  If we weren't in amend mode our file lists are
350         #    accurate and we can avoid the rescan.
351         #
352         unlock_index
353         set selected_commit_type new
354         if {[string match amend* $commit_type]} {
355                 $ui_comm delete 0.0 end
356                 $ui_comm edit reset
357                 $ui_comm edit modified false
358                 rescan {set ui_status_value "Checked out branch '$current_branch'."}
359         } else {
360                 repository_state commit_type HEAD MERGE_HEAD
361                 set PARENT $HEAD
362                 set ui_status_value "Checked out branch '$current_branch'."
363         }