1# git-gui branch (create/delete) support
2# Copyright (C) 2006, 2007 Shawn Pearce
3
4proc load_all_heads {} {
5 global all_heads
6
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
15
16 set all_heads [lsort $all_heads]
17}
18
19proc 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
27
28 return [lsort $all_tags]
29}
30
31proc populate_branch_menu {} {
32 global all_heads disable_on_lock
33
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 }
49
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}
63
64proc 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
70
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 }
103
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 }
135
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 }
143}
144
145proc radio_selector {varname value args} {
146 upvar #0 $varname var
147 set var $value
148}
149
150trace add variable create_branch_head write \
151 [list radio_selector create_branch_revtype head]
152trace add variable create_branch_trackinghead write \
153 [list radio_selector create_branch_revtype tracking]
154trace add variable create_branch_tag write \
155 [list radio_selector create_branch_revtype tag]
156
157trace add variable delete_branch_head write \
158 [list radio_selector delete_branch_checktype head]
159trace add variable delete_branch_trackinghead write \
160 [list radio_selector delete_branch_checktype tracking]
161
162proc 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
168
169 set w .branch_editor
170 toplevel $w
171 wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
172
173 label $w.header -text {Create New Branch} \
174 -font font_uibold
175 pack $w.header -side top -fill x
176
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
186
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
202
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
252
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
259
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 {}
265
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
275}
276
277proc do_delete_branch_action {w} {
278 global all_heads
279 global delete_branch_checktype delete_branch_head delete_branch_trackinghead
280
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 }
298
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:
316
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.
328
329Delete 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 }
339
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 }
353
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 }
362
363 set all_heads [lsort $all_heads]
364 populate_branch_menu
365 destroy $w
366}
367
368proc do_delete_branch {} {
369 global all_heads tracking_branches current_branch
370 global delete_branch_checktype delete_branch_head delete_branch_trackinghead
371
372 set w .branch_editor
373 toplevel $w
374 wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
375
376 label $w.header -text {Delete Local Branch} \
377 -font font_uibold
378 pack $w.header -side top -fill x
379
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
388
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
404
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
431
432 set delete_branch_head $current_branch
433 set delete_branch_checktype head
434
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
439}
440
441proc switch_branch {new_branch} {
442 global HEAD commit_type current_branch repo_config
443
444 if {![lock_index switch]} return
445
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.
454
455Another Git program has modified this repository since the last scan. A rescan must be performed before the current branch can be changed.
456
457The rescan will be automatically started now.
458}
459 unlock_index
460 rescan {set ui_status_value {Ready.}}
461 return
462 }
463
464 # -- Don't do a pointless switch.
465 #
466 if {$current_branch eq $new_branch} {
467 unlock_index
468 return
469 }
470
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 }
485}
486
487proc switch_branch_stage2 {fd_rf new_branch} {
488 global ui_status_value HEAD
489
490 if {$fd_rf ne {}} {
491 read $fd_rf
492 if {![eof $fd_rf]} return
493 close $fd_rf
494 }
495
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]
507}
508
509proc 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
513
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 }
522
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.
530
531$err
532
533Staying 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 }
538
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.
545
546This working directory is only partially switched. We successfully updated your files, but failed to update an internal Git file.
547
548This should not have occurred. [appname] will now close and give up.
549
550$err"
551 do_quit
552 return
553 }
554
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 }
572}