在某些场景下我们可能有通过Java代码操作Git仓库的需求,例如您要开发一个工具,通过拉取远程仓库的代码,分析并统计某个时间范围内各提交人的代码行数等,那么接下来的这个工具可帮助你应对Git操作的问题

首先我们在项目中添加依赖

<dependency>
    <groupId>org.eclipse.jgit</groupId>
    <artifactId>org.eclipse.jgit</artifactId>
    <version>6.7.0.202309050840-r</version>
</dependency>

在使用前先对几个对象进行说明

  1. Repository:表示一个Git存储库(在使用时不要认为一定是线程安全的,并且使用完后记得关闭)
  2. Git:使用类似Git高层命令方式的API来操作Git存储库的对象
  3. Gi对象(Git Objects):就是git的对象,它们在git中用SHA-1来表示。在JGit中用AnyObjectId和ObjectId表示,而它又包含了四种类型:
  • 二进制大对象(blob):文件数据
  • 树(Tree):指向其它的tree和blob
  • 提交(commit):指向某一棵tree
  • 标签(tag):把一个commit标记为一个标签
  1. Ref:对某一个Git对象的引用
  2. RevWalk:该类用于从commit的关系图(graph)中遍历commit。
  3. RevCommit:表示一个git的commit
  4. RevTag:表示一个git的tag
  5. RevTree:表示一个git的tree
  6. TreeWalk:类似RevWalk,但是用于遍历一棵tree
  7. CredentialsProvider:提供访问远程Git仓库的访问凭证,可以是用户名和密码,也可以是access token,其默认实现为


1、构建远程仓库凭借

通过用户名/密码构建凭据

//通过用户名和密码构建认证凭证
String username = "" ;
String password = "" ;
CredentialsProvider credentialsProvider = 
	new UsernamePasswordCredentialsProvider(username,password);

通过访问token构建凭据

//通过个人或项目的access token构建凭证
//需要注意,此时的用户名设置为:Automation token
String token = "dfewf244r4rg" ;
CredentialsProvider credentialsProvider = 
	new UsernamePasswordCredentialsProvider("Automation token",token);


2、从远程仓库进行克隆

//远程仓库地址
private String remoteUrl ;

//本地工作路径(在该目录或子目录下对代码进行克隆)
private File rootWorkDir ;

//源码目录(.git所在路径)
private File workDir ;

//远程仓库认证方式
private CredentialsProvider credentialsProvider ;

//GIT仓库示例对象
private Repository repository ;

/**
 * 从远程仓库克隆
 */
public Repository clone(Collection<String> branchesToClone,
                        Integer depth) 
  throws GitAPIException {
    CloneCommand cloneCommand  = Git.cloneRepository()
            .setURI(this.remoteUrl)
            .setCredentialsProvider(this.credentialsProvider)
            .setDirectory(this.workDir)
            .setProgressMonitor(new TextProgressMonitor())
            .setTimeout(120);
    if(depth != null){
        cloneCommand.setDepth(depth) ;
    }
    if(branchesToClone != null && branchesToClone.size() > 0){
        cloneCommand.setBranchesToClone(branchesToClone) ;
    }
    Git git = cloneCommand.call() ;
    return git.getRepository() ;
}

/**
 * 从远程仓库克隆
 */
public Repository gitClone() throws GitAPIException {
    return gitClone(null,null) ;
}

3、判断某路径下

/**
 * 判断指定路径是否已经是仓库
 */
public boolean isLocalRepoExist(File projectPath){
    File gitDir = Paths.get(projectPath.getAbsolutePath(),
                            Constants.DOT_GIT).toFile();
    return (gitDir.exists() && gitDir.isDirectory()) ;
}


4、打开本地已有的仓库

当本地仓库已经存在且不打算重新克隆时,可通过下面的方式打开仓库

/** 
 * 打开本地仓库
 * @gitDir  .git文件夹所在路径
 */
public Repository openRepository(Fie gitDir) throws IOException {
    //基于本地.git构建仓库对象
    return FileRepositoryBuilder.create(gitDir) ;
}

5、获取当前分支名

/**
 * 当前分支
 */
public Ref currentBranch() throws IOException {
    //String branchName = git.getRepository().getBranch();
    return repository.exactRef(Constants.HEAD)
        .getTarget().getName();
}


6、获取所有远程分支

/**
 * 创建GIT指令操作对象
 * @param repo 仓库对象
 */
public Git newGit(Repository repo){
    return new Git(repo);
}

/**
 * 所有的远程分支
 */
public List<Ref> remoteBranches(Repository repository) 
    throws GitAPIException {
    try(Git git = newGit(repository)){
        return git.branchList()
            .setListMode(ListBranchCommand.ListMode.REMOTE).call();
    }
}


7、获取所有本地分支

/**
 * 所有的远程分支信息
 */
public List<Ref> localBranches(Repository repository) 
    throws GitAPIException {
    try(Git git = newGit(this.repository)){
        return git.branchList().call();
    }
}


8、判断分支是否存在

/**
 * 判断指定的分支是否为本地分支
 */
private boolean isExistLocalBranch(Repository repository,
                                   String branchName) throws GitAPIException {
    List<Ref> localBranchs = localBranches(repository) ;
    if(localBranchs == null){
        return false ;
    }
    for(Ref ref : localBranchs){
        if(ref.getName().equals(branchName) 
           || toShrotNeme(ref.getName()).equals(branchName)){
            return true ;
        }
    }
    return false ;
}

/**
 * 将分支名转换成短名
 * @param type 分支类型,远程分支:1; 本地分支:0
 */
private String toShrotNeme(int type,String branchName){
	if(type == 1){
        return toRemoteShortName() ;
    }
    return toLocalShortName() ;
}

private static final String REMOTE_PREFIX = Constants.R_REMOTES + Constants.DEFAULT_REMOTE_NAME+"/" ;
private static final String LOCAL_PREFIX = Constants.R_HEADS  ;

private String toRemoteShortName(String branchName){
    return branchName.replaceAll(REMOTE_PREFIX,"") ;
}
private String toLocalShortName(String branchName){
    return branchName.replaceAll(LOCAL_PREFIX,"") ;
}

9、切换分支

/**
 * 切换到指定分支.
 * 拉取特定分支的特定提交
 */
public void checkout(Repository repository,
                     String branchName) throws GitAPIException {
    if(StringUtils.isBlank(branchName)){
        return;
    }
    try(Git git = new Git(repository)) {
        //指定要签出的分支或提交的名称,或者指定新的分支名称
        //当只签出路径而不切换分支时,请使用setStartPoint(String)或setStartPoint(RevCommit)指定从哪个分支或提交签出文件。
        //当setCreateBranch(boolean)设置为true时,请使用此方法设置要创建的新分支的名称,并使用setStartPoint(String)
        // 或setStartPoint(RevCommit)指定分支的起点。
        CheckoutCommand checkoutCommand = git.checkout().setName(branchName);
        checkoutCommand.setCreateBranch(true);
        checkoutCommand.setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.TRACK) ;
        checkoutCommand.setStartPoint(Constants.DEFAULT_REMOTE_NAME+"/" + branchName) ;
        checkoutCommand.call();
    }
}

10、拉取最新代码

/**
 *拉取远程最新代码
 */
public void pull(Repository repository) throws Exception{
    try (Git git = new Git(repository)) {
        git.pull()
        .setCredentialsProvider(this.credentialsProvider)
        .call();
    }
}


11、获取提交日志

public class CommitLogInfo {
    //提交对象引用
    private RevCommit revCommit ;
    
    //作者姓名
    private String authorName ;
    //作者的邮箱
    private String authorEmail ;

    //提交人姓名
    private String committerName ;
    //提交人邮箱
    private String committerEmail ;
    //提交时间
    private Date commitTime ;
}

/**
 * 当前分支的所有提交日志
 * 注意:返回的数据是时间的倒序,也就是第一个元素为最新的提交
 */
public List<CommitLogInfo> commitLogs() throws GitAPIException {
     return commitLogs(null) ;
}

public List<CommitLogInfo>  commitLogs(RevFilter aFilter) throws GitAPIException {
    List<CommitLogInfo> commitLogs = new ArrayList<>() ;
    try(Git git = newGit(repository) ) {
        LogCommand logCommand = git.log() ;
        if(aFilter != null){
            logCommand.setRevFilter(aFilter) ;
        }
        Iterable<RevCommit> logs = logCommand.call();
        for (RevCommit rev : logs) {
            commitLogs.add(new CommitLogInfo(rev,rev.getAuthorIdent().getEmailAddress(),
                    rev.getCommitterIdent().getEmailAddress(),
                    rev.getCommitterIdent().getWhen())
                    .withAuthorName(rev.getAuthorIdent().getName())
                    .withCommitterName(rev.getCommitterIdent().getName()));
        }
        return commitLogs ;
    }
}

/**
 * 提取指定范围所有提交
 * @param since 开始时间(包含)
 * @param until 结束时间(包含)
 */
public List<CommitLogInfo> listCommit(Date since, Date until) throws GitAPIException {
    RevFilter revFilter = null ;
    if(since != null && until != null){
        revFilter = CommitTimeRevFilter.between(since,until) ;
    }else if(since != null){
        revFilter = CommitTimeRevFilter.after(since) ;
    }else if(until != null){
        revFilter = CommitTimeRevFilter.before(until) ;
    }
    return commitLogs(revFilter) ;
}


12、重置commit

 /**
 * @param ref  重置到指定的commit id
 */
public void reset(Repository repository,String ref){
    try(Git git = newGit(repository) ) {
        ResetCommand reset = git.reset()
            .setMode(ResetCommand.ResetType.HARD)
            .setRef(ref) ;
        reset.call();
    } catch (CheckoutConflictException e) {
        throw new RuntimeException(e);
    } catch (GitAPIException e) {
        throw new RuntimeException(e);
    }
}

13、获取变更文件

public class EditRow {
    //INSERT、DELETE、REPLACE、EMPTY
    public final String editType;
    public final int startLine;
    public final int endLine;
}

public class ChangedFile {
    //获取与该文件关联的旧路径[相对.git目录的相对路径]
    private String oldPath ;
    
    //获取与该文件关联的新路径[相对.git目录的相对路径]
    //例如:src/main/java/com/geega/sonar/Application.java
    private String newPath ;
    
    //变更类型:ADD、MODIFY、DELETE、RENAME、COPY
    private String changeType ;

    private List<EditRow> editRows ;
}
 
 /**
 * 指定两个提交间的变更
 */
public List<ChangedFile> listChangedFiles(String oldCommit, String newCommit) throws IOException {
    AbstractTreeIterator oldTree = prepareTreeParser(repository,oldCommit) ;
    AbstractTreeIterator newTree = prepareTreeParser(repository,newCommit) ;
    return listChangedFiles(oldTree,newTree) ;
}

private AbstractTreeIterator prepareTreeParser(Repository repository, String objectId) throws IOException {
    // from the commit we can build the tree which allows us to construct the TreeParser
    //noinspection Duplicates
    try (RevWalk walk = new RevWalk(repository)) {
        RevCommit commit = walk.parseCommit(repository.resolve(objectId));
        RevTree tree = walk.parseTree(commit.getTree().getId());

        CanonicalTreeParser treeParser = new CanonicalTreeParser();
        try (ObjectReader reader = repository.newObjectReader()) {
            treeParser.reset(reader, tree.getId());
        }
        walk.dispose();
        return treeParser;
    }
}

public List<ChangedFile> listChangedFiles(AbstractTreeIterator oldTree,AbstractTreeIterator newTree) throws IOException {
    List<ChangedFile> result = new ArrayList<>() ;
    // finally get the list of changed files
    try (Git git = new Git(repository)) {
        List<DiffEntry> diffs = git.diff()
                .setNewTree(newTree)
                .setOldTree(oldTree)
                .call();
        PatchIdDiffFormatter formatter = new PatchIdDiffFormatter() ;
        //设置比较器为忽略空白字符对比(Ignores all whitespace)
        formatter.setDiffComparator(RawTextComparator.WS_IGNORE_ALL);
        formatter.setRepository(this.repository);
        //List<DiffEntry> entries = formatter.scan(oldTree,newTree) ;

        for (DiffEntry entry : diffs) {
            //获取与该文件关联的旧名称(相对工作路径)
            String oldPath = entry.getOldPath() ;
            //获取与该文件关联的新名称(相对工作路径)
            String newPath = entry.getNewPath() ;
            if(!newPath.endsWith(com.geega.im.workunit.module.comm.Constants.SRC_JAVA_SUFFIX)){
                continue;
            }
            //打印文件差异具体内容
            //formatter.format(entry);
            //获取文件差异位置,从而统计差异的行数,如增加行数,减少行数
            FileHeader fileHeader = formatter.toFileHeader(entry) ;

            List<EditRow> editRows = new ArrayList<>() ;
            //每次编辑都会描述插入、删除或替换的区域以及受影响的线。
            //这些行从零开始计数,可以使用getBeginA()、getEndA()、getBeginB()和getEndB()进行查询
            EditList editList = fileHeader.toEditList() ;
            for(Edit edit : editList){
                int start = 0 ;
                int end = 0 ;
                /*
                 * beginA==endA && beginB<endB   的编辑是插入编辑,即序列B在beginA插入区域[beginB,endB)中的元素
                 * beginA<endA && beginB==endB   的编辑是删除编辑,即序列B删除了[beginA,endA)之间的元素
                 * beginA<endA && beginB<endB   的编辑是替换编辑,即序列B已将[beginA,endA)之间的元素范围替换为[beginB,endB)中的元素
                 */
                if(edit.getType() == Edit.Type.INSERT){
                    start = edit.getBeginB() ;
                    end = edit.getEndB() ;
                }else{
                    start = edit.getBeginA() ;
                    end = edit.getEndA() ;
                }
                editRows.add(new EditRow(edit.getType().name(),start,end)) ;
            }
            /*
            List<HunkHeader> hunks = (List<HunkHeader>) fileHeader.getHunks();
            for(HunkHeader hunkHeader:hunks){
                EditList editList = hunkHeader.toEditList();
            }
            */
            result.add(new ChangedFile(oldPath,newPath,entry.getChangeType().name(),editRows));
        }
    } catch (GitAPIException e) {
        throw new RuntimeException(e);
    }
    return result ;
}

14、获取文件历史修改记录


public class BlameLine {

    //文件路径
    private String filePath ;

    //提交时间
    private Date commitTime;
    //提交人
    private String committerName;
    //提交人邮件
    private String committerEmail;

    //修订ID
    private Date authorTime;
    //修订人
    private String authorName;
    private String authorEmail;

    //更新的文件行
    private int sourceLine;
    //变更前文件内容
    private String rowContent ;
    //变更后文件内容
    private String content ;
}

/**
 * git blame用来追溯一个指定文件的历史修改记录.它能显示任何文件中每行最后一次修改的提交记录
 * 我们可以查出某个文件的每一行内容到底是由哪位大神所写【报告没有告诉你任何关于被删除或替换的行】
 * https://git-scm.com/docs/git-blame
 * - git blame filename
 * - git blame -L n1,n2 filename   [-L 指定文件的行数范围]
 * @param filename
 */
public List<BlameLine> blame(String filename){
    Git git = newGit(this.repository) ;
    BlameResult blameResult;
    try {
        blameResult = git.blame()
                // Equivalent to -w command line option
                .setTextComparator(RawTextComparator.WS_IGNORE_ALL)
                .setFilePath(filename).call();
    } catch (Exception e) {
        throw new IllegalStateException("Unable to blame file " + filename, e);
    }
    List<BlameLine> lines = new ArrayList<>() ;
    for (int i = 0; i < blameResult.getResultContents().size(); i++) {
        if (blameResult.getSourceAuthor(i) == null
                || blameResult.getSourceCommit(i) == null) {
            continue;
        }
        ByteBuffer byteBuffer = blameResult.getResultContents().getRawString(i);
        String rowContent = StandardCharsets.UTF_8.decode(byteBuffer).toString();
        byteBuffer.flip() ;
        String content = blameResult.getResultContents().getString(i) ;
        //添加变化行信息
        lines.add(new BlameLine()
                  .commitTime(blameResult.getSourceCommitter(i).getWhen())
                  .committerName(blameResult.getSourceCommitter(i).getName())
                  .committerEmail(blameResult.getSourceCommitter(i).getEmailAddress())
                  .authorName(blameResult.getSourceAuthor(i).getName())
                  .authorEmail(blameResult.getSourceAuthor(i).getEmailAddress())
                  .authorTime(blameResult.getSourceAuthor(i).getWhen())
                  .authorEmail(blameResult.getSourceAuthor(i).getEmailAddress())
                  .sourceLine(blameResult.getSourceLine(i))
                  .filePath(blameResult.getSourcePath(i))
                  .rowContent(rowContent)
                  .content(content));
        }
        return lines ;
    }

15、列出指定commit下的所有文件

public List<String> listAllFile(String commitId) throws Exception {
    List<String> items = new ArrayList<>();
    RevCommit revCommit = buildRevCommit(commitId);
    RevTree tree = revCommit.getTree();
    try (TreeWalk treeWalk = new TreeWalk(repository)) {
        treeWalk.addTree(tree);
        treeWalk.setRecursive(false);
        treeWalk.setPostOrderTraversal(false);

        while(treeWalk.next()) {
            items.add(treeWalk.getPathString());
        }
    }
    return items ;
}

public RevCommit buildRevCommit(String commit) throws IOException {
    // a RevWalk allows to walk over commits based on some filtering that is defined
    try (RevWalk revWalk = new RevWalk(this.repository)) {
        return revWalk.parseCommit(ObjectId.fromString(commit));
    }
}


示例代码地址:https://github.com/centic9/jgit-cookbook