Code

gitk: Improve cherry-pick error handling
[git.git] / gitk
diff --git a/gitk b/gitk
index 998a05194204dbf238a792df19c2936c27941295..58ad47d18370b17e837174c73bbc5b76fd2da42c 100755 (executable)
--- a/gitk
+++ b/gitk
@@ -22,11 +22,11 @@ proc gitdir {} {
 # run before X event handlers, so reading from a fast source can
 # make the GUI completely unresponsive.
 proc run args {
-    global isonrunq runq
+    global isonrunq runq currunq
 
     set script $args
     if {[info exists isonrunq($script)]} return
-    if {$runq eq {}} {
+    if {$runq eq {} && ![info exists currunq]} {
        after idle dorunq
     }
     lappend runq [list {} $script]
@@ -38,10 +38,10 @@ proc filerun {fd script} {
 }
 
 proc filereadable {fd script} {
-    global runq
+    global runq currunq
 
     fileevent $fd readable {}
-    if {$runq eq {}} {
+    if {$runq eq {} && ![info exists currunq]} {
        after idle dorunq
     }
     lappend runq [list $fd $script]
@@ -60,17 +60,19 @@ proc nukefile {fd} {
 }
 
 proc dorunq {} {
-    global isonrunq runq
+    global isonrunq runq currunq
 
     set tstart [clock clicks -milliseconds]
     set t0 $tstart
     while {[llength $runq] > 0} {
        set fd [lindex $runq 0 0]
        set script [lindex $runq 0 1]
+       set currunq [lindex $runq 0]
+       set runq [lrange $runq 1 end]
        set repeat [eval $script]
+       unset currunq
        set t1 [clock clicks -milliseconds]
        set t [expr {$t1 - $t0}]
-       set runq [lrange $runq 1 end]
        if {$repeat ne {} && $repeat} {
            if {$fd eq {} || $repeat == 2} {
                # script returns 1 if it wants to be readded
@@ -90,6 +92,15 @@ proc dorunq {} {
     }
 }
 
+proc reg_instance {fd} {
+    global commfd leftover loginstance
+
+    set i [incr loginstance]
+    set commfd($i) $fd
+    set leftover($i) {}
+    return $i
+}
+
 proc unmerged_files {files} {
     global nr_unmerged
 
@@ -258,7 +269,7 @@ proc parseviewrevs {view revs} {
                lappend badrev $line
            }
        }                   
-       error_popup "Error parsing revisions: $err"
+       error_popup "[mc "Error parsing revisions:"] $err"
        return {}
     }
     set ret {}
@@ -294,11 +305,11 @@ proc parseviewrevs {view revs} {
 # Start off a git log process and arrange to read its output
 proc start_rev_list {view} {
     global startmsecs commitidx viewcomplete curview
-    global commfd leftover tclencoding
+    global tclencoding
     global viewargs viewargscmd viewfiles vfilelimit
-    global showlocalchanges commitinterest mainheadid
-    global viewactive loginstance viewinstances vmergeonly
-    global pending_select mainheadid
+    global showlocalchanges
+    global viewactive viewinstances vmergeonly
+    global mainheadid
     global vcanopt vflags vrevs vorigargs
 
     set startmsecs [clock clicks -milliseconds]
@@ -313,7 +324,7 @@ proc start_rev_list {view} {
        if {[catch {
            set str [exec sh -c $viewargscmd($view)]
        } err]} {
-           error_popup "Error executing --argscmd command: $err"
+           error_popup "[mc "Error executing --argscmd command:"] $err"
            return 0
        }
        set args [concat $args [split $str "\n"]]
@@ -354,12 +365,10 @@ proc start_rev_list {view} {
        error_popup "[mc "Error executing git log:"] $err"
        return 0
     }
-    set i [incr loginstance]
+    set i [reg_instance $fd]
     set viewinstances($view) [list $i]
-    set commfd($i) $fd
-    set leftover($i) {}
-    if {$showlocalchanges} {
-       lappend commitinterest($mainheadid) {dodiffindex}
+    if {$showlocalchanges && $mainheadid ne {}} {
+       interestedin $mainheadid dodiffindex
     }
     fconfigure $fd -blocking 0 -translation lf -eofchar {}
     if {$tclencoding != {}} {
@@ -367,36 +376,65 @@ proc start_rev_list {view} {
     }
     filerun $fd [list getcommitlines $fd $i $view 0]
     nowbusy $view [mc "Reading"]
-    if {$view == $curview} {
-       set pending_select $mainheadid
-    }
     set viewcomplete($view) 0
     set viewactive($view) 1
     return 1
 }
 
-proc stop_rev_list {view} {
-    global commfd viewinstances leftover
+proc stop_instance {inst} {
+    global commfd leftover
 
-    foreach inst $viewinstances($view) {
-       set fd $commfd($inst)
-       catch {
-           set pid [pid $fd]
+    set fd $commfd($inst)
+    catch {
+       set pid [pid $fd]
+
+       if {$::tcl_platform(platform) eq {windows}} {
+           exec kill -f $pid
+       } else {
            exec kill $pid
        }
-       catch {close $fd}
-       nukefile $fd
-       unset commfd($inst)
-       unset leftover($inst)
+    }
+    catch {close $fd}
+    nukefile $fd
+    unset commfd($inst)
+    unset leftover($inst)
+}
+
+proc stop_backends {} {
+    global commfd
+
+    foreach inst [array names commfd] {
+       stop_instance $inst
+    }
+}
+
+proc stop_rev_list {view} {
+    global viewinstances
+
+    foreach inst $viewinstances($view) {
+       stop_instance $inst
     }
     set viewinstances($view) {}
 }
 
-proc getcommits {} {
+proc reset_pending_select {selid} {
+    global pending_select mainheadid selectheadid
+
+    if {$selid ne {}} {
+       set pending_select $selid
+    } elseif {$selectheadid ne {}} {
+       set pending_select $selectheadid
+    } else {
+       set pending_select $mainheadid
+    }
+}
+
+proc getcommits {selid} {
     global canv curview need_redisplay viewactive
 
     initlayout
     if {[start_rev_list $curview]} {
+       reset_pending_select $selid
        show_status [mc "Reading commits..."]
        set need_redisplay 1
     } else {
@@ -406,8 +444,8 @@ proc getcommits {} {
 
 proc updatecommits {} {
     global curview vcanopt vorigargs vfilelimit viewinstances
-    global viewactive viewcomplete loginstance tclencoding mainheadid
-    global startmsecs commfd showneartags showlocalchanges leftover
+    global viewactive viewcomplete tclencoding
+    global startmsecs showneartags showlocalchanges
     global mainheadid pending_select
     global isworktree
     global varcid vposids vnegids vflags vrevs
@@ -462,16 +500,14 @@ proc updatecommits {} {
        set fd [open [concat | git log --no-color -z --pretty=raw --parents \
                          --boundary $args "--" $vfilelimit($view)] r]
     } err]} {
-       error_popup "Error executing git log: $err"
+       error_popup "[mc "Error executing git log:"] $err"
        return
     }
     if {$viewactive($view) == 0} {
        set startmsecs [clock clicks -milliseconds]
     }
-    set i [incr loginstance]
+    set i [reg_instance $fd]
     lappend viewinstances($view) $i
-    set commfd($i) $fd
-    set leftover($i) {}
     fconfigure $fd -blocking 0 -translation lf -eofchar {}
     if {$tclencoding != {}} {
        fconfigure $fd -encoding $tclencoding
@@ -479,7 +515,7 @@ proc updatecommits {} {
     filerun $fd [list getcommitlines $fd $i $view 1]
     incr viewactive($view)
     set viewcomplete($view) 0
-    set pending_select $mainheadid
+    reset_pending_select {}
     nowbusy $view "Reading"
     if {$showneartags} {
        getallcommits
@@ -491,11 +527,16 @@ proc reloadcommits {} {
     global showneartags treediffs commitinterest cached_commitrow
     global targetid
 
+    set selid {}
+    if {$selectedline ne {}} {
+       set selid $currentid
+    }
+
     if {!$viewcomplete($curview)} {
        stop_rev_list $curview
     }
     resetvarcs $curview
-    catch {unset selectedline}
+    set selectedline {}
     catch {unset currentid}
     catch {unset thickerline}
     catch {unset treediffs}
@@ -509,7 +550,7 @@ proc reloadcommits {} {
     catch {unset cached_commitrow}
     catch {unset targetid}
     setcanvscroll
-    getcommits
+    getcommits $selid
     return 0
 }
 
@@ -927,7 +968,7 @@ proc removefakerow {id} {
     modify_arc $v $a $i
     if {[info exist currentid] && $id eq $currentid} {
        unset currentid
-       unset selectedline
+       set selectedline {}
     }
     if {[info exists targetid] && $targetid eq $id} {
        set targetid $p
@@ -1190,7 +1231,7 @@ proc commitonrow {row} {
 
 proc closevarcs {v} {
     global varctok varccommits varcid parents children
-    global cmitlisted commitidx commitinterest vtokmod
+    global cmitlisted commitidx vtokmod
 
     set missing_parents 0
     set scripts {}
@@ -1215,12 +1256,7 @@ proc closevarcs {v} {
            }
            lappend varccommits($v,$b) $p
            incr commitidx($v)
-           if {[info exists commitinterest($p)]} {
-               foreach script $commitinterest($p) {
-                   lappend scripts [string map [list "%I" $p] $script]
-               }
-               unset commitinterest($id)
-           }
+           set scripts [check_interest $p $scripts]
        }
     }
     if {$missing_parents > 0} {
@@ -1256,8 +1292,41 @@ proc rewrite_commit {v id rwid} {
     }
 }
 
+# Mechanism for registering a command to be executed when we come
+# across a particular commit.  To handle the case when only the
+# prefix of the commit is known, the commitinterest array is now
+# indexed by the first 4 characters of the ID.  Each element is a
+# list of id, cmd pairs.
+proc interestedin {id cmd} {
+    global commitinterest
+
+    lappend commitinterest([string range $id 0 3]) $id $cmd
+}
+
+proc check_interest {id scripts} {
+    global commitinterest
+
+    set prefix [string range $id 0 3]
+    if {[info exists commitinterest($prefix)]} {
+       set newlist {}
+       foreach {i script} $commitinterest($prefix) {
+           if {[string match "$i*" $id]} {
+               lappend scripts [string map [list "%I" $id "%P" $i] $script]
+           } else {
+               lappend newlist $i $script
+           }
+       }
+       if {$newlist ne {}} {
+           set commitinterest($prefix) $newlist
+       } else {
+           unset commitinterest($prefix)
+       }
+    }
+    return $scripts
+}
+
 proc getcommitlines {fd inst view updating}  {
-    global cmitlisted commitinterest leftover
+    global cmitlisted leftover
     global commitidx commitdata vdatemode
     global parents children curview hlview
     global idpending ordertok
@@ -1433,12 +1502,7 @@ proc getcommitlines {fd inst view updating}  {
            incr i
        }
 
-       if {[info exists commitinterest($id)]} {
-           foreach script $commitinterest($id) {
-               lappend scripts [string map [list "%I" $id] $script]
-           }
-           unset commitinterest($id)
-       }
+       set scripts [check_interest $id $scripts]
        set gotsome 1
     }
     if {$gotsome} {
@@ -1467,11 +1531,17 @@ proc chewcommits {} {
     if {$viewcomplete($curview)} {
        global commitidx varctok
        global numcommits startmsecs
-       global mainheadid nullid
 
        if {[info exists pending_select]} {
-           set row [first_real_row]
-           selectline $row 1
+           update
+           reset_pending_select {}
+
+           if {[commitinview $pending_select $curview]} {
+               selectline [rowofcommit $pending_select] 1
+           } else {
+               set row [first_real_row]
+               selectline $row 1
+           }
        }
        if {$commitidx($curview) > 0} {
            #set ms [expr {[clock clicks -milliseconds] - $startmsecs}]
@@ -1561,9 +1631,23 @@ proc getcommit {id} {
     return 1
 }
 
+# Expand an abbreviated commit ID to a list of full 40-char IDs that match
+# and are present in the current view.
+# This is fairly slow...
+proc longid {prefix} {
+    global varcid curview
+
+    set ids {}
+    foreach match [array names varcid "$curview,$prefix*"] {
+       lappend ids [lindex [split $match ","] 1]
+    }
+    return $ids
+}
+
 proc readrefs {} {
     global tagids idtags headids idheads tagobjid
     global otherrefids idotherrefs mainhead mainheadid
+    global selecthead selectheadid
 
     foreach v {tagids idtags headids idheads otherrefids idotherrefs} {
        catch {unset $v}
@@ -1604,12 +1688,16 @@ proc readrefs {} {
     set mainhead {}
     set mainheadid {}
     catch {
+       set mainheadid [exec git rev-parse HEAD]
        set thehead [exec git symbolic-ref HEAD]
        if {[string match "refs/heads/*" $thehead]} {
            set mainhead [string range $thehead 11 end]
-           if {[info exists headids($mainhead)]} {
-               set mainheadid $headids($mainhead)
-           }
+       }
+    }
+    set selectheadid {}
+    if {$selecthead ne {}} {
+       catch {
+           set selectheadid [exec git rev-parse --verify $selecthead]
        }
     }
 }
@@ -1658,22 +1746,24 @@ proc show_error {w top msg} {
     pack $w.ok -side bottom -fill x
     bind $top <Visibility> "grab $top; focus $top"
     bind $top <Key-Return> "destroy $top"
+    bind $top <Key-space>  "destroy $top"
+    bind $top <Key-Escape> "destroy $top"
     tkwait window $top
 }
 
-proc error_popup msg {
+proc error_popup {msg {owner .}} {
     set w .error
     toplevel $w
-    wm transient $w .
+    wm transient $w $owner
     show_error $w $w $msg
 }
 
-proc confirm_popup msg {
+proc confirm_popup {msg {owner .}} {
     global confirm_ok
     set confirm_ok 0
     set w .confirm
     toplevel $w
-    wm transient $w .
+    wm transient $w $owner
     message $w.m -text $msg -justify center -aspect 400
     pack $w.m -side top -fill x -padx 20 -pady 20
     button $w.ok -text [mc OK] -command "set confirm_ok 1; destroy $w"
@@ -1681,6 +1771,9 @@ proc confirm_popup msg {
     button $w.cancel -text [mc Cancel] -command "destroy $w"
     pack $w.cancel -side right -fill x
     bind $w <Visibility> "grab $w; focus $w"
+    bind $w <Key-Return> "set confirm_ok 1; destroy $w"
+    bind $w <Key-space>  "set confirm_ok 1; destroy $w"
+    bind $w <Key-Escape> "destroy $w"
     tkwait window $w
     return $confirm_ok
 }
@@ -1698,6 +1791,53 @@ proc setoptions {} {
     option add *Entry.font uifont startupFile
 }
 
+# Make a menu and submenus.
+# m is the window name for the menu, items is the list of menu items to add.
+# Each item is a list {mc label type description options...}
+# mc is ignored; it's so we can put mc there to alert xgettext
+# label is the string that appears in the menu
+# type is cascade, command or radiobutton (should add checkbutton)
+# description depends on type; it's the sublist for cascade, the
+# command to invoke for command, or {variable value} for radiobutton
+proc makemenu {m items} {
+    menu $m
+    foreach i $items {
+       set name [mc [lindex $i 1]]
+       set type [lindex $i 2]
+       set thing [lindex $i 3]
+       set params [list $type]
+       if {$name ne {}} {
+           set u [string first "&" [string map {&& x} $name]]
+           lappend params -label [string map {&& & & {}} $name]
+           if {$u >= 0} {
+               lappend params -underline $u
+           }
+       }
+       switch -- $type {
+           "cascade" {
+               set submenu [string tolower [string map {& ""} [lindex $i 1]]]
+               lappend params -menu $m.$submenu
+           }
+           "command" {
+               lappend params -command $thing
+           }
+           "radiobutton" {
+               lappend params -variable [lindex $thing 0] \
+                   -value [lindex $thing 1]
+           }
+       }
+       eval $m add $params [lrange $i 4 end]
+       if {$type eq "cascade"} {
+           makemenu $m.$submenu $thing
+       }
+    }
+}
+
+# translate string and remove ampersands
+proc mca {str} {
+    return [string map {&& & & {}} [mc $str]]
+}
+
 proc makewindow {} {
     global canv canv2 canv3 linespc charspc ctext cflist cscroll
     global tabstop
@@ -1715,33 +1855,31 @@ proc makewindow {} {
     global rprogitem rprogcoord rownumsel numcommits
     global have_tk85
 
-    menu .bar
-    .bar add cascade -label [mc "File"] -menu .bar.file
-    menu .bar.file
-    .bar.file add command -label [mc "Update"] -command updatecommits
-    .bar.file add command -label [mc "Reload"] -command reloadcommits
-    .bar.file add command -label [mc "Reread references"] -command rereadrefs
-    .bar.file add command -label [mc "List references"] -command showrefs
-    .bar.file add command -label [mc "Quit"] -command doquit
-    menu .bar.edit
-    .bar add cascade -label [mc "Edit"] -menu .bar.edit
-    .bar.edit add command -label [mc "Preferences"] -command doprefs
-
-    menu .bar.view
-    .bar add cascade -label [mc "View"] -menu .bar.view
-    .bar.view add command -label [mc "New view..."] -command {newview 0}
-    .bar.view add command -label [mc "Edit view..."] -command editview \
-       -state disabled
-    .bar.view add command -label [mc "Delete view"] -command delview -state disabled
-    .bar.view add separator
-    .bar.view add radiobutton -label [mc "All files"] -command {showview 0} \
-       -variable selectedview -value 0
-
-    menu .bar.help
-    .bar add cascade -label [mc "Help"] -menu .bar.help
-    .bar.help add command -label [mc "About gitk"] -command about
-    .bar.help add command -label [mc "Key bindings"] -command keys
-    .bar.help configure
+    # The "mc" arguments here are purely so that xgettext
+    # sees the following string as needing to be translated
+    makemenu .bar {
+       {mc "File" cascade {
+           {mc "Update" command updatecommits -accelerator F5}
+           {mc "Reload" command reloadcommits}
+           {mc "Reread references" command rereadrefs}
+           {mc "List references" command showrefs}
+           {mc "Quit" command doquit}
+       }}
+       {mc "Edit" cascade {
+           {mc "Preferences" command doprefs}
+       }}
+       {mc "View" cascade {
+           {mc "New view..." command {newview 0}}
+           {mc "Edit view..." command editview -state disabled}
+           {mc "Delete view" command delview -state disabled}
+           {xx "" separator}
+           {mc "All files" radiobutton {selectedview 0} -command {showview 0}}
+       }}
+       {mc "Help" cascade {
+           {mc "About gitk" command about}
+           {mc "Key bindings" command keys}
+       }}
+    }
     . configure -menu .bar
 
     # the gui has upper and lower half, parts of a paned window.
@@ -1838,7 +1976,7 @@ proc makewindow {} {
     pack .tf.bar.rowlabel .tf.bar.rownum .tf.bar.rowlabel2 .tf.bar.numcommits \
        -side left
     global selectedline
-    trace add variable selectedline {write unset} selectedline_change
+    trace add variable selectedline write selectedline_change
 
     # Status label and progress bar
     set statusw .tf.bar.status
@@ -2106,61 +2244,67 @@ proc makewindow {} {
     bind . <$M1B-minus> {incrfont -1}
     bind . <$M1B-KP_Subtract> {incrfont -1}
     wm protocol . WM_DELETE_WINDOW doquit
+    bind . <Destroy> {stop_backends}
     bind . <Button-1> "click %W"
     bind $fstring <Key-Return> {dofind 1 1}
-    bind $sha1entry <Key-Return> gotocommit
+    bind $sha1entry <Key-Return> {gotocommit; break}
     bind $sha1entry <<PasteSelection>> clearsha1
     bind $cflist <1> {sel_flist %W %x %y; break}
     bind $cflist <B1-Motion> {sel_flist %W %x %y; break}
     bind $cflist <ButtonRelease-1> {treeclick %W %x %y}
-    bind $cflist <Button-3> {pop_flist_menu %W %X %Y %x %y}
+    global ctxbut
+    bind $cflist $ctxbut {pop_flist_menu %W %X %Y %x %y}
+    bind $ctext $ctxbut {pop_diff_menu %W %X %Y %x %y}
 
     set maincursor [. cget -cursor]
     set textcursor [$ctext cget -cursor]
     set curtextcursor $textcursor
 
     set rowctxmenu .rowctxmenu
-    menu $rowctxmenu -tearoff 0
-    $rowctxmenu add command -label [mc "Diff this -> selected"] \
-       -command {diffvssel 0}
-    $rowctxmenu add command -label [mc "Diff selected -> this"] \
-       -command {diffvssel 1}
-    $rowctxmenu add command -label [mc "Make patch"] -command mkpatch
-    $rowctxmenu add command -label [mc "Create tag"] -command mktag
-    $rowctxmenu add command -label [mc "Write commit to file"] -command writecommit
-    $rowctxmenu add command -label [mc "Create new branch"] -command mkbranch
-    $rowctxmenu add command -label [mc "Cherry-pick this commit"] \
-       -command cherrypick
-    $rowctxmenu add command -label [mc "Reset HEAD branch to here"] \
-       -command resethead
+    makemenu $rowctxmenu {
+       {mc "Diff this -> selected" command {diffvssel 0}}
+       {mc "Diff selected -> this" command {diffvssel 1}}
+       {mc "Make patch" command mkpatch}
+       {mc "Create tag" command mktag}
+       {mc "Write commit to file" command writecommit}
+       {mc "Create new branch" command mkbranch}
+       {mc "Cherry-pick this commit" command cherrypick}
+       {mc "Reset HEAD branch to here" command resethead}
+    }
+    $rowctxmenu configure -tearoff 0
 
     set fakerowmenu .fakerowmenu
-    menu $fakerowmenu -tearoff 0
-    $fakerowmenu add command -label [mc "Diff this -> selected"] \
-       -command {diffvssel 0}
-    $fakerowmenu add command -label [mc "Diff selected -> this"] \
-       -command {diffvssel 1}
-    $fakerowmenu add command -label [mc "Make patch"] -command mkpatch
-#    $fakerowmenu add command -label [mc "Commit"] -command {mkcommit 0}
-#    $fakerowmenu add command -label [mc "Commit all"] -command {mkcommit 1}
-#    $fakerowmenu add command -label [mc "Revert local changes"] -command revertlocal
+    makemenu $fakerowmenu {
+       {mc "Diff this -> selected" command {diffvssel 0}}
+       {mc "Diff selected -> this" command {diffvssel 1}}
+       {mc "Make patch" command mkpatch}
+    }
+    $fakerowmenu configure -tearoff 0
 
     set headctxmenu .headctxmenu
-    menu $headctxmenu -tearoff 0
-    $headctxmenu add command -label [mc "Check out this branch"] \
-       -command cobranch
-    $headctxmenu add command -label [mc "Remove this branch"] \
-       -command rmbranch
+    makemenu $headctxmenu {
+       {mc "Check out this branch" command cobranch}
+       {mc "Remove this branch" command rmbranch}
+    }
+    $headctxmenu configure -tearoff 0
 
     global flist_menu
     set flist_menu .flistctxmenu
-    menu $flist_menu -tearoff 0
-    $flist_menu add command -label [mc "Highlight this too"] \
-       -command {flist_hl 0}
-    $flist_menu add command -label [mc "Highlight this only"] \
-       -command {flist_hl 1}
-    $flist_menu add command -label [mc "External diff"] \
-        -command {external_diff}
+    makemenu $flist_menu {
+       {mc "Highlight this too" command {flist_hl 0}}
+       {mc "Highlight this only" command {flist_hl 1}}
+       {mc "External diff" command {external_diff}}
+       {mc "Blame parent commit" command {external_blame 1}}
+    }
+    $flist_menu configure -tearoff 0
+
+    global diff_menu
+    set diff_menu .diffctxmenu
+    makemenu $diff_menu {
+       {mc "Show origin of this line" command show_line_source}
+       {mc "Run git gui blame on this line" command {external_blame_diff}}
+    }
+    $diff_menu configure -tearoff 0
 }
 
 # Windows sends all mouse wheel events to the current focused window, not
@@ -2185,7 +2329,7 @@ proc windows_mousewheel_redirector {W X Y D} {
 proc selectedline_change {n1 n2 op} {
     global selectedline rownumsel
 
-    if {$op eq "unset"} {
+    if {$selectedline eq {}} {
        set rownumsel {}
     } else {
        set rownumsel [expr {$selectedline + 1}]
@@ -2276,7 +2420,7 @@ proc savestuff {w} {
     global viewname viewfiles viewargs viewargscmd viewperm nextviewnum
     global cmitmode wrapcomment datetimeformat limitdiffs
     global colors bgcolor fgcolor diffcolors diffcontext selectbgcolor
-    global autoselect extdifftool
+    global autoselect extdifftool perfile_attrs markbgcolor
 
     if {$stuffsaved} return
     if {![winfo viewable .]} return
@@ -2300,9 +2444,11 @@ proc savestuff {w} {
        puts $f [list set fgcolor $fgcolor]
        puts $f [list set colors $colors]
        puts $f [list set diffcolors $diffcolors]
+       puts $f [list set markbgcolor $markbgcolor]
        puts $f [list set diffcontext $diffcontext]
        puts $f [list set selectbgcolor $selectbgcolor]
        puts $f [list set extdifftool $extdifftool]
+       puts $f [list set perfile_attrs $perfile_attrs]
 
        puts $f "set geometry(main) [wm geometry .]"
        puts $f "set geometry(topwidth) [winfo width .tf]"
@@ -2400,6 +2546,7 @@ proc about {} {
     }
     toplevel $w
     wm title $w [mc "About gitk"]
+    wm transient $w .
     message $w.m -text [mc "
 Gitk - a commit viewer for git
 
@@ -2428,6 +2575,7 @@ proc keys {} {
     }
     toplevel $w
     wm title $w [mc "Gitk key bindings"]
+    wm transient $w .
     message $w.m -text "
 [mc "Gitk key bindings:"]
 
@@ -2470,6 +2618,7 @@ proc keys {} {
            -justify left -bg white -border 2 -relief groove
     pack $w.m -side top -fill both -padx 2 -pady 2
     button $w.ok -text [mc "Close"] -command "destroy $w" -default active
+    bind $w <Key-Escape> [list destroy $w]
     pack $w.ok -side bottom
     bind $w <Visibility> "focus $w.ok"
     bind $w <Key-Escape> "destroy $w"
@@ -2650,7 +2799,7 @@ proc treeopendir {w dir} {
            $w insert e:$ix $e [highlight_tag $de]
        }
     }
-    $w mark gravity e:$ix left
+    $w mark gravity e:$ix right
     $w conf -state disabled
     set treediropen($dir) 1
     set top [lindex [split [$w index @0,0] .] 0]
@@ -2691,9 +2840,15 @@ proc treeclick {w x y} {
 }
 
 proc setfilelist {id} {
-    global treefilelist cflist
+    global treefilelist cflist jump_to_here
 
     treeview $cflist $treefilelist($id) 0
+    if {$jump_to_here ne {}} {
+       set f [lindex $jump_to_here 0]
+       if {[lsearch -exact $treefilelist($id) $f] >= 0} {
+           showfile $f
+       }
+    }
 }
 
 image create bitmap tri-rt -background black -foreground blue -data {
@@ -2862,6 +3017,38 @@ proc pop_flist_menu {w X Y x y} {
     tk_popup $flist_menu $X $Y
 }
 
+proc find_ctext_fileinfo {line} {
+    global ctext_file_names ctext_file_lines
+
+    set ok [bsearch $ctext_file_lines $line]
+    set tline [lindex $ctext_file_lines $ok]
+
+    if {$ok >= [llength $ctext_file_lines] || $line < $tline} {
+        return {}
+    } else {
+        return [list [lindex $ctext_file_names $ok] $tline]
+    }
+}
+
+proc pop_diff_menu {w X Y x y} {
+    global ctext diff_menu flist_menu_file
+    global diff_menu_txtpos diff_menu_line
+    global diff_menu_filebase
+
+    set diff_menu_txtpos [split [$w index "@$x,$y"] "."]
+    set diff_menu_line [lindex $diff_menu_txtpos 0]
+    # don't pop up the menu on hunk-separator or file-separator lines
+    if {[lsearch -glob [$ctext tag names $diff_menu_line.0] "*sep"] >= 0} {
+       return
+    }
+    stopfinding
+    set f [find_ctext_fileinfo $diff_menu_line]
+    if {$f eq {}} return
+    set flist_menu_file [lindex $f 0]
+    set diff_menu_filebase [lindex $f 1]
+    tk_popup $diff_menu $X $Y
+}
+
 proc flist_hl {only} {
     global flist_menu_file findstring gdttype
 
@@ -2881,7 +3068,7 @@ proc save_file_from_commit {filename output what} {
        if {[string match "fatal: bad revision *" $err]} {
            return $nullfile
        }
-       error_popup "Error getting \"$filename\" from $what: $err"
+       error_popup "[mc "Error getting \"%s\" from %s:" $filename $what] $err"
        return {}
     }
     return $output
@@ -2938,7 +3125,7 @@ proc external_diff {} {
        set gitktmpdir [file join [file dirname $gitdir] \
                            [format ".gitk-tmp.%s" [pid]]]
        if {[catch {file mkdir $gitktmpdir} err]} {
-           error_popup "Error creating temporary directory $gitktmpdir: $err"
+           error_popup "[mc "Error creating temporary directory %s:" $gitktmpdir] $err"
            unset gitktmpdir
            return
        }
@@ -2947,7 +3134,7 @@ proc external_diff {} {
     incr diffnum
     set diffdir [file join $gitktmpdir $diffnum]
     if {[catch {file mkdir $diffdir} err]} {
-       error_popup "Error creating temporary directory $diffdir: $err"
+       error_popup "[mc "Error creating temporary directory %s:" $diffdir] $err"
        return
     }
 
@@ -2960,7 +3147,7 @@ proc external_diff {} {
                     [list $difffromfile $difftofile]]
         if {[catch {set fl [open $cmd r]} err]} {
             file delete -force $diffdir
-            error_popup [mc "$extdifftool: command failed: $err"]
+            error_popup "$extdifftool: [mc "command failed:"] $err"
         } else {
             fconfigure $fl -blocking 0
             filerun $fl [list delete_at_eof $fl $diffdir]
@@ -2968,12 +3155,214 @@ proc external_diff {} {
     }
 }
 
+proc find_hunk_blamespec {base line} {
+    global ctext
+
+    # Find and parse the hunk header
+    set s_lix [$ctext search -backwards -regexp ^@@ "$line.0 lineend" $base.0]
+    if {$s_lix eq {}} return
+
+    set s_line [$ctext get $s_lix "$s_lix + 1 lines"]
+    if {![regexp {^@@@*(( -\d+(,\d+)?)+) \+(\d+)(,\d+)? @@} $s_line \
+           s_line old_specs osz osz1 new_line nsz]} {
+       return
+    }
+
+    # base lines for the parents
+    set base_lines [list $new_line]
+    foreach old_spec [lrange [split $old_specs " "] 1 end] {
+       if {![regexp -- {-(\d+)(,\d+)?} $old_spec \
+               old_spec old_line osz]} {
+           return
+       }
+       lappend base_lines $old_line
+    }
+
+    # Now scan the lines to determine offset within the hunk
+    set max_parent [expr {[llength $base_lines]-2}]
+    set dline 0
+    set s_lno [lindex [split $s_lix "."] 0]
+
+    # Determine if the line is removed
+    set chunk [$ctext get $line.0 "$line.1 + $max_parent chars"]
+    if {[string match {[-+ ]*} $chunk]} {
+       set removed_idx [string first "-" $chunk]
+       # Choose a parent index
+       if {$removed_idx >= 0} {
+           set parent $removed_idx
+       } else {
+           set unchanged_idx [string first " " $chunk]
+           if {$unchanged_idx >= 0} {
+               set parent $unchanged_idx
+           } else {
+               # blame the current commit
+               set parent -1
+           }
+       }
+       # then count other lines that belong to it
+       for {set i $line} {[incr i -1] > $s_lno} {} {
+           set chunk [$ctext get $i.0 "$i.1 + $max_parent chars"]
+           # Determine if the line is removed
+           set removed_idx [string first "-" $chunk]
+           if {$parent >= 0} {
+               set code [string index $chunk $parent]
+               if {$code eq "-" || ($removed_idx < 0 && $code ne "+")} {
+                   incr dline
+               }
+           } else {
+               if {$removed_idx < 0} {
+                   incr dline
+               }
+           }
+       }
+       incr parent
+    } else {
+       set parent 0
+    }
+
+    incr dline [lindex $base_lines $parent]
+    return [list $parent $dline]
+}
+
+proc external_blame_diff {} {
+    global currentid diffmergeid cmitmode
+    global diff_menu_txtpos diff_menu_line
+    global diff_menu_filebase flist_menu_file
+
+    if {$cmitmode eq "tree"} {
+       set parent_idx 0
+       set line [expr {$diff_menu_line - $diff_menu_filebase}]
+    } else {
+       set hinfo [find_hunk_blamespec $diff_menu_filebase $diff_menu_line]
+       if {$hinfo ne {}} {
+           set parent_idx [lindex $hinfo 0]
+           set line [lindex $hinfo 1]
+       } else {
+           set parent_idx 0
+           set line 0
+       }
+    }
+
+    external_blame $parent_idx $line
+}
+
+proc external_blame {parent_idx {line {}}} {
+    global flist_menu_file
+    global nullid nullid2
+    global parentlist selectedline currentid
+
+    if {$parent_idx > 0} {
+       set base_commit [lindex $parentlist $selectedline [expr {$parent_idx-1}]]
+    } else {
+       set base_commit $currentid
+    }
+
+    if {$base_commit eq {} || $base_commit eq $nullid || $base_commit eq $nullid2} {
+       error_popup [mc "No such commit"]
+       return
+    }
+
+    set cmdline [list git gui blame]
+    if {$line ne {} && $line > 1} {
+       lappend cmdline "--line=$line"
+    }
+    lappend cmdline $base_commit $flist_menu_file
+    if {[catch {eval exec $cmdline &} err]} {
+       error_popup "[mc "git gui blame: command failed:"] $err"
+    }
+}
+
+proc show_line_source {} {
+    global cmitmode currentid parents curview blamestuff blameinst
+    global diff_menu_line diff_menu_filebase flist_menu_file
+
+    if {$cmitmode eq "tree"} {
+       set id $currentid
+       set line [expr {$diff_menu_line - $diff_menu_filebase}]
+    } else {
+       set h [find_hunk_blamespec $diff_menu_filebase $diff_menu_line]
+       if {$h eq {}} return
+       set pi [lindex $h 0]
+       if {$pi == 0} {
+           mark_ctext_line $diff_menu_line
+           return
+       }
+       set id [lindex $parents($curview,$currentid) [expr {$pi - 1}]]
+       set line [lindex $h 1]
+    }
+    if {[catch {
+       set f [open [list | git blame -p -L$line,+1 $id -- $flist_menu_file] r]
+    } err]} {
+       error_popup [mc "Couldn't start git blame: %s" $err]
+       return
+    }
+    fconfigure $f -blocking 0
+    set i [reg_instance $f]
+    set blamestuff($i) {}
+    set blameinst $i
+    filerun $f [list read_line_source $f $i]
+}
+
+proc stopblaming {} {
+    global blameinst
+
+    if {[info exists blameinst]} {
+       stop_instance $blameinst
+       unset blameinst
+    }
+}
+
+proc read_line_source {fd inst} {
+    global blamestuff curview commfd blameinst
+
+    while {[gets $fd line] >= 0} {
+       lappend blamestuff($inst) $line
+    }
+    if {![eof $fd]} {
+       return 1
+    }
+    unset commfd($inst)
+    unset blameinst
+    fconfigure $fd -blocking 1
+    if {[catch {close $fd} err]} {
+       error_popup [mc "Error running git blame: %s" $err]
+       return 0
+    }
+
+    set fname {}
+    set line [split [lindex $blamestuff($inst) 0] " "]
+    set id [lindex $line 0]
+    set lnum [lindex $line 1]
+    if {[string length $id] == 40 && [string is xdigit $id] &&
+       [string is digit -strict $lnum]} {
+       # look for "filename" line
+       foreach l $blamestuff($inst) {
+           if {[string match "filename *" $l]} {
+               set fname [string range $l 9 end]
+               break
+           }
+       }
+    }
+    if {$fname ne {}} {
+       # all looks good, select it
+       if {[commitinview $id $curview]} {
+           selectline [rowofcommit $id] 1 [list $fname $lnum]
+       } else {
+           error_popup [mc "That line comes from commit %s, \
+                            which is not in this view" [shortids $id]]
+       }
+    } else {
+       puts "oops couldn't parse git blame output"
+    }
+    return 0
+}
+
 # delete $dir when we see eof on $f (presumably because the child has exited)
 proc delete_at_eof {f dir} {
     while {[gets $f line] >= 0} {}
     if {[eof $f]} {
        if {[catch {close $f} err]} {
-           error_popup "External diff viewer failed: $err"
+           error_popup "[mc "External diff viewer failed:"] $err"
        }
        file delete -force $dir
        return 0
@@ -3116,6 +3505,7 @@ proc vieweditor {top n title} {
 
     toplevel $top
     wm title $top $title
+    wm transient $top .
     label $top.nl -text [mc "Name"]
     entry $top.name -width 20 -textvariable newviewname($n)
     grid $top.nl $top.name -sticky w -pady 5
@@ -3152,6 +3542,7 @@ proc vieweditor {top n title} {
     frame $top.buts
     button $top.buts.ok -text [mc "OK"] -command [list newviewok $top $n]
     button $top.buts.can -text [mc "Cancel"] -command [list destroy $top]
+    bind $top <Escape> [list destroy $top]
     grid $top.buts.ok $top.buts.can
     grid columnconfigure $top.buts 0 -weight 1 -uniform a
     grid columnconfigure $top.buts 1 -weight 1 -uniform a
@@ -3184,9 +3575,7 @@ proc newviewok {top n} {
     if {[catch {
        set newargs [shellsplit $newviewargs($n)]
     } err]} {
-       error_popup "[mc "Error in commit selection arguments:"] $err"
-       wm raise $top
-       focus $top
+       error_popup "[mc "Error in commit selection arguments:"] $err" $top
        return
     }
     set files {}
@@ -3274,7 +3663,7 @@ proc showview {n} {
     set ytop [expr {[lindex $span 0] * $ymax}]
     set ybot [expr {[lindex $span 1] * $ymax}]
     set yscreen [expr {($ybot - $ytop) / 2}]
-    if {[info exists selectedline]} {
+    if {$selectedline ne {}} {
        set selid $currentid
        set y [yc $selectedline]
        if {$ytop < $y && $y < $ybot} {
@@ -3298,15 +3687,12 @@ proc showview {n} {
 
     set curview $n
     set selectedview $n
-    .bar.view entryconf [mc "Edit view..."] -state [expr {$n == 0? "disabled": "normal"}]
-    .bar.view entryconf [mc "Delete view"] -state [expr {$n == 0? "disabled": "normal"}]
+    .bar.view entryconf [mca "Edit view..."] -state [expr {$n == 0? "disabled": "normal"}]
+    .bar.view entryconf [mca "Delete view"] -state [expr {$n == 0? "disabled": "normal"}]
 
     run refill_reflist
     if {![info exists viewcomplete($n)]} {
-       if {$selid ne {}} {
-           set pending_select $selid
-       }
-       getcommits
+       getcommits $selid
        return
     }
 
@@ -3340,18 +3726,18 @@ proc showview {n} {
     drawvisible
     if {$row ne {}} {
        selectline $row 0
-    } elseif {$mainheadid ne {} && [commitinview $mainheadid $curview]} {
-       selectline [rowofcommit $mainheadid] 1
     } elseif {!$viewcomplete($n)} {
-       if {$selid ne {}} {
-           set pending_select $selid
-       } else {
-           set pending_select $mainheadid
-       }
+       reset_pending_select $selid
     } else {
-       set row [first_real_row]
-       if {$row < $numcommits} {
-           selectline $row 0
+       reset_pending_select {}
+
+       if {[commitinview $pending_select $curview]} {
+           selectline [rowofcommit $pending_select] 1
+       } else {
+           set row [first_real_row]
+           if {$row < $numcommits} {
+               selectline $row 0
+           }
        }
     }
     if {!$viewcomplete($n)} {
@@ -3388,7 +3774,7 @@ proc bolden {row font} {
 
     lappend boldrows $row
     $canv itemconf $linehtag($row) -font $font
-    if {[info exists selectedline] && $row == $selectedline} {
+    if {$row == $selectedline} {
        $canv delete secsel
        set t [eval $canv create rect [$canv bbox $linehtag($row)] \
                   -outline {{}} -tags secsel \
@@ -3402,7 +3788,7 @@ proc bolden_name {row font} {
 
     lappend boldnamerows $row
     $canv2 itemconf $linentag($row) -font $font
-    if {[info exists selectedline] && $row == $selectedline} {
+    if {$row == $selectedline} {
        $canv2 delete secsel
        set t [eval $canv2 create rect [$canv2 bbox $linentag($row)] \
                   -outline {{}} -tags secsel \
@@ -3831,7 +4217,7 @@ proc askrelhighlight {row id} {
     global descendent highlight_related iddrawn rhighlights
     global selectedline ancestor
 
-    if {![info exists selectedline]} return
+    if {$selectedline eq {}} return
     set isbold 0
     if {$highlight_related eq [mc "Descendant"] ||
        $highlight_related eq [mc "Not descendant"]} {
@@ -4005,8 +4391,8 @@ proc visiblerows {} {
 
 proc layoutmore {} {
     global commitidx viewcomplete curview
-    global numcommits pending_select selectedline curview
-    global lastscrollset lastscrollrows commitinterest
+    global numcommits pending_select curview
+    global lastscrollset lastscrollrows
 
     if {$lastscrollrows < 100 || $viewcomplete($curview) ||
        [clock clicks -milliseconds] - $lastscrollset > 500} {
@@ -4014,6 +4400,7 @@ proc layoutmore {} {
     }
     if {[info exists pending_select] &&
        [commitinview $pending_select $curview]} {
+       update
        selectline [rowofcommit $pending_select] 1
     }
     drawvisible
@@ -4022,10 +4409,11 @@ proc layoutmore {} {
 proc doshowlocalchanges {} {
     global curview mainheadid
 
+    if {$mainheadid eq {}} return
     if {[commitinview $mainheadid $curview]} {
        dodiffindex
     } else {
-       lappend commitinterest($mainheadid) {dodiffindex}
+       interestedin $mainheadid dodiffindex
     }
 }
 
@@ -4050,10 +4438,11 @@ proc dodiffindex {} {
     incr lserial
     set fd [open "|git diff-index --cached HEAD" r]
     fconfigure $fd -blocking 0
-    filerun $fd [list readdiffindex $fd $lserial]
+    set i [reg_instance $fd]
+    filerun $fd [list readdiffindex $fd $lserial $i]
 }
 
-proc readdiffindex {fd serial} {
+proc readdiffindex {fd serial inst} {
     global mainheadid nullid nullid2 curview commitinfo commitdata lserial
 
     set isdiff 1
@@ -4064,7 +4453,7 @@ proc readdiffindex {fd serial} {
        set isdiff 0
     }
     # we only need to see one line and we don't really care what it says...
-    close $fd
+    stop_instance $inst
 
     if {$serial != $lserial} {
        return 0
@@ -4073,7 +4462,8 @@ proc readdiffindex {fd serial} {
     # now see if there are any local changes not checked in to the index
     set fd [open "|git diff-files" r]
     fconfigure $fd -blocking 0
-    filerun $fd [list readdifffiles $fd $serial]
+    set i [reg_instance $fd]
+    filerun $fd [list readdifffiles $fd $serial $i]
 
     if {$isdiff && ![commitinview $nullid2 $curview]} {
        # add the line for the changes in the index to the graph
@@ -4090,7 +4480,7 @@ proc readdiffindex {fd serial} {
     return 0
 }
 
-proc readdifffiles {fd serial} {
+proc readdifffiles {fd serial inst} {
     global mainheadid nullid nullid2 curview
     global commitinfo commitdata lserial
 
@@ -4102,7 +4492,7 @@ proc readdifffiles {fd serial} {
        set isdiff 0
     }
     # we only need to see one line and we don't really care what it says...
-    close $fd
+    stop_instance $inst
 
     if {$serial != $lserial} {
        return 0
@@ -4841,7 +5231,8 @@ proc drawcmittext {id row col} {
     global cmitlisted commitinfo rowidlist parentlist
     global rowtextx idpos idtags idheads idotherrefs
     global linehtag linentag linedtag selectedline
-    global canvxmax boldrows boldnamerows fgcolor nullid nullid2
+    global canvxmax boldrows boldnamerows fgcolor
+    global mainheadid nullid nullid2 circleitem circlecolors ctxbut
 
     # listed is 0 for boundary, 1 for normal, 2 for negative, 3 for left, 4 for right
     set listed $cmitlisted($curview,$id)
@@ -4849,8 +5240,10 @@ proc drawcmittext {id row col} {
        set ofill red
     } elseif {$id eq $nullid2} {
        set ofill green
+    } elseif {$id eq $mainheadid} {
+       set ofill yellow
     } else {
-       set ofill [expr {$listed != 0 ? $listed == 2 ? "gray" : "blue" : "white"}]
+       set ofill [lindex $circlecolors $listed]
     }
     set x [xc $row $col]
     set y [yc $row]
@@ -4874,6 +5267,7 @@ proc drawcmittext {id row col} {
                   [expr {$x - $orad}] [expr {$y + $orad - 1}] \
                   -fill $ofill -outline $fgcolor -width 1 -tags circle]
     }
+    set circleitem($row) $t
     $canv raise $t
     $canv bind $t <1> {selcanvline {} %x %y}
     set rmx [llength [lindex $rowidlist $row]]
@@ -4911,12 +5305,12 @@ proc drawcmittext {id row col} {
     }
     set linehtag($row) [$canv create text $xt $y -anchor w -fill $fgcolor \
                            -text $headline -font $font -tags text]
-    $canv bind $linehtag($row) <Button-3> "rowmenu %X %Y $id"
+    $canv bind $linehtag($row) $ctxbut "rowmenu %X %Y $id"
     set linentag($row) [$canv2 create text 3 $y -anchor w -fill $fgcolor \
                            -text $name -font $nfont -tags text]
     set linedtag($row) [$canv3 create text 3 $y -anchor w -fill $fgcolor \
                            -text $date -font mainfont -tags text]
-    if {[info exists selectedline] && $selectedline == $row} {
+    if {$selectedline == $row} {
        make_secsel $row
     }
     set xr [expr {$xt + [font measure $font $headline]}]
@@ -5107,7 +5501,7 @@ proc drawvisible {} {
     if {$endrow >= $vrowmod($curview)} {
        update_arcrows $curview
     }
-    if {[info exists selectedline] &&
+    if {$selectedline ne {} &&
        $row <= $selectedline && $selectedline <= $endrow} {
        set targetrow $selectedline
     } elseif {[info exists targetid]} {
@@ -5125,7 +5519,7 @@ proc drawvisible {} {
 proc clear_display {} {
     global iddrawn linesegs need_redisplay nrows_drawn
     global vhighlights fhighlights nhighlights rhighlights
-    global linehtag linentag linedtag
+    global linehtag linentag linedtag boldrows boldnamerows
 
     allcanvs delete all
     catch {unset iddrawn}
@@ -5133,6 +5527,8 @@ proc clear_display {} {
     catch {unset linehtag}
     catch {unset linentag}
     catch {unset linedtag}
+    set boldrows {}
+    set boldnamerows {}
     catch {unset vhighlights}
     catch {unset fhighlights}
     catch {unset nhighlights}
@@ -5251,7 +5647,7 @@ proc bindline {t id} {
 proc drawtags {id x xt y1} {
     global idtags idheads idotherrefs mainhead
     global linespc lthickness
-    global canv rowtextx curview fgcolor bgcolor
+    global canv rowtextx curview fgcolor bgcolor ctxbut
 
     set marks {}
     set ntags 0
@@ -5329,7 +5725,7 @@ proc drawtags {id x xt y1} {
        if {$ntags >= 0} {
            $canv bind $t <1> [list showtag $tag 1]
        } elseif {$nheads >= 0} {
-           $canv bind $t <Button-3> [list headmenu %X %Y $id $tag]
+           $canv bind $t $ctxbut [list headmenu %X %Y $id $tag]
        }
     }
     return $xt
@@ -5427,7 +5823,7 @@ proc dofind {{dirn 1} {wrap 1}} {
     }
     focus .
     if {$findstring eq {} || $numcommits == 0} return
-    if {![info exists selectedline]} {
+    if {$selectedline eq {}} {
        set findstartline [lindex [visiblerows] [expr {$dirn < 0}]]
     } else {
        set findstartline $selectedline
@@ -5453,6 +5849,7 @@ proc stopfinding {} {
        set fprogcoord 0
        adjustprogress
     }
+    stopblaming
 }
 
 proc findmore {} {
@@ -5623,7 +6020,7 @@ proc markmatches {canv l str tag matches font row} {
                   [expr {$x0+$xlen+2}] $y1 \
                   -outline {} -tags [list match$l matches] -fill yellow]
        $canv lower $t
-       if {[info exists selectedline] && $row == $selectedline} {
+       if {$row == $selectedline} {
            $canv raise $t secsel
        }
     }
@@ -5672,11 +6069,11 @@ proc commit_descriptor {p} {
 # append some text to the ctext widget, and make any SHA1 ID
 # that we know about be a clickable link.
 proc appendwithlinks {text tags} {
-    global ctext linknum curview pendinglinks
+    global ctext linknum curview
 
     set start [$ctext index "end - 1c"]
     $ctext insert end $text $tags
-    set links [regexp -indices -all -inline {[0-9a-f]{40}} $text]
+    set links [regexp -indices -all -inline {\m[0-9a-f]{6,40}\M} $text]
     foreach l $links {
        set s [lindex $l 0]
        set e [lindex $l 1]
@@ -5690,16 +6087,27 @@ proc appendwithlinks {text tags} {
 }
 
 proc setlink {id lk} {
-    global curview ctext pendinglinks commitinterest
+    global curview ctext pendinglinks
 
-    if {[commitinview $id $curview]} {
+    set known 0
+    if {[string length $id] < 40} {
+       set matches [longid $id]
+       if {[llength $matches] > 0} {
+           if {[llength $matches] > 1} return
+           set known 1
+           set id [lindex $matches 0]
+       }
+    } else {
+       set known [commitinview $id $curview]
+    }
+    if {$known} {
        $ctext tag conf $lk -foreground blue -underline 1
-       $ctext tag bind $lk <1> [list selectline [rowofcommit $id] 1]
+       $ctext tag bind $lk <1> [list selbyid $id]
        $ctext tag bind $lk <Enter> {linkcursor %W 1}
        $ctext tag bind $lk <Leave> {linkcursor %W -1}
     } else {
        lappend pendinglinks($id) $lk
-       lappend commitinterest($id) {makelink %I}
+       interestedin $id {makelink %P}
     }
 }
 
@@ -5782,7 +6190,7 @@ proc appendrefs {pos ids var} {
 proc dispneartags {delay} {
     global selectedline currentid showneartags tagphase
 
-    if {![info exists selectedline] || !$showneartags} return
+    if {$selectedline eq {} || !$showneartags} return
     after cancel dispnexttag
     if {$delay} {
        after 200 dispnexttag
@@ -5796,7 +6204,7 @@ proc dispneartags {delay} {
 proc dispnexttag {} {
     global selectedline currentid showneartags tagphase ctext
 
-    if {![info exists selectedline] || !$showneartags} return
+    if {$selectedline eq {} || !$showneartags} return
     switch -- $tagphase {
        0 {
            set dtags [desctags $currentid]
@@ -5846,7 +6254,7 @@ proc make_secsel {l} {
     $canv3 lower $t
 }
 
-proc selectline {l isnew} {
+proc selectline {l isnew {desired_loc {}}} {
     global canv ctext commitinfo selectedline
     global canvy0 linespc parents children curview
     global currentid sha1entry
@@ -5854,7 +6262,7 @@ proc selectline {l isnew} {
     global mergemax numcommits pending_select
     global cmitmode showneartags allcommits
     global targetrow targetid lastscrollrows
-    global autoselect
+    global autoselect jump_to_here
 
     catch {unset pending_select}
     $canv delete hover
@@ -5993,6 +6401,7 @@ proc selectline {l isnew} {
     $ctext conf -state disabled
     set commentend [$ctext index "end - 1c"]
 
+    set jump_to_here $desired_loc
     init_flist [mc "Comments"]
     if {$cmitmode eq "tree"} {
        gettree $id
@@ -6018,7 +6427,7 @@ proc sellastline {} {
 proc selnextline {dir} {
     global selectedline
     focus .
-    if {![info exists selectedline]} return
+    if {$selectedline eq {}} return
     set l [expr {$selectedline + $dir}]
     unmarkmatches
     selectline $l 1
@@ -6033,7 +6442,7 @@ proc selnextpage {dir} {
     }
     allcanvs yview scroll [expr {$dir * $lpp}] units
     drawvisible
-    if {![info exists selectedline]} return
+    if {$selectedline eq {}} return
     set l [expr {$selectedline + $dir * $lpp}]
     if {$l < 0} {
        set l 0
@@ -6047,7 +6456,7 @@ proc selnextpage {dir} {
 proc unselectline {} {
     global selectedline currentid
 
-    catch {unset selectedline}
+    set selectedline {}
     catch {unset currentid}
     allcanvs delete secsel
     rhighlight_none
@@ -6056,7 +6465,7 @@ proc unselectline {} {
 proc reselectline {} {
     global selectedline
 
-    if {[info exists selectedline]} {
+    if {$selectedline ne {}} {
        selectline $selectedline 0
     }
 }
@@ -6145,7 +6554,7 @@ proc gettree {id} {
            set treepending $id
            set treefilelist($id) {}
            set treeidlist($id) {}
-           fconfigure $gtf -blocking 0
+           fconfigure $gtf -blocking 0 -encoding binary
            filerun $gtf [list gettreeline $gtf $id]
        }
     } else {
@@ -6167,11 +6576,12 @@ proc gettreeline {gtf id} {
            set line [string range $line 0 [expr {$i-1}]]
            if {$diffids ne $nullid2 && [lindex $line 1] ne "blob"} continue
            set sha1 [lindex $line 2]
-           if {[string index $fname 0] eq "\""} {
-               set fname [lindex $fname 0]
-           }
            lappend treeidlist($id) $sha1
        }
+       if {[string index $fname 0] eq "\""} {
+           set fname [lindex $fname 0]
+       }
+       set fname [encoding convertfrom $fname]
        lappend treefilelist($id) $fname
     }
     if {![eof $gtf]} {
@@ -6193,6 +6603,7 @@ proc gettreeline {gtf id} {
 
 proc showfile {f} {
     global treefilelist treeidlist diffids nullid nullid2
+    global ctext_file_names ctext_file_lines
     global ctext commentend
 
     set i [lsearch -exact $treefilelist($diffids) $f]
@@ -6212,10 +6623,12 @@ proc showfile {f} {
            return
        }
     }
-    fconfigure $bf -blocking 0
+    fconfigure $bf -blocking 0 -encoding [get_path_encoding $f]
     filerun $bf [list getblobline $bf $diffids]
     $ctext config -state normal
     clear_ctext $commentend
+    lappend ctext_file_names $f
+    lappend ctext_file_lines [lindex [split $commentend "."] 0]
     $ctext insert end "\n"
     $ctext insert end "$f\n" filesep
     $ctext config -state disabled
@@ -6236,24 +6649,45 @@ proc getblobline {bf id} {
        $ctext insert end "$line\n"
     }
     if {[eof $bf]} {
+       global jump_to_here ctext_file_names commentend
+
        # delete last newline
        $ctext delete "end - 2c" "end - 1c"
        close $bf
+       if {$jump_to_here ne {} &&
+           [lindex $jump_to_here 0] eq [lindex $ctext_file_names 0]} {
+           set lnum [expr {[lindex $jump_to_here 1] +
+                           [lindex [split $commentend .] 0]}]
+           mark_ctext_line $lnum
+       }
        return 0
     }
     $ctext config -state disabled
     return [expr {$nl >= 1000? 2: 1}]
 }
 
+proc mark_ctext_line {lnum} {
+    global ctext markbgcolor
+
+    $ctext tag delete omark
+    $ctext tag add omark $lnum.0 "$lnum.0 + 1 line"
+    $ctext tag conf omark -background $markbgcolor
+    $ctext see $lnum.0
+}
+
 proc mergediff {id} {
     global diffmergeid mdifffd
-    global diffids
+    global diffids treediffs
     global parents
     global diffcontext
+    global diffencoding
     global limitdiffs vfilelimit curview
+    global targetline
 
     set diffmergeid $id
     set diffids $id
+    set treediffs($id) {}
+    set targetline {}
     # this doesn't seem to actually affect anything...
     set cmd [concat | git diff-tree --no-commit-id --cc -U$diffcontext $id]
     if {$limitdiffs && $vfilelimit($curview) ne {}} {
@@ -6263,16 +6697,19 @@ proc mergediff {id} {
        error_popup "[mc "Error getting merge diffs:"] $err"
        return
     }
-    fconfigure $mdf -blocking 0
+    fconfigure $mdf -blocking 0 -encoding binary
     set mdifffd($id) $mdf
     set np [llength $parents($curview,$id)]
+    set diffencoding [get_path_encoding {}]
     settabs $np
     filerun $mdf [list getmergediffline $mdf $id $np]
 }
 
 proc getmergediffline {mdf id np} {
     global diffmergeid ctext cflist mergemax
-    global difffilestart mdifffd
+    global difffilestart mdifffd treediffs
+    global ctext_file_names ctext_file_lines
+    global diffencoding jump_to_here targetline diffline
 
     $ctext conf -state normal
     set nr 0
@@ -6284,18 +6721,33 @@ proc getmergediffline {mdf id np} {
        }
        if {[regexp {^diff --cc (.*)} $line match fname]} {
            # start of a new file
+           set fname [encoding convertfrom $fname]
            $ctext insert end "\n"
            set here [$ctext index "end - 1c"]
            lappend difffilestart $here
+           lappend treediffs($id) $fname
            add_flist [list $fname]
+           lappend ctext_file_names $fname
+           lappend ctext_file_lines [lindex [split $here "."] 0]
+           set diffencoding [get_path_encoding $fname]
            set l [expr {(78 - [string length $fname]) / 2}]
            set pad [string range "----------------------------------------" 1 $l]
            $ctext insert end "$pad $fname $pad\n" filesep
+           set targetline {}
+           if {$jump_to_here ne {} && [lindex $jump_to_here 0] eq $fname} {
+               set targetline [lindex $jump_to_here 1]
+           }
+           set diffline 0
        } elseif {[regexp {^@@} $line]} {
+           set line [encoding convertfrom $diffencoding $line]
            $ctext insert end "$line\n" hunksep
+           if {[regexp { \+(\d+),\d+ @@} $line m nl]} {
+               set diffline $nl
+           }
        } elseif {[regexp {^[0-9a-f]{40}$} $line] || [regexp {^index} $line]} {
            # do nothing
        } else {
+           set line [encoding convertfrom $diffencoding $line]
            # parse the prefix - one ' ', '-' or '+' for each parent
            set spaces {}
            set minuses {}
@@ -6331,6 +6783,15 @@ proc getmergediffline {mdf id np} {
                lappend tags m$num
            }
            $ctext insert end "$line\n" $tags
+           if {$targetline ne {} && $minuses eq {}} {
+               if {$diffline == $targetline} {
+                   set here [$ctext index "end - 1 line"]
+                   mark_ctext_line [lindex [split $here .] 0]
+                   set targetline {}
+               } else {
+                   incr diffline
+               }
+           }
        }
     }
     $ctext conf -state disabled
@@ -6426,30 +6887,46 @@ proc diffcmd {ids flags} {
 proc gettreediffs {ids} {
     global treediff treepending
 
+    if {[catch {set gdtf [open [diffcmd $ids {--no-commit-id}] r]}]} return
+
     set treepending $ids
     set treediff {}
-    if {[catch {set gdtf [open [diffcmd $ids {--no-commit-id}] r]}]} return
-    fconfigure $gdtf -blocking 0
+    fconfigure $gdtf -blocking 0 -encoding binary
     filerun $gdtf [list gettreediffline $gdtf $ids]
 }
 
 proc gettreediffline {gdtf ids} {
     global treediff treediffs treepending diffids diffmergeid
-    global cmitmode vfilelimit curview limitdiffs
+    global cmitmode vfilelimit curview limitdiffs perfile_attrs
 
     set nr 0
-    while {[incr nr] <= 1000 && [gets $gdtf line] >= 0} {
+    set sublist {}
+    set max 1000
+    if {$perfile_attrs} {
+       # cache_gitattr is slow, and even slower on win32 where we
+       # have to invoke it for only about 30 paths at a time
+       set max 500
+       if {[tk windowingsystem] == "win32"} {
+           set max 120
+       }
+    }
+    while {[incr nr] <= $max && [gets $gdtf line] >= 0} {
        set i [string first "\t" $line]
        if {$i >= 0} {
            set file [string range $line [expr {$i+1}] end]
            if {[string index $file 0] eq "\""} {
                set file [lindex $file 0]
            }
+           set file [encoding convertfrom $file]
            lappend treediff $file
+           lappend sublist $file
        }
     }
+    if {$perfile_attrs} {
+       cache_gitattr encoding $sublist
+    }
     if {![eof $gdtf]} {
-       return [expr {$nr >= 1000? 2: 1}]
+       return [expr {$nr >= $max? 2: 1}]
     }
     close $gdtf
     if {$limitdiffs && $vfilelimit($curview) ne {}} {
@@ -6502,6 +6979,7 @@ proc getblobdiffs {ids} {
     global diffcontext
     global ignorespace
     global limitdiffs vfilelimit curview
+    global diffencoding targetline
 
     set cmd [diffcmd $ids "-p -C --no-commit-id -U$diffcontext"]
     if {$ignorespace} {
@@ -6514,8 +6992,10 @@ proc getblobdiffs {ids} {
        puts "error getting diffs: $err"
        return
     }
+    set targetline {}
     set diffinhdr 0
-    fconfigure $bdf -blocking 0
+    set diffencoding [get_path_encoding {}]
+    fconfigure $bdf -blocking 0 -encoding binary
     set blobdifffd($ids) $bdf
     filerun $bdf [list getblobdiffline $bdf $diffids]
 }
@@ -6535,20 +7015,29 @@ proc setinlist {var i val} {
 
 proc makediffhdr {fname ids} {
     global ctext curdiffstart treediffs
+    global ctext_file_names jump_to_here targetline diffline
 
     set i [lsearch -exact $treediffs($ids) $fname]
     if {$i >= 0} {
        setinlist difffilestart $i $curdiffstart
     }
+    set ctext_file_names [lreplace $ctext_file_names end end $fname]
     set l [expr {(78 - [string length $fname]) / 2}]
     set pad [string range "----------------------------------------" 1 $l]
     $ctext insert $curdiffstart "$pad $fname $pad" filesep
+    set targetline {}
+    if {$jump_to_here ne {} && [lindex $jump_to_here 0] eq $fname} {
+       set targetline [lindex $jump_to_here 1]
+    }
+    set diffline 0
 }
 
 proc getblobdiffline {bdf ids} {
     global diffids blobdifffd ctext curdiffstart
     global diffnexthead diffnextnote difffilestart
+    global ctext_file_names ctext_file_lines
     global diffinhdr treediffs
+    global diffencoding jump_to_here targetline diffline
 
     set nr 0
     $ctext conf -state normal
@@ -6564,6 +7053,8 @@ proc getblobdiffline {bdf ids} {
            # start of a new file
            $ctext insert end "\n"
            set curdiffstart [$ctext index "end - 1c"]
+           lappend ctext_file_names ""
+           lappend ctext_file_lines [lindex [split $curdiffstart "."] 0]
            $ctext insert end "\n" filesep
            # If the name hasn't changed the length will be odd,
            # the middle char will be a space, and the two bits either
@@ -6586,12 +7077,16 @@ proc getblobdiffline {bdf ids} {
            } else {
                set fname [string range $line 2 [expr {$i - 1}]]
            }
+           set fname [encoding convertfrom $fname]
+           set diffencoding [get_path_encoding $fname]
            makediffhdr $fname $ids
 
        } elseif {[regexp {^@@ -([0-9]+)(,[0-9]+)? \+([0-9]+)(,[0-9]+)? @@(.*)} \
                       $line match f1l f1c f2l f2c rest]} {
+           set line [encoding convertfrom $diffencoding $line]
            $ctext insert end "$line\n" hunksep
            set diffinhdr 0
+           set diffline $f2l
 
        } elseif {$diffinhdr} {
            if {![string compare -length 12 "rename from " $line]} {
@@ -6599,6 +7094,7 @@ proc getblobdiffline {bdf ids} {
                if {[string index $fname 0] eq "\""} {
                    set fname [lindex $fname 0]
                }
+               set fname [encoding convertfrom $fname]
                set i [lsearch -exact $treediffs($ids) $fname]
                if {$i >= 0} {
                    setinlist difffilestart $i $curdiffstart
@@ -6609,6 +7105,8 @@ proc getblobdiffline {bdf ids} {
                if {[string index $fname 0] eq "\""} {
                    set fname [lindex $fname 0]
                }
+               set fname [encoding convertfrom $fname]
+               set diffencoding [get_path_encoding $fname]
                makediffhdr $fname $ids
            } elseif {[string compare -length 3 $line "---"] == 0} {
                # do nothing
@@ -6620,7 +7118,9 @@ proc getblobdiffline {bdf ids} {
            $ctext insert end "$line\n" filesep
 
        } else {
+           set line [encoding convertfrom $diffencoding $line]
            set x [string range $line 0 0]
+           set here [$ctext index "end - 1 chars"]
            if {$x == "-" || $x == "+"} {
                set tag [expr {$x == "+"}]
                $ctext insert end "$line\n" d$tag
@@ -6631,6 +7131,14 @@ proc getblobdiffline {bdf ids} {
                # or something else we don't recognize
                $ctext insert end "$line\n" hunksep
            }
+           if {$targetline ne {} && ($x eq " " || $x eq "+")} {
+               if {$diffline == $targetline} {
+                   mark_ctext_line [lindex [split $here .] 0]
+                   set targetline {}
+               } else {
+                   incr diffline
+               }
+           }
        }
     }
     $ctext conf -state disabled
@@ -6693,6 +7201,7 @@ proc nextfile {} {
 
 proc clear_ctext {{first 1.0}} {
     global ctext smarktop smarkbot
+    global ctext_file_names ctext_file_lines
     global pendinglinks
 
     set l [lindex [split $first .] 0]
@@ -6706,6 +7215,8 @@ proc clear_ctext {{first 1.0}} {
     if {$first eq "1.0"} {
        catch {unset pendinglinks}
     }
+    set ctext_file_names {}
+    set ctext_file_lines {}
 }
 
 proc settabs {{firstab {}}} {
@@ -6868,7 +7379,7 @@ proc redisplay {} {
     setcanvscroll
     allcanvs yview moveto [lindex $span 0]
     drawvisible
-    if {[info exists selectedline]} {
+    if {$selectedline ne {}} {
        selectline $selectedline 0
        allcanvs yview moveto [lindex $span 0]
     }
@@ -6981,13 +7492,13 @@ proc gotocommit {} {
     } else {
        set id [string tolower $sha1string]
        if {[regexp {^[0-9a-f]{4,39}$} $id]} {
-           set matches [array names varcid "$curview,$id*"]
+           set matches [longid $id]
            if {$matches ne {}} {
                if {[llength $matches] > 1} {
                    error_popup [mc "Short SHA1 id %s is ambiguous" $id]
                    return
                }
-               set id [lindex [split [lindex $matches 0] ","] 1]
+               set id [lindex $matches 0]
            }
        }
     }
@@ -7189,8 +7700,7 @@ proc rowmenu {x y id} {
 
     stopfinding
     set rowmenuid $id
-    if {![info exists selectedline]
-       || [rowofcommit $id] eq $selectedline} {
+    if {$selectedline eq {} || [rowofcommit $id] eq $selectedline} {
        set state disabled
     } else {
        set state normal
@@ -7205,16 +7715,16 @@ proc rowmenu {x y id} {
     } else {
        set menu $fakerowmenu
     }
-    $menu entryconfigure [mc "Diff this -> selected"] -state $state
-    $menu entryconfigure [mc "Diff selected -> this"] -state $state
-    $menu entryconfigure [mc "Make patch"] -state $state
+    $menu entryconfigure [mca "Diff this -> selected"] -state $state
+    $menu entryconfigure [mca "Diff selected -> this"] -state $state
+    $menu entryconfigure [mca "Make patch"] -state $state
     tk_popup $menu $x $y
 }
 
 proc diffvssel {dirn} {
     global rowmenuid selectedline
 
-    if {![info exists selectedline]} return
+    if {$selectedline eq {}} return
     if {$dirn} {
        set oldid [commitonrow $selectedline]
        set newid $rowmenuid
@@ -7261,6 +7771,7 @@ proc mkpatch {} {
     set patchtop $top
     catch {destroy $top}
     toplevel $top
+    wm transient $top .
     label $top.title -text [mc "Generate patch"]
     grid $top.title - -pady 10
     label $top.from -text [mc "From:"]
@@ -7291,6 +7802,8 @@ proc mkpatch {} {
     frame $top.buts
     button $top.buts.gen -text [mc "Generate"] -command mkpatchgo
     button $top.buts.can -text [mc "Cancel"] -command mkpatchcan
+    bind $top <Key-Return> mkpatchgo
+    bind $top <Key-Escape> mkpatchcan
     grid $top.buts.gen $top.buts.can
     grid columnconfigure $top.buts 0 -weight 1 -uniform a
     grid columnconfigure $top.buts 1 -weight 1 -uniform a
@@ -7325,7 +7838,7 @@ proc mkpatchgo {} {
     set cmd [lrange $cmd 1 end]
     lappend cmd >$fname &
     if {[catch {eval exec $cmd} err]} {
-       error_popup "[mc "Error creating patch:"] $err"
+       error_popup "[mc "Error creating patch:"] $err" $patchtop
     }
     catch {destroy $patchtop}
     unset patchtop
@@ -7345,6 +7858,7 @@ proc mktag {} {
     set mktagtop $top
     catch {destroy $top}
     toplevel $top
+    wm transient $top .
     label $top.title -text [mc "Create tag"]
     grid $top.title - -pady 10
     label $top.id -text [mc "ID:"]
@@ -7362,6 +7876,8 @@ proc mktag {} {
     frame $top.buts
     button $top.buts.gen -text [mc "Create"] -command mktaggo
     button $top.buts.can -text [mc "Cancel"] -command mktagcan
+    bind $top <Key-Return> mktaggo
+    bind $top <Key-Escape> mktagcan
     grid $top.buts.gen $top.buts.can
     grid columnconfigure $top.buts 0 -weight 1 -uniform a
     grid columnconfigure $top.buts 1 -weight 1 -uniform a
@@ -7375,18 +7891,18 @@ proc domktag {} {
     set id [$mktagtop.sha1 get]
     set tag [$mktagtop.tag get]
     if {$tag == {}} {
-       error_popup [mc "No tag name specified"]
-       return
+       error_popup [mc "No tag name specified"] $mktagtop
+       return 0
     }
     if {[info exists tagids($tag)]} {
-       error_popup [mc "Tag \"%s\" already exists" $tag]
-       return
+       error_popup [mc "Tag \"%s\" already exists" $tag] $mktagtop
+       return 0
     }
     if {[catch {
        exec git tag $tag $id
     } err]} {
-       error_popup "[mc "Error creating tag:"] $err"
-       return
+       error_popup "[mc "Error creating tag:"] $err" $mktagtop
+       return 0
     }
 
     set tagids($tag) $id
@@ -7395,15 +7911,22 @@ proc domktag {} {
     addedtag $id
     dispneartags 0
     run refill_reflist
+    return 1
 }
 
 proc redrawtags {id} {
-    global canv linehtag idpos currentid curview
-    global canvxmax iddrawn
+    global canv linehtag idpos currentid curview cmitlisted
+    global canvxmax iddrawn circleitem mainheadid circlecolors
 
     if {![commitinview $id $curview]} return
     if {![info exists iddrawn($id)]} return
     set row [rowofcommit $id]
+    if {$id eq $mainheadid} {
+       set ofill yellow
+    } else {
+       set ofill [lindex $circlecolors $cmitlisted($curview,$id)]
+    }
+    $canv itemconf $circleitem($row) -fill $ofill
     $canv delete tag.$id
     set xt [eval drawtags $id $idpos($id)]
     $canv coords $linehtag($row) $xt [lindex $idpos($id) 2]
@@ -7427,7 +7950,7 @@ proc mktagcan {} {
 }
 
 proc mktaggo {} {
-    domktag
+    if {![domktag]} return
     mktagcan
 }
 
@@ -7438,6 +7961,7 @@ proc writecommit {} {
     set wrcomtop $top
     catch {destroy $top}
     toplevel $top
+    wm transient $top .
     label $top.title -text [mc "Write commit to file"]
     grid $top.title - -pady 10
     label $top.id -text [mc "ID:"]
@@ -7459,6 +7983,8 @@ proc writecommit {} {
     frame $top.buts
     button $top.buts.gen -text [mc "Write"] -command wrcomgo
     button $top.buts.can -text [mc "Cancel"] -command wrcomcan
+    bind $top <Key-Return> wrcomgo
+    bind $top <Key-Escape> wrcomcan
     grid $top.buts.gen $top.buts.can
     grid columnconfigure $top.buts 0 -weight 1 -uniform a
     grid columnconfigure $top.buts 1 -weight 1 -uniform a
@@ -7473,7 +7999,7 @@ proc wrcomgo {} {
     set cmd "echo $id | [$wrcomtop.cmd get]"
     set fname [$wrcomtop.fname get]
     if {[catch {exec sh -c $cmd >$fname &} err]} {
-       error_popup "[mc "Error writing commit:"] $err"
+       error_popup "[mc "Error writing commit:"] $err" $wrcomtop
     }
     catch {destroy $wrcomtop}
     unset wrcomtop
@@ -7492,6 +8018,7 @@ proc mkbranch {} {
     set top .makebranch
     catch {destroy $top}
     toplevel $top
+    wm transient $top .
     label $top.title -text [mc "Create new branch"]
     grid $top.title - -pady 10
     label $top.id -text [mc "ID:"]
@@ -7501,10 +8028,13 @@ proc mkbranch {} {
     grid $top.id $top.sha1 -sticky w
     label $top.nlab -text [mc "Name:"]
     entry $top.name -width 40
+    bind $top.name <Key-Return> "[list mkbrgo $top]"
     grid $top.nlab $top.name -sticky w
     frame $top.buts
     button $top.buts.go -text [mc "Create"] -command [list mkbrgo $top]
     button $top.buts.can -text [mc "Cancel"] -command "catch {destroy $top}"
+    bind $top <Key-Return> [list mkbrgo $top]
+    bind $top <Key-Escape> "catch {destroy $top}"
     grid $top.buts.go $top.buts.can
     grid columnconfigure $top.buts 0 -weight 1 -uniform a
     grid columnconfigure $top.buts 1 -weight 1 -uniform a
@@ -7517,29 +8047,73 @@ proc mkbrgo {top} {
 
     set name [$top.name get]
     set id [$top.sha1 get]
+    set cmdargs {}
+    set old_id {}
     if {$name eq {}} {
-       error_popup [mc "Please specify a name for the new branch"]
+       error_popup [mc "Please specify a name for the new branch"] $top
        return
     }
+    if {[info exists headids($name)]} {
+       if {![confirm_popup [mc \
+               "Branch '%s' already exists. Overwrite?" $name] $top]} {
+           return
+       }
+       set old_id $headids($name)
+       lappend cmdargs -f
+    }
     catch {destroy $top}
+    lappend cmdargs $name $id
     nowbusy newbranch
     update
     if {[catch {
-       exec git branch $name $id
+       eval exec git branch $cmdargs
     } err]} {
        notbusy newbranch
        error_popup $err
     } else {
-       set headids($name) $id
-       lappend idheads($id) $name
-       addedhead $id $name
        notbusy newbranch
-       redrawtags $id
+       if {$old_id ne {}} {
+           movehead $id $name
+           movedhead $id $name
+           redrawtags $old_id
+           redrawtags $id
+       } else {
+           set headids($name) $id
+           lappend idheads($id) $name
+           addedhead $id $name
+           redrawtags $id
+       }
        dispneartags 0
        run refill_reflist
     }
 }
 
+proc exec_citool {tool_args {baseid {}}} {
+    global commitinfo env
+
+    set save_env [array get env GIT_AUTHOR_*]
+
+    if {$baseid ne {}} {
+       if {![info exists commitinfo($baseid)]} {
+           getcommit $baseid
+       }
+       set author [lindex $commitinfo($baseid) 1]
+       set date [lindex $commitinfo($baseid) 2]
+       if {[regexp {^\s*(\S.*\S|\S)\s*<(.*)>\s*$} \
+                   $author author name email]
+           && $date ne {}} {
+           set env(GIT_AUTHOR_NAME) $name
+           set env(GIT_AUTHOR_EMAIL) $email
+           set env(GIT_AUTHOR_DATE) $date
+       }
+    }
+
+    eval exec git citool $tool_args &
+
+    array unset env GIT_AUTHOR_*
+    array set env $save_env
+}
+
 proc cherrypick {} {
     global rowmenuid curview
     global mainhead mainheadid
@@ -7558,7 +8132,26 @@ proc cherrypick {} {
     # no error occurs, and exec takes that as an indication of error...
     if {[catch {exec sh -c "git cherry-pick -r $rowmenuid 2>&1"} err]} {
        notbusy cherrypick
-       error_popup $err
+       if {[regexp -line \
+                {Entry '(.*)' (would be overwritten by merge|not uptodate)} \
+                $err msg fname]} {
+           error_popup [mc "Cherry-pick failed because of local changes\
+                       to file '%s'.\nPlease commit, reset or stash\
+                       your changes and try again." $fname]
+       } elseif {[regexp -line \
+                      {^(CONFLICT \(.*\):|Automatic cherry-pick failed)} \
+                      $err]} {
+           if {[confirm_popup [mc "Cherry-pick failed because of merge\
+                       conflict.\nDo you wish to run git citool to\
+                       resolve it?"]]} {
+               # Force citool to read MERGE_MSG
+               file delete [file join [gitdir] "GITGUI_MSG"]
+               exec_citool {} $rowmenuid
+           }
+       } else {
+           error_popup $err
+       }
+       run updatecommits
        return
     }
     set newhead [exec git rev-parse HEAD]
@@ -7573,8 +8166,8 @@ proc cherrypick {} {
        if {$mainhead ne {}} {
            movehead $newhead $mainhead
            movedhead $newhead $mainhead
-           set mainheadid $newhead
        }
+       set mainheadid $newhead
        redrawtags $oldhead
        redrawtags $newhead
        selbyid $newhead
@@ -7611,6 +8204,7 @@ proc resethead {} {
     button $w.ok -text [mc OK] -command "set confirm_ok 1; destroy $w"
     pack $w.ok -side left -fill x -padx 20 -pady 20
     button $w.cancel -text [mc Cancel] -command "destroy $w"
+    bind $w <Key-Escape> [list destroy $w]
     pack $w.cancel -side right -fill x -padx 20 -pady 20
     bind $w <Visibility> "grab $w; focus $w"
     tkwait window $w
@@ -7674,7 +8268,7 @@ proc headmenu {x y id head} {
 }
 
 proc cobranch {} {
-    global headmenuid headmenuhead mainhead headids
+    global headmenuid headmenuhead headids
     global showlocalchanges mainheadid
 
     # check the tree is clean first??
@@ -7710,12 +8304,10 @@ proc readcheckoutstat {fd newhead newheadid} {
     if {[catch {close $fd} err]} {
        error_popup $err
     }
-    set oldmainhead $mainhead
+    set oldmainid $mainheadid
     set mainhead $newhead
     set mainheadid $newheadid
-    if {[info exists headids($oldmainhead)]} {
-       redrawtags $headids($oldmainhead)
-    }
+    redrawtags $oldmainid
     redrawtags $newheadid
     selbyid $newheadid
     if {$showlocalchanges} {
@@ -7769,6 +8361,7 @@ proc showrefs {} {
     }
     toplevel $top
     wm title $top [mc "Tags and heads: %s" [file tail [pwd]]]
+    wm transient $top .
     text $top.list -background $bgcolor -foreground $fgcolor \
        -selectbackground $selectbgcolor -font mainfont \
        -xscrollcommand "$top.xsb set" -yscrollcommand "$top.ysb set" \
@@ -7790,6 +8383,7 @@ proc showrefs {} {
     pack $top.f.l -side left
     grid $top.f - -sticky ew -pady 2
     button $top.close -command [list destroy $top] -text [mc "Close"]
+    bind $top <Key-Escape> [list destroy $top]
     grid $top.close -
     grid columnconfigure $top 0 -weight 1
     grid rowconfigure $top 0 -weight 1
@@ -7831,7 +8425,7 @@ proc reflistfilter_change {n1 n2 op} {
 
 proc refill_reflist {} {
     global reflist reflistfilter showrefstop headids tagids otherrefids
-    global curview commitinterest
+    global curview
 
     if {![info exists showrefstop] || ![winfo exists $showrefstop]} return
     set refs {}
@@ -7840,7 +8434,7 @@ proc refill_reflist {} {
            if {[commitinview $headids($n) $curview]} {
                lappend refs [list $n H]
            } else {
-               set commitinterest($headids($n)) {run refill_reflist}
+               interestedin $headids($n) {run refill_reflist}
            }
        }
     }
@@ -7849,7 +8443,7 @@ proc refill_reflist {} {
            if {[commitinview $tagids($n) $curview]} {
                lappend refs [list $n T]
            } else {
-               set commitinterest($tagids($n)) {run refill_reflist}
+               interestedin $tagids($n) {run refill_reflist}
            }
        }
     }
@@ -7858,7 +8452,7 @@ proc refill_reflist {} {
            if {[commitinview $otherrefids($n) $curview]} {
                lappend refs [list $n o]
            } else {
-               set commitinterest($otherrefids($n)) {run refill_reflist}
+               interestedin $otherrefids($n) {run refill_reflist}
            }
        }
     }
@@ -9015,12 +9609,14 @@ proc rereadrefs {} {
                        [array names idheads] [array names idotherrefs]]]
     foreach id $refids {
        set v [listrefs $id]
-       if {![info exists ref($id)] || $ref($id) != $v ||
-           ($id eq $oldmainhead && $id ne $mainheadid) ||
-           ($id eq $mainheadid && $id ne $oldmainhead)} {
+       if {![info exists ref($id)] || $ref($id) != $v} {
            redrawtags $id
        }
     }
+    if {$oldmainhead ne $mainheadid} {
+       redrawtags $oldmainhead
+       redrawtags $mainheadid
+    }
     run refill_reflist
 }
 
@@ -9093,6 +9689,7 @@ proc mkfontdisp {font top which} {
 
 proc choosefont {font which} {
     global fontparam fontlist fonttop fontattr
+    global prefstop
 
     set fontparam(which) $which
     set fontparam(font) $font
@@ -9106,6 +9703,7 @@ proc choosefont {font which} {
        font create sample
        eval font config sample [font actual $font]
        toplevel $top
+       wm transient $top $prefstop
        wm title $top [mc "Gitk font chooser"]
        label $top.l -textvariable fontparam(which)
        pack $top.l -side top
@@ -9139,6 +9737,8 @@ proc choosefont {font which} {
        frame $top.buts
        button $top.buts.ok -text [mc "OK"] -command fontok -default active
        button $top.buts.can -text [mc "Cancel"] -command fontcan -default normal
+       bind $top <Key-Return> fontok
+       bind $top <Key-Escape> fontcan
        grid $top.buts.ok $top.buts.can
        grid columnconfigure $top.buts 0 -weight 1 -uniform a
        grid columnconfigure $top.buts 1 -weight 1 -uniform a
@@ -9205,8 +9805,8 @@ proc chg_fontparam {v sub op} {
 proc doprefs {} {
     global maxwidth maxgraphpct
     global oldprefs prefstop showneartags showlocalchanges
-    global bgcolor fgcolor ctext diffcolors selectbgcolor
-    global tabstop limitdiffs autoselect extdifftool
+    global bgcolor fgcolor ctext diffcolors selectbgcolor markbgcolor
+    global tabstop limitdiffs autoselect extdifftool perfile_attrs
 
     set top .gitkprefs
     set prefstop $top
@@ -9215,11 +9815,12 @@ proc doprefs {} {
        return
     }
     foreach v {maxwidth maxgraphpct showneartags showlocalchanges \
-                  limitdiffs tabstop} {
+                  limitdiffs tabstop perfile_attrs} {
        set oldprefs($v) [set $v]
     }
     toplevel $top
     wm title $top [mc "Gitk preferences"]
+    wm transient $top .
     label $top.ldisp -text [mc "Commit list display options"]
     grid $top.ldisp - -sticky w -pady 10
     label $top.spacer -text " "
@@ -9257,6 +9858,11 @@ proc doprefs {} {
     checkbutton $top.ldiff.b -variable limitdiffs
     pack $top.ldiff.b $top.ldiff.l -side left
     grid x $top.ldiff -sticky w
+    frame $top.lattr
+    label $top.lattr.l -text [mc "Support per-file encodings"] -font optionfont
+    checkbutton $top.lattr.b -variable perfile_attrs
+    pack $top.lattr.b $top.lattr.l -side left
+    grid x $top.lattr -sticky w
 
     entry $top.extdifft -textvariable extdifftool
     frame $top.extdifff
@@ -9293,6 +9899,12 @@ proc doprefs {} {
                      "diff hunk header" \
                      [list $ctext tag conf hunksep -foreground]]
     grid x $top.hunksepbut $top.hunksep -sticky w
+    label $top.markbgsep -padx 40 -relief sunk -background $markbgcolor
+    button $top.markbgbut -text [mc "Marked line bg"] -font optionfont \
+       -command [list choosecolor markbgcolor {} $top.markbgsep \
+                     [mc "marked line background"] \
+                     [list $ctext tag conf omark -background]]
+    grid x $top.markbgbut $top.markbgsep -sticky w
     label $top.selbgsep -padx 40 -relief sunk -background $selectbgcolor
     button $top.selbgbut -text [mc "Select bg"] -font optionfont \
        -command [list choosecolor selectbgcolor {} $top.selbgsep background setselbg]
@@ -9307,6 +9919,8 @@ proc doprefs {} {
     frame $top.buts
     button $top.buts.ok -text [mc "OK"] -command prefsok -default active
     button $top.buts.can -text [mc "Cancel"] -command prefscan -default normal
+    bind $top <Key-Return> prefsok
+    bind $top <Key-Escape> prefscan
     grid $top.buts.ok $top.buts.can
     grid columnconfigure $top.buts 0 -weight 1 -uniform a
     grid columnconfigure $top.buts 1 -weight 1 -uniform a
@@ -9366,7 +9980,7 @@ proc prefscan {} {
     global oldprefs prefstop
 
     foreach v {maxwidth maxgraphpct showneartags showlocalchanges \
-                  limitdiffs tabstop} {
+                  limitdiffs tabstop perfile_attrs} {
        global $v
        set $v $oldprefs($v)
     }
@@ -9379,7 +9993,7 @@ proc prefsok {} {
     global maxwidth maxgraphpct
     global oldprefs prefstop showneartags showlocalchanges
     global fontpref mainfont textfont uifont
-    global limitdiffs treediffs
+    global limitdiffs treediffs perfile_attrs
 
     catch {destroy $prefstop}
     unset prefstop
@@ -9412,8 +10026,10 @@ proc prefsok {} {
            dohidelocalchanges
        }
     }
-    if {$limitdiffs != $oldprefs(limitdiffs)} {
-       # treediffs elements are limited by path
+    if {$limitdiffs != $oldprefs(limitdiffs) ||
+       ($perfile_attrs && !$oldprefs(perfile_attrs))} {
+       # treediffs elements are limited by path;
+       # won't have encodings cached if perfile_attrs was just turned on
        catch {unset treediffs}
     }
     if {$fontchanged || $maxwidth != $oldprefs(maxwidth)
@@ -9637,7 +10253,7 @@ set encoding_aliases {
     { ISO-8859-16 iso-ir-226 ISO_8859-16:2001 ISO_8859-16 latin10 l10 }
     { GBK CP936 MS936 windows-936 }
     { JIS_Encoding csJISEncoding }
-    { Shift_JIS MS_Kanji csShiftJIS }
+    { Shift_JIS MS_Kanji csShiftJIS ShiftJIS Shift-JIS }
     { Extended_UNIX_Code_Packed_Format_for_Japanese csEUCPkdFmtJapanese
       EUC-JP }
     { Extended_UNIX_Code_Fixed_Width_for_Japanese csEUCFixWidJapanese }
@@ -9672,14 +10288,17 @@ set encoding_aliases {
 }
 
 proc tcl_encoding {enc} {
-    global encoding_aliases
+    global encoding_aliases tcl_encoding_cache
+    if {[info exists tcl_encoding_cache($enc)]} {
+       return $tcl_encoding_cache($enc)
+    }
     set names [encoding names]
     set lcnames [string tolower $names]
     set enc [string tolower $enc]
     set i [lsearch -exact $lcnames $enc]
     if {$i < 0} {
        # look for "isonnn" instead of "iso-nnn" or "iso_nnn"
-       if {[regsub {^iso[-_]} $enc iso encx]} {
+       if {[regsub {^(iso|cp|ibm|jis)[-_]} $enc {\1} encx]} {
            set i [lsearch -exact $lcnames $encx]
        }
     }
@@ -9691,7 +10310,7 @@ proc tcl_encoding {enc} {
            foreach e $ll {
                set i [lsearch -exact $lcnames $e]
                if {$i < 0} {
-                   if {[regsub {^iso[-_]} $e iso ex]} {
+                   if {[regsub {^(iso|cp|ibm|jis)[-_]} $e {\1} ex]} {
                        set i [lsearch -exact $lcnames $ex]
                    }
                }
@@ -9700,10 +10319,70 @@ proc tcl_encoding {enc} {
            break
        }
     }
+    set tclenc {}
     if {$i >= 0} {
-       return [lindex $names $i]
+       set tclenc [lindex $names $i]
     }
-    return {}
+    set tcl_encoding_cache($enc) $tclenc
+    return $tclenc
+}
+
+proc gitattr {path attr default} {
+    global path_attr_cache
+    if {[info exists path_attr_cache($attr,$path)]} {
+       set r $path_attr_cache($attr,$path)
+    } else {
+       set r "unspecified"
+       if {![catch {set line [exec git check-attr $attr -- $path]}]} {
+           regexp "(.*): encoding: (.*)" $line m f r
+       }
+       set path_attr_cache($attr,$path) $r
+    }
+    if {$r eq "unspecified"} {
+       return $default
+    }
+    return $r
+}
+
+proc cache_gitattr {attr pathlist} {
+    global path_attr_cache
+    set newlist {}
+    foreach path $pathlist {
+       if {![info exists path_attr_cache($attr,$path)]} {
+           lappend newlist $path
+       }
+    }
+    set lim 1000
+    if {[tk windowingsystem] == "win32"} {
+       # windows has a 32k limit on the arguments to a command...
+       set lim 30
+    }
+    while {$newlist ne {}} {
+       set head [lrange $newlist 0 [expr {$lim - 1}]]
+       set newlist [lrange $newlist $lim end]
+       if {![catch {set rlist [eval exec git check-attr $attr -- $head]}]} {
+           foreach row [split $rlist "\n"] {
+               if {[regexp "(.*): encoding: (.*)" $row m path value]} {
+                   if {[string index $path 0] eq "\""} {
+                       set path [encoding convertfrom [lindex $path 0]]
+                   }
+                   set path_attr_cache($attr,$path) $value
+               }
+           }
+       }
+    }
+}
+
+proc get_path_encoding {path} {
+    global gui_encoding perfile_attrs
+    set tcl_enc $gui_encoding
+    if {$path ne {} && $perfile_attrs} {
+       set enc2 [tcl_encoding [gitattr $path encoding $tcl_enc]]
+       if {$enc2 ne {}} {
+           set tcl_enc $enc2
+       }
+    }
+    return $tcl_enc
 }
 
 # First check that Tcl/Tk is recent enough
@@ -9728,6 +10407,19 @@ if {$tclencoding == {}} {
     puts stderr "Warning: encoding $gitencoding is not supported by Tcl/Tk"
 }
 
+set gui_encoding [encoding system]
+catch {
+    set enc [exec git config --get gui.encoding]
+    if {$enc ne {}} {
+       set tclenc [tcl_encoding $enc]
+       if {$tclenc ne {}} {
+           set gui_encoding $tclenc
+       } else {
+           puts stderr "Warning: encoding $enc is not supported by Tcl/Tk"
+       }
+    }
+}
+
 set mainfont {Helvetica 9}
 set textfont {Courier 9}
 set uifont {Helvetica 9 bold}
@@ -9749,6 +10441,7 @@ set showlocalchanges 1
 set limitdiffs 1
 set datetimeformat "%Y-%m-%d %H:%M:%S"
 set autoselect 1
+set perfile_attrs 0
 
 set extdifftool "meld"
 
@@ -9759,6 +10452,16 @@ set diffcolors {red "#00a000" blue}
 set diffcontext 3
 set ignorespace 0
 set selectbgcolor gray85
+set markbgcolor "#e0e0ff"
+
+set circlecolors {white blue gray blue blue}
+
+# button for popping up context menus
+if {[tk windowingsystem] eq "aqua"} {
+    set ctxbut <Button-2>
+} else {
+    set ctxbut <Button-3>
+}
 
 ## For msgcat loading, first locate the installation location.
 if { [info exists ::env(GITK_MSGSDIR)] } {
@@ -9806,6 +10509,9 @@ if {![file isdirectory $gitdir]} {
     exit 1
 }
 
+set selecthead {}
+set selectheadid {}
+
 set revtreeargs {}
 set cmdline_files {}
 set i 0
@@ -9817,6 +10523,9 @@ foreach arg $argv {
            set cmdline_files [lrange $argv [expr {$i + 1}] end]
            break
        }
+       "--select-commit=*" {
+           set selecthead [string range $arg 16 end]
+       }
        "--argscmd=*" {
            set revtreeargscmd [string range $arg 10 end]
        }
@@ -9827,6 +10536,10 @@ foreach arg $argv {
     incr i
 }
 
+if {$selecthead eq "HEAD"} {
+    set selecthead {}
+}
+
 if {$i >= [llength $argv] && $revtreeargs ne {}} {
     # no -- on command line, but some arguments (other than --argscmd)
     if {[catch {
@@ -9890,6 +10603,7 @@ set viewperm(0) 0
 set viewargs(0) {}
 set viewargscmd(0) {}
 
+set selectedline {}
 set numcommits 0
 set loginstance 0
 set cmdlineok 0
@@ -9917,8 +10631,8 @@ if {$cmdline_files ne {} || $revtreeargs ne {} || $revtreeargscmd ne {}} {
     set viewperm(1) 0
     set vdatemode(1) 0
     addviewmenu 1
-    .bar.view entryconf [mc "Edit view..."] -state normal
-    .bar.view entryconf [mc "Delete view"] -state normal
+    .bar.view entryconf [mca "Edit view..."] -state normal
+    .bar.view entryconf [mca "Delete view"] -state normal
 }
 
 if {[info exists permviews]} {
@@ -9933,4 +10647,4 @@ if {[info exists permviews]} {
        addviewmenu $n
     }
 }
-getcommits
+getcommits {}