You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
574 lines
15 KiB
574 lines
15 KiB
# git-gui branch (create/delete) support |
|
# Copyright (C) 2006, 2007 Shawn Pearce |
|
|
|
proc load_all_heads {} { |
|
global all_heads |
|
|
|
set all_heads [list] |
|
set fd [open "| git for-each-ref --format=%(refname) refs/heads" r] |
|
while {[gets $fd line] > 0} { |
|
if {[is_tracking_branch $line]} continue |
|
if {![regsub ^refs/heads/ $line {} name]} continue |
|
lappend all_heads $name |
|
} |
|
close $fd |
|
|
|
set all_heads [lsort $all_heads] |
|
} |
|
|
|
proc load_all_tags {} { |
|
set all_tags [list] |
|
set fd [open "| git for-each-ref --format=%(refname) refs/tags" r] |
|
while {[gets $fd line] > 0} { |
|
if {![regsub ^refs/tags/ $line {} name]} continue |
|
lappend all_tags $name |
|
} |
|
close $fd |
|
|
|
return [lsort $all_tags] |
|
} |
|
|
|
proc populate_branch_menu {} { |
|
global all_heads disable_on_lock |
|
|
|
set m .mbar.branch |
|
set last [$m index last] |
|
for {set i 0} {$i <= $last} {incr i} { |
|
if {[$m type $i] eq {separator}} { |
|
$m delete $i last |
|
set new_dol [list] |
|
foreach a $disable_on_lock { |
|
if {[lindex $a 0] ne $m || [lindex $a 2] < $i} { |
|
lappend new_dol $a |
|
} |
|
} |
|
set disable_on_lock $new_dol |
|
break |
|
} |
|
} |
|
|
|
if {$all_heads ne {}} { |
|
$m add separator |
|
} |
|
foreach b $all_heads { |
|
$m add radiobutton \ |
|
-label $b \ |
|
-command [list switch_branch $b] \ |
|
-variable current_branch \ |
|
-value $b |
|
lappend disable_on_lock \ |
|
[list $m entryconf [$m index last] -state] |
|
} |
|
} |
|
|
|
proc do_create_branch_action {w} { |
|
global all_heads null_sha1 repo_config |
|
global create_branch_checkout create_branch_revtype |
|
global create_branch_head create_branch_trackinghead |
|
global create_branch_name create_branch_revexp |
|
global create_branch_tag |
|
|
|
set newbranch $create_branch_name |
|
if {$newbranch eq {} |
|
|| $newbranch eq $repo_config(gui.newbranchtemplate)} { |
|
tk_messageBox \ |
|
-icon error \ |
|
-type ok \ |
|
-title [wm title $w] \ |
|
-parent $w \ |
|
-message "Please supply a branch name." |
|
focus $w.desc.name_t |
|
return |
|
} |
|
if {![catch {git show-ref --verify -- "refs/heads/$newbranch"}]} { |
|
tk_messageBox \ |
|
-icon error \ |
|
-type ok \ |
|
-title [wm title $w] \ |
|
-parent $w \ |
|
-message "Branch '$newbranch' already exists." |
|
focus $w.desc.name_t |
|
return |
|
} |
|
if {[catch {git check-ref-format "heads/$newbranch"}]} { |
|
tk_messageBox \ |
|
-icon error \ |
|
-type ok \ |
|
-title [wm title $w] \ |
|
-parent $w \ |
|
-message "We do not like '$newbranch' as a branch name." |
|
focus $w.desc.name_t |
|
return |
|
} |
|
|
|
set rev {} |
|
switch -- $create_branch_revtype { |
|
head {set rev $create_branch_head} |
|
tracking {set rev $create_branch_trackinghead} |
|
tag {set rev $create_branch_tag} |
|
expression {set rev $create_branch_revexp} |
|
} |
|
if {[catch {set cmt [git rev-parse --verify "${rev}^0"]}]} { |
|
tk_messageBox \ |
|
-icon error \ |
|
-type ok \ |
|
-title [wm title $w] \ |
|
-parent $w \ |
|
-message "Invalid starting revision: $rev" |
|
return |
|
} |
|
if {[catch { |
|
git update-ref \ |
|
-m "branch: Created from $rev" \ |
|
"refs/heads/$newbranch" \ |
|
$cmt \ |
|
$null_sha1 |
|
} err]} { |
|
tk_messageBox \ |
|
-icon error \ |
|
-type ok \ |
|
-title [wm title $w] \ |
|
-parent $w \ |
|
-message "Failed to create '$newbranch'.\n\n$err" |
|
return |
|
} |
|
|
|
lappend all_heads $newbranch |
|
set all_heads [lsort $all_heads] |
|
populate_branch_menu |
|
destroy $w |
|
if {$create_branch_checkout} { |
|
switch_branch $newbranch |
|
} |
|
} |
|
|
|
proc radio_selector {varname value args} { |
|
upvar #0 $varname var |
|
set var $value |
|
} |
|
|
|
trace add variable create_branch_head write \ |
|
[list radio_selector create_branch_revtype head] |
|
trace add variable create_branch_trackinghead write \ |
|
[list radio_selector create_branch_revtype tracking] |
|
trace add variable create_branch_tag write \ |
|
[list radio_selector create_branch_revtype tag] |
|
|
|
trace add variable delete_branch_head write \ |
|
[list radio_selector delete_branch_checktype head] |
|
trace add variable delete_branch_trackinghead write \ |
|
[list radio_selector delete_branch_checktype tracking] |
|
|
|
proc do_create_branch {} { |
|
global all_heads current_branch repo_config |
|
global create_branch_checkout create_branch_revtype |
|
global create_branch_head create_branch_trackinghead |
|
global create_branch_name create_branch_revexp |
|
global create_branch_tag |
|
|
|
set w .branch_editor |
|
toplevel $w |
|
wm geometry $w "+[winfo rootx .]+[winfo rooty .]" |
|
|
|
label $w.header -text {Create New Branch} \ |
|
-font font_uibold |
|
pack $w.header -side top -fill x |
|
|
|
frame $w.buttons |
|
button $w.buttons.create -text Create \ |
|
-default active \ |
|
-command [list do_create_branch_action $w] |
|
pack $w.buttons.create -side right |
|
button $w.buttons.cancel -text {Cancel} \ |
|
-command [list destroy $w] |
|
pack $w.buttons.cancel -side right -padx 5 |
|
pack $w.buttons -side bottom -fill x -pady 10 -padx 10 |
|
|
|
labelframe $w.desc -text {Branch Description} |
|
label $w.desc.name_l -text {Name:} |
|
entry $w.desc.name_t \ |
|
-borderwidth 1 \ |
|
-relief sunken \ |
|
-width 40 \ |
|
-textvariable create_branch_name \ |
|
-validate key \ |
|
-validatecommand { |
|
if {%d == 1 && [regexp {[~^:?*\[\0- ]} %S]} {return 0} |
|
return 1 |
|
} |
|
grid $w.desc.name_l $w.desc.name_t -sticky we -padx {0 5} |
|
grid columnconfigure $w.desc 1 -weight 1 |
|
pack $w.desc -anchor nw -fill x -pady 5 -padx 5 |
|
|
|
labelframe $w.from -text {Starting Revision} |
|
if {$all_heads ne {}} { |
|
radiobutton $w.from.head_r \ |
|
-text {Local Branch:} \ |
|
-value head \ |
|
-variable create_branch_revtype |
|
eval tk_optionMenu $w.from.head_m create_branch_head $all_heads |
|
grid $w.from.head_r $w.from.head_m -sticky w |
|
} |
|
set all_trackings [all_tracking_branches] |
|
if {$all_trackings ne {}} { |
|
set create_branch_trackinghead [lindex $all_trackings 0] |
|
radiobutton $w.from.tracking_r \ |
|
-text {Tracking Branch:} \ |
|
-value tracking \ |
|
-variable create_branch_revtype |
|
eval tk_optionMenu $w.from.tracking_m \ |
|
create_branch_trackinghead \ |
|
$all_trackings |
|
grid $w.from.tracking_r $w.from.tracking_m -sticky w |
|
} |
|
set all_tags [load_all_tags] |
|
if {$all_tags ne {}} { |
|
set create_branch_tag [lindex $all_tags 0] |
|
radiobutton $w.from.tag_r \ |
|
-text {Tag:} \ |
|
-value tag \ |
|
-variable create_branch_revtype |
|
eval tk_optionMenu $w.from.tag_m create_branch_tag $all_tags |
|
grid $w.from.tag_r $w.from.tag_m -sticky w |
|
} |
|
radiobutton $w.from.exp_r \ |
|
-text {Revision Expression:} \ |
|
-value expression \ |
|
-variable create_branch_revtype |
|
entry $w.from.exp_t \ |
|
-borderwidth 1 \ |
|
-relief sunken \ |
|
-width 50 \ |
|
-textvariable create_branch_revexp \ |
|
-validate key \ |
|
-validatecommand { |
|
if {%d == 1 && [regexp {\s} %S]} {return 0} |
|
if {%d == 1 && [string length %S] > 0} { |
|
set create_branch_revtype expression |
|
} |
|
return 1 |
|
} |
|
grid $w.from.exp_r $w.from.exp_t -sticky we -padx {0 5} |
|
grid columnconfigure $w.from 1 -weight 1 |
|
pack $w.from -anchor nw -fill x -pady 5 -padx 5 |
|
|
|
labelframe $w.postActions -text {Post Creation Actions} |
|
checkbutton $w.postActions.checkout \ |
|
-text {Checkout after creation} \ |
|
-variable create_branch_checkout |
|
pack $w.postActions.checkout -anchor nw |
|
pack $w.postActions -anchor nw -fill x -pady 5 -padx 5 |
|
|
|
set create_branch_checkout 1 |
|
set create_branch_head $current_branch |
|
set create_branch_revtype head |
|
set create_branch_name $repo_config(gui.newbranchtemplate) |
|
set create_branch_revexp {} |
|
|
|
bind $w <Visibility> " |
|
grab $w |
|
$w.desc.name_t icursor end |
|
focus $w.desc.name_t |
|
" |
|
bind $w <Key-Escape> "destroy $w" |
|
bind $w <Key-Return> "do_create_branch_action $w;break" |
|
wm title $w "[appname] ([reponame]): Create Branch" |
|
tkwait window $w |
|
} |
|
|
|
proc do_delete_branch_action {w} { |
|
global all_heads |
|
global delete_branch_checktype delete_branch_head delete_branch_trackinghead |
|
|
|
set check_rev {} |
|
switch -- $delete_branch_checktype { |
|
head {set check_rev $delete_branch_head} |
|
tracking {set check_rev $delete_branch_trackinghead} |
|
always {set check_rev {:none}} |
|
} |
|
if {$check_rev eq {:none}} { |
|
set check_cmt {} |
|
} elseif {[catch {set check_cmt [git rev-parse --verify "${check_rev}^0"]}]} { |
|
tk_messageBox \ |
|
-icon error \ |
|
-type ok \ |
|
-title [wm title $w] \ |
|
-parent $w \ |
|
-message "Invalid check revision: $check_rev" |
|
return |
|
} |
|
|
|
set to_delete [list] |
|
set not_merged [list] |
|
foreach i [$w.list.l curselection] { |
|
set b [$w.list.l get $i] |
|
if {[catch {set o [git rev-parse --verify $b]}]} continue |
|
if {$check_cmt ne {}} { |
|
if {$b eq $check_rev} continue |
|
if {[catch {set m [git merge-base $o $check_cmt]}]} continue |
|
if {$o ne $m} { |
|
lappend not_merged $b |
|
continue |
|
} |
|
} |
|
lappend to_delete [list $b $o] |
|
} |
|
if {$not_merged ne {}} { |
|
set msg "The following branches are not completely merged into $check_rev: |
|
|
|
- [join $not_merged "\n - "]" |
|
tk_messageBox \ |
|
-icon info \ |
|
-type ok \ |
|
-title [wm title $w] \ |
|
-parent $w \ |
|
-message $msg |
|
} |
|
if {$to_delete eq {}} return |
|
if {$delete_branch_checktype eq {always}} { |
|
set msg {Recovering deleted branches is difficult. |
|
|
|
Delete the selected branches?} |
|
if {[tk_messageBox \ |
|
-icon warning \ |
|
-type yesno \ |
|
-title [wm title $w] \ |
|
-parent $w \ |
|
-message $msg] ne yes} { |
|
return |
|
} |
|
} |
|
|
|
set failed {} |
|
foreach i $to_delete { |
|
set b [lindex $i 0] |
|
set o [lindex $i 1] |
|
if {[catch {git update-ref -d "refs/heads/$b" $o} err]} { |
|
append failed " - $b: $err\n" |
|
} else { |
|
set x [lsearch -sorted -exact $all_heads $b] |
|
if {$x >= 0} { |
|
set all_heads [lreplace $all_heads $x $x] |
|
} |
|
} |
|
} |
|
|
|
if {$failed ne {}} { |
|
tk_messageBox \ |
|
-icon error \ |
|
-type ok \ |
|
-title [wm title $w] \ |
|
-parent $w \ |
|
-message "Failed to delete branches:\n$failed" |
|
} |
|
|
|
set all_heads [lsort $all_heads] |
|
populate_branch_menu |
|
destroy $w |
|
} |
|
|
|
proc do_delete_branch {} { |
|
global all_heads tracking_branches current_branch |
|
global delete_branch_checktype delete_branch_head delete_branch_trackinghead |
|
|
|
set w .branch_editor |
|
toplevel $w |
|
wm geometry $w "+[winfo rootx .]+[winfo rooty .]" |
|
|
|
label $w.header -text {Delete Local Branch} \ |
|
-font font_uibold |
|
pack $w.header -side top -fill x |
|
|
|
frame $w.buttons |
|
button $w.buttons.create -text Delete \ |
|
-command [list do_delete_branch_action $w] |
|
pack $w.buttons.create -side right |
|
button $w.buttons.cancel -text {Cancel} \ |
|
-command [list destroy $w] |
|
pack $w.buttons.cancel -side right -padx 5 |
|
pack $w.buttons -side bottom -fill x -pady 10 -padx 10 |
|
|
|
labelframe $w.list -text {Local Branches} |
|
listbox $w.list.l \ |
|
-height 10 \ |
|
-width 70 \ |
|
-selectmode extended \ |
|
-yscrollcommand [list $w.list.sby set] |
|
foreach h $all_heads { |
|
if {$h ne $current_branch} { |
|
$w.list.l insert end $h |
|
} |
|
} |
|
scrollbar $w.list.sby -command [list $w.list.l yview] |
|
pack $w.list.sby -side right -fill y |
|
pack $w.list.l -side left -fill both -expand 1 |
|
pack $w.list -fill both -expand 1 -pady 5 -padx 5 |
|
|
|
labelframe $w.validate -text {Delete Only If} |
|
radiobutton $w.validate.head_r \ |
|
-text {Merged Into Local Branch:} \ |
|
-value head \ |
|
-variable delete_branch_checktype |
|
eval tk_optionMenu $w.validate.head_m delete_branch_head $all_heads |
|
grid $w.validate.head_r $w.validate.head_m -sticky w |
|
set all_trackings [all_tracking_branches] |
|
if {$all_trackings ne {}} { |
|
set delete_branch_trackinghead [lindex $all_trackings 0] |
|
radiobutton $w.validate.tracking_r \ |
|
-text {Merged Into Tracking Branch:} \ |
|
-value tracking \ |
|
-variable delete_branch_checktype |
|
eval tk_optionMenu $w.validate.tracking_m \ |
|
delete_branch_trackinghead \ |
|
$all_trackings |
|
grid $w.validate.tracking_r $w.validate.tracking_m -sticky w |
|
} |
|
radiobutton $w.validate.always_r \ |
|
-text {Always (Do not perform merge checks)} \ |
|
-value always \ |
|
-variable delete_branch_checktype |
|
grid $w.validate.always_r -columnspan 2 -sticky w |
|
grid columnconfigure $w.validate 1 -weight 1 |
|
pack $w.validate -anchor nw -fill x -pady 5 -padx 5 |
|
|
|
set delete_branch_head $current_branch |
|
set delete_branch_checktype head |
|
|
|
bind $w <Visibility> "grab $w; focus $w" |
|
bind $w <Key-Escape> "destroy $w" |
|
wm title $w "[appname] ([reponame]): Delete Branch" |
|
tkwait window $w |
|
} |
|
|
|
proc switch_branch {new_branch} { |
|
global HEAD commit_type current_branch repo_config |
|
|
|
if {![lock_index switch]} return |
|
|
|
# -- Our in memory state should match the repository. |
|
# |
|
repository_state curType curHEAD curMERGE_HEAD |
|
if {[string match amend* $commit_type] |
|
&& $curType eq {normal} |
|
&& $curHEAD eq $HEAD} { |
|
} elseif {$commit_type ne $curType || $HEAD ne $curHEAD} { |
|
info_popup {Last scanned state does not match repository state. |
|
|
|
Another Git program has modified this repository since the last scan. A rescan must be performed before the current branch can be changed. |
|
|
|
The rescan will be automatically started now. |
|
} |
|
unlock_index |
|
rescan {set ui_status_value {Ready.}} |
|
return |
|
} |
|
|
|
# -- Don't do a pointless switch. |
|
# |
|
if {$current_branch eq $new_branch} { |
|
unlock_index |
|
return |
|
} |
|
|
|
if {$repo_config(gui.trustmtime) eq {true}} { |
|
switch_branch_stage2 {} $new_branch |
|
} else { |
|
set ui_status_value {Refreshing file status...} |
|
set cmd [list git update-index] |
|
lappend cmd -q |
|
lappend cmd --unmerged |
|
lappend cmd --ignore-missing |
|
lappend cmd --refresh |
|
set fd_rf [open "| $cmd" r] |
|
fconfigure $fd_rf -blocking 0 -translation binary |
|
fileevent $fd_rf readable \ |
|
[list switch_branch_stage2 $fd_rf $new_branch] |
|
} |
|
} |
|
|
|
proc switch_branch_stage2 {fd_rf new_branch} { |
|
global ui_status_value HEAD |
|
|
|
if {$fd_rf ne {}} { |
|
read $fd_rf |
|
if {![eof $fd_rf]} return |
|
close $fd_rf |
|
} |
|
|
|
set ui_status_value "Updating working directory to '$new_branch'..." |
|
set cmd [list git read-tree] |
|
lappend cmd -m |
|
lappend cmd -u |
|
lappend cmd --exclude-per-directory=.gitignore |
|
lappend cmd $HEAD |
|
lappend cmd $new_branch |
|
set fd_rt [open "| $cmd" r] |
|
fconfigure $fd_rt -blocking 0 -translation binary |
|
fileevent $fd_rt readable \ |
|
[list switch_branch_readtree_wait $fd_rt $new_branch] |
|
} |
|
|
|
proc switch_branch_readtree_wait {fd_rt new_branch} { |
|
global selected_commit_type commit_type HEAD MERGE_HEAD PARENT |
|
global current_branch |
|
global ui_comm ui_status_value |
|
|
|
# -- We never get interesting output on stdout; only stderr. |
|
# |
|
read $fd_rt |
|
fconfigure $fd_rt -blocking 1 |
|
if {![eof $fd_rt]} { |
|
fconfigure $fd_rt -blocking 0 |
|
return |
|
} |
|
|
|
# -- The working directory wasn't in sync with the index and |
|
# we'd have to overwrite something to make the switch. A |
|
# merge is required. |
|
# |
|
if {[catch {close $fd_rt} err]} { |
|
regsub {^fatal: } $err {} err |
|
warn_popup "File level merge required. |
|
|
|
$err |
|
|
|
Staying on branch '$current_branch'." |
|
set ui_status_value "Aborted checkout of '$new_branch' (file level merging is required)." |
|
unlock_index |
|
return |
|
} |
|
|
|
# -- Update the symbolic ref. Core git doesn't even check for failure |
|
# here, it Just Works(tm). If it doesn't we are in some really ugly |
|
# state that is difficult to recover from within git-gui. |
|
# |
|
if {[catch {git symbolic-ref HEAD "refs/heads/$new_branch"} err]} { |
|
error_popup "Failed to set current branch. |
|
|
|
This working directory is only partially switched. We successfully updated your files, but failed to update an internal Git file. |
|
|
|
This should not have occurred. [appname] will now close and give up. |
|
|
|
$err" |
|
do_quit |
|
return |
|
} |
|
|
|
# -- Update our repository state. If we were previously in amend mode |
|
# we need to toss the current buffer and do a full rescan to update |
|
# our file lists. If we weren't in amend mode our file lists are |
|
# accurate and we can avoid the rescan. |
|
# |
|
unlock_index |
|
set selected_commit_type new |
|
if {[string match amend* $commit_type]} { |
|
$ui_comm delete 0.0 end |
|
$ui_comm edit reset |
|
$ui_comm edit modified false |
|
rescan {set ui_status_value "Checked out branch '$current_branch'."} |
|
} else { |
|
repository_state commit_type HEAD MERGE_HEAD |
|
set PARENT $HEAD |
|
set ui_status_value "Checked out branch '$current_branch'." |
|
} |
|
}
|
|
|