在某些场景下我们可能有通过Java代码操作Git仓库的需求,例如您要开发一个工具,通过拉取远程仓库的代码,分析并统计某个时间范围内各提交人的代码行数等,那么接下来的这个工具可帮助你应对Git操作的问题
首先我们在项目中添加依赖
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>6.7.0.202309050840-r</version>
</dependency>
在使用前先对几个对象进行说明
- Repository:表示一个Git存储库(在使用时不要认为一定是线程安全的,并且使用完后记得关闭)
- Git:使用类似Git高层命令方式的API来操作Git存储库的对象
- Gi对象(Git Objects):就是git的对象,它们在git中用SHA-1来表示。在JGit中用AnyObjectId和ObjectId表示,而它又包含了四种类型:
- 二进制大对象(blob):文件数据
- 树(Tree):指向其它的tree和blob
- 提交(commit):指向某一棵tree
- 标签(tag):把一个commit标记为一个标签
- Ref:对某一个Git对象的引用
- RevWalk:该类用于从commit的关系图(graph)中遍历commit。
- RevCommit:表示一个git的commit
- RevTag:表示一个git的tag
- RevTree:表示一个git的tree
- TreeWalk:类似RevWalk,但是用于遍历一棵tree
- 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));
}
}