{"thread":{"id":"36991","subject":"[PATCH 0/4 v2] gitk: show latest change to region","startedAt":"2014-06-24T18:15:14Z","lastAt":"2014-06-24T18:21:07Z","messageCount":5,"participants":["Max Kirillov"],"isPatch":true,"patchVersion":2,"patchTotal":4},"messages":[{"id":"244936","messageId":"20140624181514.GA23051@wheezy.local","threadId":"36991","inReplyTo":null,"subject":"[PATCH 0/4 v2] gitk: show latest change to region","fromName":"Max Kirillov","fromEmail":"max@max630.net","sentAt":"2014-06-24T18:15:14Z","receivedAt":"2014-06-24T18:15:14Z","isPatch":true,"sender":{"key":"max@max630.net","avatar":"https://avatars.githubusercontent.com/u/381560?v=4"},"body":"Changes vs v1:\n* Rebase to latest gitk master\n* Fix typos in commments\n* Switch to patch mode at showing the found change\n\nMax Kirillov (4):\n      gitk: use single blamestuff for all show_line_source{} calls\n      gitk: refactor: separate generic hunk parsing out of find_hunk_blamespecs{}\n      gitk: refactor: separate io from logic in the searching origin of line\n      gitk: show latest change to region\n\n gitk | 265 +++++++++++++++++++++++++++++++++++++++++++++++++++++++------------\n 1 file changed, 220 insertions(+), 45 deletions(-)\n"},{"id":"244937","messageId":"20140624181738.GA23114@wheezy.local","threadId":"36991","inReplyTo":"20140624181514.GA23051@wheezy.local","subject":"[PATCH v2 1/4] gitk: use single blamestuff for all show_line_source{} calls","fromName":"Max Kirillov","fromEmail":"max@max630.net","sentAt":"2014-06-24T18:18:04Z","receivedAt":"2014-06-24T18:18:04Z","isPatch":true,"sender":{"key":"max@max630.net","avatar":"https://avatars.githubusercontent.com/u/381560?v=4"},"body":"There seems to be no point to search for several origins at once.\nProbably is is not even fully working (because there is one blameinst),\nbut blamestuff for some reason is an array. Also, it is not cleaned\nafter blame is completed.\n\nSigned-off-by: Max Kirillov <max@max630.net>\n---\n gitk | 13 ++++++++-----\n 1 file changed, 8 insertions(+), 5 deletions(-)\n\ndiff --git a/gitk b/gitk\nindex 41e5071..8ef6aa8 100755\n--- a/gitk\n+++ b/gitk\n@@ -3824,17 +3824,18 @@ proc show_line_source {} {\n     nowbusy blaming [mc \"Searching\"]\n     fconfigure $f -blocking 0\n     set i [reg_instance $f]\n-    set blamestuff($i) {}\n+    set blamestuff {}\n     set blameinst $i\n     filerun $f [list read_line_source $f $i]\n }\n \n proc stopblaming {} {\n-    global blameinst\n+    global blameinst blamestuff\n \n     if {[info exists blameinst]} {\n \tstop_instance $blameinst\n \tunset blameinst\n+\tunset blamestuff\n \tnotbusy blaming\n     }\n }\n@@ -3843,7 +3844,7 @@ proc read_line_source {fd inst} {\n     global blamestuff curview commfd blameinst nullid nullid2\n \n     while {[gets $fd line] >= 0} {\n-\tlappend blamestuff($inst) $line\n+\tlappend blamestuff $line\n     }\n     if {![eof $fd]} {\n \treturn 1\n@@ -3854,17 +3855,18 @@ proc read_line_source {fd inst} {\n     fconfigure $fd -blocking 1\n     if {[catch {close $fd} err]} {\n \terror_popup [mc \"Error running git blame: %s\" $err]\n+\tunset blamestuff\n \treturn 0\n     }\n \n     set fname {}\n-    set line [split [lindex $blamestuff($inst) 0] \" \"]\n+    set line [split [lindex $blamestuff 0] \" \"]\n     set id [lindex $line 0]\n     set lnum [lindex $line 1]\n     if {[string length $id] == 40 && [string is xdigit $id] &&\n \t[string is digit -strict $lnum]} {\n \t# look for \"filename\" line\n-\tforeach l $blamestuff($inst) {\n+\tforeach l $blamestuff {\n \t    if {[string match \"filename *\" $l]} {\n \t\tset fname [string range $l 9 end]\n \t\tbreak\n@@ -3887,6 +3889,7 @@ proc read_line_source {fd inst} {\n     } else {\n \tputs \"oops couldn't parse git blame output\"\n     }\n+    unset blamestuff\n     return 0\n }\n \n-- \n2.0.0.526.g5318336\n"},{"id":"244938","messageId":"20140624181922.GB23114@wheezy.local","threadId":"36991","inReplyTo":"20140624181514.GA23051@wheezy.local","subject":"[PATCH v2 2/4] gitk: refactor: separate generic hunk parsing out of find_hunk_blamespecs{}","fromName":"Max Kirillov","fromEmail":"max@max630.net","sentAt":"2014-06-24T18:19:23Z","receivedAt":"2014-06-24T18:19:23Z","isPatch":true,"sender":{"key":"max@max630.net","avatar":"https://avatars.githubusercontent.com/u/381560?v=4"},"body":"For requesting a region blame, it is necessary to parse a hunk and\nfind the region in the parent file corresponding to the selected region.\nThere is already hunk parsing functionality in find_hunk_blamespec{},\nbut it returns only information for a single line.\n\nThe new function, resolve_hunk_lines{}, scans the hunk once and returns\nfor all hunk lines between $start_diffline and $end_diffline, in which parent\neach of them exists and which is its number there.\n\nSigned-off-by: Max Kirillov <max@max630.net>\n---\n gitk | 93 ++++++++++++++++++++++++++++++++++++++++++--------------------------\n 1 file changed, 57 insertions(+), 36 deletions(-)\n\ndiff --git a/gitk b/gitk\nindex 8ef6aa8..7fa7015 100755\n--- a/gitk\n+++ b/gitk\n@@ -3599,11 +3599,11 @@ proc external_diff {} {\n     }\n }\n \n-proc find_hunk_blamespec {base line} {\n+proc resolve_hunk_lines {base start_diffline end_diffline} {\n     global ctext\n \n     # Find and parse the hunk header\n-    set s_lix [$ctext search -backwards -regexp ^@@ \"$line.0 lineend\" $base.0]\n+    set s_lix [$ctext search -backwards -regexp ^@@ \"$start_diffline.0 lineend\" $base.0]\n     if {$s_lix eq {}} return\n \n     set s_line [$ctext get $s_lix \"$s_lix + 1 lines\"]\n@@ -3623,49 +3623,70 @@ proc find_hunk_blamespec {base line} {\n     }\n \n     # Now scan the lines to determine offset within the hunk\n-    set max_parent [expr {[llength $base_lines]-2}]\n-    set dline 0\n+    set max_parent [expr {[llength $base_lines]-1}]\n     set s_lno [lindex [split $s_lix \".\"] 0]\n \n-    # Determine if the line is removed\n-    set chunk [$ctext get $line.0 \"$line.1 + $max_parent chars\"]\n-    if {[string match {[-+ ]*} $chunk]} {\n-\tset removed_idx [string first \"-\" $chunk]\n-\t# Choose a parent index\n-\tif {$removed_idx >= 0} {\n-\t    set parent $removed_idx\n+    set commitlines_by_diffline {}\n+    array unset commit_lines\n+    for {set p 0} {$p <= $max_parent} {incr p} {\n+\tset commit_lines($p) [expr [lindex $base_lines $p] - 1]\n+    }\n+    for {set diffline [expr $s_lno + 1]} {$diffline <= $end_diffline} {incr diffline} {\n+\tset chunk [$ctext get $diffline.0 \"$diffline.0 + $max_parent chars\"]\n+\tif {$chunk eq {} || [string match \"\\[\\n@\\]*\" $chunk]} {\n+\t    # region is larger than hunk\n+\t    return {}\n+\t}\n+\tset is_removed [expr [string first \"-\" $chunk] >= 0]\n+\tif {!$is_removed} {\n+\t    incr commit_lines(0)\n+\t    set commitlines [list [list 0 $commit_lines(0)]]\n \t} else {\n-\t    set unchanged_idx [string first \" \" $chunk]\n-\t    if {$unchanged_idx >= 0} {\n-\t\tset parent $unchanged_idx\n-\t    } else {\n-\t\t# blame the current commit\n-\t\tset parent -1\n-\t    }\n-\t}\n-\t# then count other lines that belong to it\n-\tfor {set i $line} {[incr i -1] > $s_lno} {} {\n-\t    set chunk [$ctext get $i.0 \"$i.1 + $max_parent chars\"]\n-\t    # Determine if the line is removed\n-\t    set removed_idx [string first \"-\" $chunk]\n-\t    if {$parent >= 0} {\n-\t\tset code [string index $chunk $parent]\n-\t\tif {$code eq \"-\" || ($removed_idx < 0 && $code ne \"+\")} {\n-\t\t    incr dline\n+\t    set commitlines {}\n+\t}\n+\tfor {set p 1} {$p <= $max_parent} {incr p} {\n+\t    switch -- [string index $chunk \"$p-1\"] {\n+\t\t\"+\" {\n \t\t}\n-\t    } else {\n-\t\tif {$removed_idx < 0} {\n-\t\t    incr dline\n+\t\t\"-\" {\n+\t\t    incr commit_lines($p)\n+\t\t    lappend commitlines [list $p $commit_lines($p)]\n+\t\t}\n+\t\t\" \" {\n+\t\t    if {!$is_removed} {\n+\t\t\tincr commit_lines($p)\n+\t\t\tlappend commitlines [list $p $commit_lines($p)]\n+\t\t    }\n+\t\t}\n+\t\tdefault {\n+\t\t    error_popup \"resolve_hunk_lines: unexpected diff line($diffline): $chunk\"\n+\t\t    break\n \t\t}\n \t    }\n \t}\n-\tincr parent\n-    } else {\n-\tset parent 0\n+\tif {$diffline >= $start_diffline} {\n+\t    lappend commitlines_by_diffline [list $diffline $commitlines]\n+\t}\n     }\n+    return $commitlines_by_diffline\n+}\n \n-    incr dline [lindex $base_lines $parent]\n-    return [list $parent $dline]\n+proc find_hunk_blamespec {base line} {\n+    foreach cl_spec [resolve_hunk_lines $base $line $line] {\n+\tif {[lindex $cl_spec 0] == $line} {\n+\t    set commitlines [lindex $cl_spec 1]\n+\t    if {[llength $commitlines] > 0} {\n+\t\tif {[llength $commitlines] > 1 && [lindex $commitlines 0 0] eq 0} {\n+\t\t    return [lindex $commitlines 1]\n+\t\t} else {\n+\t\t    return [lindex $commitlines 0]\n+\t\t}\n+\t    } else {\n+\t\terror_popup \"find_hunk_blamespec: invalid commitlines: $commitlines\"\n+\t    }\n+\t}\n+    }\n+    return {}\n }\n \n proc external_blame_diff {} {\n-- \n2.0.0.526.g5318336\n"},{"id":"244939","messageId":"20140624182013.GC23114@wheezy.local","threadId":"36991","inReplyTo":"20140624181514.GA23051@wheezy.local","subject":"[PATCH v2 3/4] gitk: refactor: separate io from logic in the searching origin of line","fromName":"Max Kirillov","fromEmail":"max@max630.net","sentAt":"2014-06-24T18:20:13Z","receivedAt":"2014-06-24T18:20:13Z","isPatch":true,"sender":{"key":"max@max630.net","avatar":"https://avatars.githubusercontent.com/u/381560?v=4"},"body":"The pattern of maintaining blame command and collecting output\ncan be reused for searching of latest change to region.\nIt still can use the blame's global variables, because the two\nsearch commands should not run concurrently as well as two instances\nof blame.\n\nSigned-off-by: Max Kirillov <max@max630.net>\n---\n gitk | 23 ++++++++++++++++++-----\n 1 file changed, 18 insertions(+), 5 deletions(-)\n\ndiff --git a/gitk b/gitk\nindex 7fa7015..9c8dca8 100755\n--- a/gitk\n+++ b/gitk\n@@ -3780,7 +3780,7 @@ proc external_blame {parent_idx {line {}}} {\n }\n \n proc show_line_source {} {\n-    global cmitmode currentid parents curview blamestuff blameinst\n+    global cmitmode currentid parents curview\n     global diff_menu_line diff_menu_filebase flist_menu_file\n     global nullid nullid2 gitdir cdup\n \n@@ -3836,6 +3836,12 @@ proc show_line_source {} {\n \tlappend blameargs $id\n     }\n     lappend blameargs -- [file join $cdup $flist_menu_file]\n+    startblaming $blameargs read_line_source\n+}\n+\n+proc startblaming {blameargs blamecommand} {\n+    global blamestuff blameinst\n+\n     if {[catch {\n \tset f [open $blameargs r]\n     } err]} {\n@@ -3847,7 +3853,7 @@ proc show_line_source {} {\n     set i [reg_instance $f]\n     set blamestuff {}\n     set blameinst $i\n-    filerun $f [list read_line_source $f $i]\n+    filerun $f [list blameiocallback $f $i $blamecommand]\n }\n \n proc stopblaming {} {\n@@ -3861,8 +3867,8 @@ proc stopblaming {} {\n     }\n }\n \n-proc read_line_source {fd inst} {\n-    global blamestuff curview commfd blameinst nullid nullid2\n+proc blameiocallback {fd inst blamecommand} {\n+    global blamestuff blameinst commfd\n \n     while {[gets $fd line] >= 0} {\n \tlappend blamestuff $line\n@@ -3880,6 +3886,14 @@ proc read_line_source {fd inst} {\n \treturn 0\n     }\n \n+    $blamecommand\n+    unset blamestuff\n+    return 0\n+}\n+\n+proc read_line_source {} {\n+    global blamestuff curview nullid nullid2\n+\n     set fname {}\n     set line [split [lindex $blamestuff 0] \" \"]\n     set id [lindex $line 0]\n@@ -3910,7 +3924,6 @@ proc read_line_source {fd inst} {\n     } else {\n \tputs \"oops couldn't parse git blame output\"\n     }\n-    unset blamestuff\n     return 0\n }\n \n-- \n2.0.0.526.g5318336\n"},{"id":"244941","messageId":"20140624182107.GD23114@wheezy.local","threadId":"36991","inReplyTo":"20140624181514.GA23051@wheezy.local","subject":"[PATCH v2 4/4] gitk: show latest change to region","fromName":"Max Kirillov","fromEmail":"max@max630.net","sentAt":"2014-06-24T18:21:07Z","receivedAt":"2014-06-24T18:21:07Z","isPatch":true,"sender":{"key":"max@max630.net","avatar":"https://avatars.githubusercontent.com/u/381560?v=4"},"body":"Add a new command to the diffmenu, \"Show the latest change of selected\nregion\".  The menu command picks selection, and if it exists and covers\na single hunk, locates the latest change which has been made to the\nselected lines in the file.\n\nThe menu command is disabled if the region blame is impossible, for\nexample if nothing is selected or the selection does not lie fully in a\nsingle diff hunk.\n\nThe search is implemented by use of \"git log -L...\" command. Unlike git\nblame, it can locate merges which brings together changes to the region\nfrom different branches. This is what is desired, actually.\n\nUnfortunally, using git log -L for finding a single line origin is\nsuboptimal, because (a) it does not support the \"--contents\" commandline\nargument, or any other way to blame uncommitted changes, and (b) it is\nnoticeably slower. Hopely in some future git log -L will be mature\nenough to be used for picking the single line origin, for now the best\noption is to implement region logic separately, reusing the blame's basic io.\n\nFor diffs, the first parent is always searched. This decision is quite\nvoluntary, just to avoid complications to UI.\n\nSigned-off-by: Max Kirillov <max@max630.net>\n---\n gitk | 138 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n 1 file changed, 138 insertions(+)\n\ndiff --git a/gitk b/gitk\nindex 9c8dca8..4b0dc06 100755\n--- a/gitk\n+++ b/gitk\n@@ -2651,6 +2651,7 @@ proc makewindow {} {\n     makemenu $diff_menu {\n \t{mc \"Show origin of this line\" command show_line_source}\n \t{mc \"Run git gui blame on this line\" command {external_blame_diff}}\n+\t{mc \"Show the latest change of selected region\" command show_selection_source}\n     }\n     $diff_menu configure -tearoff 0\n }\n@@ -3465,6 +3466,7 @@ proc pop_diff_menu {w X Y x y} {\n     global ctext diff_menu flist_menu_file\n     global diff_menu_txtpos diff_menu_line\n     global diff_menu_filebase\n+    global selection_source_data\n \n     set diff_menu_txtpos [split [$w index \"@$x,$y\"] \".\"]\n     set diff_menu_line [lindex $diff_menu_txtpos 0]\n@@ -3477,6 +3479,12 @@ proc pop_diff_menu {w X Y x y} {\n     if {$f eq {}} return\n     set flist_menu_file [lindex $f 0]\n     set diff_menu_filebase [lindex $f 1]\n+    prepare_show_selection_source\n+    if {$selection_source_data ne {}} {\n+\t$diff_menu entryconf 2 -state normal\n+    } else {\n+\t$diff_menu entryconf 2 -state disabled\n+    }\n     tk_popup $diff_menu $X $Y\n }\n \n@@ -3927,6 +3935,136 @@ proc read_line_source {} {\n     return 0\n }\n \n+proc show_selection_source {} {\n+    global selection_source_data\n+    global blameinst blamestuff\n+\n+    if {$selection_source_data eq {}} {\n+\treturn\n+    }\n+\n+    foreach var {id fname lnum lnumber} val $selection_source_data { set $var $val }\n+    set args [list | git log --pretty=format:%H \"-L$lnum,+$lnumber:$fname\" -M -n1 $id]\n+\n+    startblaming $args selsource_read\n+}\n+\n+proc selsource_read {} {\n+    global blamestuff\n+    global curview\n+\n+    set id {}\n+    set lnum {}\n+    set state start\n+    foreach l $blamestuff {\n+\tswitch -- $state {\n+\t    start { if {[regexp {^([0-9a-f]{40})$} $l _ id_match]} {\n+\t\t\tset id $id_match\n+\t\t\tset state diff_header_diff\n+\t\t    } else {\n+\t\t\tbreak\n+\t\t    }\n+\t    }\n+\t    diff_header_diff { if {[regexp {^diff --git} $l]} { set state diff_header_oldfile } else { break } }\n+\t    diff_header_oldfile { if {[regexp {^---} $l]} {set state diff_header_newfile} else { break } }\n+\t    diff_header_newfile {\n+\t\tif {[regexp {^\\+\\+\\+ b/(.*)$} $l _ fname_match]} {\n+\t\t    set fname $fname_match\n+\t\t    set state diff_hunk_header\n+\t\t} else {\n+\t\t    break\n+\t\t}\n+\t    }\n+\t    diff_hunk_header {\n+\t\tif {[regexp {^@@@*.* \\+([0-9]+),[0-9]+ @@@*$} $l _ lnum_matched]} {\n+\t\t    set lnum $lnum_matched\n+\t\t    break\n+\t\t}\n+\t    }\n+\t}\n+    }\n+\n+    if {$id eq {}} {\n+\terror_popup [mc \"Parsing output of git log failed\"]\n+\treturn 0\n+    }\n+\n+    if {![commitinview $id $curview]} {\n+\terror_popup [mc \"The latest change is in commit %s, \\\n+\t\t\t which is not in this view\" [shortids $id]]\n+\treturn 0\n+    }\n+\n+    selectline [rowofcommit $id] 1 [list $fname $lnum] 1\n+}\n+\n+proc prepare_show_selection_source {} {\n+    global ctext\n+    global selection_source_data\n+\n+    set selection [$ctext tag ranges sel]\n+    if {[llength $selection] != 2} {\n+\tset selection_source_data {}\n+\treturn\n+    }\n+    set start_line [lindex [split [lindex $selection 0] \".\"] 0]\n+    set end_index [split [lindex $selection 1] \".\"]\n+    if {[lindex $end_index 1] eq 0} {\n+\tset end_line [expr [lindex $end_index 0] - 1]\n+    } else {\n+\tset end_line [lindex $end_index 0]\n+    }\n+    set selection_source_data [prepare_show_region_source $start_line $end_line]\n+}\n+\n+proc prepare_show_region_source {start_line end_line} {\n+    global cmitmode\n+    global currentid parents curview\n+    global nullid\n+    if {$start_line > $end_line} {\n+\terror_popup \"Cannot blame region: $start_line > $end_line\"\n+\treturn {}\n+    }\n+    set f [find_ctext_fileinfo $start_line]\n+    if {$f eq {}} {\n+\treturn {}\n+    }\n+    if {$currentid eq $nullid} {\n+\treturn {}\n+    }\n+    set fname [lindex $f 0]\n+    set filebase [lindex $f 1]\n+    if {$cmitmode eq \"tree\"} {\n+\tset id $currentid\n+\tset start_lnum [expr $start_line - $filebase]\n+\tset end_lnum [expr $end_line - $filebase]\n+    } else {\n+\tset id [lindex $parents($curview,$currentid) 0]\n+\tset start_lnum {}\n+\tset end_lnum {}\n+\tforeach cl_spec [resolve_hunk_lines $filebase $start_line $end_line] {\n+\t    set diffline [lindex $cl_spec 0]\n+\t    if {$diffline > $end_line} {\n+\t\tbreak\n+\t    } elseif {$diffline >= $start_line} {\n+\t\tforeach branch [lindex $cl_spec 1] {\n+\t\t    if {[lindex $branch 0] == 1} {\n+\t\t\tif {$start_lnum eq {}} {\n+\t\t\t    set start_lnum [lindex $branch 1]\n+\t\t\t}\n+\t\t\tset end_lnum [lindex $branch 1]\n+\t\t    }\n+\t\t}\n+\t    }\n+\t}\n+    }\n+    if {$start_lnum ne {} && $end_lnum ne {}} {\n+\treturn [list $id $fname $start_lnum [expr $end_lnum - $start_lnum + 1]]\n+    } else {\n+\treturn {}\n+    }\n+}\n+\n # delete $dir when we see eof on $f (presumably because the child has exited)\n proc delete_at_eof {f dir} {\n     while {[gets $f line] >= 0} {}\n-- \n2.0.0.526.g5318336\n"}]}