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 if {$all_heads ne {}} {
205 radiobutton $w.from.head_r \
206 -text {Local Branch:} \
207 -value head \
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
211 }
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:} \
217 -value tracking \
218 -variable create_branch_revtype
219 eval tk_optionMenu $w.from.tracking_m \
220 create_branch_trackinghead \
221 $all_trackings
222 grid $w.from.tracking_r $w.from.tracking_m -sticky w
223 }
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 \
228 -text {Tag:} \
229 -value tag \
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
233 }
234 radiobutton $w.from.exp_r \
235 -text {Revision Expression:} \
236 -value expression \
237 -variable create_branch_revtype
238 entry $w.from.exp_t \
239 -borderwidth 1 \
240 -relief sunken \
241 -width 50 \
242 -textvariable create_branch_revexp \
243 -validate key \
244 -validatecommand {
245 if {%d == 1 && [regexp {\s} %S]} {return 0}
246 if {%d == 1 && [string length %S] > 0} {
247 set create_branch_revtype expression
248 }
249 return 1
250 }
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
254
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
261
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 {}
267
268 bind $w <Visibility> "
269 grab $w
270 $w.desc.name_t icursor end
271 focus $w.desc.name_t
272 "
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"
276 tkwait window $w
277}
278
279proc do_delete_branch_action {w} {
280 global all_heads
281 global delete_branch_checktype delete_branch_head delete_branch_trackinghead
282
283 set check_rev {}
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}}
288 }
289 if {$check_rev eq {:none}} {
290 set check_cmt {}
291 } elseif {[catch {set check_cmt [git rev-parse --verify "${check_rev}^0"]}]} {
292 tk_messageBox \
293 -icon error \
294 -type ok \
295 -title [wm title $w] \
296 -parent $w \
297 -message "Invalid check revision: $check_rev"
298 return
299 }
300
301 set to_delete [list]
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
309 if {$o ne $m} {
310 lappend not_merged $b
311 continue
312 }
313 }
314 lappend to_delete [list $b $o]
315 }
316 if {$not_merged ne {}} {
317 set msg "The following branches are not completely merged into $check_rev:
318
319 - [join $not_merged "\n - "]"
320 tk_messageBox \
321 -icon info \
322 -type ok \
323 -title [wm title $w] \
324 -parent $w \
325 -message $msg
326 }
327 if {$to_delete eq {}} return
328 if {$delete_branch_checktype eq {always}} {
329 set msg {Recovering deleted branches is difficult.
330
331Delete the selected branches?}
332 if {[tk_messageBox \
333 -icon warning \
334 -type yesno \
335 -title [wm title $w] \
336 -parent $w \
337 -message $msg] ne yes} {
338 return
339 }
340 }
341
342 set failed {}
343 foreach i $to_delete {
344 set b [lindex $i 0]
345 set o [lindex $i 1]
346 if {[catch {git update-ref -d "refs/heads/$b" $o} err]} {
347 append failed " - $b: $err\n"
348 } else {
349 set x [lsearch -sorted -exact $all_heads $b]
350 if {$x >= 0} {
351 set all_heads [lreplace $all_heads $x $x]
352 }
353 }
354 }
355
356 if {$failed ne {}} {
357 tk_messageBox \
358 -icon error \
359 -type ok \
360 -title [wm title $w] \
361 -parent $w \
362 -message "Failed to delete branches:\n$failed"
363 }
364
365 set all_heads [lsort $all_heads]
366 populate_branch_menu
367 destroy $w
368}
369
370proc do_delete_branch {} {
371 global all_heads tracking_branches current_branch
372 global delete_branch_checktype delete_branch_head delete_branch_trackinghead
373
374 set w .branch_editor
375 toplevel $w
376 wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
377
378 label $w.header -text {Delete Local Branch} \
379 -font font_uibold
380 pack $w.header -side top -fill x
381
382 frame $w.buttons
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
390
391 labelframe $w.list -text {Local Branches}
392 listbox $w.list.l \
393 -height 10 \
394 -width 70 \
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
400 }
401 }
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
406
407 labelframe $w.validate -text {Delete Only If}
408 radiobutton $w.validate.head_r \
409 -text {Merged Into Local Branch:} \
410 -value head \
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:} \
419 -value tracking \
420 -variable delete_branch_checktype
421 eval tk_optionMenu $w.validate.tracking_m \
422 delete_branch_trackinghead \
423 $all_trackings
424 grid $w.validate.tracking_r $w.validate.tracking_m -sticky w
425 }
426 radiobutton $w.validate.always_r \
427 -text {Always (Do not perform merge checks)} \
428 -value always \
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
433
434 set delete_branch_head $current_branch
435 set delete_branch_checktype head
436
437 bind $w <Visibility> "grab $w; focus $w"
438 bind $w <Key-Escape> "destroy $w"
439 wm title $w "[appname] ([reponame]): Delete Branch"
440 tkwait window $w
441}
442
443proc switch_branch {new_branch} {
444 global HEAD commit_type current_branch repo_config
445
446 if {![lock_index switch]} return
447
448 # -- Our in memory state should match the repository.
449 #
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.
456
457Another Git program has modified this repository since the last scan. A rescan must be performed before the current branch can be changed.
458
459The rescan will be automatically started now.
460}
461 unlock_index
462 rescan {set ui_status_value {Ready.}}
463 return
464 }
465
466 # -- Don't do a pointless switch.
467 #
468 if {$current_branch eq $new_branch} {
469 unlock_index
470 return
471 }
472
473 if {$repo_config(gui.trustmtime) eq {true}} {
474 switch_branch_stage2 {} $new_branch
475 } else {
476 set ui_status_value {Refreshing file status...}
477 set cmd [list git update-index]
478 lappend cmd -q
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]
486 }
487}
488
489proc switch_branch_stage2 {fd_rf new_branch} {
490 global ui_status_value HEAD
491
492 if {$fd_rf ne {}} {
493 read $fd_rf
494 if {![eof $fd_rf]} return
495 close $fd_rf
496 }
497
498 set ui_status_value "Updating working directory to '$new_branch'..."
499 set cmd [list git read-tree]
500 lappend cmd -m
501 lappend cmd -u
502 lappend cmd --exclude-per-directory=.gitignore
503 lappend cmd $HEAD
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]
509}
510
511proc 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
515
516 # -- We never get interesting output on stdout; only stderr.
517 #
518 read $fd_rt
519 fconfigure $fd_rt -blocking 1
520 if {![eof $fd_rt]} {
521 fconfigure $fd_rt -blocking 0
522 return
523 }
524
525 # -- The working directory wasn't in sync with the index and
526 # we'd have to overwrite something to make the switch. A
527 # merge is required.
528 #
529 if {[catch {close $fd_rt} err]} {
530 regsub {^fatal: } $err {} err
531 warn_popup "File level merge required.
532
533$err
534
535Staying on branch '$current_branch'."
536 set ui_status_value "Aborted checkout of '$new_branch' (file level merging is required)."
537 unlock_index
538 return
539 }
540
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.
544 #
545 if {[catch {git symbolic-ref HEAD "refs/heads/$new_branch"} err]} {
546 error_popup "Failed to set current branch.
547
548This working directory is only partially switched. We successfully updated your files, but failed to update an internal Git file.
549
550This should not have occurred. [appname] will now close and give up.
551
552$err"
553 do_quit
554 return
555 }
556
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.
561 #
562 unlock_index
563 set selected_commit_type new
564 if {[string match amend* $commit_type]} {
565 $ui_comm delete 0.0 end
566 $ui_comm edit reset
567 $ui_comm edit modified false
568 rescan {set ui_status_value "Checked out branch '$current_branch'."}
569 } else {
570 repository_state commit_type HEAD MERGE_HEAD
571 set PARENT $HEAD
572 set ui_status_value "Checked out branch '$current_branch'."
573 }
574}