7

Can you add annotate/blame-alike information to svn diff, so that for every changed line it includes which user and revision changed that line?

For example, an annotate-diff comparing revisions 8-10 could output something like:

9    user1   - some line that user1 deleted in revision 9
10   user2   + some line that user2 added in revision 10

The context, lines around it which haven't changed, may be included as well or not, doesn't matter.

It's not just a matter of "quickly" writing a shell script combining the output of svn diff and svn annotate. annotate for example will never show you who removed a line. It's also not a matter of doing annotate on a revision in the past: We're not interested in who originally added the line that got removed (that's not the one who "caused" the diff), we want to know who removed it. I suspect the only way to implement something to do this is to inspect each and every commit between the two revisions being compared (and somehow map all the changes in the separate diffs to lines in the total diff)...

Does there exist a tool that does something like that?

1

I am not entirely sure i understood exactly what you want, but i did this with TortoiseSVN:

  1. Create Blame from revision 1 to revision A - save as A.txt
  2. Create Blame from revision 1 to revision B - save as B.txt
  3. Remove first column (line) from both text files (i used Pspad editor, which can delete a column of a text file)
  4. Merge A.txt and B.txt (TortoiseMerge.exe /base:"a.txt" /mine:"b.txt" )

It shows changed, added and removed lines between revisions A and B together with date, user and branch, i think that is what you wanted.

  • That would fall into the same category as the "a shell script combining the output of svn diff and svn annotate" I mentioned, and has the same problem: For lines that were removed, it shows you who originally added the line, not who removed it. – Wouter Coekaerts Jun 28 '11 at 10:42
1

SVN diff takes exactly two revisions and generates output on the fly. SVN annotate takes exactly one revision. Your suspicion that the proposed utility will need to iterate over N revisions is correct; SVN stores revision states as whole objects.

You might have better luck with git and the git-svn gateway...

0

In your basic vanilla SVN setup there is no command which can merge the outputs of blame and diff on a particular revision. If you specified the particular SVN client you used I might have been able to find a plugin that does this, but you didn't so I couldn't search for one.

The only alternative is to use a script or small program to match up the two outputs. This shouldn't be so hard if you can get line numbers on diff and blame. Even though this isn't the answer you were hoping for it is an answer and this question has sat open since 2009.

  • I didn't specify a particular SVN client because it doesn't matter: I'd be happy to (just when I need this feature) use any other client. – Wouter Coekaerts Dec 11 '10 at 12:22
  • 1
    As I already said in the description, it's not just a matter of matching up the output of a diff and and an annotate. It looks like it's a matter of merging a series of patches into one, while keeping track of which change came from which diff (and the committer of that diff). That is definitely not an easy task. Sorry, but claiming it's easy is not really an answer to the question of how to do it. – Wouter Coekaerts Dec 11 '10 at 12:27
0

This isn't quite what you were after, but you can get an annotated view of changes in OpenGrok.

0

Does there exist a tool that does something like that?

Well, I guess there does now.

Usage; blameDiff <path> [rev1] [rev2]

bash function

function blameDiff() {
    file="$1"
    rev1="$2"
    rev2="$3"

    #default to HEAD if omitted
    if [ -n "$rev1" ]
    then
        title1="(revision $rev1)"
    else
        title1="(working copy)"
        rev1='HEAD'
    fi
    if [ -n "$rev2" ]
    then
        title2="(revision $rev2)"
    else
        title2="(working copy)"
        rev2='HEAD'
    fi

    #check that the svn urls are the same
    tmp1="$(svn info -r $rev1 "$file" |\
        grep '^Relative URL' |\
        sed 's/Relative URL: //' \
    )"
    tmp2="$(svn info -r $rev2 "$file" |\
        grep '^Relative URL' |\
        sed 's/Relative URL: //' \
    )"
    if [ "$tmp1" != "$tmp2" ]
    then
        #if not, then one of these revisions is in another branch
        #lets have this in the output
        title1="($tmp1) $title1"
        title2="($tmp2) $title2"
    fi

#can just print this but you wont get deleted revision/blame
#    diff -u \
#        <(svn blame -r "$rev1" "$file") \
#        <(svn blame -r "$rev2" "$file") \
#    | sed "s|^--- .*$|--- $file $title1|" \
#    | sed "s|^+++ .*$|+++ $file $title2|"
#    return 0

    #an array of commitNumber|committer pairs for the file
    history=()
    #a map between elements in `history` and a list of line numbers changed.
    #each item in the list is a lineNumber|newLineNumber pair
    declare -A revisions

    #the sed match and replace expressions to pull data from the
    #diff-line-number&cat-line-number combo and give it to the cache
    grabData='^ *\([0-9]\+\)\t\([0-9]\+\)$'
    formatData='\2 \1'

    #for each revision between the ones given
    last=''
    while read -r line
    do
        #read in the revision number and submitter
        IFS=' |' read next by tmp <<<"$line"

        if [ -n "$last" ]
        then
            #save them
            history+=("$next $by")
            #associate and format the list
            revisions["${history[-1]}"]="$(\
                diff \
                    --unchanged-line-format="%dn%c'\012'" \
                    --new-line-format="?%c'\012'" \
                    --old-line-format='' \
                    <(svn cat -r "$last" "$file") \
                    <(svn cat -r "$next" "$file") \
                | cat -n \
                | grep -v '?$' \
                | sed "s/$grabData/$formatData/" \
            )"
        fi

        #remember the last revision looked at
        last="$next"
    done <<<"$(
        svn log -r "$rev1:$rev2" "$file" \
        | grep '^r[0-9]\+ | ' \
        | sed 's/^r//' \
    )"

    #pull the full diff
    diff \
        --new-line-format='+%L' \
        --old-line-format='-%L' \
        --unchanged-line-format='=%L' \
        <(svn blame -r "$rev1" "$file") \
        <(svn blame -r "$rev2" "$file") \
    | {
        #header stuff
        echo "Index: $file"
        echo '==================================================================='
        echo "--- $file $title1"
        echo "+++ $file $title2"

        #count the line number we're up to for the original file
        origLine=0
        #count the line number we're up to for the new file
        newLine=0

        #keep a few of the output lines, and their line number contexts
        buffer=()
        origContext=()
        newContext=()

        #tells the script to print the buffer if <3;
        #the context lines around real differences
        printing=4
        #whether or not the next print needs to show line numbers
        needsContext=true

        #the sed match and replace expressions to pull data from diff
        #and give it to read
        grabData='^\([+=-]\)\( *[0-9]\+\)\( *[^ ]\+\)\(.*\)$'
        formatData='\1\v\2\v\3\v\4'

        #for each line in the full diff
        while read -r data
        do
            IFS=$'\v' read flag committed who line <<<"$(\
                sed $'s/\t/    /g' \
                <<<"$data" \
                | sed "s/$grabData/$formatData/" \
            )"
            #the last surviving revision of the line
            edited="$rev2"
            #who killed this line
            by=''

            case "$flag" in
            +)
                #a new line was introduced
                ((++newLine))
                printing=0
            ;;
            -)
                #an old line was removed
                ((++origLine))
                printing=0
                #the line number that changes throughout history
                number="$origLine"
                #for each commit
                for revision in "${history[@]}"
                do
                    #read in the two line numbers from the matching change
                    number="$(grep "^$number " <<<"${revisions["$revision"]}")"
                    IFS=' ' read edited by <<<"$revision"

                    #not present; this was the revision where it was destroyed
                    if [ -z "$number" ]
                    then
                        break
                    fi

                    #pull the new line number for the next revision
                    IFS=' ' read tmp number <<<"$number"
                done
            ;;
            =)
                #an old line continues to exist in the new file
                ((++newLine))
                ((++origLine))
                flag=' '
                ((++printing))
            ;;
            esac

            #format the line to print
            buffer+=("$(printf "%s %s:%-${#committed}s%s:%-${#who}s%s" \
                "$flag" \
                "$committed" \
                "$edited" \
                "$who" \
                "$by" \
                "$line" \
            )")
#can just end it here, but it will print the whole file/s
#            echo "${buffer[-1]}"
#            buffer=()
#            continue
            #and add the context
            origContext+=("$origLine")
            newContext+=("$newLine")

            if ((printing < 4))
            then
                if $needsContext
                then
                    echo "@@ -${origContext[0]} +${newContext[0]} @@"
                    needsContext=false
                fi

                #print all lines in the buffer
                for line in "${buffer[@]}"
                do
                    echo "$line"
                done

                #and reset it
                origContext=()
                newContext=()
                buffer=()
            fi

            #if there are too many lines in the buffer
            if ((${#buffer[@]} > 3))
            then
                #remove the overflow
                origContext=("${origContext[@]:1}")
                newContext=("${newContext[@]:1}")
                buffer=("${buffer[@]:1}")
                #and note that we now need to show the context because of this
                needsContext=true
            fi
        done
    }
}

I added comments as an explanation, so I wont get into it here.
Tested to work with the outputs of diff (fedora 27), svn info (1.10.2) on my system, YMMV (but for all my effort I hope not that much!).

It basically reimplements the svn diff using just svn cat and regular diff to take into account the revision and line numbers, tracking exactly where in the history a given line was removed.
Even takes into account whether the files are in different branches and displays it as svn would.

Here's screenshots of the two following commands, with the code redacted for work-reasons.

~/data/<redacted>/svn-2.4.2/$ svn diff -r 6600 services/<redacted>.w3p | gvim -
~/data/<redacted>/svn-2.4.2/$ blameDiff services/<redacted>.w3p 6600 | gvim -

enter image description here

As you can see a bunch of extra info is given in the new format on the right; the first columns show ashley added a couple lines back in r6631, and deleted a whole bunch in r6639 originally committed by zes long ago @r6466&6483.

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.