fe47c9cb5f
* https://github.com/prati0100/git-gui: git-gui: allow opening currently selected file in default app git-gui: allow closing console window with Escape git gui: fix branch name encoding error git-gui: revert untracked files by deleting them git-gui: update status bar to track operations git-gui: consolidate naming conventions
647 lines
15 KiB
Tcl
647 lines
15 KiB
Tcl
# git-gui commit checkout support
|
|
# Copyright (C) 2007 Shawn Pearce
|
|
|
|
class checkout_op {
|
|
|
|
field w {}; # our window (if we have one)
|
|
field w_cons {}; # embedded console window object
|
|
|
|
field new_expr ; # expression the user saw/thinks this is
|
|
field new_hash ; # commit SHA-1 we are switching to
|
|
field new_ref ; # ref we are updating/creating
|
|
field old_hash ; # commit SHA-1 that was checked out when we started
|
|
|
|
field parent_w .; # window that started us
|
|
field merge_type none; # type of merge to apply to existing branch
|
|
field merge_base {}; # merge base if we have another ref involved
|
|
field fetch_spec {}; # refetch tracking branch if used?
|
|
field checkout 1; # actually checkout the branch?
|
|
field create 0; # create the branch if it doesn't exist?
|
|
field remote_source {}; # same as fetch_spec, to setup tracking
|
|
|
|
field reset_ok 0; # did the user agree to reset?
|
|
field fetch_ok 0; # did the fetch succeed?
|
|
|
|
field readtree_d {}; # buffered output from read-tree
|
|
field update_old {}; # was the update-ref call deferred?
|
|
field reflog_msg {}; # log message for the update-ref call
|
|
|
|
constructor new {expr hash {ref {}}} {
|
|
set new_expr $expr
|
|
set new_hash $hash
|
|
set new_ref $ref
|
|
|
|
return $this
|
|
}
|
|
|
|
method parent {path} {
|
|
set parent_w [winfo toplevel $path]
|
|
}
|
|
|
|
method enable_merge {type} {
|
|
set merge_type $type
|
|
}
|
|
|
|
method enable_fetch {spec} {
|
|
set fetch_spec $spec
|
|
}
|
|
|
|
method remote_source {spec} {
|
|
set remote_source $spec
|
|
}
|
|
|
|
method enable_checkout {co} {
|
|
set checkout $co
|
|
}
|
|
|
|
method enable_create {co} {
|
|
set create $co
|
|
}
|
|
|
|
method run {} {
|
|
if {$fetch_spec ne {}} {
|
|
global M1B
|
|
|
|
# We were asked to refresh a single tracking branch
|
|
# before we get to work. We should do that before we
|
|
# consider any ref updating.
|
|
#
|
|
set fetch_ok 0
|
|
set l_trck [lindex $fetch_spec 0]
|
|
set remote [lindex $fetch_spec 1]
|
|
set r_head [lindex $fetch_spec 2]
|
|
regsub ^refs/heads/ $r_head {} r_name
|
|
|
|
set cmd [list git fetch $remote]
|
|
if {$l_trck ne {}} {
|
|
lappend cmd +$r_head:$l_trck
|
|
} else {
|
|
lappend cmd $r_head
|
|
}
|
|
|
|
_toplevel $this {Refreshing Tracking Branch}
|
|
set w_cons [::console::embed \
|
|
$w.console \
|
|
[mc "Fetching %s from %s" $r_name $remote]]
|
|
pack $w.console -fill both -expand 1
|
|
$w_cons exec $cmd [cb _finish_fetch]
|
|
|
|
bind $w <$M1B-Key-w> break
|
|
bind $w <$M1B-Key-W> break
|
|
bind $w <Visibility> "
|
|
[list grab $w]
|
|
[list focus $w]
|
|
"
|
|
wm protocol $w WM_DELETE_WINDOW [cb _noop]
|
|
tkwait window $w
|
|
|
|
if {!$fetch_ok} {
|
|
delete_this
|
|
return 0
|
|
}
|
|
}
|
|
|
|
if {$new_ref ne {}} {
|
|
# If we have a ref we need to update it before we can
|
|
# proceed with a checkout (if one was enabled).
|
|
#
|
|
if {![_update_ref $this]} {
|
|
delete_this
|
|
return 0
|
|
}
|
|
}
|
|
|
|
if {$checkout} {
|
|
_checkout $this
|
|
return 1
|
|
}
|
|
|
|
delete_this
|
|
return 1
|
|
}
|
|
|
|
method _noop {} {}
|
|
|
|
method _finish_fetch {ok} {
|
|
if {$ok} {
|
|
set l_trck [lindex $fetch_spec 0]
|
|
if {$l_trck eq {}} {
|
|
set l_trck FETCH_HEAD
|
|
}
|
|
if {[catch {set new_hash [git rev-parse --verify "$l_trck^0"]} err]} {
|
|
set ok 0
|
|
$w_cons insert [mc "fatal: Cannot resolve %s" $l_trck]
|
|
$w_cons insert $err
|
|
}
|
|
}
|
|
|
|
$w_cons done $ok
|
|
set w_cons {}
|
|
wm protocol $w WM_DELETE_WINDOW {}
|
|
|
|
if {$ok} {
|
|
destroy $w
|
|
set w {}
|
|
} else {
|
|
button $w.close -text [mc Close] -command [list destroy $w]
|
|
pack $w.close -side bottom -anchor e -padx 10 -pady 10
|
|
}
|
|
|
|
set fetch_ok $ok
|
|
}
|
|
|
|
method _update_ref {} {
|
|
global null_sha1 current_branch repo_config
|
|
|
|
set ref $new_ref
|
|
set new $new_hash
|
|
|
|
set is_current 0
|
|
set rh refs/heads/
|
|
set rn [string length $rh]
|
|
if {[string equal -length $rn $rh $ref]} {
|
|
set newbranch [string range $ref $rn end]
|
|
if {$current_branch eq $newbranch} {
|
|
set is_current 1
|
|
}
|
|
} else {
|
|
set newbranch $ref
|
|
}
|
|
|
|
if {[catch {set cur [git rev-parse --verify "$ref^0"]}]} {
|
|
# Assume it does not exist, and that is what the error was.
|
|
#
|
|
if {!$create} {
|
|
_error $this [mc "Branch '%s' does not exist." $newbranch]
|
|
return 0
|
|
}
|
|
|
|
set reflog_msg "branch: Created from $new_expr"
|
|
set cur $null_sha1
|
|
|
|
if {($repo_config(branch.autosetupmerge) eq {true}
|
|
|| $repo_config(branch.autosetupmerge) eq {always})
|
|
&& $remote_source ne {}
|
|
&& "refs/heads/$newbranch" eq $ref} {
|
|
|
|
set c_remote [lindex $remote_source 1]
|
|
set c_merge [lindex $remote_source 2]
|
|
if {[catch {
|
|
git config branch.$newbranch.remote $c_remote
|
|
git config branch.$newbranch.merge $c_merge
|
|
} err]} {
|
|
_error $this [strcat \
|
|
[mc "Failed to configure simplified git-pull for '%s'." $newbranch] \
|
|
"\n\n$err"]
|
|
}
|
|
}
|
|
} elseif {$create && $merge_type eq {none}} {
|
|
# We were told to create it, but not do a merge.
|
|
# Bad. Name shouldn't have existed.
|
|
#
|
|
_error $this [mc "Branch '%s' already exists." $newbranch]
|
|
return 0
|
|
} elseif {!$create && $merge_type eq {none}} {
|
|
# We aren't creating, it exists and we don't merge.
|
|
# We are probably just a simple branch switch.
|
|
# Use whatever value we just read.
|
|
#
|
|
set new $cur
|
|
set new_hash $cur
|
|
} elseif {$new eq $cur} {
|
|
# No merge would be required, don't compute anything.
|
|
#
|
|
} else {
|
|
catch {set merge_base [git merge-base $new $cur]}
|
|
if {$merge_base eq $cur} {
|
|
# The current branch is older.
|
|
#
|
|
set reflog_msg "merge $new_expr: Fast-forward"
|
|
} else {
|
|
switch -- $merge_type {
|
|
ff {
|
|
if {$merge_base eq $new} {
|
|
# The current branch is actually newer.
|
|
#
|
|
set new $cur
|
|
set new_hash $cur
|
|
} else {
|
|
_error $this [mc "Branch '%s' already exists.\n\nIt cannot fast-forward to %s.\nA merge is required." $newbranch $new_expr]
|
|
return 0
|
|
}
|
|
}
|
|
reset {
|
|
# The current branch will lose things.
|
|
#
|
|
if {[_confirm_reset $this $cur]} {
|
|
set reflog_msg "reset $new_expr"
|
|
} else {
|
|
return 0
|
|
}
|
|
}
|
|
default {
|
|
_error $this [mc "Merge strategy '%s' not supported." $merge_type]
|
|
return 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if {$new ne $cur} {
|
|
if {$is_current} {
|
|
# No so fast. We should defer this in case
|
|
# we cannot update the working directory.
|
|
#
|
|
set update_old $cur
|
|
return 1
|
|
}
|
|
|
|
if {[catch {
|
|
git update-ref -m $reflog_msg $ref $new $cur
|
|
} err]} {
|
|
_error $this [strcat [mc "Failed to update '%s'." $newbranch] "\n\n$err"]
|
|
return 0
|
|
}
|
|
}
|
|
|
|
return 1
|
|
}
|
|
|
|
method _checkout {} {
|
|
if {[lock_index checkout_op]} {
|
|
after idle [cb _start_checkout]
|
|
} else {
|
|
_error $this [mc "Staging area (index) is already locked."]
|
|
delete_this
|
|
}
|
|
}
|
|
|
|
method _start_checkout {} {
|
|
global HEAD commit_type
|
|
|
|
# -- Our in memory state should match the repository.
|
|
#
|
|
repository_state curType old_hash curMERGE_HEAD
|
|
if {[string match amend* $commit_type]
|
|
&& $curType eq {normal}
|
|
&& $old_hash eq $HEAD} {
|
|
} elseif {$commit_type ne $curType || $HEAD ne $old_hash} {
|
|
info_popup [mc "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 ui_ready
|
|
delete_this
|
|
return
|
|
}
|
|
|
|
if {$old_hash eq $new_hash} {
|
|
_after_readtree $this
|
|
} elseif {[is_config_true gui.trustmtime]} {
|
|
_readtree $this
|
|
} else {
|
|
ui_status [mc "Refreshing file status..."]
|
|
set fd [git_read update-index \
|
|
-q \
|
|
--unmerged \
|
|
--ignore-missing \
|
|
--refresh \
|
|
]
|
|
fconfigure $fd -blocking 0 -translation binary
|
|
fileevent $fd readable [cb _refresh_wait $fd]
|
|
}
|
|
}
|
|
|
|
method _refresh_wait {fd} {
|
|
read $fd
|
|
if {[eof $fd]} {
|
|
close $fd
|
|
_readtree $this
|
|
}
|
|
}
|
|
|
|
method _name {} {
|
|
if {$new_ref eq {}} {
|
|
return [string range $new_hash 0 7]
|
|
}
|
|
|
|
set rh refs/heads/
|
|
set rn [string length $rh]
|
|
if {[string equal -length $rn $rh $new_ref]} {
|
|
return [string range $new_ref $rn end]
|
|
} else {
|
|
return $new_ref
|
|
}
|
|
}
|
|
|
|
method _readtree {} {
|
|
global HEAD
|
|
|
|
set readtree_d {}
|
|
set status_bar_operation [$::main_status start \
|
|
[mc "Updating working directory to '%s'..." [_name $this]] \
|
|
[mc "files checked out"]]
|
|
|
|
set fd [git_read --stderr read-tree \
|
|
-m \
|
|
-u \
|
|
-v \
|
|
--exclude-per-directory=.gitignore \
|
|
$HEAD \
|
|
$new_hash \
|
|
]
|
|
fconfigure $fd -blocking 0 -translation binary
|
|
fileevent $fd readable [cb _readtree_wait $fd $status_bar_operation]
|
|
}
|
|
|
|
method _readtree_wait {fd status_bar_operation} {
|
|
global current_branch
|
|
|
|
set buf [read $fd]
|
|
$status_bar_operation update_meter $buf
|
|
append readtree_d $buf
|
|
|
|
fconfigure $fd -blocking 1
|
|
if {![eof $fd]} {
|
|
fconfigure $fd -blocking 0
|
|
$status_bar_operation stop
|
|
return
|
|
}
|
|
|
|
if {[catch {close $fd}]} {
|
|
set err $readtree_d
|
|
regsub {^fatal: } $err {} err
|
|
$status_bar_operation stop [mc "Aborted checkout of '%s' (file level merging is required)." [_name $this]]
|
|
warn_popup [strcat [mc "File level merge required."] "
|
|
|
|
$err
|
|
|
|
" [mc "Staying on branch '%s'." $current_branch]]
|
|
unlock_index
|
|
delete_this
|
|
return
|
|
}
|
|
|
|
$status_bar_operation stop
|
|
_after_readtree $this
|
|
}
|
|
|
|
method _after_readtree {} {
|
|
global commit_type HEAD MERGE_HEAD PARENT
|
|
global current_branch is_detached
|
|
global ui_comm
|
|
|
|
set name [_name $this]
|
|
set log "checkout: moving"
|
|
if {!$is_detached} {
|
|
append log " from $current_branch"
|
|
}
|
|
|
|
# -- Move/create HEAD as a symbolic ref. Core git does not
|
|
# 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.
|
|
#
|
|
set rh refs/heads/
|
|
set rn [string length $rh]
|
|
if {[string equal -length $rn $rh $new_ref]} {
|
|
set new_branch [string range $new_ref $rn end]
|
|
if {$is_detached || $current_branch ne $new_branch} {
|
|
append log " to $new_branch"
|
|
if {[catch {
|
|
git symbolic-ref -m $log HEAD $new_ref
|
|
} err]} {
|
|
_fatal $this $err
|
|
}
|
|
set current_branch $new_branch
|
|
set is_detached 0
|
|
}
|
|
} else {
|
|
if {!$is_detached || $new_hash ne $HEAD} {
|
|
append log " to $new_expr"
|
|
if {[catch {
|
|
_detach_HEAD $log $new_hash
|
|
} err]} {
|
|
_fatal $this $err
|
|
}
|
|
}
|
|
set current_branch HEAD
|
|
set is_detached 1
|
|
}
|
|
|
|
# -- We had to defer updating the branch itself until we
|
|
# knew the working directory would update. So now we
|
|
# need to finish that work. If it fails we're in big
|
|
# trouble.
|
|
#
|
|
if {$update_old ne {}} {
|
|
if {[catch {
|
|
git update-ref \
|
|
-m $reflog_msg \
|
|
$new_ref \
|
|
$new_hash \
|
|
$update_old
|
|
} err]} {
|
|
_fatal $this $err
|
|
}
|
|
}
|
|
|
|
if {$is_detached} {
|
|
info_popup [mc "You are no longer on a local branch.
|
|
|
|
If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."]
|
|
}
|
|
|
|
# -- Run the post-checkout hook.
|
|
#
|
|
set fd_ph [githook_read post-checkout $old_hash $new_hash 1]
|
|
if {$fd_ph ne {}} {
|
|
global pch_error
|
|
set pch_error {}
|
|
fconfigure $fd_ph -blocking 0 -translation binary -eofchar {}
|
|
fileevent $fd_ph readable [cb _postcheckout_wait $fd_ph]
|
|
} else {
|
|
_update_repo_state $this
|
|
}
|
|
}
|
|
|
|
method _postcheckout_wait {fd_ph} {
|
|
global pch_error
|
|
|
|
append pch_error [read $fd_ph]
|
|
fconfigure $fd_ph -blocking 1
|
|
if {[eof $fd_ph]} {
|
|
if {[catch {close $fd_ph}]} {
|
|
hook_failed_popup post-checkout $pch_error 0
|
|
}
|
|
unset pch_error
|
|
_update_repo_state $this
|
|
return
|
|
}
|
|
fconfigure $fd_ph -blocking 0
|
|
}
|
|
|
|
method _update_repo_state {} {
|
|
# -- 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.
|
|
#
|
|
global commit_type_is_amend commit_type HEAD MERGE_HEAD PARENT
|
|
global ui_comm
|
|
|
|
unlock_index
|
|
set name [_name $this]
|
|
set commit_type_is_amend 0
|
|
if {[string match amend* $commit_type]} {
|
|
$ui_comm delete 0.0 end
|
|
$ui_comm edit reset
|
|
$ui_comm edit modified false
|
|
rescan [list ui_status [mc "Checked out '%s'." $name]]
|
|
} else {
|
|
repository_state commit_type HEAD MERGE_HEAD
|
|
set PARENT $HEAD
|
|
ui_status [mc "Checked out '%s'." $name]
|
|
}
|
|
delete_this
|
|
}
|
|
|
|
git-version proc _detach_HEAD {log new} {
|
|
>= 1.5.3 {
|
|
git update-ref --no-deref -m $log HEAD $new
|
|
}
|
|
default {
|
|
set p [gitdir HEAD]
|
|
file delete $p
|
|
set fd [open $p w]
|
|
fconfigure $fd -translation lf -encoding utf-8
|
|
puts $fd $new
|
|
close $fd
|
|
}
|
|
}
|
|
|
|
method _confirm_reset {cur} {
|
|
set reset_ok 0
|
|
set name [_name $this]
|
|
set gitk [list do_gitk [list $cur ^$new_hash]]
|
|
|
|
_toplevel $this {Confirm Branch Reset}
|
|
pack [label $w.msg1 \
|
|
-anchor w \
|
|
-justify left \
|
|
-text [mc "Resetting '%s' to '%s' will lose the following commits:" $name $new_expr]\
|
|
] -anchor w
|
|
|
|
set list $w.list.l
|
|
frame $w.list
|
|
text $list \
|
|
-font font_diff \
|
|
-width 80 \
|
|
-height 10 \
|
|
-wrap none \
|
|
-xscrollcommand [list $w.list.sbx set] \
|
|
-yscrollcommand [list $w.list.sby set]
|
|
scrollbar $w.list.sbx -orient h -command [list $list xview]
|
|
scrollbar $w.list.sby -orient v -command [list $list yview]
|
|
pack $w.list.sbx -fill x -side bottom
|
|
pack $w.list.sby -fill y -side right
|
|
pack $list -fill both -expand 1
|
|
pack $w.list -fill both -expand 1 -padx 5 -pady 5
|
|
|
|
pack [label $w.msg2 \
|
|
-anchor w \
|
|
-justify left \
|
|
-text [mc "Recovering lost commits may not be easy."] \
|
|
]
|
|
pack [label $w.msg3 \
|
|
-anchor w \
|
|
-justify left \
|
|
-text [mc "Reset '%s'?" $name] \
|
|
]
|
|
|
|
frame $w.buttons
|
|
button $w.buttons.visualize \
|
|
-text [mc Visualize] \
|
|
-command $gitk
|
|
pack $w.buttons.visualize -side left
|
|
button $w.buttons.reset \
|
|
-text [mc Reset] \
|
|
-command "
|
|
set @reset_ok 1
|
|
destroy $w
|
|
"
|
|
pack $w.buttons.reset -side right
|
|
button $w.buttons.cancel \
|
|
-default active \
|
|
-text [mc Cancel] \
|
|
-command [list destroy $w]
|
|
pack $w.buttons.cancel -side right -padx 5
|
|
pack $w.buttons -side bottom -fill x -pady 10 -padx 10
|
|
|
|
set fd [git_read rev-list --pretty=oneline $cur ^$new_hash]
|
|
while {[gets $fd line] > 0} {
|
|
set abbr [string range $line 0 7]
|
|
set subj [string range $line 41 end]
|
|
$list insert end "$abbr $subj\n"
|
|
}
|
|
close $fd
|
|
$list configure -state disabled
|
|
|
|
bind $w <Key-v> $gitk
|
|
bind $w <Visibility> "
|
|
grab $w
|
|
focus $w.buttons.cancel
|
|
"
|
|
bind $w <Key-Return> [list destroy $w]
|
|
bind $w <Key-Escape> [list destroy $w]
|
|
tkwait window $w
|
|
return $reset_ok
|
|
}
|
|
|
|
method _error {msg} {
|
|
if {[winfo ismapped $parent_w]} {
|
|
set p $parent_w
|
|
} else {
|
|
set p .
|
|
}
|
|
|
|
tk_messageBox \
|
|
-icon error \
|
|
-type ok \
|
|
-title [wm title $p] \
|
|
-parent $p \
|
|
-message $msg
|
|
}
|
|
|
|
method _toplevel {title} {
|
|
regsub -all {::} $this {__} w
|
|
set w .$w
|
|
|
|
if {[winfo ismapped $parent_w]} {
|
|
set p $parent_w
|
|
} else {
|
|
set p .
|
|
}
|
|
|
|
toplevel $w
|
|
wm title $w $title
|
|
wm geometry $w "+[winfo rootx $p]+[winfo rooty $p]"
|
|
}
|
|
|
|
method _fatal {err} {
|
|
error_popup [strcat [mc "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. %s will now close and give up." [appname]] "
|
|
|
|
$err"]
|
|
exit 1
|
|
}
|
|
|
|
}
|