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 radiobutton $w.from.head_r \
205 -text {Local Branch:} \
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:} \
216 -variable create_branch_revtype
217 eval tk_optionMenu $w.from.tracking_m \
218 create_branch_trackinghead \
220 grid $w.from.tracking_r $w.from.tracking_m -sticky w
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 \
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
232 radiobutton $w.from.exp_r \
233 -text {Revision Expression:} \
235 -variable create_branch_revtype
236 entry $w.from.exp_t \
240 -textvariable create_branch_revexp \
243 if {%d == 1 && [regexp {\s} %S]} {return 0}
244 if {%d == 1 && [string length %S] > 0} {
245 set create_branch_revtype expression
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> "
268 $w.desc.name_t icursor end
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"
277 proc do_delete_branch_action {w} {
279 global delete_branch_checktype delete_branch_head delete_branch_trackinghead
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}}
287 if {$check_rev eq {:none}} {
289 } elseif {[catch {set check_cmt [git rev-parse --verify "${check_rev}^0"]}]} {
293 -title [wm title $w] \
295 -message "Invalid check revision: $check_rev"
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
308 lappend not_merged $b
312 lappend to_delete [list $b $o]
314 if {$not_merged ne {}} {
315 set msg "The following branches are not completely merged into $check_rev:
317 - [join $not_merged "\n - "]"
321 -title [wm title $w] \
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?}
333 -title [wm title $w] \
335 -message $msg] ne yes} {
341 foreach i $to_delete {
344 if {[catch {git update-ref -d "refs/heads/$b" $o} err]} {
345 append failed " - $b: $err\n"
347 set x [lsearch -sorted -exact $all_heads $b]
349 set all_heads [lreplace $all_heads $x $x]
358 -title [wm title $w] \
360 -message "Failed to delete branches:\n$failed"
363 set all_heads [lsort $all_heads]
368 proc do_delete_branch {} {
369 global all_heads tracking_branches current_branch
370 global delete_branch_checktype delete_branch_head delete_branch_trackinghead
374 wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
376 label $w.header -text {Delete Local Branch} \
378 pack $w.header -side top -fill x
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}
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
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:} \
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:} \
418 -variable delete_branch_checktype
419 eval tk_optionMenu $w.validate.tracking_m \
420 delete_branch_trackinghead \
422 grid $w.validate.tracking_r $w.validate.tracking_m -sticky w
424 radiobutton $w.validate.always_r \
425 -text {Always (Do not perform merge checks)} \
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"
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.
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.
460 rescan {set ui_status_value {Ready.}}
464 # -- Don't do a pointless switch.
466 if {$current_branch eq $new_branch} {
471 if {$repo_config(gui.trustmtime) eq {true}} {
472 switch_branch_stage2 {} $new_branch
474 set ui_status_value {Refreshing file status...}
475 set cmd [list git update-index]
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]
487 proc switch_branch_stage2 {fd_rf new_branch} {
488 global ui_status_value HEAD
492 if {![eof $fd_rf]} return
496 set ui_status_value "Updating working directory to '$new_branch'..."
497 set cmd [list git read-tree]
500 lappend cmd --exclude-per-directory=.gitignore
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.
517 fconfigure $fd_rt -blocking 1
519 fconfigure $fd_rt -blocking 0
523 # -- The working directory wasn't in sync with the index and
524 # we'd have to overwrite something to make the switch. A
527 if {[catch {close $fd_rt} err]} {
528 regsub {^fatal: } $err {} err
529 warn_popup "File level merge required.
533 Staying on branch '$current_branch'."
534 set ui_status_value "Aborted checkout of '$new_branch' (file level merging is required)."
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.
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.
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.
561 set selected_commit_type new
562 if {[string match amend* $commit_type]} {
563 $ui_comm delete 0.0 end
565 $ui_comm edit modified false
566 rescan {set ui_status_value "Checked out branch '$current_branch'."}
568 repository_state commit_type HEAD MERGE_HEAD
570 set ui_status_value "Checked out branch '$current_branch'."