mirror of
https://github.com/git/git.git
synced 2024-11-05 08:47:56 +01:00
573 lines
15 KiB
Tcl
573 lines
15 KiB
Tcl
|
# 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}
|
||
|
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'."
|
||
|
}
|
||
|
}
|