Java有支持注释/责任的Diff库吗?

8
我正在搜索谷歌上免费的(开源的)Java差异库,并且似乎有相当多这样的库(其中一些甚至可以使用通用对象而不仅仅是字符串)。
在我浏览大量的搜索结果却找不到我想要的之前,我先在这里问一下:
这些差异库中有没有支持像cvs annotate或svn blame这样的功能。我希望能够:
- 将当前的String[]传递给一个函数; - 继续将旧版本的String[]传递给该函数,直到我耗尽了所有旧版本,或者库告诉我没有未注释的原始行(最后一件事情并不是必须的,但非常有用,因为检索旧版本的String[]很昂贵,所以我想尽早停止); - 调用一个函数,该函数为我提供一个int[],告诉我每个当前版本的行最后一次更改是在哪个版本,或者它根本没有更改(即在第一个版本中最后一次更改)。
虽然支持不是字符串的对象很好,但并不是必须的。如果API不完全是这样的,我想我可以接受。
如果没有,有人可以建议一个可扩展的差异库,可以轻松地添加这个功能,最好是那些愿意接受这个功能作为贡献的库(并且在接受贡献之前不需要填写大量文件的,就像GNU项目一样)吗?我愿意去那里(至少试着)添加它。
3个回答

2

我决定自己实现Dmitry Naumenko的java-diff-utils库:

/*
   Copyright 2010 Michael Schierl (schierlm@gmx.de)

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 */
package difflib.annotate;

import java.util.*;

import difflib.*;

/**
 * Generates an annotated version of a revision based on a list of older
 * revisions, like <tt>cvs annotate</tt> or <tt>svn blame</tt>.
 * 
 * @author <a href="schierlm@gmx.de">Michael Schierl</a>
 * 
 * @param <R>
 *            Type of the revision metadata
 */
public class Annotate<R> {

    private final List<R> revisions;
    private final int[] lineNumbers;
    private R currentRevision;
    private final List<Object> currentLines;
    private final List<Integer> currentLineMap;

    /**
     * Creates a new annotation generator.
     * 
     * @param revision
     *            Revision metadata for the revision to be annotated
     * @param targetLines
     *            Lines of the revision to be annotated
     */
    public Annotate(R revision, List<?> targetLines) {
        revisions = new ArrayList<R>();
        lineNumbers = new int[targetLines.size()];
        currentRevision = revision;
        currentLines = new ArrayList<Object>(targetLines);
        currentLineMap = new ArrayList<Integer>();
        for (int i = 0; i < lineNumbers.length; i++) {
            lineNumbers[i] = -1;
            revisions.add(null);
            currentLineMap.add(i);
        }
    }

    /**
     * Check whether there are still lines that are unannotated. In that case,
     * more older revisions should be retrieved and passed to the function. Note
     * that as soon as you pass an empty revision, all lines will be annotated
     * (with a later revision), therefore if you do not have any more revisions,
     * pass an empty revision to annotate the rest of the lines.
     */
    public boolean areLinesUnannotated() {
        for (int i = 0; i < lineNumbers.length; i++) {
            if (lineNumbers[i] == -1 || revisions.get(i) == null)
                return true;
        }
        return false;
    }

    /**
     * Add the previous revision and update annotation info.
     * 
     * @param revision
     *            Revision metadata for this revision
     * @param lines
     *            Lines of this revision
     */
    public void addRevision(R revision, List<?> lines) {
        Patch patch = DiffUtils.diff(currentLines, lines);
        int lineOffset = 0; // remember number of already deleted/added lines
        for (Delta d : patch.getDeltas()) {
            Chunk original = d.getOriginal();
            Chunk revised = d.getRevised();
            int pos = original.getPosition() + lineOffset;
            // delete lines
            for (int i = 0; i < original.getSize(); i++) {
                int origLine = currentLineMap.remove(pos);
                currentLines.remove(pos);
                if (origLine != -1) {
                    lineNumbers[origLine] = original.getPosition() + i;
                    revisions.set(origLine, currentRevision);
                }
            }
            for (int i = 0; i < revised.getSize(); i++) {
                currentLines.add(pos + i, revised.getLines().get(i));
                currentLineMap.add(pos + i, -1);
            }
            lineOffset += revised.getSize() - original.getSize();
        }

        currentRevision = revision;
        if (!currentLines.equals(lines))
            throw new RuntimeException("Patch application failed");
    }

    /**
     * Return the result of the annotation. It will be a List of the same length
     * as the target revision, for which every entry states the revision where
     * the line appeared last.
     */
    public List<R> getAnnotatedRevisions() {
        return Collections.unmodifiableList(revisions);
    }

    /**
     * Return the result of the annotation. It will be a List of the same length
     * as the target revision, for which every entry states the line number in
     * the revision where the line appeared last.
     */
    public int[] getAnnotatedLineNumbers() {
        return (int[]) lineNumbers.clone();
    }
}

我也将它发送给Dmitry Naumenko(附带一些测试用例),以便他想要包含它。

1

我可能错了,但我认为注释/责备需要版本控制系统才能工作,因为它需要访问文件的历史记录。通用的差异库无法做到这一点。 因此,如果这是您的目标,请查看与这些版本控制系统一起工作的库,例如svnkit。 如果不是,请使用此类库作为开始注释/责备的良好起点,通常涉及对文件所有版本链进行差异处理。


我确实拥有文件的所有旧版本,这不是问题(如问题所述)。只是没有在支持Blame的VCS中。而将它们提交到临时SVN中仅为了责备一个文件并不是我想要的...而算法并不是难点(是的,它涉及到比较所有版本并跟踪行号何时“消失”),只是你需要实现它 :) - mihi
1
明白了。另一个想法是查看基于Java的维基百科的源代码。由于它们通常必须实现,所以代码可能在那里。http://c2.com/cgi/wiki?JavaWikiEngines列出了一些。 - vasquez

1
你可以使用xwiki-commons-blame-api。它实际上使用了本线程被接受的答案中的代码(感谢Michael Schierl在StackOverflow上分享此代码)。
你可以在它的单元测试中看到如何在Java中使用它。
或者在Scala中像这样:
import java.util
import org.xwiki.blame.AnnotatedContent
import org.xwiki.blame.internal.DefaultBlameManager

case class Revision(id: Int,
                    parentId: Option[Int] = None,
                    content: Option[String] = None)

def createAnnotation(revisions: Seq[Revision]): Option[AnnotatedContent[Revision, String]] = {
    val blameManager = new DefaultBlameManager()

    val annotatedContent = revisions.foldLeft(null.asInstanceOf[AnnotatedContent[Revision, String]]){
      (annotation, revision) =>
        blameManager.blame(annotation, revision, splitByWords(revision.content))
    }
    Option(annotatedContent)
}

def splitByWords(content: Option[String]): util.List[String] = {
    val array = content.fold(Array[String]())(_.split("[^\\pL_\\pN]+"))
    util.Arrays.asList(array:_*)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接