chat
背景
给出一段 java 代码,建议使用 java 稳定推荐的 git 开源包。
给出 pom 依赖。
给出一个代码仓库,上面3个命令,获取近一年的提交数据的数据构建,面向对象的写法,给出核心代码
设计
以下是基于 JGit 的完整 Java 实现,包含 Maven 依赖和核心代码。
一、Maven 依赖(pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.git.history</groupId>
<artifactId>git-history-analyzer</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- JGit 核心库(当前最新稳定版本) -->
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>7.4.0.202509020913-r</version>
</dependency>
<!-- SLF4J 日志接口(JGit 依赖) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
</dependency>
<!-- Lombok(可选,用于简化 POJO) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
二、数据模型(POJO 类)
2.1 CommitInfo.java(提交元信息)
package com.git.history.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CommitInfo {
/** 完整 commit SHA(40 字符) */
private String hash;
/** 作者名 */
private String authorName;
/** 作者邮箱 */
private String authorEmail;
/** 作者时间(Unix 时间戳,秒) */
private Long authorTime;
/** 提交消息第一行(subject) */
private String subject;
/** 变更的文件列表 */
private List<FileChangeInfo> fileChanges;
}
2.2 FileChangeInfo.java(文件变更信息)
package com.git.history.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FileChangeInfo {
/** 文件路径(相对于仓库根目录) */
private String path;
/** 变更类型:ADD, DELETE, MODIFY, RENAME */
private ChangeType changeType;
/** 变更前的文件路径(仅重命名时有效) */
private String oldPath;
/** 变更块的列表(diff hunks) */
private List<DiffHunkInfo> hunks;
public enum ChangeType {
ADD, // 新增文件
DELETE, // 删除文件
MODIFY, // 修改文件
RENAME // 重命名文件
}
}
2.3 DiffHunkInfo.java(差异块信息)
package com.git.history.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DiffHunkInfo {
/** 旧文件起始行号 */
private int oldStartLine;
/** 旧文件总行数 */
private int oldLineCount;
/** 新文件起始行号 */
private int newStartLine;
/** 新文件总行数 */
private int newLineCount;
/** 变更行的列表(每行内容,'-' 表示删除,'+' 表示新增) */
private List<String> lines;
}
三、核心服务类
3.1 GitHistoryAnalyzer.java(主分析器)
package com.git.history.service;
import com.git.history.model.CommitInfo;
import com.git.history.model.FileChangeInfo;
import com.git.history.model.DiffHunkInfo;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.Edit;
import org.eclipse.jgit.diff.EditList;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.patch.FileHeader;
import org.eclipse.jgit.patch.HunkHeader;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Slf4j
public class GitHistoryAnalyzer implements AutoCloseable {
private final Repository repository;
private final Git git;
private final RevWalk revWalk;
/**
* 构造函数,打开本地 Git 仓库
*
* @param repoPath .git 目录的路径或工作目录路径
* @throws IOException 如果无法打开仓库
*/
public GitHistoryAnalyzer(String repoPath) throws IOException {
FileRepositoryBuilder builder = new FileRepositoryBuilder();
// 如果传入的是 .git 目录,直接设置;否则尝试查找
File gitDir = new File(repoPath);
if (gitDir.isDirectory() && gitDir.getName().equals(".git")) {
builder.setGitDir(gitDir);
} else {
builder.setWorkTree(gitDir).findGitDir();
}
this.repository = builder
.readEnvironment()
.build();
this.git = new Git(repository);
this.revWalk = new RevWalk(repository);
}
/**
* 获取近一年内的所有提交(按时间正序)
*
* @return 提交 SHA 列表
* @throws GitAPIException Git 操作异常
*/
public List<String> getCommitsSinceOneYear() throws GitAPIException {
// 计算一年前的日期
Date since = Date.from(Instant.now().minus(365, ChronoUnit.DAYS));
log.info("获取 {} 之后的提交记录", since);
List<String> commitHashes = new ArrayList<>();
// 使用 LogCommand,添加时间过滤器(按正序输出)
Iterable<RevCommit> commits = git.log()
.setRevFilter(CommitTimeRevFilter.after(since))
.call();
// 转换为正序(git.log() 默认按时间倒序)
List<RevCommit> revCommitList = new ArrayList<>();
for (RevCommit commit : commits) {
revCommitList.add(commit);
}
// 反转得到正序
for (int i = revCommitList.size() - 1; i >= 0; i--) {
commitHashes.add(revCommitList.get(i).getName());
}
log.info("共获取 {} 个提交", commitHashes.size());
return commitHashes;
}
/**
* 获取单个提交的元信息
*
* @param commitHash 提交 SHA
* @return CommitInfo 对象
* @throws IOException IO 异常
*/
public CommitInfo getCommitMeta(String commitHash) throws IOException {
ObjectId objectId = repository.resolve(commitHash);
if (objectId == null) {
throw new IllegalArgumentException("无效的 commit hash: " + commitHash);
}
RevCommit commit = revWalk.parseCommit(objectId);
revWalk.parseBody(commit);
return CommitInfo.builder()
.hash(commit.getName())
.authorName(commit.getAuthorIdent().getName())
.authorEmail(commit.getAuthorIdent().getEmailAddress())
.authorTime(commit.getAuthorIdent().getWhen().getTime() / 1000L)
.subject(commit.getShortMessage())
.build();
}
/**
* 获取单个提交的 Diff 信息(不含元信息)
*
* @param commitHash 提交 SHA
* @return 文件变更列表
* @throws IOException IO 异常
*/
public List<FileChangeInfo> getCommitDiff(String commitHash) throws IOException {
ObjectId objectId = repository.resolve(commitHash);
if (objectId == null) {
throw new IllegalArgumentException("无效的 commit hash: " + commitHash);
}
RevCommit commit = revWalk.parseCommit(objectId);
revWalk.parseBody(commit);
// 获取父提交
RevCommit parent = null;
if (commit.getParentCount() > 0) {
parent = revWalk.parseCommit(commit.getParent(0).getId());
}
return computeDiff(parent, commit);
}
/**
* 计算两个提交之间的差异
*
* @param oldCommit 旧提交(可为 null,表示空树)
* @param newCommit 新提交
* @return 文件变更列表
* @throws IOException IO 异常
*/
private List<FileChangeInfo> computeDiff(RevCommit oldCommit, RevCommit newCommit) throws IOException {
List<FileChangeInfo> fileChanges = new ArrayList<>();
try (DiffFormatter diffFormatter = new DiffFormatter(new ByteArrayOutputStream())) {
diffFormatter.setRepository(repository);
// 关闭上下文行数(相当于 --unified=0),只显示变更行
diffFormatter.setContext(0);
diffFormatter.setDiffComparator(RawTextComparator.DEFAULT);
diffFormatter.setDetectRenames(true);
List<DiffEntry> diffs;
try (ObjectReader reader = repository.newObjectReader()) {
if (oldCommit == null) {
// 初始提交:对比空树
CanonicalTreeParser newTreeParser = new CanonicalTreeParser();
newTreeParser.reset(reader, newCommit.getTree());
diffs = diffFormatter.scan(new EmptyTreeIterator(), newTreeParser);
} else {
CanonicalTreeParser oldTreeParser = new CanonicalTreeParser();
CanonicalTreeParser newTreeParser = new CanonicalTreeParser();
oldTreeParser.reset(reader, oldCommit.getTree());
newTreeParser.reset(reader, newCommit.getTree());
diffs = diffFormatter.scan(oldTreeParser, newTreeParser);
}
}
for (DiffEntry diff : diffs) {
FileChangeInfo changeInfo = parseDiffEntry(diff, diffFormatter);
fileChanges.add(changeInfo);
}
}
return fileChanges;
}
/**
* 解析单个 DiffEntry 为 FileChangeInfo
*
* @param diffEntry DiffEntry 对象
* @param diffFormatter DiffFormatter(用于获取 FileHeader)
* @return FileChangeInfo
* @throws IOException IO 异常
*/
private FileChangeInfo parseDiffEntry(DiffEntry diffEntry, DiffFormatter diffFormatter) throws IOException {
FileChangeInfo.ChangeType changeType;
String path = diffEntry.getNewPath();
String oldPath = null;
switch (diffEntry.getChangeType()) {
case ADD:
changeType = FileChangeInfo.ChangeType.ADD;
break;
case DELETE:
changeType = FileChangeInfo.ChangeType.DELETE;
path = diffEntry.getOldPath();
break;
case RENAME:
changeType = FileChangeInfo.ChangeType.RENAME;
oldPath = diffEntry.getOldPath();
break;
default:
changeType = FileChangeInfo.ChangeType.MODIFY;
break;
}
// 获取 FileHeader 以提取 hunk 信息
FileHeader fileHeader = diffFormatter.toFileHeader(diffEntry);
List<DiffHunkInfo> hunks = parseHunks(fileHeader);
return FileChangeInfo.builder()
.path(path)
.oldPath(oldPath)
.changeType(changeType)
.hunks(hunks)
.build();
}
/**
* 解析 FileHeader 中的 Hunk 信息
*
* @param fileHeader FileHeader 对象
* @return Hunk 列表
*/
private List<DiffHunkInfo> parseHunks(FileHeader fileHeader) {
List<DiffHunkInfo> hunks = new ArrayList<>();
for (HunkHeader hunkHeader : fileHeader.getHunks()) {
// 获取旧文件的行号范围
int oldStart = hunkHeader.getOldStart();
int oldLines = hunkHeader.getOldLines();
// 获取新文件的行号范围
int newStart = hunkHeader.getNewStart();
int newLines = hunkHeader.getNewLines();
// 获取 hunk 中的具体变更行
List<String> lines = new ArrayList<>();
for (Edit edit : hunkHeader.toEditList()) {
// Edit 包含变更的详细信息
// 实际行内容需要从原始文件读取,这里简化处理
lines.add(formatEditInfo(edit));
}
DiffHunkInfo hunk = DiffHunkInfo.builder()
.oldStartLine(oldStart)
.oldLineCount(oldLines)
.newStartLine(newStart)
.newLineCount(newLines)
.lines(lines)
.build();
hunks.add(hunk);
}
return hunks;
}
/**
* 格式化 Edit 信息(用于展示)
*
* @param edit Edit 对象
* @return 格式化的字符串
*/
private String formatEditInfo(Edit edit) {
switch (edit.getType()) {
case INSERT:
return String.format("+ insert at line %d", edit.getBeginB());
case DELETE:
return String.format("- delete at line %d", edit.getBeginA());
case REPLACE:
return String.format("~ replace lines %d-%d with lines %d-%d",
edit.getBeginA(), edit.getEndA() - 1,
edit.getBeginB(), edit.getEndB() - 1);
default:
return " unchanged";
}
}
/**
* 关闭资源
*/
@Override
public void close() {
if (revWalk != null) {
revWalk.close();
}
if (git != null) {
git.close();
}
if (repository != null) {
repository.close();
}
}
}
四、使用示例
Main.java(入口示例)
package com.git.history;
import com.git.history.model.CommitInfo;
import com.git.history.model.FileChangeInfo;
import com.git.history.service.GitHistoryAnalyzer;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@Slf4j
public class Main {
public static void main(String[] args) {
// 仓库路径(可以是 .git 目录或工作目录)
String repoPath = "/path/to/your/repo";
try (GitHistoryAnalyzer analyzer = new GitHistoryAnalyzer(repoPath)) {
// 1. 获取近一年所有提交 hash
List<String> commitHashes = analyzer.getCommitsSinceOneYear();
log.info("找到 {} 个提交", commitHashes.size());
// 2. 遍历每个提交
for (int i = 0; i < commitHashes.size(); i++) {
String hash = commitHashes.get(i);
log.info("处理第 {}/{} 个提交: {}", i + 1, commitHashes.size(), hash);
// 获取提交元信息
CommitInfo meta = analyzer.getCommitMeta(hash);
log.info("作者: {}, 时间: {}, 消息: {}",
meta.getAuthorName(),
meta.getAuthorTime(),
meta.getSubject());
// 获取 diff 信息
List<FileChangeInfo> changes = analyzer.getCommitDiff(hash);
for (FileChangeInfo change : changes) {
log.info(" 文件: {}, 变更类型: {}",
change.getPath(),
change.getChangeType());
log.info(" Hunk 数量: {}", change.getHunks().size());
}
}
} catch (Exception e) {
log.error("解析失败", e);
}
}
}
五、代码结构总结
src/main/java/com/git/history/
├── model/
│ ├── CommitInfo.java // 提交元信息 POJO
│ ├── FileChangeInfo.java // 文件变更信息 POJO
│ └── DiffHunkInfo.java // Diff Hunk 信息 POJO
├── service/
│ └── GitHistoryAnalyzer.java // 核心分析服务(主要逻辑)
└── Main.java // 使用示例入口
六、关键技术点说明
| 功能 | JGit API | 说明 |
|---|---|---|
| 获取提交列表 | git.log().setRevFilter(CommitTimeRevFilter.after(since)).call() |
使用 CommitTimeRevFilter 按时间过滤,相当于 --since="1 year ago" |
| 获取提交元信息 | RevCommit.getAuthorIdent().getName() / .getEmailAddress() / .getWhen() / .getShortMessage() |
对应命令2的 %an, %ae, %at, %s |
| 获取纯净 diff | DiffFormatter + setContext(0) + setDetectRenames(true) |
setContext(0) 相当于 --unified=0 |
| 处理初始提交 | new EmptyTreeIterator() |
父提交为空时,对比空树 |
| 提取 Hunk 信息 | FileHeader.getHunks() → HunkHeader.getOldStart(), .getNewStart() 等 |
提取行号范围 |
| 检测重命名 | DiffFormatter.setDetectRenames(true) |
自动检测文件重命名,DiffEntry.getChangeType() 返回 RENAME |
