git-gui: Abstract the revision picker into a mega widget
[git/jrn.git] / lib / branch.tcl
blobe7559f978912c143453391bca98330818cc13394
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
14 close $fd
16 set all_heads [lsort $all_heads]
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
26 close $fd
28 return [lsort $all_tags]
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
45 set disable_on_lock $new_dol
46 break
50 if {$all_heads ne {}} {
51 $m add separator
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]
64 proc radio_selector {varname value args} {
65 upvar #0 $varname var
66 set var $value
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}}
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
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
104 lappend to_delete [list $b $o]
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
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
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]
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"
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
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
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.
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
256 # -- Don't do a pointless switch.
258 if {$current_branch eq $new_branch} {
259 unlock_index
260 return
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]
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
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.
308 read $fd_rt
309 fconfigure $fd_rt -blocking 1
310 if {![eof $fd_rt]} {
311 fconfigure $fd_rt -blocking 0
312 return
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.
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
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.
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
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.
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'."