From 3cc928d7a7e01a22bc325579ae73a7a0ce15ff4f Mon Sep 17 00:00:00 2001 From: shizhendong Date: Thu, 21 Nov 2024 14:59:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20ASW-182=20=E6=96=B0=E5=A2=9E=20tag,rele?= =?UTF-8?q?ase=20=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 7 + .../net/geedge/asw/common/util/Constants.java | 4 +- .../net/geedge/asw/common/util/RCode.java | 3 + .../ApplicationReleaseController.java | 78 +++++++ .../module/app/controller/TagController.java | 52 +++++ .../module/app/dao/ApplicationReleaseDao.java | 17 ++ .../app/entity/ApplicationReleaseEntity.java | 40 ++++ .../service/IApplicationReleaseService.java | 21 ++ .../asw/module/app/service/ITagService.java | 16 ++ .../impl/ApplicationReleaseServiceImpl.java | 164 +++++++++++++++ .../app/service/impl/BranchServiceImpl.java | 6 +- .../app/service/impl/TagServiceImpl.java | 194 ++++++++++++++++++ .../geedge/asw/module/app/util/JGitUtils.java | 62 +++++- .../mapper/app/ApplicationReleaseMapper.xml | 55 +++++ .../resources/db/migration/R__AZ_sys_i18n.sql | 6 + .../db/migration/V1.0.01__INIT_TABLES.sql | 19 ++ 16 files changed, 733 insertions(+), 11 deletions(-) create mode 100644 src/main/java/net/geedge/asw/module/app/controller/ApplicationReleaseController.java create mode 100644 src/main/java/net/geedge/asw/module/app/controller/TagController.java create mode 100644 src/main/java/net/geedge/asw/module/app/dao/ApplicationReleaseDao.java create mode 100644 src/main/java/net/geedge/asw/module/app/entity/ApplicationReleaseEntity.java create mode 100644 src/main/java/net/geedge/asw/module/app/service/IApplicationReleaseService.java create mode 100644 src/main/java/net/geedge/asw/module/app/service/ITagService.java create mode 100644 src/main/java/net/geedge/asw/module/app/service/impl/ApplicationReleaseServiceImpl.java create mode 100644 src/main/java/net/geedge/asw/module/app/service/impl/TagServiceImpl.java create mode 100644 src/main/resources/db/mapper/app/ApplicationReleaseMapper.xml diff --git a/pom.xml b/pom.xml index 6507b1a..f4fee16 100644 --- a/pom.xml +++ b/pom.xml @@ -186,6 +186,13 @@ 7.0.0.202409031743-r + + + org.eclipse.jgit + org.eclipse.jgit.archive + 7.0.0.202409031743-r + + org.springframework.boot spring-boot-starter-freemarker diff --git a/src/main/java/net/geedge/asw/common/util/Constants.java b/src/main/java/net/geedge/asw/common/util/Constants.java index 80a11cc..1ab1ef2 100644 --- a/src/main/java/net/geedge/asw/common/util/Constants.java +++ b/src/main/java/net/geedge/asw/common/util/Constants.java @@ -146,7 +146,9 @@ public class Constants { PLAYBOOK("playbook"), - JOB("job"); + JOB("job"), + + RELEASE("release"); private String type; 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 d43dd33..e25bc3d 100644 --- a/src/main/java/net/geedge/asw/common/util/RCode.java +++ b/src/main/java/net/geedge/asw/common/util/RCode.java @@ -67,6 +67,9 @@ public enum RCode { GIT_BINARY_CONFLICT_ERROR(203004, "Binary file conflict found; resolve conflicts in binary files manually"), GIT_MERGE_NOT_SUPPORTED(203005, "Cannot merge in the {0} state"), GIT_MERGE_TARGET_BRANCH_NOT_EXIST(203006, "The target branch {0} does not exist."), + GIT_TAG_ALREADY_EXISTS(203007, "Tag {0} already exists"), + GIT_TAG_NOT_FOUND(203008, "Tag {0} not found"), + GIT_TAG_ALREADY_IN_USE(203009,"Tag is already in use. Choose another tag."), // Runner diff --git a/src/main/java/net/geedge/asw/module/app/controller/ApplicationReleaseController.java b/src/main/java/net/geedge/asw/module/app/controller/ApplicationReleaseController.java new file mode 100644 index 0000000..4184e95 --- /dev/null +++ b/src/main/java/net/geedge/asw/module/app/controller/ApplicationReleaseController.java @@ -0,0 +1,78 @@ +package net.geedge.asw.module.app.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jakarta.servlet.http.HttpServletResponse; +import net.geedge.asw.common.util.*; +import net.geedge.asw.module.app.entity.ApplicationReleaseEntity; +import net.geedge.asw.module.app.service.IApplicationReleaseService; +import net.geedge.asw.module.workspace.entity.WorkspaceEntity; +import net.geedge.asw.module.workspace.service.IWorkspaceService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/workspace") +public class ApplicationReleaseController { + + @Autowired + private IWorkspaceService workspaceService; + + @Autowired + private IApplicationReleaseService releaseService; + + @GetMapping("/{workspaceId}/release/{id}") + public R detail(@PathVariable("workspaceId") String workspaceId, @PathVariable("id") String id) { + ApplicationReleaseEntity record = releaseService.queryInfo(id); + return R.ok().putData("record", record); + } + + @GetMapping("/{workspaceId}/release") + public R list(@PathVariable("workspaceId") String workspaceId, @RequestParam Map params) { + // workspaceId + params = T.MapUtil.defaultIfEmpty(params, new HashMap<>()); + params.put("workspaceId", workspaceId); + + Page page = releaseService.queryList(params); + return R.ok(page); + } + + @PostMapping("/{workspaceId}/release") + public R add(@PathVariable("workspaceId") String workspaceId, @RequestBody Map requestBody) { + String name = T.MapUtil.getStr(requestBody, "name", ""); + String tagName = T.MapUtil.getStr(requestBody, "tagName", ""); + String description = T.MapUtil.getStr(requestBody, "description", ""); + if (T.StrUtil.hasEmpty(name, tagName)) { + throw new ASWException(RCode.PARAM_CANNOT_EMPTY); + } + + ApplicationReleaseEntity record = releaseService.saveRelease(workspaceId, name, tagName, description); + return R.ok().putData("record", record); + } + + @DeleteMapping("/{workspaceId}/release/{id}") + public R delete(@PathVariable("workspaceId") String workspaceId, @PathVariable("id") String id) { + releaseService.removeRelease(id); + return R.ok(); + } + + @GetMapping("/{workspaceId}/release/{id}/file") + public void download(@PathVariable("workspaceId") String workspaceId, + @PathVariable("id") String id, + HttpServletResponse response) throws IOException { + ApplicationReleaseEntity release = releaseService.getById(id); + T.VerifyUtil.is(release).notNull(RCode.SYS_RECORD_NOT_FOUND); + + WorkspaceEntity workspace = workspaceService.getById(workspaceId); + T.VerifyUtil.is(workspace).notNull(RCode.SYS_RECORD_NOT_FOUND); + + String fileName = T.StrUtil.concat(true, workspace.getName(), "-", release.getTagName(), ".zip"); + byte[] fileBytes = T.FileUtil.readBytes(T.FileUtil.file(release.getPath())); + ResponseUtil.downloadFile(response, MediaType.APPLICATION_OCTET_STREAM_VALUE, fileName, fileBytes); + } + +} \ No newline at end of file diff --git a/src/main/java/net/geedge/asw/module/app/controller/TagController.java b/src/main/java/net/geedge/asw/module/app/controller/TagController.java new file mode 100644 index 0000000..83ac810 --- /dev/null +++ b/src/main/java/net/geedge/asw/module/app/controller/TagController.java @@ -0,0 +1,52 @@ +package net.geedge.asw.module.app.controller; + +import net.geedge.asw.common.util.ASWException; +import net.geedge.asw.common.util.R; +import net.geedge.asw.common.util.RCode; +import net.geedge.asw.common.util.T; +import net.geedge.asw.module.app.service.ITagService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/workspace") +public class TagController { + + @Autowired + private ITagService tagService; + + @GetMapping("/{workspaceId}/tag/{tagName}") + public R infoTag(@PathVariable("workspaceId") String workspaceId, @PathVariable("tagName") String name) { + Map record = tagService.infoTag(workspaceId, name); + return R.ok().putData("record", record); + } + + @GetMapping("/{workspaceId}/tag") + public R listTag(@PathVariable("workspaceId") String workspaceId, + @RequestParam(value = "search", required = false) String search) { + List> list = tagService.listTag(workspaceId, search); + return R.ok().putData("records", list); + } + + @PostMapping("/{workspaceId}/tag") + public synchronized R newTag(@PathVariable("workspaceId") String workspaceId, @RequestBody Map requestBody) { + String name = T.MapUtil.getStr(requestBody, "name", ""); + String ref = T.MapUtil.getStr(requestBody, "ref", ""); + String description = T.MapUtil.getStr(requestBody, "description", ""); + if (T.StrUtil.hasEmpty(name, ref)) { + throw new ASWException(RCode.PARAM_CANNOT_EMPTY); + } + Map record = tagService.newTag(workspaceId, name, ref, description); + return R.ok().putData("record", record); + } + + @DeleteMapping("/{workspaceId}/tag/{tagName}") + public synchronized R deleteTag(@PathVariable("workspaceId") String workspaceId, @PathVariable("tagName") String name) { + tagService.deleteTag(workspaceId, name); + return R.ok(); + } + +} \ No newline at end of file diff --git a/src/main/java/net/geedge/asw/module/app/dao/ApplicationReleaseDao.java b/src/main/java/net/geedge/asw/module/app/dao/ApplicationReleaseDao.java new file mode 100644 index 0000000..05fb2ee --- /dev/null +++ b/src/main/java/net/geedge/asw/module/app/dao/ApplicationReleaseDao.java @@ -0,0 +1,17 @@ +package net.geedge.asw.module.app.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import net.geedge.asw.module.app.entity.ApplicationReleaseEntity; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; +import java.util.Map; + + +@Mapper +public interface ApplicationReleaseDao extends BaseMapper { + + List queryList(Page page, Map params); + +} diff --git a/src/main/java/net/geedge/asw/module/app/entity/ApplicationReleaseEntity.java b/src/main/java/net/geedge/asw/module/app/entity/ApplicationReleaseEntity.java new file mode 100644 index 0000000..69f288a --- /dev/null +++ b/src/main/java/net/geedge/asw/module/app/entity/ApplicationReleaseEntity.java @@ -0,0 +1,40 @@ +package net.geedge.asw.module.app.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import net.geedge.asw.module.sys.entity.SysUserEntity; + +@Data +@TableName(value = "application_release", autoResultMap = true) +public class ApplicationReleaseEntity { + + @TableId(type = IdType.ASSIGN_UUID) + private String id; + private String name; + private String tagName; + + private String path; + private String description; + + private String createUserId; + private Long createTimestamp; + + private String updateUserId; + private Long updateTimestamp; + + private String workspaceId; + + + @TableField(exist = false) + private SysUserEntity createUser; + + @TableField(exist = false) + private SysUserEntity updateUser; + + @TableField(exist = false) + private Object commit; + +} \ No newline at end of file diff --git a/src/main/java/net/geedge/asw/module/app/service/IApplicationReleaseService.java b/src/main/java/net/geedge/asw/module/app/service/IApplicationReleaseService.java new file mode 100644 index 0000000..26f0dc4 --- /dev/null +++ b/src/main/java/net/geedge/asw/module/app/service/IApplicationReleaseService.java @@ -0,0 +1,21 @@ +package net.geedge.asw.module.app.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import net.geedge.asw.module.app.entity.ApplicationReleaseEntity; + +import java.util.Map; + +public interface IApplicationReleaseService extends IService { + + ApplicationReleaseEntity queryInfo(String id); + + Page queryList(Map params); + + ApplicationReleaseEntity saveRelease(String workspaceId, String name, String tagName, String description); + + void removeRelease(String id); + + void removeRelease(String workspaceId, String tagName); + +} \ No newline at end of file diff --git a/src/main/java/net/geedge/asw/module/app/service/ITagService.java b/src/main/java/net/geedge/asw/module/app/service/ITagService.java new file mode 100644 index 0000000..1096549 --- /dev/null +++ b/src/main/java/net/geedge/asw/module/app/service/ITagService.java @@ -0,0 +1,16 @@ +package net.geedge.asw.module.app.service; + +import java.util.List; +import java.util.Map; + +public interface ITagService { + + Map infoTag(String workspaceId, String name); + + List> listTag(String workspaceId, String search); + + Map newTag(String workspaceId, String name, String branch, String message); + + void deleteTag(String workspaceId, String name); + +} \ No newline at end of file diff --git a/src/main/java/net/geedge/asw/module/app/service/impl/ApplicationReleaseServiceImpl.java b/src/main/java/net/geedge/asw/module/app/service/impl/ApplicationReleaseServiceImpl.java new file mode 100644 index 0000000..6d529e3 --- /dev/null +++ b/src/main/java/net/geedge/asw/module/app/service/impl/ApplicationReleaseServiceImpl.java @@ -0,0 +1,164 @@ +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.query.LambdaQueryWrapper; +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.*; +import net.geedge.asw.module.app.dao.ApplicationReleaseDao; +import net.geedge.asw.module.app.entity.ApplicationReleaseEntity; +import net.geedge.asw.module.app.service.IApplicationReleaseService; +import net.geedge.asw.module.app.util.JGitUtils; +import net.geedge.asw.module.sys.entity.SysUserEntity; +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.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +@Service +public class ApplicationReleaseServiceImpl extends ServiceImpl implements IApplicationReleaseService { + + private final static Log log = Log.get(); + + @Value("${asw.resources.path:resources}") + private String aswResourcesPath; + + @Autowired + private ISysUserService userService; + + @Autowired + private IWorkspaceService workspaceService; + + @Override + public ApplicationReleaseEntity queryInfo(String id) { + ApplicationReleaseEntity entity = this.getById(id); + T.VerifyUtil.is(entity).notNull(RCode.SYS_RECORD_NOT_FOUND); + + SysUserEntity createUser = userService.getById(entity.getCreateUserId()); + SysUserEntity updateUser = userService.getById(entity.getUpdateUserId()); + createUser.setPwd(null); + updateUser.setPwd(null); + entity.setCreateUser(createUser); + entity.setUpdateUser(updateUser); + + File gitDir = workspaceService.getGitDir(entity.getWorkspaceId()); + try (Repository repository = JGitUtils.openRepository(gitDir);) { + Ref tagRef = repository.findRef(JGitUtils.getFullTagName(entity.getTagName())); + String tgtCommitId = tagRef.getTarget().getObjectId().getName(); + RevCommit revCommit = JGitUtils.infoCommit(repository, tgtCommitId); + entity.setCommit(JGitUtils.buildAswCommitInfo(revCommit)); + return entity; + } catch (IOException e) { + log.error(e, "[queryInfo] [id: {}]", id); + throw new RuntimeException(e); + } + } + + @Override + public Page queryList(Map params) { + Page page = new Query(ApplicationReleaseEntity.class).getPage(params); + List entityList = this.getBaseMapper().queryList(page, params); + page.setRecords(entityList); + return page; + } + + @Override + public ApplicationReleaseEntity saveRelease(String workspaceId, String name, String tagName, String description) { + ApplicationReleaseEntity checkTagInUse = this.getOne(new LambdaQueryWrapper() + .eq(ApplicationReleaseEntity::getWorkspaceId, workspaceId) + .eq(ApplicationReleaseEntity::getTagName, tagName) + ); + if (null != checkTagInUse) { + throw new ASWException(RCode.GIT_TAG_ALREADY_IN_USE); + } + + String uuid = T.StrUtil.uuid(); + // release file + File releaseFile = FileResourceUtil.createFile(this.aswResourcesPath, workspaceId, Constants.FileTypeEnum.RELEASE.getType(), uuid, uuid + ".zip"); + + File gitDir = workspaceService.getGitDir(workspaceId); + try (Repository repository = JGitUtils.openRepository(gitDir);) { + Ref tagRef = repository.findRef(JGitUtils.getFullTagName(tagName)); + T.VerifyUtil.is(tagRef).notNull(RCode.GIT_TAG_NOT_FOUND.setParam(tagName)); + + // archive + WorkspaceEntity workspace = workspaceService.getById(workspaceId); + String prefix = T.StrUtil.concat(true, workspace.getName(), "-", tagName, "/"); + JGitUtils.archive(repository, tagName, releaseFile, prefix, "zip"); + + ApplicationReleaseEntity entity = new ApplicationReleaseEntity(); + entity.setId(uuid); + entity.setName(name); + entity.setTagName(tagName); + entity.setPath(releaseFile.getAbsolutePath()); + + entity.setDescription(description); + entity.setWorkspaceId(workspaceId); + + entity.setCreateUserId(StpUtil.getLoginIdAsString()); + entity.setCreateTimestamp(System.currentTimeMillis()); + entity.setUpdateUserId(StpUtil.getLoginIdAsString()); + entity.setUpdateTimestamp(System.currentTimeMillis()); + this.save(entity); + + // build record info + SysUserEntity createUser = userService.getById(entity.getCreateUserId()); + SysUserEntity updateUser = userService.getById(entity.getUpdateUserId()); + createUser.setPwd(null); + updateUser.setPwd(null); + entity.setCreateUser(createUser); + entity.setUpdateUser(updateUser); + + String tgtCommitId = tagRef.getTarget().getObjectId().getName(); + RevCommit revCommit = JGitUtils.infoCommit(repository, tgtCommitId); + entity.setCommit(JGitUtils.buildAswCommitInfo(revCommit)); + return entity; + } catch (IOException | GitAPIException e) { + T.FileUtil.del(releaseFile); + log.error(e, "[saveRelease] [error] [workspaceId: {}] [name: {}] [tagName: {}]", workspaceId, name, tagName); + throw new RuntimeException(e); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeRelease(String id) { + ApplicationReleaseEntity entity = this.getById(id); + if (null != entity) { + // del file + T.FileUtil.del(entity.getPath()); + // del record + this.removeById(id); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeRelease(String workspaceId, String tagName) { + ApplicationReleaseEntity entity = this.getOne(new LambdaQueryWrapper() + .eq(ApplicationReleaseEntity::getWorkspaceId, workspaceId) + .eq(ApplicationReleaseEntity::getTagName, tagName) + ); + if (null != entity) { + // del file + T.FileUtil.del(entity.getPath()); + // del record + this.removeById(entity.getId()); + } + } + +} \ No newline at end of file diff --git a/src/main/java/net/geedge/asw/module/app/service/impl/BranchServiceImpl.java b/src/main/java/net/geedge/asw/module/app/service/impl/BranchServiceImpl.java index 42bebc9..6f0109a 100644 --- a/src/main/java/net/geedge/asw/module/app/service/impl/BranchServiceImpl.java +++ b/src/main/java/net/geedge/asw/module/app/service/impl/BranchServiceImpl.java @@ -46,8 +46,8 @@ public class BranchServiceImpl implements IBranchService { String fullBranch = repository.getFullBranch(); String defaultBranch = "main"; - if (fullBranch != null && fullBranch.startsWith(JGitUtils.LOCAL_BRANCH_PREFIX)) { - defaultBranch = fullBranch.substring(JGitUtils.LOCAL_BRANCH_PREFIX.length()); + if (fullBranch != null && fullBranch.startsWith(JGitUtils.R_HEADS)) { + defaultBranch = fullBranch.substring(JGitUtils.R_HEADS.length()); } // 默认行为,进查询本地分支 @@ -57,7 +57,7 @@ public class BranchServiceImpl implements IBranchService { for (Ref ref : call) { String branchName = ref.getName(); // 返回时去掉前缀 - branchName = branchName.replaceAll(JGitUtils.LOCAL_BRANCH_PREFIX, ""); + branchName = branchName.replaceAll(JGitUtils.R_HEADS, ""); if (T.StrUtil.isNotEmpty(search)) { if (!T.StrUtil.contains(branchName, search)) { continue; diff --git a/src/main/java/net/geedge/asw/module/app/service/impl/TagServiceImpl.java b/src/main/java/net/geedge/asw/module/app/service/impl/TagServiceImpl.java new file mode 100644 index 0000000..c5624ee --- /dev/null +++ b/src/main/java/net/geedge/asw/module/app/service/impl/TagServiceImpl.java @@ -0,0 +1,194 @@ +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.query.LambdaQueryWrapper; +import net.geedge.asw.common.util.ASWException; +import net.geedge.asw.common.util.RCode; +import net.geedge.asw.common.util.T; +import net.geedge.asw.module.app.entity.ApplicationReleaseEntity; +import net.geedge.asw.module.app.service.IApplicationReleaseService; +import net.geedge.asw.module.app.service.ITagService; +import net.geedge.asw.module.app.util.JGitUtils; +import net.geedge.asw.module.sys.entity.SysUserEntity; +import net.geedge.asw.module.sys.service.ISysUserService; +import net.geedge.asw.module.workspace.service.IWorkspaceService; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.IOException; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +public class TagServiceImpl implements ITagService { + + private final static Log log = Log.get(); + + @Autowired + private ISysUserService userService; + + @Autowired + private IWorkspaceService workspaceService; + + @Autowired + private IApplicationReleaseService releaseService; + + private String getShortTagName(String name) { + return name.startsWith(JGitUtils.R_TAGS) ? name.replaceAll(JGitUtils.R_TAGS, "") : name; + } + + @Override + public Map infoTag(String workspaceId, String name) { + File gitDir = workspaceService.getGitDir(workspaceId); + try (Repository repository = JGitUtils.openRepository(gitDir);) { + + Map infoTag = this.infoTag(repository, name); + return infoTag; + } catch (IOException e) { + log.error(e, "[infoTag] [error] [workspaceId: {}] [name: {}]", workspaceId, name); + throw new RuntimeException(e); + } + } + + /** + * tag 详情 + * + * @param repository + * @param name + * @return + * @throws IOException + */ + private Map infoTag(Repository repository, String name) throws IOException { + Ref ref = repository.findRef(JGitUtils.getFullTagName(name)); + T.VerifyUtil.is(ref).notNull(RCode.GIT_TAG_NOT_FOUND.setParam(name)); + + // tag + RevTag revTag = JGitUtils.infoTag(repository, ref.getObjectId()); + String message = revTag.getFullMessage(); + + // commit + RevCommit revCommit = JGitUtils.infoCommit(repository, ref.getObjectId().getName()); + Map aswCommitInfo = JGitUtils.buildAswCommitInfo(revCommit); + + // release + ApplicationReleaseEntity releaseEntity = releaseService.getOne(new LambdaQueryWrapper() + .eq(ApplicationReleaseEntity::getWorkspaceId, repository.getDirectory().getName()) + .eq(ApplicationReleaseEntity::getTagName, this.getShortTagName(name)) + ); + + Map m = T.MapUtil.builder() + .put("name", this.getShortTagName(name)) + .put("message", message) + .put("createdAt", revTag.getTaggerIdent().getWhen().getTime()) + .put("commit", aswCommitInfo) + .put("release", releaseEntity) + .build(); + return m; + } + + @Override + public List> listTag(String workspaceId, String search) { + File gitDir = workspaceService.getGitDir(workspaceId); + try (Repository repository = JGitUtils.openRepository(gitDir); + Git git = Git.open(repository.getDirectory())) { + + List refList = git.tagList().call(); + if (T.StrUtil.isNotEmpty(search)) { + refList = refList.stream() + .filter(ref -> { + String name = ref.getName(); + return T.StrUtil.containsIgnoreCase(this.getShortTagName(name), search); + }) + .collect(Collectors.toList()); + } + + List> list = T.ListUtil.list(true); + for (Ref ref : refList) { + list.add(this.infoTag(repository, ref.getName())); + } + + // 默认根据 createdAt 字段排序 + list = list.stream() + .sorted(Comparator.comparing(map -> T.MapUtil.getLong((Map) map, "createdAt")).reversed()) + .collect(Collectors.toList()); + return list; + } catch (IOException | GitAPIException e) { + log.error(e, "[listTag] [error] [workspaceId: {}] [search: {}]", workspaceId, search); + throw new RuntimeException(e); + } + } + + @Override + public Map newTag(String workspaceId, String name, String branch, String message) { + File gitDir = workspaceService.getGitDir(workspaceId); + try (Repository repository = JGitUtils.openRepository(gitDir); + Git git = Git.open(repository.getDirectory())) { + + // check tag exists + List tagListInDb = git.tagList().call(); + Ref checkRefExists = tagListInDb.parallelStream() + .filter(ref -> T.StrUtil.equals(name, this.getShortTagName(ref.getName()))) + .findFirst() + .orElse(null); + if (null != checkRefExists) { + throw new ASWException(RCode.GIT_TAG_ALREADY_EXISTS.setParam(name)); + } + + // check branch exists + if (T.BooleanUtil.negate( + JGitUtils.isBranchExists(repository, branch) + )) { + throw new ASWException(RCode.GIT_MERGE_TARGET_BRANCH_NOT_EXIST.setParam(branch)); + } + + SysUserEntity loginUser = userService.getById(StpUtil.getLoginIdAsString()); + PersonIdent personIdent = JGitUtils.buildPersonIdent(loginUser.getName()); + RevCommit branchLatestCommit = JGitUtils.getBranchLatestCommit(repository, branch); + + // add tag + git.tag() + .setName(name) + .setMessage(message) + .setTagger(personIdent) + .setObjectId(branchLatestCommit) + .setAnnotated(true) + .setForceUpdate(false) + .call(); + + // return info + return this.infoTag(repository, name); + } catch (IOException | GitAPIException e) { + log.error(e, "[newTag] [error] [workspaceId: {}] [name: {}] [branch: {}]", workspaceId, name, branch); + throw new RuntimeException(e); + } + } + + @Override + public void deleteTag(String workspaceId, String name) { + File gitDir = workspaceService.getGitDir(workspaceId); + try (Repository repository = JGitUtils.openRepository(gitDir); + Git git = Git.open(repository.getDirectory())) { + // del tag + git.tagDelete() + .setTags(name) + .call(); + // del release + releaseService.removeRelease(workspaceId, name); + } catch (IOException | GitAPIException e) { + log.error(e, "[deleteTag] [error] [workspaceId: {}] [name: {}]", workspaceId, name); + throw new RuntimeException(e); + } + } + +} \ No newline at end of file diff --git a/src/main/java/net/geedge/asw/module/app/util/JGitUtils.java b/src/main/java/net/geedge/asw/module/app/util/JGitUtils.java index 08bd526..d23675b 100644 --- a/src/main/java/net/geedge/asw/module/app/util/JGitUtils.java +++ b/src/main/java/net/geedge/asw/module/app/util/JGitUtils.java @@ -9,6 +9,7 @@ import org.eclipse.jgit.api.*; import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.archive.ArchiveFormats; import org.eclipse.jgit.diff.*; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; @@ -20,6 +21,7 @@ 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.RevTag; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.filter.RevFilter; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; @@ -30,9 +32,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; +import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -49,7 +49,9 @@ public class JGitUtils { /** * 本地分支引用前缀 */ - public static final String LOCAL_BRANCH_PREFIX = "refs/heads/"; + public static final String R_HEADS = "refs/heads/"; + + public static final String R_TAGS = "refs/tags/"; /** * 默认分支 @@ -123,7 +125,7 @@ public class JGitUtils { * @throws IOException */ public static boolean isBranchExists(Repository repository, String branch) throws IOException { - Ref ref = repository.findRef(T.StrUtil.concat(true, LOCAL_BRANCH_PREFIX, branch)); + Ref ref = repository.findRef(T.StrUtil.concat(true, R_HEADS, branch)); return null != ref; } @@ -142,6 +144,24 @@ public class JGitUtils { } } + public static String getFullTagName(String name) { + return name.startsWith(R_TAGS) ? name : T.StrUtil.concat(true, R_TAGS, name); + } + + /** + * 获取tag对象 + * + * @param repository + * @param objectId + * @return + * @throws IOException + */ + public static RevTag infoTag(Repository repository, ObjectId objectId) throws IOException { + try (RevWalk revWalk = new RevWalk(repository)) { + RevTag revTag = revWalk.parseTag(objectId); + return revTag; + } + } /** * 返回分支最新提交 @@ -357,7 +377,7 @@ public class JGitUtils { RefUpdate ru = null; RefUpdate.Result rc = null; if (null != branchRef) { - ru = repository.updateRef(T.StrUtil.concat(true, LOCAL_BRANCH_PREFIX, branch)); + ru = repository.updateRef(T.StrUtil.concat(true, R_HEADS, branch)); ru.setNewObjectId(commitId); rc = ru.update(); } else { @@ -422,7 +442,7 @@ public class JGitUtils { ObjectId commitId = odi.insert(commit); odi.flush(); - RefUpdate ru = repository.updateRef(T.StrUtil.concat(true, JGitUtils.LOCAL_BRANCH_PREFIX, branch)); + RefUpdate ru = repository.updateRef(T.StrUtil.concat(true, R_HEADS, branch)); ru.setNewObjectId(commitId); RefUpdate.Result rc = ru.update(); switch (rc) { @@ -744,6 +764,34 @@ public class JGitUtils { } } + /** + * 归档 + * + * @param repository + * @param tagName + * @param file + * @param prefix + * @param format + * @throws IOException + * @throws GitAPIException + */ + public static void archive(Repository repository, String tagName, File file, String prefix, String format) throws IOException, GitAPIException { + ArchiveFormats.registerAll(); + T.FileUtil.mkdir(T.FileUtil.getParent(file, 1)); + try (OutputStream out = new FileOutputStream(file)) { + try (Git git = new Git(repository)) { + git.archive() + .setTree(repository.resolve(tagName)) + .setFormat(format) + .setPrefix(prefix) + .setOutputStream(out) + .call(); + } + } finally { + ArchiveFormats.unregisterAll(); + } + } + /** * build asw commit info * diff --git a/src/main/resources/db/mapper/app/ApplicationReleaseMapper.xml b/src/main/resources/db/mapper/app/ApplicationReleaseMapper.xml new file mode 100644 index 0000000..84ec5ab --- /dev/null +++ b/src/main/resources/db/mapper/app/ApplicationReleaseMapper.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 4bb6672..3c4479b 100644 --- a/src/main/resources/db/migration/R__AZ_sys_i18n.sql +++ b/src/main/resources/db/migration/R__AZ_sys_i18n.sql @@ -157,5 +157,11 @@ 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 (249, '203005', 'GIT_MERGE_NOT_SUPPORTED', '无法在{0}状态下合并', 'zh', '', 'admin', 1724030366000); INSERT INTO `sys_i18n`(`id`, `name`, `code`, `value`, `lang`, `remark`, `update_user_id`, `update_timestamp`) VALUES (250, '203006', 'GIT_MERGE_TARGET_BRANCH_NOT_EXIST', 'The target branch {0} does not exist.', 'en', '', 'admin', 1724030366000); INSERT INTO `sys_i18n`(`id`, `name`, `code`, `value`, `lang`, `remark`, `update_user_id`, `update_timestamp`) VALUES (251, '203006', 'GIT_MERGE_TARGET_BRANCH_NOT_EXIST', '目标分支 {0} 不存在', 'zh', '', 'admin', 1724030366000); +INSERT INTO `sys_i18n`(`id`, `name`, `code`, `value`, `lang`, `remark`, `update_user_id`, `update_timestamp`) VALUES (253, '203007', 'GIT_TAG_ALREADY_EXISTS', 'Tag {0} already exists', 'en', '', 'admin', 1724030366000); +INSERT INTO `sys_i18n`(`id`, `name`, `code`, `value`, `lang`, `remark`, `update_user_id`, `update_timestamp`) VALUES (255, '203007', 'GIT_TAG_ALREADY_EXISTS', 'Tag {0} 已经存在', 'zh', '', 'admin', 1724030366000); +INSERT INTO `sys_i18n`(`id`, `name`, `code`, `value`, `lang`, `remark`, `update_user_id`, `update_timestamp`) VALUES (257, '203008', 'GIT_TAG_NOT_FOUND', 'Tag {0} not found', 'en', '', 'admin', 1724030366000); +INSERT INTO `sys_i18n`(`id`, `name`, `code`, `value`, `lang`, `remark`, `update_user_id`, `update_timestamp`) VALUES (259, '203008', 'GIT_TAG_NOT_FOUND', 'Tag {0} 不存在', 'zh', '', 'admin', 1724030366000); +INSERT INTO `sys_i18n`(`id`, `name`, `code`, `value`, `lang`, `remark`, `update_user_id`, `update_timestamp`) VALUES (261, '203009', 'GIT_TAG_ALREADY_IN_USE', 'Tag is already in use. Choose another tag.', 'en', '', 'admin', 1724030366000); +INSERT INTO `sys_i18n`(`id`, `name`, `code`, `value`, `lang`, `remark`, `update_user_id`, `update_timestamp`) VALUES (263, '203009', 'GIT_TAG_ALREADY_IN_USE', 'Tag 已在使用中,请选择其他 tag', 'zh', '', 'admin', 1724030366000); SET FOREIGN_KEY_CHECKS = 1; diff --git a/src/main/resources/db/migration/V1.0.01__INIT_TABLES.sql b/src/main/resources/db/migration/V1.0.01__INIT_TABLES.sql index 4444874..1123f71 100644 --- a/src/main/resources/db/migration/V1.0.01__INIT_TABLES.sql +++ b/src/main/resources/db/migration/V1.0.01__INIT_TABLES.sql @@ -375,6 +375,25 @@ CREATE TABLE `application_merge` ( KEY `idx_workspace_id` (`workspace_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +/** + * 新增 application_release 表 + */ +DROP TABLE IF EXISTS `application_release`; +CREATE TABLE `application_release` ( + `id` VARCHAR(64) NOT NULL COMMENT '主键', + `tag_name` VARCHAR(256) NOT NULL DEFAULT '' COMMENT 'git tag名称', + `name` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '标题', + `description` text NOT NULL DEFAULT '' COMMENT '描述信息', + `path` VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '打包文件路径', + `create_timestamp` bigint(20) NOT NULL COMMENT '创建时间戳', + `create_user_id` varchar(64) NOT NULL COMMENT '创建人', + `update_timestamp` bigint(20) NOT NULL COMMENT '更新时间戳', + `update_user_id` varchar(64) NOT NULL COMMENT '更新人', + `workspace_id` varchar(64) NOT NULL DEFAULT '' COMMENT '工作空间ID', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_workspace_id` (`workspace_id`) USING BTREE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + /** * 新增 package 表 */