git-gui: Allow creating a branch when none exists
[git] / lib / branch.tcl
1 # git-gui branch (create/delete) support
2 # Copyright (C) 2006, 2007 Shawn Pearce
3
4 proc 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
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
25         }
26         close $fd
27
28         return [lsort $all_tags]
29 }
30
31 proc 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
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
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
145 proc radio_selector {varname value args} {
146         upvar #0 $varname var
147         set var $value
148 }
149
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]
156
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]
161
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
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
279 proc 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
331 Delete 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
370 proc 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
443 proc 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
457 Another Git program has modified this repository since the last scan.  A rescan must be performed before the current branch can be changed.
458
459 The 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
489 proc 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
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
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
535 Staying 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
548 This working directory is only partially switched.  We successfully updated your files, but failed to update an internal Git file.
549
550 This 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 }