如何在JGit中执行“git log --follow <path>”命令?(检索包括重命名在内的完整历史记录)

5

我该如何扩展以下logCommand,以使git log命令的--follow选项正常工作?

Git git = new Git(myRepository);
Iterable<RevCommit> log = git.log().addPath("com/mycompany/myclass.java").call();

这个选项在jGit中已经实现,但我不知道怎么使用它。logCommand的方法似乎并没有什么用处。谢谢!


"jgit follow renames" 在谷歌中的第一个结果: http://dev.eclipse.org/mhonarc/lists/jgit-dev/msg00426.html - iddo
虽然它不是JGit,但我找到了另一个名为“JavaGit”的项目(http://javagit.sourceforge.net/),它似乎提供了git的整个高级API,包括[LogCommand](http://javagit.sourceforge.net/docs/javagit-0.1.0-javadoc/)的“检测重命名”选项。然而,与JGit不同的是,它需要在Linux或Windows操作系统上安装git客户端。 - OneWorld
刚刚做了进一步的研究。JavaGit自2008年以来就没有得到维护了 ;( - OneWorld
3个回答

16

在一些半夜的工作中,我得到了以下信息:

LogCommand 的最后一个提交将被检查,以查找所有旧提交中的重命名操作。这个循环会持续进行,直到找不到任何重命名操作为止。

然而,那个搜索可能需要一些时间,尤其是如果它遍历所有提交直到结束,并且再也找不到任何重命名操作的时候。所以,我希望能有任何改进。我猜 git 通常使用索引来执行更短时间内的 follow 选项。

import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.RenameDetector;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.treewalk.TreeWalk;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Create a Log command that enables the follow option: git log --follow -- < path >
 * User: OneWorld
 * Example for usage: ArrayList<RevCommit> commits =  new  LogFollowCommand(repo,"src/com/mycompany/myfile.java").call();
 */
public class LogFollowCommand {

    private final Repository repository;
    private String path;
    private Git git;

    /**
     * Create a Log command that enables the follow option: git log --follow -- < path >
     * @param repository
     * @param path
     */
    public LogFollowCommand(Repository repository, String path){
        this.repository = repository;
        this.path = path;
    }

    /**
     * Returns the result of a git log --follow -- < path >
     * @return
     * @throws IOException
     * @throws MissingObjectException
     * @throws GitAPIException
     */
    public ArrayList<RevCommit> call() throws IOException, MissingObjectException, GitAPIException {
        ArrayList<RevCommit> commits = new ArrayList<RevCommit>();
        git = new Git(repository);
        RevCommit start = null;
        do {
            Iterable<RevCommit> log = git.log().addPath(path).call();
            for (RevCommit commit : log) {
                if (commits.contains(commit)) {
                    start = null;
                } else {
                    start = commit;
                    commits.add(commit);
                }
            }
            if (start == null) return commits;
        }
        while ((path = getRenamedPath( start)) != null);

        return commits;
    }

    /**
     * Checks for renames in history of a certain file. Returns null, if no rename was found.
     * Can take some seconds, especially if nothing is found... Here might be some tweaking necessary or the LogFollowCommand must be run in a thread.
     * @param start
     * @return String or null
     * @throws IOException
     * @throws MissingObjectException
     * @throws GitAPIException
     */
    private String getRenamedPath( RevCommit start) throws IOException, MissingObjectException, GitAPIException {
        Iterable<RevCommit> allCommitsLater = git.log().add(start).call();
        for (RevCommit commit : allCommitsLater) {

            TreeWalk tw = new TreeWalk(repository);
            tw.addTree(commit.getTree());
            tw.addTree(start.getTree());
            tw.setRecursive(true);
            RenameDetector rd = new RenameDetector(repository);
            rd.addAll(DiffEntry.scan(tw));
            List<DiffEntry> files = rd.compute();
            for (DiffEntry diffEntry : files) {
                if ((diffEntry.getChangeType() == DiffEntry.ChangeType.RENAME || diffEntry.getChangeType() == DiffEntry.ChangeType.COPY) && diffEntry.getNewPath().contains(path)) {
                    System.out.println("Found: " + diffEntry.toString() + " return " + diffEntry.getOldPath());
                    return diffEntry.getOldPath();
                }
            }
        }
        return null;
    }
}

2
将路径过滤器设置为树遍历,节省了一些时间:tw.setFilter(PathFilter.create("src/main/java/")); - OneWorld
1
非常好!但我认为你应该在call()函数的日志命令中添加start ObjectId(如果!= null)。现在发生的情况是,在重命名后再次添加旧名称的文件时,它将出现在新文件的日志中。 - NickL
1
感谢您提供代码。由于您的JavaDoc注释,我立即知道如何使用您的代码。太棒了!这样好的代码示例现在很少见。+1! :) - Benny Code
1
Stackoverflow不会阻止我留下+1评论,或者也许他们会。感谢OneWorld提供这个代码,直到我偶然发现它之前,git log --follow的工作简直是一场灾难。如果你能记得你确切的意思,我希望你能详细说明一下,+NickL。我遇到了你描述的问题,但我不知道如何通过检查来解决它。 - Aj Otto
1
根据您的示例,我用Scala编写了一段代码,可以获取文件的第一个提交。非常感谢!也许这会对某些人有所帮助: https://gist.github.com/wobu/ccfaccfc6c04c02b8d1227a0ac151c36 - wobu

0

我记得之前尝试过OneWorld的解决方案,虽然它能够工作,但速度非常慢。我想在谷歌上搜索看看是否有其他可能性。

是的,在这个Eclipse thread中,有一个建议使用org.eclipse.jgit.revwalk.FollowFilter并查找RevWalkFollowFilterTest.java中的使用示例。

所以我想试一试,结果代码看起来像这样:

private static class DiffCollector extends RenameCallback {
    List<DiffEntry> diffs = new ArrayList<DiffEntry>();

    @Override
    public void renamed(DiffEntry diff) {
        diffs.add(diff);
    }
}

private DiffCollector diffCollector;

private void showFileHistory(String filepath)
{
    try
    {
        Config config = repo.getConfig();
        config.setBoolean("diff", null, "renames", true);

        RevWalk rw = new RevWalk(repo);
        diffCollector = new DiffCollector();

        org.eclipse.jgit.diff.DiffConfig dc = config.get(org.eclipse.jgit.diff.DiffConfig.KEY);
        FollowFilter followFilter =
                 FollowFilter.create(filepath, dc);
        followFilter.setRenameCallback(diffCollector);
        rw.setTreeFilter(followFilter);
        rw.markStart(rw.parseCommit(repo.resolve(Constants.HEAD)));

        for (RevCommit c : rw)
        {
            System.out.println(c.toString());
        }
    }
    catch(...

结果,嗯,我猜还好吧……RevWalk 确实可以通过 git-repo 历史中的简单文件重命名(通过“git mv {filename}”操作)。

然而,它无法处理更混乱的情况,比如当同事在 repo 历史中执行以下一系列操作时:

  • 第一次提交:使用“git mv”重命名文件
  • 第二次提交:在新的子文件夹位置添加该文件的副本
  • 第三次提交:删除旧位置的副本

在这种情况下,JGit 的跟踪能力只能让我到达第二次提交,并在那里停止。

然而,真正的“git log --follow”命令似乎足够聪明,可以找出以下内容:

  • 第二次提交添加的文件与第一次提交中的文件相同(尽管它们位于不同的位置)
  • 它将为您提供整个历史记录:
    • 从HEAD到第二次提交(在新位置添加新命名文件的副本)
    • 跳过任何对第三次提交的提及(删除旧路径中的旧文件)
    • 然后是第一次提交及其历史记录(文件的旧位置和名称)

所以,与真正的Git相比,JGit的跟踪功能似乎有点弱。嗯,无论如何。

但无论如何,我可以确认使用JGit的FollowFilter技术比先前建议的技术要快得多。


0

看起来很少有人遇到这个问题。我看过各种解决方案,但都不是理想的。

最后,我使用了Java子进程来调用dos命令并分析结果以获取特定的提交,然后分析该提交。这种方法避免了搜索重命名文件,并完全依赖于git本身的能力。我希望它能帮助有这种需求的人!

String command ="cmd /c cd path/to/dir && git log --follow file_you_want";
Process p = Runtime.getRuntime().exec(command);
BufferedReader input = new BufferedReader(new 
InputStreamReader(p.getInputStream()));
String line;
String text = command +"";
System.out.println(text);
while ((line = input.readLine()) != null) {
    text += line;
    System.out.println("Line:" + line);
}

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