1 # git-gui branch (create/delete) support
2 # Copyright (C) 2006, 2007 Shawn Pearce
4 proc load_all_heads {} {
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
16 set all_heads [lsort $all_heads]
19 proc load_all_tags {} {
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
28 return [lsort $all_tags]
31 proc populate_branch_menu {} {
32 global all_heads disable_on_lock
35 set last [$m index last]
36 for {set i 0} {$i <= $last} {incr i} {
37 if {[$m type $i] eq {separator}} {
40 foreach a $disable_on_lock {
41 if {[lindex $a 0] ne $m || [lindex $a 2] < $i} {
45 set disable_on_lock $new_dol
50 if {$all_heads ne {}} {
53 foreach b $all_heads {
56 -command [list switch_branch $b] \
57 -variable current_branch \
59 lappend disable_on_lock \
60 [list $m entryconf [$m index last] -state]
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
73 || $newbranch eq $repo_config(gui.newbranchtemplate)} {
77 -title [wm title $w] \
79 -message "Please supply a branch name."
83 if {![catch {git show-ref --verify -- "refs/heads/$newbranch"}]} {
87 -title [wm title $w] \
89 -message "Branch '$newbranch' already exists."
93 if {[catch {git check-ref-format "heads/$newbranch"}]} {
97 -title [wm title $w] \
99 -message "We do not like '$newbranch' as a branch name."
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}
111 if {[catch {set cmt [git rev-parse --verify "${rev}^0"]}]} {
115 -title [wm title $w] \
117 -message "Invalid starting revision: $rev"
122 -m "branch: Created from $rev" \
123 "refs/heads/$newbranch" \
130 -title [wm title $w] \
132 -message "Failed to create '$newbranch'.\n\n$err"
136 lappend all_heads $newbranch
137 set all_heads [lsort $all_heads]
140 if {$create_branch_checkout} {
141 switch_branch $newbranch
145 proc radio_selector {varname value args} {
146 upvar #0 $varname var
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
171 wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
173 label $w.header -text {Create New Branch} \
175 pack $w.header -side top -fill x
178 button $w.buttons.create -text Create \
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 \
193 -textvariable create_branch_name \
196 if {%d == 1 && [regexp {[~^:?*\[\0- ]} %S]} {return 0}
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 if {$all_heads ne {}} {
205 radiobutton $w.from.head_r \
206 -text {Local Branch:} \
208 -variable create_branch_revtype
209 eval tk_optionMenu $w.from.head_m create_branch_head $all_heads
210 grid $w.from.head_r $w.from.head_m -sticky w
212 set all_trackings [all_tracking_branches]
213 if {$all_trackings ne {}} {
214 set create_branch_trackinghead [lindex $all_trackings 0]
215 radiobutton $w.from.tracking_r \
216 -text {Tracking Branch:} \
218 -variable create_branch_revtype
219 eval tk_optionMenu $w.from.tracking_m \
220 create_branch_trackinghead \
222 grid $w.from.tracking_r $w.from.tracking_m -sticky w
224 set all_tags [load_all_tags]
225 if {$all_tags ne {}} {
226 set create_branch_tag [lindex $all_tags 0]
227 radiobutton $w.from.tag_r \
230 -variable create_branch_revtype
231 eval tk_optionMenu $w.from.tag_m create_branch_tag $all_tags
232 grid $w.from.tag_r $w.from.tag_m -sticky w
234 radiobutton $w.from.exp_r \
235 -text {Revision Expression:} \
237 -variable create_branch_revtype
238 entry $w.from.exp_t \
242 -textvariable create_branch_revexp \
245 if {%d == 1 && [regexp {\s} %S]} {return 0}
246 if {%d == 1 && [string length %S] > 0} {
247 set create_branch_revtype expression
251 grid $w.from.exp_r $w.from.exp_t -sticky we -padx {0 5}
252 grid columnconfigure $w.from 1 -weight 1
253 pack $w.from -anchor nw -fill x -pady 5 -padx 5
255 labelframe $w.postActions -text {Post Creation Actions}
256 checkbutton $w.postActions.checkout \
257 -text {Checkout after creation} \
258 -variable create_branch_checkout
259 pack $w.postActions.checkout -anchor nw
260 pack $w.postActions -anchor nw -fill x -pady 5 -padx 5
262 set create_branch_checkout 1
263 set create_branch_head $current_branch
264 set create_branch_revtype head
265 set create_branch_name $repo_config(gui.newbranchtemplate)
266 set create_branch_revexp {}
268 bind $w <Visibility> "
270 $w.desc.name_t icursor end
273 bind $w <Key-Escape> "destroy $w"
274 bind $w <Key-Return> "do_create_branch_action $w;break"
275 wm title $w "[appname] ([reponame]): Create Branch"
279 proc do_delete_branch_action {w} {
281 global delete_branch_checktype delete_branch_head delete_branch_trackinghead
284 switch -- $delete_branch_checktype {
285 head {set check_rev $delete_branch_head}
286 tracking {set check_rev $delete_branch_trackinghead}
287 always {set check_rev {:none}}
289 if {$check_rev eq {:none}} {
291 } elseif {[catch {set check_cmt [git rev-parse --verify "${check_rev}^0"]}]} {
295 -title [wm title $w] \
297 -message "Invalid check revision: $check_rev"
302 set not_merged [list]
303 foreach i [$w.list.l curselection] {
304 set b [$w.list.l get $i]
305 if {[catch {set o [git rev-parse --verify $b]}]} continue
306 if {$check_cmt ne {}} {
307 if {$b eq $check_rev} continue
308 if {[catch {set m [git merge-base $o $check_cmt]}]} continue
310 lappend not_merged $b
314 lappend to_delete [list $b $o]
316 if {$not_merged ne {}} {
317 set msg "The following branches are not completely merged into $check_rev:
319 - [join $not_merged "\n - "]"
323 -title [wm title $w] \
327 if {$to_delete eq {}} return
328 if {$delete_branch_checktype eq {always}} {
329 set msg {Recovering deleted branches is difficult.
331 Delete the selected branches?}
335 -title [wm title $w] \
337 -message $msg] ne yes} {
343 foreach i $to_delete {
346 if {[catch {git update-ref -d "refs/heads/$b" $o} err]} {
347 append failed " - $b: $err\n"
349 set x [lsearch -sorted -exact $all_heads $b]
351 set all_heads [lreplace $all_heads $x $x]
360 -title [wm title $w] \
362 -message "Failed to delete branches:\n$failed"
365 set all_heads [lsort $all_heads]
370 proc do_delete_branch {} {
371 global all_heads tracking_branches current_branch
372 global delete_branch_checktype delete_branch_head delete_branch_trackinghead
376 wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
378 label $w.header -text {Delete Local Branch} \
380 pack $w.header -side top -fill x
383 button $w.buttons.create -text Delete \
384 -command [list do_delete_branch_action $w]
385 pack $w.buttons.create -side right
386 button $w.buttons.cancel -text {Cancel} \
387 -command [list destroy $w]
388 pack $w.buttons.cancel -side right -padx 5
389 pack $w.buttons -side bottom -fill x -pady 10 -padx 10
391 labelframe $w.list -text {Local Branches}
395 -selectmode extended \
396 -yscrollcommand [list $w.list.sby set]
397 foreach h $all_heads {
398 if {$h ne $current_branch} {
399 $w.list.l insert end $h
402 scrollbar $w.list.sby -command [list $w.list.l yview]
403 pack $w.list.sby -side right -fill y
404 pack $w.list.l -side left -fill both -expand 1
405 pack $w.list -fill both -expand 1 -pady 5 -padx 5
407 labelframe $w.validate -text {Delete Only If}
408 radiobutton $w.validate.head_r \
409 -text {Merged Into Local Branch:} \
411 -variable delete_branch_checktype
412 eval tk_optionMenu $w.validate.head_m delete_branch_head $all_heads
413 grid $w.validate.head_r $w.validate.head_m -sticky w
414 set all_trackings [all_tracking_branches]
415 if {$all_trackings ne {}} {
416 set delete_branch_trackinghead [lindex $all_trackings 0]
417 radiobutton $w.validate.tracking_r \
418 -text {Merged Into Tracking Branch:} \
420 -variable delete_branch_checktype
421 eval tk_optionMenu $w.validate.tracking_m \
422 delete_branch_trackinghead \
424 grid $w.validate.tracking_r $w.validate.tracking_m -sticky w
426 radiobutton $w.validate.always_r \
427 -text {Always (Do not perform merge checks)} \
429 -variable delete_branch_checktype
430 grid $w.validate.always_r -columnspan 2 -sticky w
431 grid columnconfigure $w.validate 1 -weight 1
432 pack $w.validate -anchor nw -fill x -pady 5 -padx 5
434 set delete_branch_head $current_branch
435 set delete_branch_checktype head
437 bind $w <Visibility> "grab $w; focus $w"
438 bind $w <Key-Escape> "destroy $w"
439 wm title $w "[appname] ([reponame]): Delete Branch"
443 proc switch_branch {new_branch} {
444 global HEAD commit_type current_branch repo_config
446 if {![lock_index switch]} return
448 # -- Our in memory state should match the repository.
450 repository_state curType curHEAD curMERGE_HEAD
451 if {[string match amend* $commit_type]
452 && $curType eq {normal}
453 && $curHEAD eq $HEAD} {
454 } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
455 info_popup {Last scanned state does not match repository state.
457 Another Git program has modified this repository since the last scan. A rescan must be performed before the current branch can be changed.
459 The rescan will be automatically started now.
462 rescan {set ui_status_value {Ready.}}
466 # -- Don't do a pointless switch.
468 if {$current_branch eq $new_branch} {
473 if {$repo_config(gui.trustmtime) eq {true}} {
474 switch_branch_stage2 {} $new_branch
476 set ui_status_value {Refreshing file status...}
477 set cmd [list git update-index]
479 lappend cmd --unmerged
480 lappend cmd --ignore-missing
481 lappend cmd --refresh
482 set fd_rf [open "| $cmd" r]
483 fconfigure $fd_rf -blocking 0 -translation binary
484 fileevent $fd_rf readable \
485 [list switch_branch_stage2 $fd_rf $new_branch]
489 proc switch_branch_stage2 {fd_rf new_branch} {
490 global ui_status_value HEAD
494 if {![eof $fd_rf]} return
498 set ui_status_value "Updating working directory to '$new_branch'..."
499 set cmd [list git read-tree]
502 lappend cmd --exclude-per-directory=.gitignore
504 lappend cmd $new_branch
505 set fd_rt [open "| $cmd" r]
506 fconfigure $fd_rt -blocking 0 -translation binary
507 fileevent $fd_rt readable \
508 [list switch_branch_readtree_wait $fd_rt $new_branch]
511 proc switch_branch_readtree_wait {fd_rt new_branch} {
512 global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
513 global current_branch
514 global ui_comm ui_status_value
516 # -- We never get interesting output on stdout; only stderr.
519 fconfigure $fd_rt -blocking 1
521 fconfigure $fd_rt -blocking 0
525 # -- The working directory wasn't in sync with the index and
526 # we'd have to overwrite something to make the switch. A
529 if {[catch {close $fd_rt} err]} {
530 regsub {^fatal: } $err {} err
531 warn_popup "File level merge required.
535 Staying on branch '$current_branch'."
536 set ui_status_value "Aborted checkout of '$new_branch' (file level merging is required)."
541 # -- Update the symbolic ref. Core git doesn't even check for failure
542 # here, it Just Works(tm). If it doesn't we are in some really ugly
543 # state that is difficult to recover from within git-gui.
545 if {[catch {git symbolic-ref HEAD "refs/heads/$new_branch"} err]} {
546 error_popup "Failed to set current branch.
548 This working directory is only partially switched. We successfully updated your files, but failed to update an internal Git file.
550 This should not have occurred. [appname] will now close and give up.
557 # -- Update our repository state. If we were previously in amend mode
558 # we need to toss the current buffer and do a full rescan to update
559 # our file lists. If we weren't in amend mode our file lists are
560 # accurate and we can avoid the rescan.
563 set selected_commit_type new
564 if {[string match amend* $commit_type]} {
565 $ui_comm delete 0.0 end
567 $ui_comm edit modified false
568 rescan {set ui_status_value "Checked out branch '$current_branch'."}
570 repository_state commit_type HEAD MERGE_HEAD
572 set ui_status_value "Checked out branch '$current_branch'."