return [eval exec git $args]
}
-proc current-branch {} {
+proc load_current_branch {} {
+ global current_branch is_detached
+
set fd [open [gitdir HEAD] r]
if {[gets $fd ref] < 1} {
set ref {}
# We're on a branch. It might not exist. But
# HEAD looks good enough to be a branch.
#
- return [string range $ref $len end]
+ set current_branch [string range $ref $len end]
+ set is_detached 0
} else {
# Assume this is a detached head.
#
- return HEAD
+ set current_branch HEAD
+ set is_detached 1
}
}
set commit_type {}
set empty_tree {}
set current_branch {}
+set is_detached 0
set current_diff_path {}
set selected_commit_type new
set mh [list]
- set current_branch [current-branch]
+ load_current_branch
if {[catch {set hd [git rev-parse --verify HEAD]}]} {
set hd {}
set ct initial
$ui_comm edit modified false
}
- if {[is_enabled branch]} {
- load_all_heads
- populate_branch_menu
- }
-
if {$honor_trustmtime && $repo_config(gui.trustmtime) eq {true}} {
rescan_stage2 {} $after
} else {
lappend disable_on_lock [list .mbar.branch entryconf \
[.mbar.branch index last] -state]
+ .mbar.branch add command -label {Checkout...} \
+ -command branch_checkout::dialog \
+ -accelerator $M1T-O
+ lappend disable_on_lock [list .mbar.branch entryconf \
+ [.mbar.branch index last] -state]
+
.mbar.branch add command -label {Rename...} \
-command branch_rename::dialog
lappend disable_on_lock [list .mbar.branch entryconf \
browser {
set subcommand_args {rev?}
switch [llength $argv] {
- 0 { set current_branch [current-branch] }
+ 0 { load_current_branch }
1 { set current_branch [lindex $argv 0] }
default usage
}
unset is_path
if {$head eq {}} {
- set current_branch [current-branch]
+ load_current_branch
} else {
set current_branch $head
}
if {[is_enabled branch]} {
bind . <$M1B-Key-n> branch_create::dialog
bind . <$M1B-Key-N> branch_create::dialog
+ bind . <$M1B-Key-o> branch_checkout::dialog
+ bind . <$M1B-Key-O> branch_checkout::dialog
}
if {[is_enabled transport]} {
bind . <$M1B-Key-p> do_push_anywhere
#
if {[is_enabled transport]} {
load_all_remotes
- load_all_heads
- populate_branch_menu
populate_fetch_menu
populate_push_menu
}
# Copyright (C) 2006, 2007 Shawn Pearce
proc load_all_heads {} {
- global all_heads
global some_heads_tracking
set rh refs/heads
}
close $fd
- set all_heads [lsort $all_heads]
+ return [lsort $all_heads]
}
proc load_all_tags {} {
return $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 radio_selector {varname value args} {
upvar #0 $varname var
set var $value
}
-
-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'."
- }
-}
--- /dev/null
+# git-gui branch checkout support
+# Copyright (C) 2007 Shawn Pearce
+
+class branch_checkout {
+
+field w ; # widget path
+field w_rev ; # mega-widget to pick the initial revision
+
+field opt_fetch 1; # refetch tracking branch if used?
+field opt_detach 0; # force a detached head case?
+
+constructor dialog {} {
+ make_toplevel top w
+ wm title $top "[appname] ([reponame]): Checkout Branch"
+ if {$top ne {.}} {
+ wm geometry $top "+[winfo rootx .]+[winfo rooty .]"
+ }
+
+ label $w.header -text {Checkout Branch} -font font_uibold
+ pack $w.header -side top -fill x
+
+ frame $w.buttons
+ button $w.buttons.create -text Checkout \
+ -default active \
+ -command [cb _checkout]
+ 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
+
+ set w_rev [::choose_rev::new $w.rev {Revision}]
+ pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5
+
+ labelframe $w.options -text {Options}
+
+ checkbutton $w.options.fetch \
+ -text {Fetch Tracking Branch} \
+ -variable @opt_fetch
+ pack $w.options.fetch -anchor nw
+
+ checkbutton $w.options.detach \
+ -text {Detach From Local Branch} \
+ -variable @opt_detach
+ pack $w.options.detach -anchor nw
+
+ pack $w.options -anchor nw -fill x -pady 5 -padx 5
+
+ bind $w <Visibility> [cb _visible]
+ bind $w <Key-Escape> [list destroy $w]
+ bind $w <Key-Return> [cb _checkout]\;break
+ tkwait window $w
+}
+
+method _checkout {} {
+ set spec [$w_rev get_tracking_branch]
+ if {$spec ne {} && $opt_fetch} {
+ set new {}
+ } elseif {[catch {set new [$w_rev commit_or_die]}]} {
+ return
+ }
+
+ if {$opt_detach} {
+ set ref {}
+ } else {
+ set ref [$w_rev get_local_branch]
+ }
+
+ set co [::checkout_op::new [$w_rev get] $new $ref]
+ $co parent $w
+ $co enable_checkout 1
+ if {$spec ne {} && $opt_fetch} {
+ $co enable_fetch $spec
+ }
+
+ if {[$co run]} {
+ destroy $w
+ } else {
+ $w_rev focus_filter
+ }
+}
+
+method _visible {} {
+ grab $w
+ $w_rev focus_filter
+}
+
+}
pack $w.options.merge.l -side left
radiobutton $w.options.merge.no \
-text No \
- -value no \
+ -value none \
-variable @opt_merge
pack $w.options.merge.no -side left
radiobutton $w.options.merge.ff \
}
method _create {} {
- global repo_config current_branch
+ global repo_config
global M1B
set spec [$w_rev get_tracking_branch]
return
}
- if {$newbranch eq $current_branch} {
- tk_messageBox \
- -icon error \
- -type ok \
- -title [wm title $w] \
- -parent $w \
- -message "'$newbranch' already exists and is the current branch."
- focus $w_name
- return
- }
-
if {[catch {git check-ref-format "heads/$newbranch"}]} {
tk_messageBox \
-icon error \
}
if {$spec ne {} && $opt_fetch} {
- set l_trck [lindex $spec 0]
- set remote [lindex $spec 1]
- set r_head [lindex $spec 2]
- regsub ^refs/heads/ $r_head {} r_head
-
- set c $w.fetch_trck
- toplevel $c
- wm title $c "Refreshing Tracking Branch"
- wm geometry $c "+[winfo rootx $w]+[winfo rooty $w]"
-
- set e [::console::embed \
- $c.console \
- "Fetching $r_head from $remote"]
- pack $c.console -fill both -expand 1
- $e exec \
- [list git fetch $remote +$r_head:$l_trck] \
- [cb _finish_fetch $newbranch $c $e]
-
- bind $c <Visibility> [list grab $c]
- bind $c <$M1B-Key-w> break
- bind $c <$M1B-Key-W> break
- wm protocol $c WM_DELETE_WINDOW [cb _noop]
- } else {
- _finish_create $this $newbranch
- }
-}
-
-method _noop {} {}
-
-method _finish_fetch {newbranch c e ok} {
- wm protocol $c WM_DELETE_WINDOW {}
-
- if {$ok} {
- destroy $c
- _finish_create $this $newbranch
- } else {
- $e done $ok
- button $c.close -text Close -command [list destroy $c]
- pack $c.close -side bottom -anchor e -padx 10 -pady 10
- }
-}
-
-method _finish_create {newbranch} {
- global null_sha1 all_heads
-
- if {[catch {set new [$w_rev commit_or_die]}]} {
+ set new {}
+ } elseif {[catch {set new [$w_rev commit_or_die]}]} {
return
}
- set ref refs/heads/$newbranch
- if {[catch {set cur [git rev-parse --verify "$ref^0"]}]} {
- # Assume it does not exist, and that is what the error was.
- #
- set reflog_msg "branch: Created from [$w_rev get]"
- set cur $null_sha1
- } elseif {$opt_merge eq {no}} {
- tk_messageBox \
- -icon error \
- -type ok \
- -title [wm title $w] \
- -parent $w \
- -message "Branch '$newbranch' already exists."
- focus $w_name
- return
- } else {
- set mrb {}
- catch {set mrb [git merge-base $new $cur]}
- switch -- $opt_merge {
- ff {
- if {$mrb eq $new} {
- # The current branch is actually newer.
- #
- set new $cur
- } elseif {$mrb eq $cur} {
- # The current branch is older.
- #
- set reflog_msg "merge [$w_rev get]: Fast-forward"
- } else {
- tk_messageBox \
- -icon error \
- -type ok \
- -title [wm title $w] \
- -parent $w \
- -message "Branch '$newbranch' already exists.\n\nIt cannot fast-forward to [$w_rev get].\nA merge is required."
- focus $w_name
- return
- }
- }
- reset {
- if {$mrb eq $cur} {
- # The current branch is older.
- #
- set reflog_msg "merge [$w_rev get]: Fast-forward"
- } else {
- # The current branch will lose things.
- #
- if {[_confirm_reset $this $newbranch $cur $new]} {
- set reflog_msg "reset [$w_rev get]"
- } else {
- return
- }
- }
- }
- default {
- tk_messageBox \
- -icon error \
- -type ok \
- -title [wm title $w] \
- -parent $w \
- -message "Branch '$newbranch' already exists."
- focus $w_name
- return
- }
- }
- }
-
- if {$new ne $cur} {
- if {[catch {
- git update-ref -m $reflog_msg $ref $new $cur
- } err]} {
- tk_messageBox \
- -icon error \
- -type ok \
- -title [wm title $w] \
- -parent $w \
- -message "Failed to create '$newbranch'.\n\n$err"
- return
- }
- }
-
- if {$cur eq $null_sha1} {
- lappend all_heads $newbranch
- set all_heads [lsort -uniq $all_heads]
- populate_branch_menu
- }
-
- destroy $w
- if {$opt_checkout} {
- switch_branch $newbranch
+ set co [::checkout_op::new \
+ [$w_rev get] \
+ $new \
+ refs/heads/$newbranch]
+ $co parent $w
+ $co enable_create 1
+ $co enable_merge $opt_merge
+ $co enable_checkout $opt_checkout
+ if {$spec ne {} && $opt_fetch} {
+ $co enable_fetch $spec
}
-}
-
-method _confirm_reset {newbranch cur new} {
- set reset_ok 0
- set gitk [list do_gitk [list $cur ^$new]]
-
- set c $w.confirm_reset
- toplevel $c
- wm title $c "Confirm Branch Reset"
- wm geometry $c "+[winfo rootx $w]+[winfo rooty $w]"
- pack [label $c.msg1 \
- -anchor w \
- -justify left \
- -text "Resetting '$newbranch' to [$w_rev get] will lose the following commits:" \
- ] -anchor w
-
- set list $c.list.l
- frame $c.list
- text $list \
- -font font_diff \
- -width 80 \
- -height 10 \
- -wrap none \
- -xscrollcommand [list $c.list.sbx set] \
- -yscrollcommand [list $c.list.sby set]
- scrollbar $c.list.sbx -orient h -command [list $list xview]
- scrollbar $c.list.sby -orient v -command [list $list yview]
- pack $c.list.sbx -fill x -side bottom
- pack $c.list.sby -fill y -side right
- pack $list -fill both -expand 1
- pack $c.list -fill both -expand 1 -padx 5 -pady 5
-
- pack [label $c.msg2 \
- -anchor w \
- -justify left \
- -text "Recovering lost commits may not be easy." \
- ]
- pack [label $c.msg3 \
- -anchor w \
- -justify left \
- -text "Reset '$newbranch'?" \
- ]
-
- frame $c.buttons
- button $c.buttons.visualize \
- -text Visualize \
- -command $gitk
- pack $c.buttons.visualize -side left
- button $c.buttons.reset \
- -text Reset \
- -command "
- set @reset_ok 1
- destroy $c
- "
- pack $c.buttons.reset -side right
- button $c.buttons.cancel \
- -default active \
- -text Cancel \
- -command [list destroy $c]
- pack $c.buttons.cancel -side right -padx 5
- pack $c.buttons -side bottom -fill x -pady 10 -padx 10
-
- set fd [open "| git rev-list --pretty=oneline $cur ^$new" r]
- 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"
+ if {[$co run]} {
+ destroy $w
+ } else {
+ focus $w_name
}
- close $fd
- $list configure -state disabled
-
- bind $c <Key-v> $gitk
-
- bind $c <Visibility> "
- grab $c
- focus $c.buttons.cancel
- "
- bind $c <Key-Return> [list destroy $c]
- bind $c <Key-Escape> [list destroy $c]
- tkwait window $c
- return $reset_ok
}
method _validate {d S} {
field w_delete ; # delete button
constructor dialog {} {
- global all_heads current_branch
+ global current_branch
make_toplevel top w
wm title $top "[appname] ([reponame]): Delete Branch"
$w_check none {Always (Do not perform merge test.)}
pack $w.check -anchor nw -fill x -pady 5 -padx 5
- foreach h $all_heads {
+ foreach h [load_all_heads] {
if {$h ne $current_branch} {
$w_heads insert end $h
}
}
method _delete {} {
- global all_heads
-
if {[catch {set check_cmt [$w_check commit_or_die]}]} {
return
}
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]
- }
}
}
-message "Failed to delete branches:\n$failed"
}
- set all_heads [lsort $all_heads]
- populate_branch_menu
destroy $w
}
field newname
constructor dialog {} {
- global all_heads current_branch
+ global current_branch
make_toplevel top w
wm title $top "[appname] ([reponame]): Rename Branch"
frame $w.rename
label $w.rename.oldname_l -text {Branch:}
- eval tk_optionMenu $w.rename.oldname_m @oldname $all_heads
+ eval tk_optionMenu $w.rename.oldname_m @oldname [load_all_heads]
label $w.rename.newname_l -text {New Name:}
entry $w.rename.newname_t \
}
method _rename {} {
- global all_heads current_branch
+ global current_branch
if {$oldname eq {}} {
tk_messageBox \
return
}
- set oldidx [lsearch -exact -sorted $all_heads $oldname]
- if {$oldidx >= 0} {
- set all_heads [lreplace $all_heads $oldidx $oldidx]
- }
- lappend all_heads $newname
- set all_heads [lsort $all_heads]
- populate_branch_menu
-
if {$current_branch eq $oldname} {
set current_branch $newname
}
--- /dev/null
+# 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 parent_w .; # window that started us
+field merge_type none; # type of merge to apply to existing branch
+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 reset_ok 0; # did the user agree to reset?
+field fetch_ok 0; # did the fetch succeed?
+
+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 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
+
+ _toplevel $this {Refreshing Tracking Branch}
+ set w_cons [::console::embed \
+ $w.console \
+ "Fetching $r_name from $remote"]
+ pack $w.console -fill both -expand 1
+ $w_cons exec \
+ [list git fetch $remote +$r_head:$l_trck] \
+ [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 {[catch {set new_hash [git rev-parse --verify "$l_trck^0"]} err]} {
+ set ok 0
+ $w_cons insert "fatal: Cannot resolve $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 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
+
+ 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 "Branch '$newbranch' does not exist."
+ return 0
+ }
+
+ set reflog_msg "branch: Created from $new_expr"
+ set cur $null_sha1
+ } 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 "Branch '$newbranch' already exists."
+ 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 {
+ set mrb {}
+ catch {set mrb [git merge-base $new $cur]}
+ switch -- $merge_type {
+ ff {
+ if {$mrb eq $new} {
+ # The current branch is actually newer.
+ #
+ set new $cur
+ } elseif {$mrb eq $cur} {
+ # The current branch is older.
+ #
+ set reflog_msg "merge $new_expr: Fast-forward"
+ } else {
+ _error $this "Branch '$newbranch' already exists.\n\nIt cannot fast-forward to $new_expr.\nA merge is required."
+ return 0
+ }
+ }
+ reset {
+ if {$mrb eq $cur} {
+ # The current branch is older.
+ #
+ set reflog_msg "merge $new_expr: Fast-forward"
+ } else {
+ # The current branch will lose things.
+ #
+ if {[_confirm_reset $this $cur]} {
+ set reflog_msg "reset $new_expr"
+ } else {
+ return 0
+ }
+ }
+ }
+ default {
+ _error $this "Only 'ff' and 'reset' merge is currently supported."
+ 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 "Failed to update '$newbranch'.\n\n$err"
+ return 0
+ }
+ }
+
+ return 1
+}
+
+method _checkout {} {
+ if {[lock_index checkout_op]} {
+ after idle [cb _start_checkout]
+ } else {
+ _error $this "Index is already locked."
+ delete_this
+ }
+}
+
+method _start_checkout {} {
+ global HEAD commit_type
+
+ # -- 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 ui_ready
+ delete_this
+ return
+ }
+
+ if {[is_config_true gui.trustmtime]} {
+ _readtree $this
+ } else {
+ ui_status {Refreshing file status...}
+ set cmd [list git update-index]
+ lappend cmd -q
+ lappend cmd --unmerged
+ lappend cmd --ignore-missing
+ lappend cmd --refresh
+ set fd [open "| $cmd" r]
+ 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
+
+ ui_status "Updating working directory to '[_name $this]'..."
+ set cmd [list git read-tree]
+ lappend cmd -m
+ lappend cmd -u
+ lappend cmd --exclude-per-directory=.gitignore
+ lappend cmd $HEAD
+ lappend cmd $new_hash
+ set fd [open "| $cmd" r]
+ fconfigure $fd -blocking 0 -translation binary
+ fileevent $fd readable [cb _readtree_wait $fd]
+}
+
+method _readtree_wait {fd} {
+ global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
+ global current_branch is_detached
+ global ui_comm
+
+ # -- We never get interesting output on stdout; only stderr.
+ #
+ read $fd
+ fconfigure $fd -blocking 1
+ if {![eof $fd]} {
+ fconfigure $fd -blocking 0
+ return
+ }
+
+ set name [_name $this]
+
+ # -- 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} err]} {
+ regsub {^fatal: } $err {} err
+ warn_popup "File level merge required.
+
+$err
+
+Staying on branch '$current_branch'."
+ ui_status "Aborted checkout of '$name' (file level merging is required)."
+ unlock_index
+ delete_this
+ return
+ }
+
+ 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]
+ 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 {
+ 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 "You are no longer on a local branch.
+
+If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."
+ }
+
+ # -- 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 [list ui_status "Checked out '$name'."]
+ } else {
+ repository_state commit_type HEAD MERGE_HEAD
+ set PARENT $HEAD
+ ui_status "Checked out '$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 "Resetting '$name' to $new_expr will lose the following commits:" \
+ ] -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 {Recovering lost commits may not be easy.} \
+ ]
+ pack [label $w.msg3 \
+ -anchor w \
+ -justify left \
+ -text "Reset '$name'?" \
+ ]
+
+ frame $w.buttons
+ button $w.buttons.visualize \
+ -text Visualize \
+ -command $gitk
+ pack $w.buttons.visualize -side left
+ button $w.buttons.reset \
+ -text Reset \
+ -command "
+ set @reset_ok 1
+ destroy $w
+ "
+ pack $w.buttons.reset -side right
+ button $w.buttons.cancel \
+ -default active \
+ -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
+
+ set fd [open "| git rev-list --pretty=oneline $cur ^$new_hash" r]
+ 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 "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"
+ exit 1
+}
+
+}
field spec_tag ; # list of all tag specs
constructor new {path {title {}}} {
- global all_heads current_branch
+ global current_branch is_detached
set w $path
}
bind $w <Destroy> [cb _delete %W]
+ if {$is_detached} {
+ radiobutton $w.detachedhead_r \
+ -anchor w \
+ -text {This Detached Checkout} \
+ -value HEAD \
+ -variable @revtype
+ grid $w.detachedhead_r -sticky we -padx {0 5} -columnspan 2
+ }
+
radiobutton $w.expr_r \
-text {Revision Expression:} \
-value expr \
grid $w.list -sticky nswe -padx {20 5} -columnspan 2
grid columnconfigure $w 1 -weight 1
- grid rowconfigure $w 2 -weight 1
+ if {$is_detached} {
+ grid rowconfigure $w 3 -weight 1
+ } else {
+ grid rowconfigure $w 2 -weight 1
+ }
trace add variable @revtype write [cb _select]
bind $w_filter <Key-Return> [list focus $w_list]\;break
bind $w_filter <Key-Down> [list focus $w_list]
set spec_head [list]
- foreach name $all_heads {
+ foreach name [load_all_heads] {
lappend spec_head [list $name refs/heads/$name]
}
lappend spec_tag [list $name refs/tags/$name]
}
- if {[llength $spec_head] > 0} { set revtype head
+ if {$is_detached} { set revtype HEAD
+ } elseif {[llength $spec_head] > 0} { set revtype head
} elseif {[llength $spec_trck] > 0} { set revtype trck
} elseif {[llength $spec_tag ] > 0} { set revtype tag
} else { set revtype expr
}
}
+ HEAD { return HEAD }
expr { return $c_expr }
none { return {} }
default { error "unknown type of revision" }
set revtype trck
}
+method focus_filter {} {
+ if {[$w_filter cget -state] eq {normal}} {
+ focus $w_filter
+ }
+}
+
+method get_local_branch {} {
+ if {$revtype eq {head}} {
+ return [_expr $this]
+ } else {
+ return {}
+ }
+}
+
method get_tracking_branch {} {
set i [$w_list curselection]
if {$i eq {} || $revtype ne {trck}} {
error "Revision expression is empty."
}
}
+ HEAD { return HEAD }
none { return {} }
default { error "unknown type of revision" }
}
method _select {args} {
_rebuild $this $filter
- if {[$w_filter cget -state] eq {normal}} {
- focus $w_filter
- }
+ focus_filter $this
}
method _rebuild {pat} {
trck { set new $spec_trck }
tag { set new $spec_tag }
expr -
+ HEAD -
none {
set new [list]
set ste disabled
if {[is_enabled singlecommit]} do_quit
- # -- Make sure our current branch exists.
- #
- if {$commit_type eq {initial}} {
- lappend all_heads $current_branch
- set all_heads [lsort -unique $all_heads]
- populate_branch_menu
- }
-
# -- Update in memory status
#
set selected_commit_type new
}
}
+method insert {txt} {
+ if {![winfo exists $w.m.t]} {_init $this}
+ $w.m.t conf -state normal
+ $w.m.t insert end "$txt\n"
+ set console_cr [$w.m.t index {end -1c}]
+ $w.m.t conf -state disabled
+}
+
method done {ok} {
if {$ok} {
if {[winfo exists $w.m.s]} {
[list radio_selector push_urltype remote]
proc do_push_anywhere {} {
- global all_heads all_remotes current_branch
+ global all_remotes current_branch
global push_urltype push_remote push_url push_thin push_tags
set w .push_setup
-width 70 \
-selectmode extended \
-yscrollcommand [list $w.source.sby set]
- foreach h $all_heads {
+ foreach h [load_all_heads] {
$w.source.l insert end $h
if {$h eq $current_branch} {
$w.source.l select set end