rename com.imyeyu.server to com.imyeyu.api

This commit is contained in:
Timi
2025-07-22 15:26:14 +08:00
parent e816b885b2
commit 323e038e86
340 changed files with 1174 additions and 1175 deletions

View File

@@ -0,0 +1,14 @@
package com.imyeyu.api.modules.git.bean;
/**
* @author 夜雨
* @since 2024-02-21 17:10
*/
public enum AttachType {
ISSUE,
MERGE,
RELEASE
}

View File

@@ -0,0 +1,25 @@
package com.imyeyu.api.modules.git.bean;
import lombok.Data;
import com.imyeyu.api.modules.common.vo.user.UserView;
/**
* Git 提交信息
*
* @author 夜雨
* @since 2023-08-14 17:13
*/
@Data
public class GitCommit {
/** ID */
private String id;
/** 说明 */
private String msg;
/** 时间 */
private long time;
private UserView committer;
}

View File

@@ -0,0 +1,59 @@
package com.imyeyu.api.modules.git.bean.gitea;
import com.imyeyu.java.TimiJava;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.utils.Encoder;
import com.imyeyu.utils.StringInterpolator;
import java.util.Map;
/**
* @author 夜雨
* @since 2025-06-26 16:04
*/
public enum API {
REPO_LIST("/api/v1/repos/search"),
REPO_GET("/api/v1/repos/{owner}/{repoName}"),
REPO_BRANCHES_LIST("/api/v1/repos/{owner}/{repoName}/branches"),
REPO_FILE_LIST("/api/v1/repos/{owner}/{repoName}/contents/{path}"),
REPO_FILE_RAW("/api/v1/repos/{owner}/{repoName}/raw/{path}"),
REPO_ARCHIVE("/api/v1/repos/{owner}/{repoName}/archive/{archiveFormat}"),
REPO_COMMIT_LIST("/api/v1/repos/{owner}/{repoName}/commits"),
REPO_LANGUAGES("/api/v1/repos/{owner}/{repoName}/languages")
;
static final StringInterpolator INTERPOLATOR = new StringInterpolator(StringInterpolator.SIMPLE_OBJ);
final String uri;
API(String uri) {
this.uri = uri;
}
public String buildURL(Map<String, Object> argsURI) {
return buildURL(argsURI, null);
}
public String buildURL(Map<String, Object> argsURI, Map<String, Object> argsURL) {
SettingService settingService = TimiServerAPI.applicationContext.getBean(SettingService.class);
StringBuilder url = new StringBuilder();
url.append(settingService.getAsString(SettingKey.GIT_API));
url.append(INTERPOLATOR.inject(uri, argsURI));
if (TimiJava.isNotEmpty(argsURL)) {
url.append("?");
url.append(Encoder.urlArgs(argsURL));
}
return url.toString();
}
}

View File

@@ -0,0 +1,17 @@
package com.imyeyu.api.modules.git.bean.gitea;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
/**
* @author 夜雨
* @since 2025-06-28 00:31
*/
@Data
public class Branch {
private String name;
@SerializedName("protected")
private boolean isProtected;
}

View File

@@ -0,0 +1,41 @@
package com.imyeyu.api.modules.git.bean.gitea;
import com.google.gson.annotations.JsonAdapter;
import com.imyeyu.api.modules.git.util.GiteaTimestampAdapter;
import lombok.Data;
/**
* @author 夜雨
* @since 2025-06-29 18:43
*/
@Data
public class File {
/**
*
*
* @author 夜雨
* @since 2025-06-29 19:00
*/
public enum Type {
file,
dir
}
private String name;
private String path;
private String sha;
private Long size;
private Type type;
private String lastCommitSha;
@JsonAdapter(GiteaTimestampAdapter.class)
private Long lastCommitterDate;
}

View File

@@ -0,0 +1,14 @@
package com.imyeyu.api.modules.git.bean.gitea;
import lombok.Data;
/**
* @author 夜雨
* @since 2025-07-08 15:05
*/
@Data
public class GiteaResponse<T> {
private boolean ok;
private T data;
}

View File

@@ -0,0 +1,48 @@
package com.imyeyu.api.modules.git.bean.gitea;
import com.google.gson.annotations.JsonAdapter;
import com.imyeyu.api.modules.git.util.GiteaTimestampAdapter;
import lombok.Data;
import java.util.List;
/**
* @author 夜雨
* @since 2025-06-26 11:45
*/
@Data
public class Repository {
private Integer id;
private String name;
private String fullName;
private String description;
private Integer size;
private String language;
private String sshUrl;
private String cloneUrl;
private String avatarUrl;
private String defaultBranch;
private Boolean archived;
@JsonAdapter(GiteaTimestampAdapter.class)
private Long createdAt;
@JsonAdapter(GiteaTimestampAdapter.class)
private Long updatedAt;
@JsonAdapter(GiteaTimestampAdapter.class)
private Long archivedAt;
private List<String> licenses;
}

View File

@@ -0,0 +1,21 @@
package com.imyeyu.api.modules.git.bean.hook;
import lombok.Data;
/**
* @author 夜雨
* @since 2023-09-18 10:13
*/
@Data
public class PostReceive {
private String pusherName;
private String repositoryName;
private String branch;
private String fromSHA1;
private String toSHA1;
}

View File

@@ -0,0 +1,43 @@
package com.imyeyu.api.modules.git.controller;
import lombok.RequiredArgsConstructor;
import com.imyeyu.api.annotation.EnableSetting;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.git.entity.Developer;
import com.imyeyu.api.modules.git.service.DeveloperService;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequiredToken;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 夜雨
* @since 2023-09-19 13:48
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/git/developer")
public class DeveloperController {
private final DeveloperService service;
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("")
public Developer view() {
return service.view();
}
@AOPLog
@RequiredToken
@EnableSetting(value = SettingKey.ENABLE_USER_UPDATE, message = "user.data.off_service")
@RequestRateLimit
@PostMapping("/update")
public void update(@RequestBody Developer developer) {
service.update(developer);
}
}

View File

@@ -0,0 +1,108 @@
package com.imyeyu.api.modules.git.controller;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import com.imyeyu.java.TimiJava;
import com.imyeyu.api.annotation.CaptchaValid;
import com.imyeyu.api.bean.CaptchaFrom;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.CommentService;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.api.modules.git.entity.Issue;
import com.imyeyu.api.modules.git.service.IssueService;
import com.imyeyu.api.modules.git.vo.issue.IssuePage;
import com.imyeyu.api.modules.git.vo.issue.IssueRequest;
import com.imyeyu.api.modules.git.vo.issue.IssueView;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequiredToken;
import com.imyeyu.spring.bean.CaptchaData;
import com.imyeyu.spring.bean.PageResult;
import org.springframework.beans.BeanUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Git 反馈接口
*
* @author 夜雨
* @since 2023-08-15 15:02
*/
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/git/issue")
public class IssueController {
private final UserService userService;
private final IssueService service;
private final CommentService commentService;
private final AttachmentService attachmentService;
/**
* 查看
*
* @param id 反馈 ID
* @return 反馈数据
*/
@AOPLog
@RequestRateLimit
@GetMapping("/{id}")
public IssueView view(@Min(1) @NotNull @PathVariable Long id) {
Issue issue = service.get(id);
IssueView view = new IssueView();
BeanUtils.copyProperties(issue, view);
if (TimiJava.isNotEmpty(issue.getPublisherId())) {
view.setPublisher(userService.view(issue.getPublisherId()));
}
view.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GIT, id));
return view;
}
/**
* 获取反馈列表
*
* @param issuePage 反馈页面参数
* @return 反馈列表
*/
@AOPLog
@RequestRateLimit
@PostMapping("/list")
public PageResult<Issue> page(@Valid @RequestBody IssuePage issuePage) {
return service.page(issuePage);
}
/**
* 创建反馈,不需要登录
*
* @param captchaData 反馈请求
*/
@AOPLog
@CaptchaValid(CaptchaFrom.GIT_ISSUE)
@RequestRateLimit
@PostMapping("/create")
public void create(@Valid @RequestBody CaptchaData<IssueRequest> captchaData) {
service.create(captchaData.getData());
}
/**
* 修改反馈,非讨论类型和未完成的反馈可以修改
*
* @param issueRequest 修改反馈请求
*/
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/update")
public void update(@Valid @RequestBody IssueRequest issueRequest) {
service.update(issueRequest);
}
}

View File

@@ -0,0 +1,110 @@
package com.imyeyu.api.modules.git.controller;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import com.imyeyu.java.TimiJava;
import com.imyeyu.api.annotation.CaptchaValid;
import com.imyeyu.api.bean.CaptchaFrom;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.api.modules.git.entity.Merge;
import com.imyeyu.api.modules.git.service.IssueService;
import com.imyeyu.api.modules.git.service.MergeService;
import com.imyeyu.api.modules.git.vo.merge.MergePage;
import com.imyeyu.api.modules.git.vo.merge.MergeRequest;
import com.imyeyu.api.modules.git.vo.merge.MergeView;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequiredToken;
import com.imyeyu.spring.bean.CaptchaData;
import com.imyeyu.spring.bean.PageResult;
import org.springframework.beans.BeanUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 和并请求接口
*
* @author 夜雨
* @since 2023-08-15 15:02
*/
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/git/merge")
public class MergeController {
private final UserService userService;
private final MergeService service;
private final IssueService issueService;
private final AttachmentService attachmentService;
/**
* 合并请求详情
*
* @param id 和并请求 ID
* @return 详情数据
*/
@AOPLog
@RequestRateLimit
@GetMapping("/{id}")
public MergeView view(@Min(1) @NotNull @PathVariable Long id) {
Merge merge = service.get(id);
MergeView view = new MergeView();
BeanUtils.copyProperties(merge, view);
if (TimiJava.isNotEmpty(view.getIssueId())) {
view.setIssue(issueService.get(view.getIssueId()));
}
view.setRequester(userService.view(view.getRequesterId()));
view.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.GIT, id));
return view;
}
/**
* 获取合并请求列表
*
* @param mergePage 页面参数
* @return 列表
*/
@AOPLog
@RequestRateLimit
@PostMapping("/list")
public PageResult<Merge> page(@Valid @RequestBody MergePage mergePage) {
return service.page(mergePage);
}
/**
* 申请合并请求
*
* @param captchaData 申请数据
*/
@AOPLog
@CaptchaValid(CaptchaFrom.GIT_MERGE)
@RequiredToken
@RequestRateLimit
@PostMapping("/create")
public void create(@Valid @RequestBody CaptchaData<MergeRequest> captchaData) {
service.create(captchaData.getData());
}
/**
* 修改合并请求
*
* @param mergeRequest 修改数据
*/
@AOPLog
@RequestRateLimit
@RequiredToken
@PostMapping("/update")
public void update(@Valid @RequestBody MergeRequest mergeRequest) {
service.update(mergeRequest);
}
}

View File

@@ -0,0 +1,41 @@
package com.imyeyu.api.modules.git.controller;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import com.imyeyu.api.modules.git.service.ReleaseService;
import com.imyeyu.api.modules.git.vo.release.ReleasePage;
import com.imyeyu.api.modules.git.vo.release.ReleaseView;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.PageResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Git 项目版本发布接口
*
* @author 夜雨
* @since 2023-08-16 11:25
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/git/release")
public class ReleaseController {
private final ReleaseService service;
/**
* 获取版本发布列表
*
* @param releasePage 版本发布页面参数
* @return 版本发布列表
*/
@AOPLog
@RequestRateLimit
@PostMapping("/list")
public PageResult<ReleaseView> page(@Valid @RequestBody ReleasePage releasePage) {
return service.pageByRepositoryId(releasePage);
}
}

View File

@@ -0,0 +1,174 @@
package com.imyeyu.api.modules.git.controller;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.network.Network;
import com.imyeyu.api.modules.git.bean.gitea.File;
import com.imyeyu.api.modules.git.bean.gitea.Repository;
import com.imyeyu.api.modules.git.service.RepositoryService;
import com.imyeyu.api.modules.git.vo.repository.RepositoryView;
import com.imyeyu.api.modules.gitea.service.GiteaService;
import com.imyeyu.api.modules.gitea.vo.ActionLogView;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.IgnoreGlobalReturn;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* Git 仓库接口
*
* @author 夜雨
* @since 2023-08-06 01:19
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/git/repository")
public class RepositoryController {
private final GiteaService giteaService;
private final RepositoryService service;
@RequestRateLimit
@PostMapping("/log")
public PageResult<ActionLogView> pagePush(@Valid @RequestBody Page page) {
return giteaService.pagePush(page);
}
/**
* 查看仓库
*
* @param name 仓库 ID
* @return 仓库数据
*/
@AOPLog
@RequestRateLimit
@GetMapping("/{name}")
public RepositoryView view(@PathVariable String name) {
Repository source = service.get(name);
RepositoryView view = new RepositoryView();
BeanUtils.copyProperties(source, view);
view.setBranchList(service.listBranches(name));
return view;
}
/**
* 获取仓库列表
*
* @param page 页面参数
* @return 仓库列表
*/
@RequestRateLimit
@PostMapping("/list")
public PageResult<Repository> page(@Valid @RequestBody Page page) {
return service.page(page);
}
/**
* 获取仓库文件结构树
*
* @param repoName 仓库名称
* @param branch 分支名称
* @return 文件结构树
*/
@AOPLog
@RequestRateLimit(20)
@RequestMapping("/{repoName}:{branch}/file/list/**")
public List<File> listFile(@PathVariable String repoName, @PathVariable String branch) {
return service.listFile(repoName, branch, TimiSpring.cutURIStartAt("/file/list/"));
}
/**
* 加载仓库文件,/request/raw/ 后的路径为加载目标文件的绝对仓库路径
*
* @param repositoryName 仓库名称
* @param branch 分支名称
* @param resp 返回体
*/
@RequestRateLimit
@IgnoreGlobalReturn
@GetMapping("/{repositoryName}:{branch}/file/raw/**")
public void raw(@PathVariable String repositoryName, @PathVariable String branch, HttpServletResponse resp) {
try {
String path = TimiSpring.cutURIStartAt("/file/raw");
if (TimiJava.isEmpty(path)) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
resp.setCharacterEncoding(StandardCharsets.UTF_8.toString());
} else {
resp.setContentType(service.getFileMimeType(repositoryName, branch, path));
IO.toOutputStream(service.getFileRaw(repositoryName, branch, path), resp.getOutputStream());
}
} catch (Exception e) {
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
resp.setCharacterEncoding(StandardCharsets.UTF_8.toString());
}
}
/**
* 获取文件 MimeType
*
* @param repositoryName 仓库名称
* @param branch 分支名称
*/
@RequestRateLimit
@GetMapping("/{repositoryName}:{branch}/file/mime/**")
public String mimeType(@PathVariable String repositoryName, @PathVariable String branch) {
return service.getFileMimeType(repositoryName, branch, TimiSpring.cutURIStartAt("/file/mime"));
}
/**
* 获取仓库提交消息列表
*
* @param page 提交消息页面参数
* @return 提交消息列表
*/
@AOPLog
@RequestRateLimit
@PostMapping("/{repositoryName}:{branch}/log/push")
public PageResult<ActionLogView> pagePush(@PathVariable String repositoryName, @PathVariable String branch, @Valid @RequestBody Page page) {
if (branch.equals("all")) {
branch = null;
}
return giteaService.pagePush(repositoryName, branch, page);
}
/**
* 打包下载仓库源码文件
*
* @param repositoryName 仓库名称
* @param branch 分支名称
* @param resp 返回体
*/
@AOPLog
@RequestRateLimit
@RequestMapping("/{repositoryName}:{branch}/archive")
public void downloadArchive(@PathVariable String repositoryName, @PathVariable String branch, HttpServletResponse resp) {
try {
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader("%s-%s.tar.gz".formatted(repositoryName.toLowerCase(), branch)));
resp.setHeader("Accept-Ranges", "bytes");
IO.toOutputStream(service.getArchive(repositoryName, branch), resp.getOutputStream());
resp.flushBuffer();
} catch (Exception e) {
log.error("downloadArchive error", e);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
resp.setCharacterEncoding(StandardCharsets.UTF_8.toString());
}
}
}

View File

@@ -0,0 +1,33 @@
package com.imyeyu.api.modules.git.entity;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.annotation.table.Table;
import com.imyeyu.spring.entity.Creatable;
import com.imyeyu.spring.entity.IDEntity;
import lombok.Data;
/**
* 提交日志
*
* @author 夜雨
* @since 2023-09-19 11:34
*/
@Data
@Table("git_commit_log")
public class CommitLog implements IDEntity<Long>, Creatable {
@Id
protected Long id;
/** 所属推送 ID */
protected Long pushId;
/** SHA1 */
protected String sha1;
/** 说明 */
protected String message;
/** 提交时间 */
protected Long createdAt;
}

View File

@@ -0,0 +1,37 @@
package com.imyeyu.api.modules.git.entity;
import com.imyeyu.api.modules.common.entity.User;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.annotation.table.Table;
import com.imyeyu.spring.entity.Updatable;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 开发者
*
* @author 夜雨
* @since 2023-09-19 11:37
*/
@Data
@NoArgsConstructor
@Table("git_developer")
public class Developer implements Updatable {
/** 开发者 ID{@link User#getId()} */
@Id
protected Long developerId;
/** 昵称 */
protected String name;
/** RSA 密钥 */
protected String rsa;
/** 更新时间 */
protected Long updatedAt;
public Developer(Long developerId) {
this.developerId = developerId;
}
}

View File

@@ -0,0 +1,120 @@
package com.imyeyu.api.modules.git.entity;
import com.imyeyu.spring.annotation.table.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.api.modules.common.bean.CommentSupport;
import com.imyeyu.spring.entity.Entity;
/**
* Git 问题反馈
*
* @author 夜雨
* @since 2023-08-16 15:18
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("git_issue")
public class Issue extends Entity implements CommentSupport {
/**
* 类型
*
* @author 夜雨
* @since 2023-08-16 17:18
*/
public enum Type {
/** 异常 */
BUG,
/** 功能 */
FEATURE,
/** 安全 */
SECURITY,
/** 提问 */
QUESTION,
/** 讨论 */
DISCUSS
}
/**
* 状态
*
* @author 夜雨
* @since 2023-08-16 15:26
*/
public enum Status {
/** 待确认 */
BEFORE_CONFIRM,
/** 已确认,待开发 */
CONFIRMED,
/** 开发中 */
DEVELOPING,
/** 已完成 */
FINISHED,
/** 已关闭 */
CLOSED
}
/** 所属仓库 ID */
protected long repositoryId;
/** 发布者 ID */
protected Long publisherId;
/** 发布者昵称 */
protected String publisherNick;
/** 类型 */
protected Type type;
/** 版本 */
protected String version;
/** 标题 */
protected String title;
/** 描述 */
protected String description;
/** 状态 */
protected Status status;
/** 确认时间 */
protected Long confirmedAt;
/** 开发时间 */
protected Long developAt;
/** 关闭时间 */
protected Long closedAt;
@Override
public boolean canComment() {
return status != Status.CLOSED;
}
@Override
public boolean canNotComment() {
return !canComment();
}
/** @return true 为可更新 */
public boolean canUpdate() {
return type != Type.DISCUSS && !isClosed() && status != Status.FINISHED;
}
/** @return true 为已关闭 */
public boolean isClosed() {
return closedAt != null;
}
}

View File

@@ -0,0 +1,133 @@
package com.imyeyu.api.modules.git.entity;
import com.imyeyu.spring.annotation.table.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.api.modules.common.bean.CommentSupport;
import com.imyeyu.spring.entity.Entity;
/**
* 合并请求
*
* @author 夜雨
* @since 2023-08-16 14:52
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("git_merge")
public class Merge extends Entity implements CommentSupport {
/**
* 类型
*
* @author 夜雨
* @since 2023-08-17 11:32
*/
public enum Type {
/** 异常 */
BUG,
/** 功能 */
FEATURE,
/** 安全 */
SECURITY,
/** 文档改进 */
DOCUMENT,
/** 重构 */
REFACTOR
}
/**
* 状态
*
* @author 夜雨
* @since 2023-08-17 11:33
*/
public enum Status {
/** 等待代码审查 */
BEFORE_CHECK,
/** 等待合并 */
WAITING,
/** 已合并 */
MERGED,
/** 已拒绝 */
REJECTED,
CLOSED
}
/** 所属仓库 ID */
protected long repositoryId;
/** 申请合并用户 ID */
protected long requesterId;
/** 相关反馈 ID */
protected Long issueId;
/** 类型 */
protected Type type;
/** 来自分支 */
protected String fromBranch;
/** 去向分支(基准分支) */
protected String toBranch;
/** 说明标题 */
protected String title;
/** 合并说明 */
protected String description;
/** 通过审查时间 */
protected Long checkedAt;
/** 合并时间 */
protected Long mergedAt;
/** 拒绝时间 */
protected Long rejectedAt;
/** 拒绝原因 */
protected String rejectReason;
/** 关闭时间 */
protected Long closedAt;
/** 请求状态 */
protected Status status;
@Override
public boolean canComment() {
return status != Status.CLOSED;
}
@Override
public boolean canNotComment() {
return !canComment();
}
/** @return true 为可更新 */
public boolean canUpdate() {
return status == Status.BEFORE_CHECK && !isClosed();
}
/** @return true 为可更新 */
public boolean canNotUpdate() {
return !canUpdate();
}
/** @return true 为已关闭 */
public boolean isClosed() {
return closedAt != null;
}
}

View File

@@ -0,0 +1,39 @@
package com.imyeyu.api.modules.git.entity;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.annotation.table.Table;
import com.imyeyu.spring.entity.Creatable;
import com.imyeyu.spring.entity.IDEntity;
import lombok.Data;
/**
* Git 推送日志
*
* @author 夜雨
* @since 2023-09-19 11:35
*/
@Data
@Table("git_push_log")
public class PushLog implements IDEntity<Long>, Creatable {
@Id
protected Long id;
/** 所属仓库 ID */
protected Long repositoryId;
/** 推送 ID */
protected Long pusherId;
/** 分支 */
protected String branch;
/** 来源提交 */
protected String from;
/** 去向提交 */
protected String to;
/** 推送时间 */
protected Long createdAt;
}

View File

@@ -0,0 +1,33 @@
package com.imyeyu.api.modules.git.entity;
import com.imyeyu.spring.annotation.table.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
/**
* Git 版本发布
*
* @author 夜雨
* @since 2023-08-16 11:25
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("git_release")
public class Release extends Entity {
/** 所属仓库 ID */
protected long repositoryId;
/** 版本 */
protected String version;
/** 描述 */
protected String description;
/** SHA1 */
protected String sha1;
/** 发布时提交数量 */
protected int commits;
}

View File

@@ -0,0 +1,15 @@
package com.imyeyu.api.modules.git.mapper;
import com.imyeyu.api.modules.git.entity.Developer;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
/**
* @author 夜雨
* @since 2023-09-19 11:42
*/
public interface DeveloperMapper extends BaseMapper<Developer, Long> {
@Select("SELECT * FROM git_developer WHERE BINARY name = #{name} LIMIT 1")
Developer queryByName(String name);
}

View File

@@ -0,0 +1,35 @@
package com.imyeyu.api.modules.git.mapper;
import com.imyeyu.api.modules.git.entity.Issue;
import com.imyeyu.api.modules.git.vo.issue.IssuePage;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* Git 反馈表
*
* @author 夜雨
* @since 2023-08-16 15:19
*/
public interface IssueMapper extends BaseMapper<Issue, Long> {
/**
* 根据仓库 ID 统计总反馈数量
*
* @param issuePage 仓库 ID
* @return 仓库反馈总数量
*/
long countByIssuePage(@Param("issuePage") IssuePage issuePage);
/**
* 根据仓库 ID 查询部分反馈
*
* @param issuePage 仓库 ID
* @param offset 偏移
* @param limit 数据量
* @return 反馈列表
*/
List<Issue> listByIssuePage(@Param("issuePage") IssuePage issuePage, long offset, int limit);
}

View File

@@ -0,0 +1,23 @@
package com.imyeyu.api.modules.git.mapper;
import com.imyeyu.api.modules.git.entity.Merge;
import com.imyeyu.api.modules.git.vo.merge.MergePage;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @author 夜雨
* @since 2023-08-17 11:42
*/
public interface MergeMapper extends BaseMapper<Merge, Long> {
@Select("SELECT * FROM git_merge WHERE id = #{id}" + NOT_DELETE + LIMIT_1)
Merge select(Long id);
long countByMergePage(@Param("mergePage") MergePage mergePage);
List<Merge> listByMergePage(@Param("mergePage") MergePage mergePage, long offset, int limit);
}

View File

@@ -0,0 +1,23 @@
package com.imyeyu.api.modules.git.mapper;
import com.imyeyu.api.modules.git.entity.Release;
import com.imyeyu.api.modules.git.vo.release.ReleaseView;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @author 夜雨
* @since 2023-08-16 11:30
*/
public interface ReleaseMapper extends BaseMapper<Release, Long> {
@Select("SELECT COUNT(1) FROM git_release WHERE repository_id = #{repositoryId}" + NOT_DELETE)
long countByRepositoryId(long repositoryId);
List<ReleaseView> listByRepositoryId(long repositoryId, long offset, int limit);
@Select("SELECT * FROM git_release WHERE repository_id = #{repositoryId}" + NOT_DELETE + "ORDER BY created_at DESC" + LIMIT_1)
Release queryLatestByRepositoryId(long repositoryId);
}

View File

@@ -0,0 +1,17 @@
package com.imyeyu.api.modules.git.service;
import com.imyeyu.api.modules.git.entity.Developer;
import com.imyeyu.spring.service.CreatableService;
import com.imyeyu.spring.service.GettableService;
import com.imyeyu.spring.service.UpdatableService;
/**
* @author 夜雨
* @since 2023-09-19 11:40
*/
public interface DeveloperService extends GettableService<Developer, Long>, CreatableService<Developer>, UpdatableService<Developer> {
Developer view();
Developer getByName(String name);
}

View File

@@ -0,0 +1,20 @@
package com.imyeyu.api.modules.git.service;
import com.imyeyu.api.modules.git.entity.Issue;
import com.imyeyu.api.modules.git.vo.issue.IssuePage;
import com.imyeyu.api.modules.git.vo.issue.IssueRequest;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.GettableService;
/**
* @author 夜雨
* @since 2023-08-16 15:18
*/
public interface IssueService extends GettableService<Issue, Long> {
void create(IssueRequest issueRequest);
void update(IssueRequest issueRequest);
PageResult<Issue> page(IssuePage issuePage);
}

View File

@@ -0,0 +1,20 @@
package com.imyeyu.api.modules.git.service;
import com.imyeyu.api.modules.git.entity.Merge;
import com.imyeyu.api.modules.git.vo.merge.MergePage;
import com.imyeyu.api.modules.git.vo.merge.MergeRequest;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.GettableService;
/**
* @author 夜雨
* @since 2023-08-16 14:46
*/
public interface MergeService extends GettableService<Merge, Long> {
void create(MergeRequest mergeRequest);
void update(MergeRequest mergeRequest);
PageResult<Merge> page(MergePage mergePage);
}

View File

@@ -0,0 +1,14 @@
package com.imyeyu.api.modules.git.service;
import com.imyeyu.api.modules.git.vo.release.ReleasePage;
import com.imyeyu.api.modules.git.vo.release.ReleaseView;
import com.imyeyu.spring.bean.PageResult;
/**
* @author 夜雨
* @since 2023-08-16 11:27
*/
public interface ReleaseService {
PageResult<ReleaseView> pageByRepositoryId(ReleasePage releasePage);
}

View File

@@ -0,0 +1,44 @@
package com.imyeyu.api.modules.git.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.git.bean.gitea.Branch;
import com.imyeyu.api.modules.git.bean.gitea.File;
import com.imyeyu.api.modules.git.bean.gitea.Repository;
import com.imyeyu.spring.service.PageableService;
import java.io.InputStream;
import java.util.List;
/**
* Git 仓库服务
*
* @author 夜雨
* @since 2023-08-06 01:19
*/
public interface RepositoryService extends PageableService<Repository> {
/**
* 根据名称获取数据库仓库
*
* @param repoName 仓库名
* @return 数据库仓库
* @throws TimiException 服务异常
*/
Repository get(String repoName);
/**
* 获取仓库所有分支列表
*
* @param repoName 仓库名
* @return 分支列表
* @throws TimiException 服务异常
*/
List<Branch> listBranches(String repoName);
List<File> listFile(String repoName, String branch, String path);
InputStream getFileRaw(String repoName, String branch, String path);
String getFileMimeType(String repoName, String branch, String path);
InputStream getArchive(String repoName, String branch);
}

View File

@@ -0,0 +1,66 @@
package com.imyeyu.api.modules.git.service.implement;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.api.modules.git.entity.Developer;
import com.imyeyu.api.modules.git.mapper.DeveloperMapper;
import com.imyeyu.api.modules.git.service.DeveloperService;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author 夜雨
* @since 2023-09-19 11:41
*/
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor_ = { @Lazy})
public class DeveloperServiceImplement extends AbstractEntityService<Developer, Long> implements DeveloperService {
private final UserService userService;
private final DeveloperMapper mapper;
@Override
protected BaseMapper<Developer, Long> mapper() {
return mapper;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(Developer developer) {
Developer dbDeveloper = get(userService.getLoginUser().getId());
dbDeveloper.setName(developer.getName());
dbDeveloper.setRsa(developer.getRsa());
if (TimiJava.isNotEmpty(dbDeveloper.getName())) {
Developer developerByName = getByName(dbDeveloper.getName());
if (developerByName != null && !developerByName.getDeveloperId().equals(dbDeveloper.getDeveloperId())) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("TODO 开发者冲突,此名称已被使用");
}
}
if (TimiJava.isNotEmpty(dbDeveloper.getRsa())) {
if (!dbDeveloper.getRsa().startsWith("ssh-rsa")) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("TODO 无效的 RSA 公钥");
}
}
super.update(dbDeveloper);
}
@Override
public Developer view() {
return get(userService.getLoginUser().getId());
}
public Developer getByName(String name) {
return mapper.queryByName(name);
}
}

View File

@@ -0,0 +1,96 @@
package com.imyeyu.api.modules.git.service.implement;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.User;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.api.modules.git.entity.Issue;
import com.imyeyu.api.modules.git.mapper.IssueMapper;
import com.imyeyu.api.modules.git.service.IssueService;
import com.imyeyu.api.modules.git.vo.issue.IssuePage;
import com.imyeyu.api.modules.git.vo.issue.IssueRequest;
import com.imyeyu.api.modules.gitea.entity.Repository;
import com.imyeyu.api.modules.gitea.service.GiteaService;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* Git 反馈服务
*
* @author 夜雨
* @since 2023-08-16 15:18
*/
@Service
@RequiredArgsConstructor
public class IssueServiceImplement extends AbstractEntityService<Issue, Long> implements IssueService {
private final UserService userService;
private final GiteaService giteaService;
private final IssueMapper mapper;
@Override
protected BaseMapper<Issue, Long> mapper() {
return mapper;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(IssueRequest issueRequest) {
Repository repository = giteaService.getRepository(issueRequest.getRepositoryId());
Issue issue = new Issue();
// 令牌和账号验证
if (TimiJava.isNotEmpty(TimiSpring.getToken())) {
User publisher = userService.getLoginUser();
TimiException.requiredTrue(!publisher.isBanning() && !publisher.isMuting(), "TODO publisher.banned");
issue.setPublisherId(publisher.getId());
issue.setPublisherNick(null);
} else {
// 昵称
TimiException.required(issueRequest.getPublisherNick(), "TODO comment.nick.empty");
issue.setPublisherNick(issueRequest.getPublisherNick());
}
TimiException.required(issueRequest.getTitle(), "TODO comment.title.empty");
TimiException.required(issueRequest.getDescription(), "TODO comment.data.empty");
issue.setRepositoryId(repository.getId());
issue.setType(issueRequest.getType());
issue.setVersion(issueRequest.getVersion());
issue.setTitle(issueRequest.getTitle());
issue.setDescription(issueRequest.getDescription());
issue.setStatus(Issue.Status.BEFORE_CONFIRM);
super.create(issue);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(IssueRequest issueRequest) {
Issue issue = get(issueRequest.getId());
TimiException.requiredTrue(issue.canUpdate(), "TODO can not update");
User publisher = userService.getLoginUser();
TimiException.required(publisher.getId().equals(issue.getPublisherId()), "TODO not permission edit issue");
TimiException.requiredTrue(!publisher.isMuting(), "TODO publisher.banned");
TimiException.required(issueRequest.getTitle(), "TODO comment.title.empty");
TimiException.required(issueRequest.getDescription(), "TODO comment.data.empty");
issue.setType(issueRequest.getType());
issue.setVersion(issueRequest.getVersion());
issue.setTitle(issueRequest.getTitle());
issue.setDescription(issueRequest.getDescription());
mapper.update(issue);
}
@Override
public PageResult<Issue> page(IssuePage issuePage) {
PageResult<Issue> result = new PageResult<>();
result.setList(mapper.listByIssuePage(issuePage, issuePage.getOffset(), issuePage.getLimit()));
result.setTotal(mapper.countByIssuePage(issuePage));
return result;
}
}

View File

@@ -0,0 +1,112 @@
package com.imyeyu.api.modules.git.service.implement;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.User;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.api.modules.git.entity.Issue;
import com.imyeyu.api.modules.git.entity.Merge;
import com.imyeyu.api.modules.git.mapper.MergeMapper;
import com.imyeyu.api.modules.git.service.IssueService;
import com.imyeyu.api.modules.git.service.MergeService;
import com.imyeyu.api.modules.git.service.RepositoryService;
import com.imyeyu.api.modules.git.vo.merge.MergePage;
import com.imyeyu.api.modules.git.vo.merge.MergeRequest;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author 夜雨
* @since 2023-08-17 11:38
*/
@Service
@RequiredArgsConstructor
public class MergeServiceImplement extends AbstractEntityService<Merge, Long> implements MergeService {
private final UserService userService;
private final IssueService issueService;
private final RepositoryService repositoryService;
private final MergeMapper mapper;
@Override
protected BaseMapper<Merge, Long> mapper() {
return mapper;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(MergeRequest mergeRequest) {
// Repository repository = repositoryService.get(mergeRequest.getRepositoryId());
User requester = userService.getLoginUser();
TimiException.required(mergeRequest.getTitle(), "TODO mergeRequest.title.empty");
TimiException.required(mergeRequest.getDescription(), "TODO mergeRequest.description.empty");
TimiException.required(mergeRequest.getFromBranch(), "TODO mergeRequest.fromBranch.empty");
// List<GitBranch> branches = repositoryService.listAllBranches(repository.getId());
// if (branches.stream().filter(i -> i.getName().equals(mergeRequest.getFromBranch())).toList().isEmpty()) {
// throw new TimiException(TimiCode.ARG_MISS).msgKey("TODO 来源分支不存在");
// }
// if (branches.stream().filter(i -> i.getName().equals(mergeRequest.getToBranch())).toList().isEmpty()) {
// throw new TimiException(TimiCode.ARG_MISS).msgKey("TODO 去向分支不存在");
// }
Merge merge = new Merge();
if (TimiJava.isNotEmpty(mergeRequest.getIssueId())) {
Issue issue = issueService.get(mergeRequest.getIssueId());
merge.setIssueId(issue.getId());
}
// merge.setRepositoryId(repository.getId());
merge.setRequesterId(requester.getId());
merge.setType(mergeRequest.getType());
merge.setFromBranch(mergeRequest.getFromBranch());
merge.setToBranch(mergeRequest.getToBranch());
merge.setTitle(mergeRequest.getTitle());
merge.setDescription(mergeRequest.getDescription());
merge.setStatus(Merge.Status.BEFORE_CHECK);
mapper.insert(merge);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(MergeRequest mergeRequest) {
Merge merge = get(mergeRequest.getId());
TimiException.requiredTrue(merge.canNotUpdate(), "TODO can not update");
TimiException.required(mergeRequest.getTitle(), "TODO mergeRequest.title.empty");
TimiException.required(mergeRequest.getDescription(), "TODO mergeRequest.data.empty");
TimiException.requiredTrue(mergeRequest.getFromBranch().equals(merge.getToBranch()), "TODO 来源分支和去向分支不能相等");
User requester = userService.getLoginUser();
TimiException.requiredTrue(!requester.getId().equals(merge.getRequesterId()), "TODO not permission edit merge");
// List<GitBranch> branches = repositoryService.listAllBranches(merge.getRepositoryId());
// if (branches.stream().filter(i -> i.getName().equals(mergeRequest.getFromBranch())).toList().isEmpty()) {
// throw new TimiException(TimiCode.ARG_MISS).msgKey("TODO 来源分支不存在");
// }
// if (branches.stream().filter(i -> i.getName().equals(mergeRequest.getToBranch())).toList().isEmpty()) {
// throw new TimiException(TimiCode.ARG_MISS).msgKey("TODO 去向分支不存在");
// }
// if (TimiJava.isNotEmpty(mergeRequest.getIssueId())) {
// Issue issue = issueService.get(mergeRequest.getIssueId());
// merge.setIssueId(issue.getId());
// }
merge.setType(mergeRequest.getType());
merge.setFromBranch(mergeRequest.getFromBranch());
merge.setToBranch(mergeRequest.getToBranch());
merge.setTitle(mergeRequest.getTitle());
merge.setDescription(mergeRequest.getDescription());
mapper.update(merge);
}
@Override
public PageResult<Merge> page(MergePage mergePage) {
PageResult<Merge> result = new PageResult<>();
result.setList(mapper.listByMergePage(mergePage, mergePage.getOffset(), mergePage.getLimit()));
result.setTotal(mapper.countByMergePage(mergePage));
return result;
}
}

View File

@@ -0,0 +1,32 @@
package com.imyeyu.api.modules.git.service.implement;
import lombok.RequiredArgsConstructor;
import com.imyeyu.api.modules.git.mapper.ReleaseMapper;
import com.imyeyu.api.modules.git.service.ReleaseService;
import com.imyeyu.api.modules.git.service.RepositoryService;
import com.imyeyu.api.modules.git.vo.release.ReleasePage;
import com.imyeyu.api.modules.git.vo.release.ReleaseView;
import com.imyeyu.spring.bean.PageResult;
import org.springframework.stereotype.Service;
/**
* @author 夜雨
* @since 2023-08-16 11:27
*/
@Service
@RequiredArgsConstructor
public class ReleaseServiceImplement implements ReleaseService {
private final RepositoryService repositoryService;
private final ReleaseMapper mapper;
@Override
public PageResult<ReleaseView> pageByRepositoryId(ReleasePage releasePage) {
// Repository repository = repositoryService.get(releasePage.getRepositoryId());
PageResult<ReleaseView> result = new PageResult<>();
// result.setList(mapper.listByRepositoryId(repository.getId(), releasePage.getOffset(), releasePage.getLimit()));
// result.setTotal(mapper.countByRepositoryId(repository.getId()));
return result;
}
}

View File

@@ -0,0 +1,225 @@
package com.imyeyu.api.modules.git.service.implement;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.Network;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.api.modules.git.bean.gitea.API;
import com.imyeyu.api.modules.git.bean.gitea.Branch;
import com.imyeyu.api.modules.git.bean.gitea.File;
import com.imyeyu.api.modules.git.bean.gitea.GiteaResponse;
import com.imyeyu.api.modules.git.bean.gitea.Repository;
import com.imyeyu.api.modules.git.service.RepositoryService;
import com.imyeyu.api.modules.gitea.entity.User;
import com.imyeyu.api.modules.gitea.service.GiteaService;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import jakarta.activation.MimetypesFileTypeMap;
import jakarta.annotation.PostConstruct;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.fluent.Request;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.tika.Tika;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.springframework.stereotype.Service;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
/**
* Git 仓库服务
*
* @author 夜雨
* @since 2023-08-06 01:19
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class RepositoryServiceImplement implements RepositoryService {
private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
private final UserService userService;
private final GiteaService giteaService;
private final SettingService settingService;
private User owner;
@PostConstruct
private void postConstruct() {
owner = giteaService.getOwner();
}
@Override
public PageResult<Repository> page(Page page) {
try {
ClassicHttpResponse resp = (ClassicHttpResponse) Request.get(API.REPO_LIST.buildURL(null, new HashMap<>() {{
put("uid", owner.getId());
put("sort", "updated");
put("order", "desc");
put("page", page.getIndex() + 1);
put("limit", page.getLimit());
}})).execute().returnResponse();
String respText = EntityUtils.toString(resp.getEntity());
GiteaResponse<List<Repository>> respObj = gson.fromJson(respText, new TypeToken<GiteaResponse<List<Repository>>>() {
}.getType());
PageResult<Repository> result = new PageResult<>();
result.setTotal(Long.parseLong(resp.getHeader("X-Total-Count").getValue()));
result.setList(respObj.getData());
return result;
} catch (Exception e) {
log.error("list repository error", e);
throw new TimiException(TimiCode.ERROR, "list repository error", e);
}
}
@Override
public Repository get(String repoName) {
try {
String respText = Request.get(API.REPO_GET.buildURL(new HashMap<>() {{
put("owner", owner.getName());
put("repoName", repoName);
}})).execute().returnContent().asString();
return gson.fromJson(respText, Repository.class);
} catch (Exception e) {
log.error("get repository error", e);
throw new TimiException(TimiCode.ERROR, "get repository error", e);
}
}
@Override
public List<Branch> listBranches(String repoName) {
try {
String respText = Request.get(API.REPO_BRANCHES_LIST.buildURL(new HashMap<>() {{
put("owner", owner.getName());
put("repoName", repoName);
}})).execute().returnContent().asString();
return gson.fromJson(respText, new TypeToken<List<Branch>>() {}.getType());
} catch (Exception e) {
log.error("list repository branches error", e);
throw new TimiException(TimiCode.ERROR, "list repository branches error", e);
}
}
@Override
public List<File> listFile(String repoName, String branch, String path) {
try {
String respText = Request.get(API.REPO_FILE_LIST.buildURL(new HashMap<>() {{
put("owner", owner.getName());
put("repoName", repoName);
put("path", path);
}}, new HashMap<>() {{
put("ref", branch);
}})).execute().returnContent().asString();
List<File> list = gson.fromJson(respText, new TypeToken<List<File>>() {}.getType());
// 排序
list.sort((f1, f2) -> {
if (f1.getType() == File.Type.dir && f2.getType() == File.Type.file) {
return -1;
} else {
if (f1.getType() == File.Type.file && f2.getType() == File.Type.dir) {
return 1;
}
return f1.getName().compareToIgnoreCase(f2.getName());
}
});
return list;
} catch (Exception e) {
log.error("list repository file error", e);
throw new TimiException(TimiCode.ERROR, "list repository file error", e);
}
}
@Override
public InputStream getFileRaw(String repoName, String branch, String path) {
try {
ClassicHttpResponse resp = (ClassicHttpResponse) Request.get(API.REPO_FILE_RAW.buildURL(new HashMap<>() {{
put("owner", owner.getName());
put("repoName", repoName);
put("path", path);
}}, new HashMap<>() {{
put("ref", branch);
}})).execute().returnResponse();
return resp.getEntity().getContent();
} catch (Exception e) {
log.error("get repository file raw error", e);
throw new TimiException(TimiCode.ERROR, "get repository file raw error", e);
}
}
@Override
public String getFileMimeType(String repoName, String branch, String path) {
try {
String repoPath = settingService.getAsString(SettingKey.GIT_REPO_PATH);
if (TimiJava.isEmpty(repoPath)) {
// 根据文件名猜测
return new MimetypesFileTypeMap().getContentType(Network.uriFileName(path));
} else {
repoPath = "%s/%s.git".formatted(repoPath, repoName.toLowerCase());
// 仓库
@Cleanup
org.eclipse.jgit.lib.Repository repo = new FileRepositoryBuilder().setGitDir(Path.of(repoPath).toFile()).build();
TimiException.required(repo, "not found repository: %s".formatted(repoPath));
// 分支
ObjectId commitId = repo.resolve("refs/heads/%s".formatted(branch));
TimiException.required(commitId, "not found branch: %s".formatted(branch));
// 提交
RevWalk revWalk = new RevWalk(repo);
RevCommit commit = revWalk.parseCommit(commitId);
RevTree tree = commit.getTree();
// 文件
path = path.substring(1);
TreeWalk treeWalk = TreeWalk.forPath(repo, path, tree);
TimiException.required(treeWalk, "not found file: %s".formatted(path));
ObjectId fileObjectId = treeWalk.getObjectId(0);
// 读取文件
ObjectReader reader = repo.newObjectReader();
ObjectLoader loader = reader.open(fileObjectId);
InputStream stream = loader.openStream();
return new Tika().detect(stream);
}
} catch (Exception e) {
log.error("get repository file mime error", e);
throw new TimiException(TimiCode.ERROR, "get repository file mime error", e);
}
}
@Override
public InputStream getArchive(String repoName, String branch) {
try {
ClassicHttpResponse resp = (ClassicHttpResponse) Request.get(API.REPO_ARCHIVE.buildURL(new HashMap<>() {{
put("owner", owner.getName());
put("repoName", repoName);
put("archiveFormat", "%s.tar.gz".formatted(branch));
}})).execute().returnResponse();
return resp.getEntity().getContent();
} catch (Exception e) {
log.error("get repository archive error", e);
throw new TimiException(TimiCode.ERROR, "get repository archive error", e);
}
}
}

View File

@@ -0,0 +1,28 @@
package com.imyeyu.api.modules.git.util;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;
import java.time.Instant;
/**
* @author 夜雨
* @since 2025-06-26 11:40
*/
public class GiteaTimestampAdapter implements JsonSerializer<Long>, JsonDeserializer<Long> {
@Override
public Long deserialize(JsonElement json, Type type, JsonDeserializationContext context) {
return Instant.parse(json.getAsString()).toEpochMilli();
}
@Override
public JsonElement serialize(Long src, Type type, JsonSerializationContext context) {
return new JsonPrimitive(src);
}
}

View File

@@ -0,0 +1,15 @@
package com.imyeyu.api.modules.git.vo.developer;
import lombok.Data;
/**
* @author 夜雨
* @since 2023-09-19 11:57
*/
@Data
public class DeveloperRequest {
private String email;
private String rsa;
}

View File

@@ -0,0 +1,21 @@
package com.imyeyu.api.modules.git.vo.issue;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.api.modules.common.entity.Comment;
import com.imyeyu.spring.bean.Page;
/**
* @author 夜雨
* @since 2024-02-28 14:27
*/
@Data
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class CommentPage extends Page {
private Comment.BizType bizType;
private long bizId;
}

View File

@@ -0,0 +1,21 @@
package com.imyeyu.api.modules.git.vo.issue;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.api.modules.git.entity.Issue;
import com.imyeyu.spring.bean.Page;
/**
* @author 夜雨
* @since 2023-08-15 15:04
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class IssuePage extends Page {
private long repositoryId;
private Issue.Type type;
private Issue.Status status;
}

View File

@@ -0,0 +1,37 @@
package com.imyeyu.api.modules.git.vo.issue;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import com.imyeyu.api.modules.git.entity.Issue;
/**
* 工单反馈
*
* @author 夜雨
* @since 2023-08-15 15:51
*/
@Data
public class IssueRequest {
/** 工单 ID在编辑时携带 */
private Long id;
/** 工单类型 */
@NotNull
private Issue.Type type;
/** 所属仓库 ID创建工单时携带编辑时不需要 */
private Long repositoryId;
/** 反馈用户昵称 */
private String publisherNick;
/** 版本 */
private String version;
/** 标题 */
private String title;
/** 反馈数据 */
private String description;
}

View File

@@ -0,0 +1,22 @@
package com.imyeyu.api.modules.git.vo.issue;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.vo.user.UserView;
import com.imyeyu.api.modules.git.entity.Issue;
import java.util.List;
/**
* @author 夜雨
* @since 2023-08-17 15:40
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class IssueView extends Issue {
private UserView publisher;
private List<Attachment> attachmentList;
}

View File

@@ -0,0 +1,21 @@
package com.imyeyu.api.modules.git.vo.merge;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.api.modules.git.entity.Merge;
import com.imyeyu.spring.bean.Page;
/**
* @author 夜雨
* @since 2023-08-17 11:52
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MergePage extends Page {
private long repositoryId;
private Merge.Type type;
private Merge.Status status;
}

View File

@@ -0,0 +1,44 @@
package com.imyeyu.api.modules.git.vo.merge;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import com.imyeyu.api.modules.git.entity.Merge;
/**
* 申请合并请求
*
* @author 夜雨
* @since 2023-08-17 11:43
*/
@Data
public class MergeRequest {
/** 和并请求 ID编辑时携带 */
private Long id;
/** 所属仓库 ID */
private long repositoryId;
private Long issueId;
/** 类型 */
@NotNull(message = "请选择类型")
private Merge.Type type;
/** 来源分支 */
@NotBlank(message = "请选择来源分支")
private String fromBranch;
/** 去向分支 */
@NotBlank(message = "请选择去向分支")
private String toBranch;
/** 说明标题 */
@NotBlank(message = "请填写标题")
private String title;
/** 合并说明 */
@NotBlank(message = "请填写合并相关说明")
private String description;
}

View File

@@ -0,0 +1,25 @@
package com.imyeyu.api.modules.git.vo.merge;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.vo.user.UserView;
import com.imyeyu.api.modules.git.entity.Issue;
import com.imyeyu.api.modules.git.entity.Merge;
import java.util.List;
/**
* @author 夜雨
* @since 2023-08-17 15:40
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class MergeView extends Merge {
private Issue issue;
private UserView requester;
private List<Attachment> attachmentList;
}

View File

@@ -0,0 +1,16 @@
package com.imyeyu.api.modules.git.vo.release;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.bean.Page;
/**
* @author 夜雨
* @since 2023-08-16 11:46
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ReleasePage extends Page {
private long repositoryId;
}

View File

@@ -0,0 +1,19 @@
package com.imyeyu.api.modules.git.vo.release;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.git.entity.Release;
import java.util.List;
/**
* @author 夜雨
* @since 2024-03-11 11:29
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ReleaseView extends Release {
private List<Attachment> attachmentList;
}

View File

@@ -0,0 +1,19 @@
package com.imyeyu.api.modules.git.vo.repository;
import com.imyeyu.api.modules.git.bean.gitea.Branch;
import com.imyeyu.api.modules.git.bean.gitea.Repository;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* @author 夜雨
* @since 2024-01-09 15:21
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class RepositoryView extends Repository {
private List<Branch> branchList;
}