From 54bcd63e334e4d8257a20d30e35da112e82263ed Mon Sep 17 00:00:00 2001 From: shizhendong Date: Fri, 1 Nov 2024 18:26:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20ASW-123=20=E6=96=B0=E5=A2=9E=20applicat?= =?UTF-8?q?ion=20MR=20diff,conflict,commit=20=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 参考 gitlab 实现 merge request 功能 --- .../net/geedge/asw/common/util/RCode.java | 2 + .../module/app/controller/GitController.java | 93 ++- .../app/service/IApplicationMergeService.java | 13 + .../asw/module/app/service/IGitService.java | 30 +- .../impl/ApplicationMergeServiceImpl.java | 179 ++++- .../app/service/impl/GitServiceImpl.java | 713 +++++++++++++++--- .../resources/db/migration/R__AZ_sys_i18n.sql | 4 + 7 files changed, 932 insertions(+), 102 deletions(-) diff --git a/src/main/java/net/geedge/asw/common/util/RCode.java b/src/main/java/net/geedge/asw/common/util/RCode.java index 8d8e0ec..8563a2d 100644 --- a/src/main/java/net/geedge/asw/common/util/RCode.java +++ b/src/main/java/net/geedge/asw/common/util/RCode.java @@ -62,6 +62,8 @@ public enum RCode { // GIT GIT_COMMIT_CONFLICT_ERROR(203001, "Commit failed; fix conflicts and then commit the result"), GIT_MERGE_FAILED(203002, "Merge failed"), + GIT_PARENT_COMMITID_NOT_FOUND(203003, "Parent commitId not found"), + GIT_BINARY_CONFLICT_ERROR(203004, "Binary file conflict found; resolve conflicts in binary files manually"), // Runner diff --git a/src/main/java/net/geedge/asw/module/app/controller/GitController.java b/src/main/java/net/geedge/asw/module/app/controller/GitController.java index a48cbfa..3fdd5b7 100644 --- a/src/main/java/net/geedge/asw/module/app/controller/GitController.java +++ b/src/main/java/net/geedge/asw/module/app/controller/GitController.java @@ -11,6 +11,7 @@ import net.geedge.asw.module.app.service.IGitService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -56,6 +57,46 @@ public class GitController { return R.ok(); } + @GetMapping("/{workspaceId}/branch/{branchName}/commits") + public R listBranchCommit(@PathVariable("workspaceId") String workspaceId, + @PathVariable("branchName") String branchName, + @RequestParam(required = false) String path, + @RequestParam(required = false) Integer current, + @RequestParam(required = false) Integer size) { + List> records = gitService.listBranchCommit(workspaceId, branchName, path); + if (null != current && null != size) { + int fromIndex = (current - 1) * size; + int toIndex = Math.min(fromIndex + size, records.size()); + records = records.subList(fromIndex, toIndex); + } + return R.ok().putData("records", records); + } + + @GetMapping({ + "/{workspaceId}/branch/{branchName}/commit/{commitId}/diff", + "/{workspaceId}/branch/{branchName}/commit/{commitId}/diff/{oldCommitId}" + }) + public R branchCommitDiff(@PathVariable("workspaceId") String workspaceId, + @PathVariable("branchName") String branchName, + @PathVariable("commitId") String commitId, + @PathVariable(value = "oldCommitId", required = false) String oldCommitId) { + + if (T.StrUtil.isEmpty(oldCommitId)) { + // oldCommitId 为空默认对比 parent commitId + oldCommitId = gitService.getParentCommitId(workspaceId, branchName, commitId); + } + + List> diffList = gitService.getDiffFileListInCommits(workspaceId, commitId, oldCommitId); + Map record = T.MapUtil.builder() + .put("oldBranch", branchName) + .put("newBranch", branchName) + .put("oldCommitId", oldCommitId) + .put("newCommitId", commitId) + .put("files", diffList) + .build(); + return R.ok().putData("record", record); + } + @GetMapping("/{workspaceId}/branch/{branchName}/application") public R listApplication(@PathVariable("workspaceId") String workspaceId, @PathVariable("branchName") String branchName, @@ -153,8 +194,52 @@ public class GitController { return R.ok(page); } + @GetMapping("/{workspaceId}/mr/{mrId}") + public R infoMr(@PathVariable("mrId") String mrId) { + ApplicationMergeEntity record = applicationMergeService.queryInfo(mrId); + return R.ok().putData("record", record); + } + + @GetMapping("/{workspaceId}/mr/{mrId}/commits") + public R listMrCommit(@PathVariable("mrId") String mrId) { + List> records = applicationMergeService.listMrCommit(mrId); + return R.ok().putData("records", records); + } + + @GetMapping("/{workspaceId}/mr/{mrId}/diffs") + public R mrDiff(@PathVariable("mrId") String mrId) { + Map record = applicationMergeService.mrCommitDiff(mrId); + return R.ok().putData("record", record); + } + + @GetMapping("/{workspaceId}/mr/{mrId}/conflicts") + public R getMrConflictList(@PathVariable("mrId") String mrId) { + Map record = applicationMergeService.getMrConflictList(mrId); + return R.ok().putData("record", record); + } + + @PostMapping("/{workspaceId}/mr/{mrId}/conflicts") + public R resolveMrConflicts(@PathVariable("mrId") String mrId, @RequestBody Map body) { + String commitId = T.MapUtil.getStr(body, "commitId"); + String commitMessage = T.MapUtil.getStr(body, "commitMessage"); + List> files = T.MapUtil.get(body, "files", List.class, new ArrayList()); + T.VerifyUtil.is(commitId).notEmpty(RCode.PARAM_CANNOT_EMPTY) + .and(commitMessage).notEmpty(RCode.PARAM_CANNOT_EMPTY) + .and(files).notEmpty(RCode.PARAM_CANNOT_EMPTY); + + for (Map m : files) { + String path = T.MapUtil.getStr(m, "path"); + String content = T.MapUtil.getStr(m, "content"); + T.VerifyUtil.is(path).notEmpty(RCode.PARAM_CANNOT_EMPTY) + .and(content).notEmpty(RCode.PARAM_CANNOT_EMPTY); + } + + applicationMergeService.resolveMrConflicts(mrId, body); + return R.ok(); + } + @PostMapping("/{workspaceId}/mr") - public synchronized R newMr(@PathVariable("workspaceId") String workspaceId, @RequestBody ApplicationMergeEntity entity) { + public R newMr(@PathVariable("workspaceId") String workspaceId, @RequestBody ApplicationMergeEntity entity) { T.VerifyUtil.is(entity).notNull() .and(entity.getTitle()).notEmpty(RCode.PARAM_CANNOT_EMPTY) .and(entity.getTargetBranch()).notEmpty(RCode.PARAM_CANNOT_EMPTY) @@ -164,6 +249,12 @@ public class GitController { entity.setWorkspaceId(workspaceId); ApplicationMergeEntity record = applicationMergeService.newMr(entity); + return R.ok().putData("record", record); + } + + @PutMapping("/{workspaceId}/mr/{mrId}") + public synchronized R mergeMr(@PathVariable("mrId") String mrId) { + ApplicationMergeEntity record = applicationMergeService.mergeMr(mrId); if (T.StrUtil.equals("success", record.getStatus())) { return R.ok().putData("record", record); } diff --git a/src/main/java/net/geedge/asw/module/app/service/IApplicationMergeService.java b/src/main/java/net/geedge/asw/module/app/service/IApplicationMergeService.java index 5ce7156..74e4af9 100644 --- a/src/main/java/net/geedge/asw/module/app/service/IApplicationMergeService.java +++ b/src/main/java/net/geedge/asw/module/app/service/IApplicationMergeService.java @@ -4,12 +4,25 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; import net.geedge.asw.module.app.entity.ApplicationMergeEntity; +import java.util.List; import java.util.Map; public interface IApplicationMergeService extends IService { + ApplicationMergeEntity queryInfo(String mrId); + Page queryList(Map params); ApplicationMergeEntity newMr(ApplicationMergeEntity entity); + List> listMrCommit(String mrId); + + Map mrCommitDiff(String mrId); + + Map getMrConflictList(String mrId); + + void resolveMrConflicts(String mrId, Map body); + + ApplicationMergeEntity mergeMr(String mrId); + } diff --git a/src/main/java/net/geedge/asw/module/app/service/IGitService.java b/src/main/java/net/geedge/asw/module/app/service/IGitService.java index 5579c1e..a3ac86b 100644 --- a/src/main/java/net/geedge/asw/module/app/service/IGitService.java +++ b/src/main/java/net/geedge/asw/module/app/service/IGitService.java @@ -1,7 +1,11 @@ package net.geedge.asw.module.app.service; +import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; +import java.io.IOException; import java.util.List; import java.util.Map; @@ -13,11 +17,21 @@ public interface IGitService { List> listBranch(String workspaceId, String search); - Map infoBranch(String workspaceId, String branchName); + Map infoBranch(String workspaceId, String branch); - Map newBranch(String workspaceId, String branchName, String ref); + Map newBranch(String workspaceId, String branch, String ref); - void deleteBranch(String workspaceId, String branchName); + void deleteBranch(String workspaceId, String branch); + + List> listBranchCommit(String workspaceId, String branch, String path); + + String getParentCommitId(String workspaceId, String branch, String commitId); + + String getLatestCommitId(String workspaceId, String branch); + + List> getDiffFileListInCommits(String workspaceId, String commitId, String oldCommitId); + + List> getDiffFileListInBranches(String workspaceId, String srcBranch, String tgtBranch); String mergeBranch(String workspaceId, String sourceBranch, String targetBranch, String message) throws RuntimeException; @@ -35,4 +49,14 @@ public interface IGitService { Map infoApplicationFileContent(String workspaceId, String branch, String applicationName, String commitId, String file); + List> listCommitAfterMergeBase(String workspaceId, String srcBranch, String tgtBranch); + + List getConflictFilePathInBranches(String workspaceId, String srcBranch, String tgtBranch); + + List> getConflictFileInfoInBranches(String workspaceId, String srcBranch, String tgtBranch); + + void resolveMrConflicts(String workspaceId, String srcBranch, String tgtBranch, String commitId, String commitMessage, List> files); + + boolean commitIndex(Repository repo, String branch, DirCache index, ObjectId parentId1, ObjectId parentId2, String message) throws IOException, ConcurrentRefUpdateException; + } diff --git a/src/main/java/net/geedge/asw/module/app/service/impl/ApplicationMergeServiceImpl.java b/src/main/java/net/geedge/asw/module/app/service/impl/ApplicationMergeServiceImpl.java index a799283..04d9ac0 100644 --- a/src/main/java/net/geedge/asw/module/app/service/impl/ApplicationMergeServiceImpl.java +++ b/src/main/java/net/geedge/asw/module/app/service/impl/ApplicationMergeServiceImpl.java @@ -2,9 +2,11 @@ package net.geedge.asw.module.app.service.impl; import cn.dev33.satoken.stp.StpUtil; import cn.hutool.log.Log; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import net.geedge.asw.common.config.Query; +import net.geedge.asw.common.util.RCode; import net.geedge.asw.common.util.T; import net.geedge.asw.module.app.dao.ApplicationMergeDao; import net.geedge.asw.module.app.entity.ApplicationMergeEntity; @@ -15,6 +17,7 @@ import net.geedge.asw.module.sys.service.ISysUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -29,6 +32,25 @@ public class ApplicationMergeServiceImpl extends ServiceImpl infoCommit = gitService.infoCommit(entity.getWorkspaceId(), commitId); + entity.setCommit(infoCommit); + } catch (Exception e) { + } + } + + SysUserEntity user = userService.getById(entity.getCreateUserId()); + entity.setCreateUser(user); + return entity; + } + @Override public Page queryList(Map params) { Page page = new Query(ApplicationMergeEntity.class).getPage(params); @@ -51,23 +73,155 @@ public class ApplicationMergeServiceImpl extends ServiceImpl> listMrCommit(String mrId) { + ApplicationMergeEntity entity = this.getById(mrId); + T.VerifyUtil.is(entity).notEmpty(RCode.SYS_RECORD_NOT_FOUND); + + String workspaceId = entity.getWorkspaceId(); + String sourceBranch = entity.getSourceBranch(); + String targetBranch = entity.getTargetBranch(); + + // 获取 sourceBranch targetBranch 共同祖先之后, sourceBranch 的提交记录 + List> listCommitAfterMergeBase = gitService.listCommitAfterMergeBase(workspaceId, sourceBranch, targetBranch); + return listCommitAfterMergeBase; + } + + @Override + public Map mrCommitDiff(String mrId) { + ApplicationMergeEntity entity = this.getById(mrId); + T.VerifyUtil.is(entity).notEmpty(RCode.SYS_RECORD_NOT_FOUND); + + String workspaceId = entity.getWorkspaceId(); + String sourceBranch = entity.getSourceBranch(); + String targetBranch = entity.getTargetBranch(); + + // 获取 sourceBranch,targetBranch 合并冲突文件 + List conflictFileList = gitService.getConflictFilePathInBranches(workspaceId, sourceBranch, targetBranch); + + + // 获取 sourceBranch,targetBranch 文件差异 + String commitA = gitService.getLatestCommitId(workspaceId, sourceBranch); + String commitB = gitService.getLatestCommitId(workspaceId, targetBranch); + + List> diffFileListInCommits = gitService.getDiffFileListInCommits(workspaceId, commitA, commitB); + diffFileListInCommits.parallelStream() + .forEach(m -> { + T.MapUtil.renameKey(m, "oldContent", "sourceContent"); + T.MapUtil.renameKey(m, "newContent", "targetContent"); + + String path = T.MapUtil.getStr(m, "path"); + m.put("conflict", false); + if (conflictFileList.contains(path)) { + m.put("conflict", true); + } + }); + + Map m = T.MapUtil.builder() + .put("sourceBranch", sourceBranch) + .put("targetBranch", targetBranch) + .put("sourceCommitId", commitA) + .put("targetCommitId", commitB) + .put("files", diffFileListInCommits) + .build(); + return m; + } + + @Override + public Map getMrConflictList(String mrId) { + ApplicationMergeEntity entity = this.getById(mrId); + T.VerifyUtil.is(entity).notEmpty(RCode.SYS_RECORD_NOT_FOUND); + + String workspaceId = entity.getWorkspaceId(); + String sourceBranch = entity.getSourceBranch(); + String targetBranch = entity.getTargetBranch(); + + List> conflictFileInfoInBranches = gitService.getConflictFileInfoInBranches(workspaceId, sourceBranch, targetBranch); + if (T.CollUtil.isNotEmpty(conflictFileInfoInBranches)) { + // srcBranch->tgtBranch 有冲突,先从 tgtBranch merge 到 srcBranch,在 srcBranch 处理后再合并,参考 gitlab + StringBuilder commitMessage = new StringBuilder(); + commitMessage.append("Merge branch '").append(targetBranch).append("' into '").append(sourceBranch).append("'\n\n"); + commitMessage.append("# Conflicts:\n"); + for (Map map : conflictFileInfoInBranches) { + String path = T.MapUtil.getStr(map, "path"); + commitMessage.append("# ").append(path).append("\n"); + } + + String parentId = gitService.getLatestCommitId(workspaceId, sourceBranch); + Map m = T.MapUtil.builder() + .put("sourceBranch", sourceBranch) + .put("targetBranch", targetBranch) + .put("commitId", parentId) + .put("commitMessage", commitMessage.toString()) + .put("files", conflictFileInfoInBranches) + .build(); + return m; + } + + return T.MapUtil.builder() + .put("sourceBranch", sourceBranch) + .put("targetBranch", targetBranch) + .put("commitId", null) + .put("commitMessage", null) + .put("files", conflictFileInfoInBranches) + .build(); + } + + /** + * resolve mr conflicts + * + * @param mrId + * @param body + */ + @Override + public void resolveMrConflicts(String mrId, Map body) { + ApplicationMergeEntity entity = this.getById(mrId); + T.VerifyUtil.is(entity).notEmpty(RCode.SYS_RECORD_NOT_FOUND); + + String workspaceId = entity.getWorkspaceId(); + String sourceBranch = entity.getSourceBranch(); + String targetBranch = entity.getTargetBranch(); + + String commitId = T.MapUtil.getStr(body, "commitId"); + String commitMessage = T.MapUtil.getStr(body, "commitMessage"); + List> files = T.MapUtil.get(body, "files", List.class, new ArrayList()); + + gitService.resolveMrConflicts(workspaceId, sourceBranch, targetBranch, commitId, commitMessage, files); + } + + /** + * merge operation + * + * @param mrId + * @return + */ + @Override + public ApplicationMergeEntity mergeMr(String mrId) { + ApplicationMergeEntity entity = this.getById(mrId); + T.VerifyUtil.is(entity).notEmpty(RCode.SYS_RECORD_NOT_FOUND); + String srcBranch = entity.getSourceBranch(); String tgtBranch = entity.getTargetBranch(); String message = entity.getTitle(); String workspaceId = entity.getWorkspaceId(); - entity.setCreateTimestamp(System.currentTimeMillis()); - entity.setCreateUserId(StpUtil.getLoginIdAsString()); - // merge try { String commitId = gitService.mergeBranch(workspaceId, srcBranch, tgtBranch, message); entity.setCommitId(commitId); entity.setStatus("success"); - - Map infoCommit = gitService.infoCommit(workspaceId, commitId); - entity.setCommit(infoCommit); } catch (Exception e) { log.error(e, "[newMr] [merge error] [workspaceId: {}] [srcBranch: {}] [tgtBranch: {}] [msg: {}]", workspaceId, srcBranch, tgtBranch, e.getMessage()); entity.setCommitId(""); @@ -85,12 +239,13 @@ public class ApplicationMergeServiceImpl extends ServiceImpl() + .eq(ApplicationMergeEntity::getId, mrId) + .set(ApplicationMergeEntity::getCommitId, entity.getCommitId()) + .set(ApplicationMergeEntity::getStatus, entity.getStatus()) + .set(ApplicationMergeEntity::getMessage, entity.getMessage()) + ); return entity; } diff --git a/src/main/java/net/geedge/asw/module/app/service/impl/GitServiceImpl.java b/src/main/java/net/geedge/asw/module/app/service/impl/GitServiceImpl.java index 772df5b..aae649c 100644 --- a/src/main/java/net/geedge/asw/module/app/service/impl/GitServiceImpl.java +++ b/src/main/java/net/geedge/asw/module/app/service/impl/GitServiceImpl.java @@ -12,19 +12,24 @@ import net.geedge.asw.module.sys.service.ISysUserService; import net.geedge.asw.module.workspace.entity.WorkspaceEntity; import net.geedge.asw.module.workspace.service.IWorkspaceService; import org.apache.commons.io.FilenameUtils; -import org.eclipse.jgit.api.CreateBranchCommand; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.MergeCommand; -import org.eclipse.jgit.api.MergeResult; +import org.eclipse.jgit.api.*; +import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.diff.*; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.*; +import org.eclipse.jgit.merge.MergeFormatter; import org.eclipse.jgit.merge.MergeStrategy; +import org.eclipse.jgit.merge.RecursiveMerger; +import org.eclipse.jgit.merge.ThreeWayMerger; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevSort; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.revwalk.filter.RevFilter; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.transport.PushResult; import org.eclipse.jgit.transport.RemoteRefUpdate; @@ -36,8 +41,11 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; @@ -213,6 +221,8 @@ public class GitServiceImpl implements IGitService { resultList.add(m); } + revCommits.close(); + revCommits.dispose(); } catch (GitAPIException | IOException e) { log.error(e, "[listBranch] [error] [workspaceId: {}]", workspaceId); throw new ASWException(RCode.ERROR); @@ -259,6 +269,198 @@ public class GitServiceImpl implements IGitService { } } + @Override + public List> listBranchCommit(String workspaceId, String branch, String path) { + List> resultList = T.ListUtil.list(true); + try (Git git = this.getGitInstance(workspaceId)) { + ObjectId branchRef = git.getRepository().resolve(branch); + LogCommand command = git.log().add(branchRef); + if (T.StrUtil.isNotEmpty(path)) { + command.addPath(path); + } + + Iterable iterable = command.call(); + for (RevCommit commit : iterable) { + resultList.add(this.buildAswCommitInfo(commit)); + } + } catch (GitAPIException | IOException e) { + log.error(e, "[listBranchCommit] [error] [workspaceId: {}] [branch: {}] [path: {}]", workspaceId, branch, path); + throw new RuntimeException(e); + } + return resultList; + } + + @Override + public String getParentCommitId(String workspaceId, String branch, String commitId) { + try (Git git = this.getGitInstance(workspaceId); + Repository repository = git.getRepository(); + RevWalk revWalk = new RevWalk(repository);) { + + Ref branchRef = repository.findRef(branch); + revWalk.markStart(revWalk.parseCommit(branchRef.getObjectId())); + + String parentCommitId = null; + Iterator iterator = revWalk.iterator(); + while (iterator.hasNext()) { + RevCommit currentCommit = iterator.next(); + if (currentCommit.getId().getName().equals(commitId)) { + if (iterator.hasNext()) { + parentCommitId = iterator.next().getName(); + } + break; + } + } + + if (T.StrUtil.isEmpty(parentCommitId)) { + throw new ASWException(RCode.GIT_PARENT_COMMITID_NOT_FOUND); + } + return parentCommitId; + } catch (IOException e) { + log.error(e, "[getParentCommitId] [error] [workspaceId: {}] [branch: {}] [commitId: {}]", workspaceId, branch, commitId); + throw new RuntimeException(e); + } + } + + @Override + public String getLatestCommitId(String workspaceId, String branch) { + try (Git git = this.getGitInstance(workspaceId); + Repository repository = git.getRepository(); + RevWalk revWalk = new RevWalk(repository);) { + RevCommit commit = revWalk.parseCommit(repository.resolve(branch)); + return commit.getName(); + } catch (IOException e) { + log.error(e, "[getLatestCommitId] [error] [workspaceId: {}] [branch: {}]", workspaceId, branch); + throw new RuntimeException(e); + } + } + + /** + * 获取 commitIdA -> commitIdB 文件差异 + * + * @param workspaceId + * @param commitIdA + * @param commitIdB + * @return + */ + @Override + public List> getDiffFileListInCommits(String workspaceId, String commitIdA, String commitIdB) { + try (Git git = this.getGitInstance(workspaceId); + Repository repository = git.getRepository(); + RevWalk revWalk = new RevWalk(repository); + DiffFormatter diffFormatter = new DiffFormatter(null)) { + + CanonicalTreeParser oldTree = new CanonicalTreeParser(); + oldTree.reset(repository.newObjectReader(), revWalk.parseCommit(repository.resolve(commitIdB)).getTree()); + + CanonicalTreeParser newTree = new CanonicalTreeParser(); + newTree.reset(repository.newObjectReader(), revWalk.parseCommit(repository.resolve(commitIdA)).getTree()); + + // diff + List> files = T.ListUtil.list(true); + + diffFormatter.setRepository(repository); + diffFormatter.setDiffComparator(RawTextComparator.DEFAULT); + diffFormatter.setDetectRenames(true); + + List diffs = diffFormatter.scan(oldTree, newTree); + for (DiffEntry diff : diffs) { + int addedLines = 0; + int deletedLines = 0; + EditList edits = diffFormatter.toFileHeader(diff).toEditList(); + for (Edit edit : edits) { + switch (edit.getType()) { + case INSERT: + addedLines += edit.getLengthB(); + break; + case DELETE: + deletedLines += edit.getLengthA(); + break; + case REPLACE: + addedLines += edit.getLengthB(); + deletedLines += edit.getLengthA(); + break; + default: + break; + } + } + + String path = null, encoding = null, oldContent = null, newContent = null; + switch (diff.getChangeType()) { + case ADD: { + path = diff.getNewPath(); + Map fileContent = this.getFileContent(repository, path, diff.getNewId().toObjectId()); + encoding = T.MapUtil.getStr(fileContent, "encoding", ""); + newContent = T.MapUtil.getStr(fileContent, "content", ""); + break; + } + case DELETE: { + path = diff.getOldPath(); + Map fileContent = this.getFileContent(repository, path, diff.getOldId().toObjectId()); + encoding = T.MapUtil.getStr(fileContent, "encoding", ""); + oldContent = T.MapUtil.getStr(fileContent, "content", ""); + break; + } + case MODIFY: { + path = diff.getOldPath(); + Map fileContent = this.getFileContent(repository, path, diff.getOldId().toObjectId()); + oldContent = T.MapUtil.getStr(fileContent, "content", ""); + + Map fileContent1 = this.getFileContent(repository, path, diff.getNewId().toObjectId()); + encoding = T.MapUtil.getStr(fileContent1, "encoding", ""); + newContent = T.MapUtil.getStr(fileContent1, "content", ""); + break; + } + default: + break; + } + files.add( + T.MapUtil.builder() + .put("path", path) + .put("addedLines", (addedLines == 0 ? 0 : ++addedLines)) + .put("removedLines", (deletedLines == 0 ? 0 : ++deletedLines)) + .put("encoding", encoding) + .put("oldContent", oldContent) + .put("newContent", newContent) + .build() + ); + } + return files; + } catch (IOException e) { + log.error(e, "[getDiffFileListInCommits] [error] [workspaceId: {}] [commitId: {}] [oldCommitId: {}]", workspaceId, commitIdA, commitIdB); + throw new RuntimeException(e); + } + } + + /** + * 获取 branchA -> branchB 文件差异 + * + * @param workspaceId + * @param branchA + * @param branchB + * @return + */ + @Override + public List> getDiffFileListInBranches(String workspaceId, String branchA, String branchB) { + try (Git git = this.getGitInstance(workspaceId); + Repository repository = git.getRepository(); + RevWalk revWalk = new RevWalk(repository);) { + + RevCommit commitA = revWalk.parseCommit(repository.resolve(branchA)); + RevCommit commitB = revWalk.parseCommit(repository.resolve(branchB)); + + List> diffFileListInCommits = this.getDiffFileListInCommits(workspaceId, commitA.getName(), commitB.getName()); + diffFileListInCommits.parallelStream() + .forEach(m -> { + T.MapUtil.renameKey(m, "oldContent", "sourceContent"); + T.MapUtil.renameKey(m, "newContent", "targetContent"); + }); + return diffFileListInCommits; + } catch (IOException e) { + log.error(e, "[getDiffFileListInBranches] [error]"); + throw new RuntimeException(e); + } + } + @Override public String mergeBranch(String workspaceId, String srcBranch, String tgtBranch, String message) throws RuntimeException { log.info("[mergeBranch] [begin] [workspaceId: {}] [srcBranch: {}] [tgtBranch: {}]", workspaceId, srcBranch, tgtBranch); @@ -458,15 +660,8 @@ public class GitServiceImpl implements IGitService { .put("path", treeWalk.getPathString()) .build(); - ObjectLoader loader = repository.open(treeWalk.getObjectId(0)); - if (this.isBinary(treeWalk.getNameString())) { - String content = Base64.getEncoder().encodeToString(loader.getBytes()); - m.put("encoding", "base64"); - m.put("content", content); - } else { - m.put("encoding", ""); - m.put("content", T.StrUtil.utf8Str(loader.getBytes())); - } + Map fileContent = this.getFileContent(repository, treeWalk.getPathString(), treeWalk.getObjectId(0)); + m.putAll(fileContent); // lastCommitId Iterable iterable = git.log() @@ -551,7 +746,7 @@ public class GitServiceImpl implements IGitService { String message = String.format("feat: add %s application", applicationName); this.createCommit(repository, branch, newTreeId, message); } - } catch (IOException e) { + } catch (IOException | ConcurrentRefUpdateException e) { log.error(e, "[newApplication] [error] [workspaceId: {}] [branch: {}] [application: {}]", workspaceId, branch, applicationName); throw new ASWException(RCode.ERROR); } @@ -590,7 +785,7 @@ public class GitServiceImpl implements IGitService { String message = String.format("refactor: remove %s application", applicationName); this.createCommit(repository, branch, newTreeId, message); } - } catch (IOException e) { + } catch (IOException | ConcurrentRefUpdateException e) { log.error(e, "[deleteApplication] [error] [workspaceId: {}] [branch: {}] [application: {}]", workspaceId, branch, applicationName); throw new ASWException(RCode.ERROR); } @@ -729,7 +924,7 @@ public class GitServiceImpl implements IGitService { .put("path", path) .build(); - try (Git git = getGitInstance(workspaceId)) { + try (Git git = this.getGitInstance(workspaceId)) { Repository repository = git.getRepository(); try (TreeWalk treeWalk = new TreeWalk(repository); @@ -743,15 +938,8 @@ public class GitServiceImpl implements IGitService { while (treeWalk.next()) { if (T.StrUtil.equals(path, treeWalk.getPathString())) { - ObjectLoader loader = repository.open(treeWalk.getObjectId(0)); - if (this.isBinary(treeWalk.getNameString())) { - String content = Base64.getEncoder().encodeToString(loader.getBytes()); - result.put("encoding", "base64"); - result.put("content", content); - } else { - result.put("encoding", ""); - result.put("content", T.StrUtil.utf8Str(loader.getBytes())); - } + Map fileContent = this.getFileContent(repository, treeWalk.getPathString(), treeWalk.getObjectId(0)); + result.putAll(fileContent); } } } @@ -762,6 +950,419 @@ public class GitServiceImpl implements IGitService { return result; } + /** + * 获取 srcBranch tgtBranch 共同祖先之后, srcBranch 的提交记录 + * + * @param workspaceId + * @param srcBranch + * @param tgtBranch + * @return + */ + @Override + public List> listCommitAfterMergeBase(String workspaceId, String srcBranch, String tgtBranch) { + log.info("[listCommitAfterMergeBase] [begin] [workspaceId: {}] [srcBranch: {}] [tgtBranch: {}]", workspaceId, srcBranch, tgtBranch); + List> commitList = T.ListUtil.list(true); + try (Git git = this.getGitInstance(workspaceId); + Repository repository = git.getRepository();) { + ObjectId branch1Id = repository.resolve(srcBranch); + ObjectId branch2Id = repository.resolve(tgtBranch); + + // Find the merge base + String mergeBaseId = this.getMergeBase(repository, branch1Id, branch2Id); + log.info("[listCommitAfterMergeBase] [mergeBase: {}]", mergeBaseId); + if (T.StrUtil.isEmpty(mergeBaseId)) { + return commitList; + } + + // Walk branch1's commits since the merge base + try (RevWalk revWalk = new RevWalk(repository)) { + revWalk.sort(RevSort.TOPO); + + RevCommit mergeBase = revWalk.parseCommit(ObjectId.fromString(mergeBaseId)); + revWalk.markStart(revWalk.parseCommit(branch1Id)); + revWalk.markUninteresting(mergeBase); + + for (RevCommit commit : revWalk) { + commitList.add(this.buildAswCommitInfo(commit)); + + List parentIds = Arrays.stream(commit.getParents()).map(RevCommit::getName).collect(Collectors.toList()); + if (parentIds.contains(mergeBaseId)) { + break; + } + } + } + } catch (IOException e) { + log.error(e, "[listCommitAfterMergeBase] [error]"); + throw new RuntimeException(e.getMessage()); + } finally { + log.info("[listCommitAfterMergeBase] [finshed] [workspaceId: {}] [commits size: {}]", workspaceId, commitList.size()); + } + return commitList; + } + + /** + * 获取 srcBranch -> tgtBranch 冲突文件路径 + * srcBranch 合并到 tgtBranch + * + * @param workspaceId + * @param srcBranch + * @param tgtBranch + */ + @Override + public List getConflictFilePathInBranches(String workspaceId, String srcBranch, String tgtBranch) { + log.info("[getConflictFileListInBranches] [begin] [workspaceId: {}] [srcBranch: {}] [tgtBranch: {}]", workspaceId, srcBranch, tgtBranch); + try (Git git = this.getGitInstance(workspaceId); + Repository repository = git.getRepository(); + RevWalk revWalk = new RevWalk(repository);) { + + RevCommit commitA = revWalk.parseCommit(repository.resolve(srcBranch)); + RevCommit commitB = revWalk.parseCommit(repository.resolve(tgtBranch)); + + // Find the merge base + String mergeBaseId = this.getMergeBase(repository, commitA, commitB); + log.info("[getConflictFileListInBranches] [mergeBase: {}]", mergeBaseId); + + RevCommit mergeBase = revWalk.parseCommit(repository.resolve(mergeBaseId)); + + ThreeWayMerger threeWayMerger = (ThreeWayMerger) MergeStrategy.RECURSIVE.newMerger(repository, true); + threeWayMerger.setBase(mergeBase); + + boolean isOk = threeWayMerger.merge(commitA, commitB); + log.info("[getConflictFileListInBranches] [merge result] [isOk: {}]", isOk); + if (!isOk) { + List unmergedPaths = ((RecursiveMerger) threeWayMerger).getUnmergedPaths(); + return unmergedPaths; + } + return T.ListUtil.empty(); + } catch (IOException e) { + log.error(e, "[getConflictFileListInBranches] [error]"); + throw new RuntimeException(e); + } + } + + /** + * 三路对比 srcBranch,tgtBranch,返回冲突文件详情 + * + * @param workspaceId + * @param srcBranch + * @param tgtBranch + * @return + */ + @Override + public List> getConflictFileInfoInBranches(String workspaceId, String srcBranch, String tgtBranch) { + log.info("[getConflictFileInfoInBranches] [begin] [workspaceId: {}] [srcBranch: {}] [tgtBranch: {}]", workspaceId, srcBranch, tgtBranch); + try (Git git = this.getGitInstance(workspaceId); + Repository repository = git.getRepository(); + RevWalk revWalk = new RevWalk(repository);) { + + RevCommit commitA = revWalk.parseCommit(repository.resolve(srcBranch)); + RevCommit commitB = revWalk.parseCommit(repository.resolve(tgtBranch)); + + // Find the merge base + String mergeBaseId = this.getMergeBase(repository, commitA, commitB); + log.info("[getConflictFileInfoInBranches] [mergeBase: {}]", mergeBaseId); + + RevCommit mergeBase = revWalk.parseCommit(repository.resolve(mergeBaseId)); + + ThreeWayMerger threeWayMerger = (ThreeWayMerger) MergeStrategy.RECURSIVE.newMerger(repository, true); + threeWayMerger.setBase(mergeBase); + + boolean isOk = threeWayMerger.merge(commitA, commitB); + log.info("[getConflictFileInfoInBranches] [merge result] [isOk: {}]", isOk); + + if (!isOk) { + List> files = T.ListUtil.list(true); + + Map> mergeResults = ((RecursiveMerger) threeWayMerger).getMergeResults(); + for (Map.Entry> entry : mergeResults.entrySet()) { + String unmergedPath = entry.getKey(); + // 暂不支持处理二进制文件冲突 + if (this.isBinary(T.FileNameUtil.getName(unmergedPath))) { + throw new ASWException(RCode.GIT_BINARY_CONFLICT_ERROR); + } + + org.eclipse.jgit.merge.MergeResult mergeResult = entry.getValue(); + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + MergeFormatter formatter = new MergeFormatter(); + + String oursName = unmergedPath; + String theirsName = unmergedPath; + formatter.formatMerge(outputStream, mergeResult, mergeBaseId, oursName, theirsName, StandardCharsets.UTF_8); + files.add( + T.MapUtil.builder() + .put("path", unmergedPath) + .put("content", outputStream.toString(StandardCharsets.UTF_8.name())) + .put("conflict", true) + .build() + ); + } + } + return files; + } + return T.ListUtil.empty(); + } catch (IOException e) { + log.error(e, "[getConflictFileInfoInBranches] [error]"); + throw new RuntimeException(e); + } + } + + /** + * 将 file object 添加到 objectDatabases,也就是 ./objects 目录下 + * + * @param repository + * @param data + * @return + * @throws IOException + */ + private ObjectId insertBlobFileToDatabase(Repository repository, byte[] data) throws IOException { + try (ObjectInserter inserter = repository.getObjectDatabase().newInserter()) { + ObjectId blobId = inserter.insert(Constants.OBJ_BLOB, data); + inserter.flush(); + return blobId; + } + } + + /** + * 根据 path,objectId 读取文件内容 + * 响应 Map,key=encoding,content + * + * @param repository + * @param path + * @param objectId + * @return + * @throws IOException + */ + private Map getFileContent(Repository repository, String path, ObjectId objectId) throws IOException { + String encoding = null, content = null; + + ObjectLoader loader = repository.open(objectId); + if (this.isBinary(T.FileNameUtil.getName(path))) { + encoding = "base64"; + content = Base64.getEncoder().encodeToString(loader.getBytes()); + } else { + content = T.StrUtil.utf8Str(loader.getBytes()); + } + return T.MapUtil.builder() + .put("encoding", encoding) + .put("content", content) + .build(); + } + + /** + * Returns the merge base of two commits or null if there is no common ancestry. + * 返回两个提交的合并基础,如果没有共同祖先,则返回 null。 + * + * @param repository + * @param commitIdA + * @param commitIdB + * @return the commit id of the merge base or null if there is no common base + */ + public String getMergeBase(Repository repository, ObjectId commitIdA, ObjectId commitIdB) { + try (RevWalk rw = new RevWalk(repository)) { + RevCommit a = rw.lookupCommit(commitIdA); + RevCommit b = rw.lookupCommit(commitIdB); + + rw.setRevFilter(RevFilter.MERGE_BASE); + rw.markStart(a); + rw.markStart(b); + RevCommit mergeBase = rw.next(); + if (mergeBase == null) { + return null; + } + return mergeBase.getName(); + } catch (Exception e) { + log.error(e, "Failed to determine merge base"); + } + return null; + } + + /** + * 解决冲突提交到 srcBranch + * 提交指定 srcBranch,tgtBranch 最新 commitId,像 merge commit 一样 + * + * @param workspaceId + * @param srcBranch + * @param tgtBranch + * @param commitId + * @param commitMessage + * @param files + */ + @Override + public void resolveMrConflicts(String workspaceId, String srcBranch, String tgtBranch, String commitId, String commitMessage, List> files) { + try (Git git = this.getGitInstance(workspaceId); + Repository repository = git.getRepository(); + TreeWalk treeWalk = new TreeWalk(repository); + RevWalk revWalk = new RevWalk(repository)) { + + ObjectId srcBranchLatestCommitId = repository.resolve(srcBranch); + + // branch tree + treeWalk.addTree(revWalk.parseTree(srcBranchLatestCommitId)); + treeWalk.setRecursive(true); + + DirCache newTree = DirCache.newInCore(); + DirCacheBuilder newTreeBuilder = newTree.builder(); + + List updateFilePath = files.stream().map(map -> T.MapUtil.getStr(map, "path")).collect(Collectors.toList()); + while (treeWalk.next()) { + String pathString = treeWalk.getPathString(); + // 先删 + if (!updateFilePath.contains(pathString)) { + DirCacheEntry entry = new DirCacheEntry(pathString); + entry.setFileMode(treeWalk.getFileMode(0)); + entry.setObjectId(treeWalk.getObjectId(0)); + newTreeBuilder.add(entry); + } + } + // 后增 + for (Map map : files) { + String path = T.MapUtil.getStr(map, "path"); + DirCacheEntry dirCacheEntry = new DirCacheEntry(path); + dirCacheEntry.setFileMode(FileMode.REGULAR_FILE); + + String content = T.MapUtil.getStr(map, "content"); + ObjectId objectId = this.insertBlobFileToDatabase(repository, content.getBytes()); + dirCacheEntry.setObjectId(objectId); + newTreeBuilder.add(dirCacheEntry); + } + newTreeBuilder.finish(); + + // merge commit + ObjectId parentId1 = ObjectId.fromString(commitId); + RevCommit revCommit = revWalk.parseCommit(repository.resolve(tgtBranch)); + ObjectId parentId2 = revCommit.getId(); + + boolean success = this.commitIndex(repository, srcBranch, newTree, parentId1, parentId2, commitMessage); + log.info("[resolveMrConflicts] [workspaceId: {}] [srcBranch: {}] [tgtBranch: {}] [commitId: {}] [success: {}]", workspaceId, srcBranch, tgtBranch, commitId, success); + } catch (IOException | ConcurrentRefUpdateException e) { + log.error(e, "[resolveMrConflicts] [error] [workspaceId: {}] [srcBranch: {}] [tgtBranch: {}] [commitId: {}]", workspaceId, srcBranch, tgtBranch, commitId); + throw new RuntimeException(e); + } + } + + /** + * commit + * + * @param repo + * @param branch + * @param treeId + * @param message + * @throws IOException + */ + public void createCommit(Repository repo, String branch, ObjectId treeId, String message) throws IOException, ConcurrentRefUpdateException { + try (ObjectInserter inserter = repo.getObjectDatabase().newInserter(); + RevWalk revWalk = new RevWalk(repo);) { + + CommitBuilder builder = new CommitBuilder(); + builder.setTreeId(treeId); + builder.setMessage(message); + + ObjectId branchRef = repo.resolve(branch); + if (null != branchRef) { + RevCommit parentId = revWalk.parseCommit(branchRef); + builder.setParentId(parentId); + } + + SysUserEntity loginUserEntity = userService.getById(StpUtil.getLoginIdAsString()); + builder.setAuthor(new PersonIdent(loginUserEntity.getName(), "asw@geedgenetworks.com")); + builder.setCommitter(new PersonIdent(loginUserEntity.getName(), "asw@geedgenetworks.com")); + + // 插入新的提交对象 + ObjectId commitId = inserter.insert(builder); + inserter.flush(); + + // 更新 branch 指向新的提交 + RefUpdate ru = null; + RefUpdate.Result rc = null; + if (null != branchRef) { + ru = repo.updateRef(T.StrUtil.concat(true, LOCAL_BRANCH_PREFIX, branch)); + ru.setNewObjectId(commitId); + rc = ru.update(); + } else { + ru = repo.updateRef(Constants.HEAD); + ru.setNewObjectId(commitId); + rc = ru.update(); + } + switch (rc) { + case NEW: + case FORCED: + case FAST_FORWARD: + break; + case REJECTED: + case LOCK_FAILURE: + throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD, ru.getRef(), rc); + default: + throw new JGitInternalException(MessageFormat.format( + JGitText.get().updatingRefFailed, branch, commitId.toString(), + rc)); + } + } + } + + /** + * commit index + * + * @param repo + * @param branch + * @param index + * @param parentId1 + * @param parentId2 + * @param message + * @return + * @throws IOException + * @throws ConcurrentRefUpdateException + */ + public boolean commitIndex(Repository repo, String branch, DirCache index, ObjectId parentId1, ObjectId parentId2, String message) throws IOException, ConcurrentRefUpdateException { + boolean success = false; + try (ObjectInserter odi = repo.newObjectInserter()) { + + // new index + ObjectId indexTreeId = index.writeTree(odi); + + // PersonIdent + SysUserEntity loginUserEntity = userService.getById(StpUtil.getLoginIdAsString()); + PersonIdent ident = new PersonIdent(loginUserEntity.getName(), "asw@geedgenetworks.com"); + + CommitBuilder commit = new CommitBuilder(); + commit.setAuthor(ident); + commit.setCommitter(ident); + + if (null != parentId1 && null == parentId2) { + commit.setParentId(parentId1); + } + if (null != parentId1 && null != parentId2) { + commit.setParentIds(parentId1, parentId2); + } + + commit.setTreeId(indexTreeId); + commit.setMessage(message); + + + // insert the commit into the repository + ObjectId commitId = odi.insert(commit); + odi.flush(); + + RefUpdate ru = repo.updateRef(T.StrUtil.concat(true, LOCAL_BRANCH_PREFIX, branch)); + ru.setNewObjectId(commitId); + RefUpdate.Result rc = ru.update(); + switch (rc) { + case NEW: + case FORCED: + case FAST_FORWARD: + success = true; + break; + case REJECTED: + case LOCK_FAILURE: + throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD, ru.getRef(), rc); + default: + throw new JGitInternalException(MessageFormat.format( + JGitText.get().updatingRefFailed, branch, commitId.toString(), + rc)); + } + } + return success; + } + /** * build asw commit info * @@ -796,64 +1397,4 @@ public class GitServiceImpl implements IGitService { return m; } - /** - * 将 file object 添加到 objectDatabases,也就是 ./objects 目录下 - * - * @param repository - * @param data - * @return - * @throws IOException - */ - private ObjectId insertBlobFileToDatabase(Repository repository, byte[] data) throws IOException { - try (ObjectInserter inserter = repository.getObjectDatabase().newInserter()) { - ObjectId blobId = inserter.insert(Constants.OBJ_BLOB, data); - inserter.flush(); - return blobId; - } - } - - /** - * commit - * - * @param repo - * @param branch - * @param treeId - * @param message - * @throws IOException - */ - public void createCommit(Repository repo, String branch, ObjectId treeId, String message) throws IOException { - try (ObjectInserter inserter = repo.getObjectDatabase().newInserter(); - RevWalk revWalk = new RevWalk(repo);) { - - CommitBuilder builder = new CommitBuilder(); - builder.setTreeId(treeId); - builder.setMessage(message); - - ObjectId branchRef = repo.resolve(branch); - if (null != branchRef) { - RevCommit parentId = revWalk.parseCommit(branchRef); - builder.setParentId(parentId); - } - - SysUserEntity loginUserEntity = userService.getById(StpUtil.getLoginIdAsString()); - builder.setAuthor(new PersonIdent(loginUserEntity.getName(), "asw@geedgenetworks.com")); - builder.setCommitter(new PersonIdent(loginUserEntity.getName(), "asw@geedgenetworks.com")); - - // 插入新的提交对象 - ObjectId commitId = inserter.insert(builder); - inserter.flush(); - - // 更新 branch 指向新的提交 - if (null != branchRef) { - RefUpdate refUpdate = repo.updateRef(T.StrUtil.concat(true, LOCAL_BRANCH_PREFIX, branch)); - refUpdate.setNewObjectId(commitId); - refUpdate.update(); - } else { - RefUpdate refUpdate = repo.updateRef(Constants.HEAD); - refUpdate.setNewObjectId(commitId); - refUpdate.update(); - } - } - } - } \ No newline at end of file diff --git a/src/main/resources/db/migration/R__AZ_sys_i18n.sql b/src/main/resources/db/migration/R__AZ_sys_i18n.sql index ca92eae..75359b8 100644 --- a/src/main/resources/db/migration/R__AZ_sys_i18n.sql +++ b/src/main/resources/db/migration/R__AZ_sys_i18n.sql @@ -149,5 +149,9 @@ INSERT INTO `sys_i18n`(`id`, `name`, `code`, `value`, `lang`, `remark`, `update_ INSERT INTO `sys_i18n`(`id`, `name`, `code`, `value`, `lang`, `remark`, `update_user_id`, `update_timestamp`) VALUES (241, '302003', 'PLAYBOOK_INVALID_FILE', '无效文件', 'zh', '', 'admin', 1724030366000); INSERT INTO `sys_i18n`(`id`, `name`, `code`, `value`, `lang`, `remark`, `update_user_id`, `update_timestamp`) VALUES (242, '202004', 'PACKAGE_CANNOT_DELETE', 'The referenced package cannot be deleted', 'en', '', 'admin', 1724030366000); INSERT INTO `sys_i18n`(`id`, `name`, `code`, `value`, `lang`, `remark`, `update_user_id`, `update_timestamp`) VALUES (243, '202004', 'PACKAGE_CANNOT_DELETE', '以引用安装包不能删除', 'zh', '', 'admin', 1724030366000); +INSERT INTO `sys_i18n`(`id`, `name`, `code`, `value`, `lang`, `remark`, `update_user_id`, `update_timestamp`) VALUES (244, '203003', 'GIT_PARENT_COMMITID_NOT_FOUND', 'Parent commitId not found', 'en', '', 'admin', 1724030366000); +INSERT INTO `sys_i18n`(`id`, `name`, `code`, `value`, `lang`, `remark`, `update_user_id`, `update_timestamp`) VALUES (245, '203003', 'GIT_PARENT_COMMITID_NOT_FOUND', '未找到 parent commitId', 'zh', '', 'admin', 1724030366000); +INSERT INTO `sys_i18n`(`id`, `name`, `code`, `value`, `lang`, `remark`, `update_user_id`, `update_timestamp`) VALUES (246, '203004', 'GIT_BINARY_CONFLICT_ERROR', 'Binary file conflict found; resolve conflicts in binary files manually', 'en', '', 'admin', 1724030366000); +INSERT INTO `sys_i18n`(`id`, `name`, `code`, `value`, `lang`, `remark`, `update_user_id`, `update_timestamp`) VALUES (247, '203004', 'GIT_BINARY_CONFLICT_ERROR', '发现二进制文件冲突;手动解决二进制文件中的冲突', 'zh', '', 'admin', 1724030366000); SET FOREIGN_KEY_CHECKS = 1;