Kaynağa Gözat

Merge branch 'feat-lzl-sonar'

claws 4 yıl önce
ebeveyn
işleme
3ea42dcb33
54 değiştirilmiş dosya ile 1916 ekleme ve 604 silme
  1. 10 0
      api/src/main/java/cn/seecoder/api/ApplicationProperties.java
  2. 6 0
      api/src/main/java/cn/seecoder/api/DevcloudApiAutoConfiguration.java
  3. 14 3
      api/src/main/java/cn/seecoder/api/docker/impl/DockerApiImpl.java
  4. 6 0
      api/src/main/java/cn/seecoder/api/k8s/JobApi.java
  5. 17 0
      api/src/main/java/cn/seecoder/api/k8s/K8sApiConfiguration.java
  6. 4 0
      api/src/main/java/cn/seecoder/api/k8s/K8sConstants.java
  7. 156 0
      api/src/main/java/cn/seecoder/api/k8s/impl/JobApiImpl.java
  8. 92 0
      api/src/main/java/cn/seecoder/api/k8s/model/Job.java
  9. 3 3
      web/src/main/java/cn/seecoder/web/controller/commit/BranchController.java
  10. 70 0
      web/src/main/java/cn/seecoder/web/controller/commit/CommitController.java
  11. 85 0
      web/src/main/java/cn/seecoder/web/controller/commit/CommitLinkController.java
  12. 46 0
      web/src/main/java/cn/seecoder/web/controller/commit/CommitQualityController.java
  13. 0 229
      web/src/main/java/cn/seecoder/web/controller/gitlab/CommitController.java
  14. 4 0
      web/src/main/java/cn/seecoder/web/core/pipeline/PipelineException.java
  15. 9 6
      web/src/main/java/cn/seecoder/web/core/pipeline/PipelineFactory.java
  16. 55 33
      web/src/main/java/cn/seecoder/web/core/pipeline/config/HandlerConfigTable.java
  17. 2 1
      web/src/main/java/cn/seecoder/web/core/pipeline/handler/JavaImageBuildHandler.java
  18. 183 0
      web/src/main/java/cn/seecoder/web/core/pipeline/handler/K8sJobHandler.java
  19. 175 0
      web/src/main/java/cn/seecoder/web/core/pipeline/handler/SonarJavaImageBuildHandler.java
  20. 13 0
      web/src/main/java/cn/seecoder/web/core/pipeline/handler/SonarPostProcessHandler.java
  21. 2 1
      web/src/main/java/cn/seecoder/web/core/pipeline/template/PipelineTemplateTable.java
  22. 2 2
      web/src/main/java/cn/seecoder/web/dao/commit/CommitMapper.java
  23. 36 0
      web/src/main/java/cn/seecoder/web/dao/commit/CommitQualityMapper.java
  24. 2 0
      web/src/main/java/cn/seecoder/web/infrastructure/config/WebSecurityConfig.java
  25. 25 0
      web/src/main/java/cn/seecoder/web/model/enums/CommitCheckResultEnum.java
  26. 2 2
      web/src/main/java/cn/seecoder/web/model/po/commit/CommitPO.java
  27. 28 0
      web/src/main/java/cn/seecoder/web/model/po/commit/CommitQualityPO.java
  28. 28 0
      web/src/main/java/cn/seecoder/web/model/vo/commit/CommitQualityVO.java
  29. 5 2
      web/src/main/java/cn/seecoder/web/model/vo/commit/CommitVO.java
  30. 1 1
      web/src/main/java/cn/seecoder/web/model/vo/commit/GitWebHookVO.java
  31. 77 0
      web/src/main/java/cn/seecoder/web/model/vo/hook/SonarResultHookVO.java
  32. 2 2
      web/src/main/java/cn/seecoder/web/model/vo/tree/TreeNodeVO.java
  33. 3 1
      web/src/main/java/cn/seecoder/web/service/branch/BranchService.java
  34. 36 0
      web/src/main/java/cn/seecoder/web/service/commit/CommitLinkService.java
  35. 28 0
      web/src/main/java/cn/seecoder/web/service/commit/CommitQualityService.java
  36. 24 0
      web/src/main/java/cn/seecoder/web/service/commit/CommitService.java
  37. 12 2
      web/src/main/java/cn/seecoder/web/service/impl/branch/BranchServiceImpl.java
  38. 1 2
      web/src/main/java/cn/seecoder/web/service/impl/bug_list/BugListServiceImpl.java
  39. 216 0
      web/src/main/java/cn/seecoder/web/service/impl/commit/CommitLinkServiceImpl.java
  40. 126 0
      web/src/main/java/cn/seecoder/web/service/impl/commit/CommitQualityServiceImpl.java
  41. 124 0
      web/src/main/java/cn/seecoder/web/service/impl/commit/CommitServiceImpl.java
  42. 55 44
      web/src/main/java/cn/seecoder/web/service/impl/pipeline/DeploymentServiceImpl.java
  43. 0 210
      web/src/main/java/cn/seecoder/web/service/impl/project/CommitServiceImpl.java
  44. 2 2
      web/src/main/java/cn/seecoder/web/service/impl/tree/TreeNodeServiceImpl.java
  45. 0 50
      web/src/main/java/cn/seecoder/web/service/project/CommitService.java
  46. 7 4
      web/src/main/resources/application-lzl.yml
  47. 3 0
      web/src/main/resources/application-prod.yaml
  48. 9 3
      web/src/main/resources/sql/data_init.sql
  49. 12 0
      web/src/main/resources/sql/table_init.sql
  50. 10 0
      web/src/main/resources/template/config/SONAR_JAVA8.json
  51. 8 0
      web/src/main/resources/template/config/SPRINGBOOT_JAVA8.json
  52. 6 0
      web/src/main/resources/template/dockerfile/SONAR_JAVA8
  53. 72 0
      web/src/test/java/cn/seecoder/web/dao/commit/CommitQualityMapperTest.java
  54. 2 1
      web/src/test/java/cn/seecoder/web/dao/project/CommitMapperTest.java

+ 10 - 0
api/src/main/java/cn/seecoder/api/ApplicationProperties.java

@@ -25,6 +25,8 @@ public class ApplicationProperties {
 	private K8s k8s = new K8s();
 	@Valid
 	private Git git = new Git();
+	@Valid
+	private Sonar sonar = new Sonar();
 
 	@Data
 	public static class Gitlab {
@@ -101,6 +103,14 @@ public class ApplicationProperties {
 		private String password;
 	}
 
+	@Data
+	public static class Sonar {
+		@NotEmpty
+		private String host;
+		@NotEmpty
+		private String loginToken;
+	}
+
 
 
 }

+ 6 - 0
api/src/main/java/cn/seecoder/api/DevcloudApiAutoConfiguration.java

@@ -63,6 +63,12 @@ public class DevcloudApiAutoConfiguration {
         return new DeploymentApiImpl(k8sApiConfiguration().appsV1Api());
     }
 
+    @Bean
+    @ConditionalOnBean(K8sApiConfiguration.class)
+    public JobApi jobApi() {
+        return new JobApiImpl(k8sApiConfiguration().batchV1Api());
+    }
+
     @Bean
     @ConditionalOnBean(K8sApiConfiguration.class)
     public ExecApi execApi(){

+ 14 - 3
api/src/main/java/cn/seecoder/api/docker/impl/DockerApiImpl.java

@@ -4,11 +4,14 @@ package cn.seecoder.api.docker.impl;
 import cn.seecoder.api.ApplicationProperties;
 import com.spotify.docker.client.*;
 import com.spotify.docker.client.auth.ConfigFileRegistryAuthSupplier;
+import com.spotify.docker.client.auth.FixedRegistryAuthSupplier;
 import com.spotify.docker.client.auth.RegistryAuthSupplier;
 import com.spotify.docker.client.exceptions.DockerCertificateException;
 import com.spotify.docker.client.exceptions.DockerException;
 import com.spotify.docker.client.messages.Image;
 import com.spotify.docker.client.messages.ProgressMessage;
+import com.spotify.docker.client.messages.RegistryAuth;
+import com.spotify.docker.client.messages.RegistryConfigs;
 import lombok.extern.apachecommons.CommonsLog;
 import lombok.extern.slf4j.Slf4j;
 
@@ -22,6 +25,7 @@ import java.io.IOException;
 import java.net.URI;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 @Component
@@ -49,9 +53,15 @@ public class DockerApiImpl implements DockerApi {
         }
 
         defaultHandler = new NoActionProgressHandler();
+
+        String registryName = properties.getDocker().getRegistry();
+        String registryUsername = properties.getDocker().getRegistryUsername();
+        String registryPassword = properties.getDocker().getRegistryPassword();
+        RegistryAuth registryAuth = RegistryAuth.builder().serverAddress(registryName).username(registryUsername).password(registryPassword).build();
+        registryAuthSupplier = new FixedRegistryAuthSupplier(registryAuth, RegistryConfigs.create(Collections.singletonMap(registryName, registryAuth)));
         //这里同上,配置文件放在resource的写法会导致打包成jar后无法正常读取配置文件
-        registryAuthSupplier = new ConfigFileRegistryAuthSupplier(new DockerConfigReader(),Paths.get(applicationHome.getDir().getAbsolutePath(), "docker-config.json"));
-        log.info("已读取 "+ applicationHome.getDir().getAbsolutePath()+ " 的docker-config.json文件");
+//        registryAuthSupplier = new ConfigFileRegistryAuthSupplier(new DockerConfigReader(),Paths.get(applicationHome.getDir().getAbsolutePath(), "docker-config.json"));
+//        log.info("已读取 "+ applicationHome.getDir().getAbsolutePath()+ " 的docker-config.json文件");
     }
 
     @Override
@@ -60,7 +70,8 @@ public class DockerApiImpl implements DockerApi {
         if (imageId == null) {
             throw new IOException("Failed to build docker image under [" + directory + "]");
         }
-        client.push(registry + "/" + imageName + ":" + imageTag, new LogActionProgressHandler(), registryAuthSupplier.authFor(registry + "/" + imageName + ":" + imageTag));
+        RegistryAuth registryAuth = registryAuthSupplier.authFor(registry + "/" + imageName + ":" + imageTag);
+        client.push(registry + "/" + imageName + ":" + imageTag, new LogActionProgressHandler(), registryAuth);
     }
 
     @Override

+ 6 - 0
api/src/main/java/cn/seecoder/api/k8s/JobApi.java

@@ -0,0 +1,6 @@
+package cn.seecoder.api.k8s;
+
+import cn.seecoder.api.k8s.model.Job;
+
+public interface JobApi extends AbstractApi<Job>{
+}

+ 17 - 0
api/src/main/java/cn/seecoder/api/k8s/K8sApiConfiguration.java

@@ -4,6 +4,7 @@ import cn.seecoder.api.ApplicationProperties;
 import io.kubernetes.client.Exec;
 import io.kubernetes.client.openapi.ApiClient;
 import io.kubernetes.client.openapi.apis.AppsV1Api;
+import io.kubernetes.client.openapi.apis.BatchV1Api;
 import io.kubernetes.client.openapi.apis.CoreV1Api;
 import io.kubernetes.client.openapi.apis.ExtensionsV1beta1Api;
 import io.kubernetes.client.util.Config;
@@ -20,6 +21,8 @@ public class K8sApiConfiguration {
 
     private final AppsV1Api appsV1Api;
 
+    private final BatchV1Api batchV1Api;
+
     private final ExtensionsV1beta1Api extensionsV1beta1Api;
 
     private final Exec exec;
@@ -33,6 +36,8 @@ public class K8sApiConfiguration {
 
     private final AppsV1Api watchAppsV1Api;
 
+    private final BatchV1Api watchBatchV1Api;
+
     public K8sApiConfiguration(ApplicationProperties applicationProperties) {
         ApplicationProperties.K8s k8s = applicationProperties.getK8s();
         this.apiClient = Config.fromToken(k8s.getApiServer(), k8s.getToken(), k8s.getValidateSSL());
@@ -40,6 +45,7 @@ public class K8sApiConfiguration {
         io.kubernetes.client.openapi.Configuration.setDefaultApiClient(this.apiClient);
         this.coreV1Api = new CoreV1Api();
         this.appsV1Api = new AppsV1Api();
+        this.batchV1Api = new BatchV1Api();
         this.extensionsV1beta1Api = new ExtensionsV1beta1Api();
         this.exec = new Exec();
 
@@ -47,6 +53,7 @@ public class K8sApiConfiguration {
         this.watchApiClient = Config.fromToken(k8s.getApiServer(), k8s.getToken(), k8s.getValidateSSL());
         this.watchCoreV1Api = new CoreV1Api(watchApiClient);
         this.watchAppsV1Api = new AppsV1Api(watchApiClient);
+        this.watchBatchV1Api = new BatchV1Api(watchApiClient);
     }
 
     @Bean
@@ -64,6 +71,11 @@ public class K8sApiConfiguration {
         return appsV1Api;
     }
 
+    @Bean
+    public BatchV1Api batchV1Api() {
+        return batchV1Api;
+    }
+
     @Bean
     public ExtensionsV1beta1Api extensionsV1beta1Api() {
         return extensionsV1beta1Api;
@@ -89,4 +101,9 @@ public class K8sApiConfiguration {
     public AppsV1Api watchAppsV1Api() {
         return watchAppsV1Api;
     }
+
+    @Bean
+    public BatchV1Api watchBatchV1Api() {
+        return watchBatchV1Api;
+    }
 }

+ 4 - 0
api/src/main/java/cn/seecoder/api/k8s/K8sConstants.java

@@ -13,6 +13,8 @@ public class K8sConstants {
     //应用
     public static final String APPLICATION_LABEL = LABEL_PREFIX + "/app";
 
+    public static final String JOB_LABEL = LABEL_PREFIX + "/job";
+
     public static final String DEPLOY_ID_LABEL = LABEL_PREFIX + "/deployid";
     // 用户namespace的label
     public static final String USERSPACE_LABEL = LABEL_PREFIX +"/userspace";
@@ -51,6 +53,8 @@ public class K8sConstants {
 
     public final static String DEPLOYMENT_SUFFIX = "-deployment";
 
+    public final static String JOB_SUFFIX = "-job";
+
     public final static String SERVICE_SUFFIX = "-service";
 
     public final static String EXTERNAL_SERVICE_SUFFIX = "-external-service";

+ 156 - 0
api/src/main/java/cn/seecoder/api/k8s/impl/JobApiImpl.java

@@ -0,0 +1,156 @@
+package cn.seecoder.api.k8s.impl;
+
+import cn.seecoder.api.k8s.JobApi;
+import cn.seecoder.api.k8s.exception.K8sApiException;
+import cn.seecoder.api.k8s.model.Job;
+import cn.seecoder.api.k8s.model.K8sObjectRequest;
+
+import cn.seecoder.api.k8s.model.LabelSelector;
+import cn.seecoder.common.util.LoggerUtil;
+import com.google.gson.JsonSyntaxException;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.openapi.apis.BatchV1Api;
+import io.kubernetes.client.openapi.models.V1DeleteOptions;
+import io.kubernetes.client.openapi.models.V1DeleteOptionsBuilder;
+import io.kubernetes.client.openapi.models.V1Job;
+import io.kubernetes.client.openapi.models.V1JobList;
+import org.slf4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static cn.seecoder.api.k8s.K8sConstants.FOREGROUND_PROPAGATION_POLICY;
+import static cn.seecoder.api.k8s.K8sConstants.PRETTY_FORMAT;
+import static cn.seecoder.api.k8s.exception.K8sApiException.*;
+
+@Service
+public class JobApiImpl implements JobApi {
+
+    private static final Logger logger = LoggerUtil.getLogger(JobApi.class);
+
+    private BatchV1Api batchV1Api;
+
+    @Autowired
+    public JobApiImpl(BatchV1Api batchV1Api){
+        this.batchV1Api = batchV1Api;
+    }
+
+    @Override
+    public Job create(Job job) {
+        try {
+            V1Job v1Job = job.toK8sObject();
+            V1Job resultJob = batchV1Api.createNamespacedJob(job.getNamespace(), v1Job, PRETTY_FORMAT, null, null);
+            return resultJob == null ? null : new Job(resultJob);
+        } catch (ApiException e){
+            LoggerUtil.error(logger, e, "job创建失败, job={}, response={}", job, e.getResponseBody());
+            if (e.getCode() == NOT_FOUND) {
+                throw new K8sApiException(NOT_FOUND, "namespace不存在");
+            } else if (e.getCode() == ALREADY_EXIST) {
+                throw new K8sApiException(ALREADY_EXIST, "job已经存在");
+            } else {
+                throw K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e){
+            LoggerUtil.error(logger, e, "job创建异常, job={}", job);
+            throw e;
+        }
+    }
+
+    @Override
+    public Job update(Job job) {
+        delete(job);
+        try {
+            LoggerUtil.info(logger, "waiting 1min...");
+            Thread.sleep(60000);
+            LoggerUtil.info(logger, "10s has gone");
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        return create(job);
+    }
+
+    @Override
+    public void delete(Job job) {
+        try {
+            V1DeleteOptions v1DeleteOptions = new V1DeleteOptionsBuilder()
+                    .withApiVersion(Job.API_VERSION)
+                    .withPropagationPolicy(FOREGROUND_PROPAGATION_POLICY)
+                    .build();
+            batchV1Api.deleteNamespacedJob(
+                    job.getName(),
+                    job.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    FOREGROUND_PROPAGATION_POLICY,
+                    v1DeleteOptions
+            );
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "删除Job失败,namespace={}, name={}, response={}", job.getNamespace(), job.getName(), e.getResponseBody());
+            if (e.getCode() == NOT_FOUND) {
+                throw new K8sApiException(NOT_FOUND, "namespace不存在");
+            } else {
+                throw K8s_SYSTEM_ERROR_EXCEPTION;
+            }
+        } catch (Exception e) {
+            // k8s api的bug,其实删除成功了,但是返回值类型不正确
+            LoggerUtil.info(logger, "删除Job成功");
+        }
+
+    }
+
+    @Override
+    public List<Job> getByCondition(K8sObjectRequest request) {
+        try {
+            if (request.getName() != null) {
+                V1Job v1Job = batchV1Api.readNamespacedJob(
+                        request.getName(),
+                        request.getNamespace(),
+                        PRETTY_FORMAT,
+                        null,
+                        null
+                );
+                return v1Job == null ? Collections.emptyList() : Collections.singletonList(new Job(v1Job));
+            }
+            LabelSelector selector = null;
+            if (request.getSelector() != null) {
+                selector = request.getSelector();
+            }
+            if (request.getLabels() != null) {
+                if (selector == null) {
+                    selector = new LabelSelector();
+                }
+                selector.addAll(request.getLabels());
+            }
+            V1JobList jobList = batchV1Api.listNamespacedJob(
+                    request.getNamespace(),
+                    PRETTY_FORMAT,
+                    null,
+                    null,
+                    null,
+                    selector.toJsonString(),
+                    null,
+                    null,
+                    null,
+                    null
+            );
+            return jobList.getItems().stream().map(Job::new).collect(Collectors.toList());
+        } catch (ApiException e) {
+            LoggerUtil.error(logger, e, "查找job失败,namespace = {}, response={}",
+                    request.getNamespace(), e.getResponseBody());
+            if (e.getCode() == NOT_FOUND) {
+                return Collections.emptyList();
+            }else {
+                return Collections.emptyList();
+            }
+        } catch (Exception e) {
+            LoggerUtil.error(logger, e, "查找job失败,namespace = {}, name={}",
+                    request.getNamespace());
+            return Collections.emptyList();
+        }
+    }
+}

+ 92 - 0
api/src/main/java/cn/seecoder/api/k8s/model/Job.java

@@ -0,0 +1,92 @@
+package cn.seecoder.api.k8s.model;
+
+import io.kubernetes.client.openapi.models.*;
+import lombok.*;
+import org.springframework.data.annotation.ReadOnlyProperty;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+@EqualsAndHashCode(callSuper=true)
+public class Job extends K8sAbstractObject<V1Job>{
+
+    public static final String API_VERSION = "batch/v1";
+    private static final String KIND = "Job";
+    private static final Integer TTL_SECONDS_AFTER_FINISHED = 100;
+    private static final Long ACTIVE_DEADLINE_SECONDS = 3600L;
+
+    @Builder.Default
+    private List<Container> containers = new ArrayList<>();
+
+    @Builder.Default
+    private List<String> imagePullSecrets = new ArrayList<>();
+
+    @Builder.Default
+    private Long timeout = ACTIVE_DEADLINE_SECONDS;
+
+    @Builder.Default
+    private Integer ttlFinished = TTL_SECONDS_AFTER_FINISHED;
+
+    @ReadOnlyProperty
+    private V1JobStatus status;
+
+    public Job(V1Job v1Job) {
+        super(v1Job);
+        this.containers = v1Job.getSpec().getTemplate().getSpec().getContainers() == null ? null : v1Job.getSpec().getTemplate().getSpec().getContainers()
+                .stream().map(Container::new).collect(Collectors.toList());
+        this.imagePullSecrets = v1Job.getSpec().getTemplate().getSpec().getImagePullSecrets() == null ? null : v1Job.getSpec().getTemplate().getSpec().getImagePullSecrets()
+                .stream().map(V1LocalObjectReference::getName).collect(Collectors.toList());
+        this.timeout = v1Job.getSpec().getActiveDeadlineSeconds();
+        this.ttlFinished = v1Job.getSpec().getTtlSecondsAfterFinished();
+        this.status = v1Job.getStatus();
+    }
+
+    private V1PodTemplateSpec toPodSpecTemplate(){
+        V1ObjectMeta v1ObjectMeta = new V1ObjectMetaBuilder()
+                .withLabels(getLabels())
+                .withAnnotations(getAnnotations())
+                .build();
+//        V1PodSpec v1PodSpec = new V1PodSpecBuilder()
+//                .addAllToContainers(containers == null ? null : containers.stream().map(Container::toK8sObject).collect(Collectors.toList()))
+//                .withImagePullSecrets(references)
+//                .withVolumes(volumes)
+//                .withNodeSelector(nodeSelectors)
+//                .build();
+        List<V1LocalObjectReference> references = imagePullSecrets == null ? null : imagePullSecrets
+                .stream()
+                .map(secret -> new V1LocalObjectReference().name(secret))
+                .collect(Collectors.toList());
+        V1PodSpec v1PodSpec = new V1PodSpecBuilder()
+                .withImagePullSecrets(references)
+                .addAllToContainers(containers == null ? null : containers.stream().map(Container::toK8sObject).collect(Collectors.toList()))
+                .withRestartPolicy("Never")
+                .build();
+        return new V1PodTemplateSpecBuilder()
+                .withMetadata(v1ObjectMeta)
+                .withSpec(v1PodSpec)
+                .build();
+    }
+
+    private V1JobSpec toV1JobSpec(){
+        return new V1JobSpecBuilder()
+                .withTtlSecondsAfterFinished(ttlFinished)
+                .withActiveDeadlineSeconds(timeout)
+                .withTemplate(this.toPodSpecTemplate())
+                .build();
+    }
+
+    @Override
+    public V1Job toK8sObject() {
+        return new V1JobBuilder()
+                .withApiVersion(getApiVersion() == null ? API_VERSION : getApiVersion())
+                .withKind(getKind() == null ? KIND : getKind())
+                .withMetadata(this.toV1ObjectMeta())
+                .withSpec(this.toV1JobSpec())
+                .build();
+    }
+}

+ 3 - 3
web/src/main/java/cn/seecoder/web/controller/gitlab/BranchController.java → web/src/main/java/cn/seecoder/web/controller/commit/BranchController.java

@@ -1,7 +1,7 @@
-package cn.seecoder.web.controller.gitlab;
+package cn.seecoder.web.controller.commit;
 
 import cn.seecoder.web.model.vo.Response;
-import cn.seecoder.web.service.project.BranchService;
+import cn.seecoder.web.service.branch.BranchService;
 import com.nju.edu.gitlab.vo.BranchVO;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiImplicitParam;
@@ -25,7 +25,7 @@ import java.util.List;
 public class BranchController {
     final static String GET_BRANCHES_ERROR = "Fail to search all branches";
 
-    final BranchService branchService;
+    private final BranchService branchService;
 
     @Autowired
     public BranchController(BranchService branchService) {

+ 70 - 0
web/src/main/java/cn/seecoder/web/controller/commit/CommitController.java

@@ -0,0 +1,70 @@
+package cn.seecoder.web.controller.commit;
+
+import cn.seecoder.web.model.vo.Response;
+import cn.seecoder.web.model.vo.commit.CommitVO;
+import cn.seecoder.web.service.commit.CommitService;
+import com.nju.edu.gitlab.vo.DiffVO;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author chenyz
+ * @date 2021/2/4
+ * @description: 提交管理接口
+ */
+@Api(tags = "Commit Manage API")
+@RestController
+@Slf4j
+public class CommitController {
+    private final CommitService commitService;
+
+    @Autowired
+    public CommitController(CommitService commitService) {
+        this.commitService = commitService;
+    }
+
+    @ApiOperation(value = "获取指定项目指定分支所有的commit信息", httpMethod = "GET")
+    @GetMapping("/commits/by_branch")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "projectId", dataType = "int", paramType = "query"),
+            @ApiImplicitParam(name = "branchName", dataType = "String", paramType = "query")
+    })
+    public Response<List<CommitVO>> getCommitsByBranch(@RequestParam("projectId") Integer projectId,
+                                                                                    @RequestParam("branchName") String branchName){
+        return Response.buildSuccess(commitService.getCommitsByBranchName(projectId, branchName));
+    }
+
+    @ApiOperation(value = "获得一个commit的信息", httpMethod = "GET")
+    @GetMapping("/commit")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "projectId", dataType = "int", paramType = "query"),
+            @ApiImplicitParam(name = "hash", dataType = "String", paramType = "query")
+    })
+    public Response<CommitVO> getCommitByHash(@RequestParam("projectId") Integer projectId,
+                                              @RequestParam("hash") String hash)
+    {
+        return Response.buildSuccess(commitService.getCommitByHash(projectId, hash));
+    }
+
+    @ApiOperation(value = "获得两个提交之间的差异", httpMethod = "GET")
+    @GetMapping("/commits/diff")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "projectId", dataType = "int", paramType = "query"),
+            @ApiImplicitParam(name = "from", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "to", dataType = "String", paramType = "query")
+    })
+    public Response<Set<DiffVO>> getDiff(@RequestParam("projectId") Integer projectId,
+                                         @RequestParam("from") String from,
+                                         @RequestParam("to") String to)
+    {
+        return Response.buildSuccess(commitService.getDiff(projectId, from, to));
+    }
+}

+ 85 - 0
web/src/main/java/cn/seecoder/web/controller/commit/CommitLinkController.java

@@ -0,0 +1,85 @@
+package cn.seecoder.web.controller.commit;
+
+import cn.seecoder.common.exceptions.ServiceException;
+import cn.seecoder.web.model.enums.CommitRelatedEnum;
+import cn.seecoder.web.model.vo.commit.GitWebHookVO;
+import cn.seecoder.web.model.vo.Response;
+import cn.seecoder.web.model.vo.commit.CommitVO;
+import cn.seecoder.web.service.commit.CommitLinkService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Api(tags = "Commit Link Manage API")
+@RestController
+@Slf4j
+public class CommitLinkController {
+    private final static String LINK_FAIL_ERROR = "Commit Link Failed! No Such Bug or Task!";
+
+    @Autowired
+    CommitLinkService commitLinkService;
+    /**
+     * @author chenyz
+     * @date 2021/2/4
+     * @description: 此接口挂载在webhooks的push回调中
+     * todo webhook最多只能获取20个commit
+     *  因为gitlab官方,考虑性能原因导致
+     *  详见 https://docs.gitlab.com/ee/user/project/integrations/webhooks.html push event内容
+     *  暂时无解
+     * 只关注push event
+     * 所有分支的push都分析
+     */
+    @ApiOperation(value = "push回调接口", httpMethod = "POST")
+    @PostMapping("/hook")
+    @ApiImplicitParam(name = "gitWebHookVO",dataType = "object", paramType = "body")
+    public void pushHook(@RequestBody GitWebHookVO gitWebHookVO) throws ServiceException {
+        commitLinkService.linkCommitByInfoFromGitlab(gitWebHookVO);
+    }
+
+    @ApiOperation(value = "获取某个需求树结点的所有commit信息", httpMethod = "GET")
+    @GetMapping("/commits/tree")
+    @ApiImplicitParam(name = "treeId",dataType = "int", paramType = "query")
+    public Response<List<CommitVO>> getTreeCommits(@RequestParam("treeId") Integer treeId){
+        return Response.buildSuccess(commitLinkService.getTreeLinkedCommits(treeId));
+    }
+
+    @ApiOperation(value = "获取bug list中一个bug关联的所有commit信息", httpMethod = "GET")
+    @GetMapping("/commits/bug_list")
+    @ApiImplicitParam(name = "bugId",dataType = "int", paramType = "query")
+    public Response<List<CommitVO>> getBugCommits(@RequestParam("bugId") Integer bugId){
+        return Response.buildSuccess(commitLinkService.getBugLinkedCommits(bugId));
+    }
+
+    @ApiOperation(value = "获取指定项目的所有commit信息", httpMethod = "GET")
+    @GetMapping("/commits/list")
+    @ApiImplicitParam(name = "projectId", dataType = "int", paramType = "query")
+    public Response<List<CommitVO>> getCommits(@RequestParam("projectId") Integer projectId) {
+        return Response.buildSuccess(commitLinkService.getLinkedCommits(projectId));
+    }
+
+    @ApiOperation(value = "关联commit和缺陷、任务节点", httpMethod = "GET")
+    @GetMapping("/commits/link")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "projectId", dataType = "int", paramType = "query"),
+            @ApiImplicitParam(name = "commitId", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "relatedType", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "relatedId", dataType = "int", paramType = "query")
+    })
+    public Response<Object> linkCommit(@RequestParam("projectId") Integer projectId,
+                                       @RequestParam("commitId") String commitId,
+                                       @RequestParam("relatedType") CommitRelatedEnum relatedType,
+                                       @RequestParam("relatedId") Integer relatedId)
+    {
+        if (commitLinkService.link(projectId, commitId, relatedType, relatedId)) {
+            return Response.buildSuccess(null);
+        } else {
+            return Response.buildFailure(404, LINK_FAIL_ERROR);
+        }
+    }
+}

+ 46 - 0
web/src/main/java/cn/seecoder/web/controller/commit/CommitQualityController.java

@@ -0,0 +1,46 @@
+package cn.seecoder.web.controller.commit;
+
+import cn.seecoder.common.exceptions.ServiceException;
+import cn.seecoder.web.model.vo.Response;
+import cn.seecoder.web.model.vo.commit.CommitQualityVO;
+import cn.seecoder.web.model.vo.hook.SonarResultHookVO;
+import cn.seecoder.web.service.commit.CommitQualityService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@Api(tags = "Commit Quality Manage API")
+@RestController
+public class CommitQualityController {
+    private final static String QUALITY_QUERY_ERROR = "查询代码版本质量失败";
+
+    @Autowired
+    CommitQualityService commitQualityService;
+
+    @ApiOperation(value = "sonarqube回调接口", httpMethod = "POST")
+    @PostMapping("/sonar_hook")
+    @ApiImplicitParam(name = "sonarResultHookVO", dataType = "SonarResultHookVO", paramType = "body")
+    public Response<Object> sonarHook(@RequestBody SonarResultHookVO sonarResultHookVO) throws ServiceException {
+        if (commitQualityService.processSonarResult(sonarResultHookVO)) {
+            return Response.buildSuccess("ok");
+        } else {
+            return Response.buildFailure(500, "Post Body Process Error!");
+        }
+    }
+
+    @ApiOperation(value = "查询代码版本质量", httpMethod = "GET")
+    @GetMapping("commit/quality")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "projectId", dataType = "int", paramType = "query"),
+            @ApiImplicitParam(name = "commitId", dataType = "String", paramType = "query")
+    })
+    public Response<CommitQualityVO> getQualityByProjectIdAndCommit(@RequestParam("projectId") Integer projectId,
+                                                                    @RequestParam("commitId") String commitId)
+    {
+        CommitQualityVO quality = commitQualityService.getQualityByCommit(projectId, commitId);
+        return Response.buildSuccess(quality);
+    }
+}

+ 0 - 229
web/src/main/java/cn/seecoder/web/controller/gitlab/CommitController.java

@@ -1,229 +0,0 @@
-package cn.seecoder.web.controller.gitlab;
-
-import cn.seecoder.common.exceptions.ServiceException;
-import cn.seecoder.common.util.LogTrackingUtil;
-import cn.seecoder.common.util.OpType;
-import cn.seecoder.web.core.commit.CommitResolver;
-import cn.seecoder.web.core.commit.model.CommitRule;
-import cn.seecoder.web.dao.user.UserMapper;
-import cn.seecoder.web.model.enums.CommitRelatedEnum;
-import cn.seecoder.web.model.po.project.CommitPO;
-import cn.seecoder.web.model.vo.GitWebHookVO;
-import cn.seecoder.web.model.vo.Response;
-import cn.seecoder.web.model.vo.project.CommitVO;
-import cn.seecoder.web.service.bug_list.BugListService;
-import cn.seecoder.web.service.project.CommitService;
-import cn.seecoder.web.service.tree.TreeNodeService;
-import com.alibaba.fastjson.JSONObject;
-import com.nju.edu.gitlab.SeecoderGitlabApi;
-import com.nju.edu.gitlab.vo.DiffVO;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiImplicitParam;
-import io.swagger.annotations.ApiImplicitParams;
-import io.swagger.annotations.ApiOperation;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.*;
-
-import java.util.List;
-import java.util.Set;
-
-/**
- * @author chenyz
- * @date 2021/2/4
- * @description: 与GitLab进行交互的接口
- */
-@Api(tags = "GitLab Hook API")
-@RestController
-@Slf4j
-public class CommitController {
-
-    private final static String LINK_FAIL_ERROR = "Commit Link Failed! No Such Bug or Task Found!";
-
-    private final BugListService bugListService;
-
-    private final TreeNodeService treeNodeService;
-
-    private final CommitService commitService;
-
-    private final SeecoderGitlabApi seecoderGitlabApi;
-
-    // 赶时间暂时这么写
-    @Autowired
-    UserMapper userMapper;
-
-    @Autowired
-    public CommitController(BugListService bugListService, TreeNodeService treeNodeService, CommitService commitService, SeecoderGitlabApi seecoderGitlabApi) {
-        this.bugListService = bugListService;
-        this.treeNodeService = treeNodeService;
-        this.commitService = commitService;
-        this.seecoderGitlabApi = seecoderGitlabApi;
-    }
-
-
-    /**
-     * @author chenyz
-     * @date 2021/2/4
-     * @description: 此接口挂载在webhooks的push回调中
-     * todo webhook最多只能获取20个commit
-     *  因为gitlab官方,考虑性能原因导致
-     *  详见 https://docs.gitlab.com/ee/user/project/integrations/webhooks.html push event内容
-     *  暂时无解
-     * 只关注push event
-     * 所有分支的push都分析
-     */
-    @ApiOperation(value = "push回调接口", httpMethod = "POST")
-    @PostMapping("/hook")
-    @ApiImplicitParam(name = "gitWebHookVO",dataType = "object", paramType = "body")
-    public void pushHook(@RequestBody GitWebHookVO gitWebHookVO) throws ServiceException {
-
-//
-//        List<String> emails = new ArrayList<>();
-//        emails.add(gitWebHookVO.getUser_email());
-//        List<GitlabUserEmailDTO> users;
-//        try {
-//            users = seecoderGitlabApi.findUserByEmails(emails);
-//        } catch (SeecoderGitlabException e) {
-//            throw new ServiceException("统一代码服务出现访问错误,无法正确分析webhook信息");
-//        }
-//        Integer userId = users.size()!=0 ? users.get(0).getUserId() : null;
-
-        Integer projectId = gitWebHookVO.getProject_id();
-        for (GitWebHookVO.Commit commit: gitWebHookVO.getCommits()){
-            if (commitService.isResolved(commit.getId())){
-                //已经解析过了
-                log.info("Commits 已经解析过了, id: {}", commit.getId());
-                continue;
-            }
-            //ANA 日志需要打出用户的commit信息
-            JSONObject object = new JSONObject();
-            try{
-                object.put("commit_id",commit.getId());
-                object.put("user_id",userMapper.selectIdByEmail(commit.getAuthor().getEmail()));
-                object.put("project_id",projectId);
-                object.put("commit_time",commit.getTimestamp());
-                object.put("message",commit.getMessage());
-            }catch (Exception ignored){}
-
-
-            log.info("resolving commit id : {}, title : {}, message : {}", commit.getId(), commit.getTitle(), commit.getMessage());
-            //todo 注意目前分析的是 message
-            // 应该分析 title 还是 message 与学生的提交规范有关
-            CommitRule rule = CommitResolver.resolve(commit.getMessage());
-            if (rule == null){
-                log.info("Commits 不符合规范, id: {}", commit.getId());
-                //ANA
-                try {
-                    object.put("type","不合规范");
-                    String data = JSONObject.toJSONString(object);
-                    LogTrackingUtil.log(data, OpType.COMMIT);
-                } catch (Exception ignored) {}
-                continue;
-            }
-            switch (rule.getType()){
-                case FEAT:
-                    treeNodeService.updateNodeState(rule.getId(),rule.getState());
-                    commitService.save(new CommitPO(commit,projectId, CommitRelatedEnum.TREE_ID, rule.getId()));
-                    log.info("Commits 关联tree节点, id: {}", commit.getId());
-                    //ANA
-                    try {
-                        object.put("type","关联tree");
-                        String data = JSONObject.toJSONString(object);
-                        LogTrackingUtil.log(data, OpType.COMMIT);
-                    } catch (Exception ignored) {}
-                    break;
-                case FIX:
-                    bugListService.updateState(rule.getState(), rule.getId());
-                    commitService.save(new CommitPO(commit,projectId, CommitRelatedEnum.BUG_ID, rule.getId()));
-                    log.info("Commits 关联bug节点, id: {}", commit.getId());
-                    //ANA
-                    try {
-                        object.put("type","关联bug");
-                        String data = JSONObject.toJSONString(object);
-                        LogTrackingUtil.log(data, OpType.COMMIT);
-                    } catch (Exception ignored) {}
-                    break;
-                case UNKNOWN:
-                    break;
-            }
-        }
-    }
-
-    @ApiOperation(value = "获取某个需求树结点的所有commit信息", httpMethod = "GET")
-    @GetMapping("/commits/tree")
-    @ApiImplicitParam(name = "treeId",dataType = "int", paramType = "query")
-    public Response<List<CommitVO>> getTreeCommits(@RequestParam("treeId") Integer treeId){
-        return Response.buildSuccess(commitService.getTreeCommit(treeId));
-    }
-
-    @ApiOperation(value = "获取bug list中一个bug关联的所有commit信息", httpMethod = "GET")
-    @GetMapping("/commits/bug_list")
-    @ApiImplicitParam(name = "bugId",dataType = "int", paramType = "query")
-    public Response<List<CommitVO>> getBugCommits(@RequestParam("bugId") Integer bugId){
-        return Response.buildSuccess(commitService.getBugCommit(bugId));
-    }
-
-    @ApiOperation(value = "获取指定项目的所有commit信息", httpMethod = "GET")
-    @GetMapping("/commits/list")
-    @ApiImplicitParam(name = "projectId", dataType = "int", paramType = "query")
-    public Response<List<CommitVO>> getCommits(@RequestParam("projectId") Integer projectId) {
-        return Response.buildSuccess(commitService.getCommits(projectId));
-    }
-
-    @ApiOperation(value = "获取指定项目指定分支所有的commit信息", httpMethod = "GET")
-    @GetMapping("/commits/by_branch")
-    @ApiImplicitParams({
-            @ApiImplicitParam(name = "projectId", dataType = "int", paramType = "query"),
-            @ApiImplicitParam(name = "branchName", dataType = "String", paramType = "query")
-    })
-    public Response<List<CommitVO>> getCommitsByBranch(@RequestParam("projectId") Integer projectId,
-                                                                                    @RequestParam("branchName") String branchName){
-        return Response.buildSuccess(commitService.getCommitsByBranchName(projectId, branchName));
-    }
-
-    @ApiOperation(value = "获得一个commit的信息", httpMethod = "GET")
-    @GetMapping("/commit")
-    @ApiImplicitParams({
-            @ApiImplicitParam(name = "projectId", dataType = "int", paramType = "query"),
-            @ApiImplicitParam(name = "hash", dataType = "String", paramType = "query")
-    })
-    public Response<CommitVO> getCommitByHash(@RequestParam("projectId") Integer projectId,
-                                              @RequestParam("hash") String hash)
-    {
-        return Response.buildSuccess(commitService.getCommitByHash(projectId, hash));
-    }
-
-    @ApiOperation(value = "获得两个提交之间的差异", httpMethod = "GET")
-    @GetMapping("/commits/diff")
-    @ApiImplicitParams({
-            @ApiImplicitParam(name = "projectId", dataType = "int", paramType = "query"),
-            @ApiImplicitParam(name = "from", dataType = "String", paramType = "query"),
-            @ApiImplicitParam(name = "to", dataType = "String", paramType = "query")
-    })
-    public Response<Set<DiffVO>> getDiff(@RequestParam("projectId") Integer projectId,
-                                         @RequestParam("from") String from,
-                                         @RequestParam("to") String to)
-    {
-        return Response.buildSuccess(commitService.getDiff(projectId, from, to));
-    }
-
-    @ApiOperation(value = "关联commit和缺陷、任务节点", httpMethod = "GET")
-    @GetMapping("/commits/link")
-    @ApiImplicitParams({
-            @ApiImplicitParam(name = "projectId", dataType = "int", paramType = "query"),
-            @ApiImplicitParam(name = "commitId", dataType = "String", paramType = "query"),
-            @ApiImplicitParam(name = "relatedType", dataType = "String", paramType = "query"),
-            @ApiImplicitParam(name = "relatedId", dataType = "int", paramType = "query")
-    })
-    public Response<Object> linkCommit(@RequestParam("projectId") Integer projectId,
-                                       @RequestParam("commitId") String commitId,
-                                       @RequestParam("relatedType") CommitRelatedEnum relatedType,
-                                       @RequestParam("relatedId") Integer relatedId)
-    {
-        if (commitService.link(projectId, commitId, relatedType, relatedId)){
-            return Response.buildSuccess(null);
-        } else {
-            return Response.buildFailure(404, LINK_FAIL_ERROR);
-        }
-    }
-}

+ 4 - 0
web/src/main/java/cn/seecoder/web/core/pipeline/PipelineException.java

@@ -10,12 +10,16 @@ public class PipelineException extends Exception{
     public static final String TEMPLATE_NAME_NOT_EXIST = "Pipeline模板名不存在";
     public static final String CONFIG_TRANSFER_ERROR = "流水线配置转换发生错误";
     public static final String GIT_CLONE_ERROR = "从仓库拉取代码发生错误";
+    public static final String BRANCH_NOT_FOUND_ERROR = "branch不存在";
+    public static final String PROJECT_NOT_FOUND_ERROR = "project id对应的项目不存在";
     public static final String DOCKERFILE_CREATE_ERROR = "创建dockerfile文件错误";
     public static final String NGINX_CONFIG_CREATE_ERROR = "创建nginx配置文件错误";
     public static final String IMAGE_BUILD_ERROR = "构建镜像构建推送到仓库错误";
     public static final String INGRESS_CREATE_ERROR = "K8S ingress 创建错误";
     public static final String SERVICE_CREATE_ERROR = "K8S service 创建错误";
     public static final String DEPLOYMENT_CREATE_ERROR = "K8S deployment 创建错误";
+    public static final String JOB_CREATE_ERROR = "K8S job 创建错误";
+    public static final String JOB_IMAGE_NOT_EXIST_ERROR = "镜像不存在,需要重试";
     public static final String LIMIT_RANGE_CREATE_ERROR = "K8S limit range 创建错误";
 
     public static final String NAMESPACE_CREATE_ERROR = "K8S namespace 创建错误";

+ 9 - 6
web/src/main/java/cn/seecoder/web/core/pipeline/PipelineFactory.java

@@ -19,12 +19,12 @@ import java.util.List;
 public class PipelineFactory {
     /**
      * 流水线初始化
-     *
+     * <p>
      * 注意 namespace deployName 只能为 小写英文字母 . - _ 的组合,这是因为docker镜像名的限制
      *
-     * @param configJson   configJson 流水线配置的json字符串
-     * @param namespace    流水线的k8s namespace,devcloud后端采用Project中的"name-id"作为namespace
-     * @param deployName   流水线部署k8s的应用名称,devcloud后端采用Pipeline中的name作为project name
+     * @param configJson configJson 流水线配置的json字符串
+     * @param namespace  流水线的k8s namespace,devcloud后端采用Project中的"name-id"作为namespace
+     * @param deployName 流水线部署k8s的应用名称,devcloud后端采用Pipeline中的name作为project name
      * @return
      */
     public static Pipeline init(Integer id, String configJson, String namespace, String deployName) throws PipelineException {
@@ -57,13 +57,16 @@ public class PipelineFactory {
             });
             for (ObjectNode node : handlers) {
                 //用于判断此步骤是否启用
-                if (!node.get("active").asBoolean()){
+                if (!node.get("active").asBoolean()) {
                     continue;
                 }
                 String name = node.get("name").toString().replaceAll("\"", "");
                 JsonNode configs = node.get("configs");
+                // 每个步骤并不一定对应一个Handler,而是可能对应多个Handler
                 cur.setNextHandler(HandlerConfigTable.transToHandler(name, configs));
-                cur = cur.getNextHandler();
+                while (cur.getNextHandler() != null) {
+                    cur = cur.getNextHandler();
+                }
             }
         } catch (Exception e) {
             throw new PipelineException(HttpStatus.SC_BAD_REQUEST, PipelineException.CONFIG_TRANSFER_ERROR, e);

+ 55 - 33
web/src/main/java/cn/seecoder/web/core/pipeline/config/HandlerConfigTable.java

@@ -7,16 +7,14 @@ import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.http.HttpStatus;
+
 import java.lang.reflect.Field;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
+import java.util.*;
 
 /**
  * @author PuHong Weng
  * @date 2021/3/10
- * @description:
- * 记录流水线配置json中 name到具体Handler的映射
+ * @description: 记录流水线配置json中 name到具体Handler的映射
  */
 @Slf4j
 public class HandlerConfigTable {
@@ -24,44 +22,68 @@ public class HandlerConfigTable {
     /**
      * 根据名称映射对应的流水线执行类
      */
-    private static final Map<String, Class<?>> handlerConfigMap;
+    private static final Map<String, List<Class<?>>> handlerConfigMap;
 
 
     static {
         handlerConfigMap = new HashMap<>();
-        handlerConfigMap.put("node-image-build", NodeImageBuildHandler.class);
-        handlerConfigMap.put("java-image-build", JavaImageBuildHandler.class);
-        handlerConfigMap.put("k8s-deployment", K8sDeploymentHandler.class);
-        handlerConfigMap.put("k8s-service", K8sServiceHandler.class);
-        handlerConfigMap.put("k8s-ingress", K8sIngressHandler.class);
-        handlerConfigMap.put("k8s-namespace", K8sNamespaceHandler.class);
-        handlerConfigMap.put("deploy", DeployHandler.class);
-        handlerConfigMap.put("api-test", ApiTestHandler.class);
-//        handlerConfigMap.put("interface-test", InterfaceTestHandler.class);
-        handlerConfigMap.put("mysql-deploy", MysqlDeployHandler.class);
+        handlerConfigMap.put("node-image-build", Collections.singletonList(NodeImageBuildHandler.class));
+        handlerConfigMap.put("java-image-build", Collections.singletonList(JavaImageBuildHandler.class));
+        handlerConfigMap.put("k8s-deployment", Collections.singletonList(K8sDeploymentHandler.class));
+        handlerConfigMap.put("k8s-service", Collections.singletonList(K8sServiceHandler.class));
+        handlerConfigMap.put("k8s-ingress", Collections.singletonList(K8sIngressHandler.class));
+        handlerConfigMap.put("k8s-namespace", Collections.singletonList(K8sNamespaceHandler.class));
+        handlerConfigMap.put("deploy", Collections.singletonList(DeployHandler.class));
+        handlerConfigMap.put("api-test", Collections.singletonList(ApiTestHandler.class));
+        handlerConfigMap.put("interface-test", Collections.singletonList(InterfaceTestHandler.class));
+        handlerConfigMap.put("mysql-deploy", Collections.singletonList(MysqlDeployHandler.class));
+        handlerConfigMap.put("sonar-java-image-build", Collections.singletonList(SonarJavaImageBuildHandler.class));
+        handlerConfigMap.put("k8s-job", Collections.singletonList(K8sJobHandler.class));
+
+        handlerConfigMap.put("sonar-java", Arrays.asList(SonarJavaImageBuildHandler.class, K8sJobHandler.class, SonarPostProcessHandler.class));
+        handlerConfigMap.put("sonar-java-deploy", Arrays.asList(SonarJavaImageBuildHandler.class, K8sJobHandler.class, SonarPostProcessHandler.class));
     }
 
     public static Handler transToHandler(String name, JsonNode config) throws Exception {
-        Handler handler;
+        Handler firstHandler = null;
+        Handler lastHandler = null;
         try {
-            handler = (Handler) handlerConfigMap.get(name).newInstance();
+            List<Class<?>> handlers = handlerConfigMap.get(name);
+            // 对所有Handler注入依赖
+            for (Class<?> handlerClass : handlers) {
+                Handler handler = (Handler) handlerClass.newInstance();
+                //通过反射注入配置对应Handler类
+                Iterator<Map.Entry<String, JsonNode>> jsonNodeIterator = config.fields();
+                while (jsonNodeIterator.hasNext()) {
+                    Map.Entry<String, JsonNode> entry = jsonNodeIterator.next();
+                    String propertyName = entry.getKey();
+                    JsonNode propertyValues = entry.getValue();
+                    ObjectMapper mapper = new ObjectMapper();
+                    // 遍历json,去handler中获取对应的成员变量,因此要处理找不到变量的情况
+                    Field field;
+                    try {
+                        field = handler.getClass().getDeclaredField(propertyName);
+                        field.setAccessible(true);
+                        //注意,这种写法无法自动注入List<xx>等的字段,因为泛型信息由于java的机制被删除了
+                        // 所以最好还是传入一些基本类型或者String,在对应的handler自己解析
+                        field.set(handler, mapper.readValue(propertyValues.traverse(), field.getType()));
+                    } catch (NoSuchFieldException ignored){
+                        // 如果Handler中没有对应json中的config属性,跳过,直接取下一个属性。
+                        // 注意java中异常当场处理之后会从try后面继续运行
+                    }
+                }
+                // 设置 Handler
+                if (firstHandler == null) {
+                    firstHandler = handler;
+                }
+                if (lastHandler != null) {
+                    lastHandler.setNextHandler(handler);
+                }
+                lastHandler = handler;
+            }
         } catch (InstantiationException | IllegalAccessException e) {
             throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.CONFIG_TRANSFER_ERROR, e);
         }
-        //通过反射注入配置对应Handler类
-        Iterator<Map.Entry<String, JsonNode>> jsonNodeIterator = config.fields();
-        while (jsonNodeIterator.hasNext()) {
-            Map.Entry<String, JsonNode> entry = jsonNodeIterator.next();
-            String propertyName = entry.getKey();
-            JsonNode propertyValues = entry.getValue();
-            ObjectMapper mapper = new ObjectMapper();
-            Field field ;
-            field = handler.getClass().getDeclaredField(propertyName);
-            field.setAccessible(true);
-            //注意,这种写法无法自动注入List<xx>等的字段,因为泛型信息由于java的机制被删除了
-            // 所以最好还是传入一些基本类型或者String,在对应的handler自己解析
-            field.set(handler, mapper.readValue(propertyValues.traverse(), field.getType()));
-        }
-        return handler;
+        return firstHandler;
     }
 }

+ 2 - 1
web/src/main/java/cn/seecoder/web/core/pipeline/handler/JavaImageBuildHandler.java

@@ -29,7 +29,8 @@ import java.util.List;
 /**
  * @author PuHong Weng
  * @date 2021/1/18
- * @description: docker镜像构建
+ * @description: docker镜像构建。构建Java版的质量检查镜像,需要maven作为base,直接在上面运行maven的命令即可完成检查。如果需要构建其他语言
+ * 的检查镜像,需要使用 SonarQube 官方提供的带有可执行 client 的镜像,在其中运行 client 的检查命令。后续步骤都是一致的。
  */
 @Slf4j
 @Data

+ 183 - 0
web/src/main/java/cn/seecoder/web/core/pipeline/handler/K8sJobHandler.java

@@ -0,0 +1,183 @@
+package cn.seecoder.web.core.pipeline.handler;
+
+
+import cn.seecoder.api.ApplicationProperties;
+import cn.seecoder.api.k8s.*;
+import cn.seecoder.api.k8s.exception.K8sApiException;
+import cn.seecoder.api.k8s.model.*;
+import cn.seecoder.api.k8s.vo.SecretVO;
+import cn.seecoder.common.util.SpringUtil;
+import cn.seecoder.web.core.pipeline.Context;
+import cn.seecoder.web.core.pipeline.PipelineException;
+import io.kubernetes.client.openapi.models.V1JobStatus;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.HttpStatus;
+
+import java.util.Collections;
+import java.util.List;
+
+@Slf4j
+public class K8sJobHandler extends AbstractHandler{
+
+    private final SecretApi secretApi;
+    private final JobApi jobApi;
+    private final NamespaceApi namespaceApi;
+    private final LimitRangeApi limitRangeApi;
+    private final ApplicationProperties applicationProperties;
+    private final static String DEFAULT_IMAGE_PULL_SECRET_NAME = "seecoder-devcloud-image-pull-secret";
+
+    public K8sJobHandler() {
+        this.applicationProperties = SpringUtil.getBean(ApplicationProperties.class);
+        secretApi = SpringUtil.getBean(SecretApi.class);
+        jobApi = SpringUtil.getBean(JobApi.class);
+        namespaceApi = SpringUtil.getBean(NamespaceApi.class);
+        limitRangeApi = SpringUtil.getBean(LimitRangeApi.class);
+    }
+
+    @Override
+    public void process(Context context) throws PipelineException {
+        //1. 创建namespace 和 limit range
+        Namespace k8sNamespace = new Namespace();
+        k8sNamespace.setName(context.getNamespace());
+        //todo namespace在底下资源未删除时,不能轻易删除重建,只能直接通过异常查是否已创建,丑陋,待修改
+        try {
+            List<Namespace> namespaces = namespaceApi.getByCondition(K8sObjectRequest.builder()
+                    .name(context.getNamespace()).build());
+            if (namespaces.size() == 0) {
+                namespaceApi.create(k8sNamespace);
+                log.info("Namespace创建成功: [namespace: " + context.getNamespace() + ", deployName: " + context.getDeployName() + "]");
+            } else {
+                log.info("Namespace已经存在, 无需新建: [namespace: " + context.getNamespace() + ", deployName: " + context.getDeployName() + "]");
+            }
+        } catch (K8sApiException e) {
+            context.appendErrorResult(PipelineException.NAMESPACE_CREATE_ERROR, e);
+            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.NAMESPACE_CREATE_ERROR, e);
+        }
+        context.appendSuccessResult("K8s Namespace创建成功: [namespace: " + context.getNamespace() + ", deployName: " + context.getDeployName() + "]");
+
+        LimitRange limitRange = new LimitRange();
+        limitRange.setNamespace(context.getNamespace());
+        limitRange.setName(context.getDeployName()+K8sConstants.LIMIT_RANGE_SUFFIX);
+//        limitRange.setMinCpu("100m");
+//        limitRange.setMaxCpu("2000m");
+//        limitRange.setMinMemory("512Mi");
+//        limitRange.setMaxMemory("1024Mi");
+        try {
+            List<LimitRange> limitRanges = limitRangeApi.getByCondition(K8sObjectRequest.builder()
+                    .namespace(limitRange.getNamespace()).name(limitRange.getName()).build());
+            if (limitRanges.size() == 0) {
+                limitRangeApi.create(limitRange);
+                log.info("limitRange创建成功: [namespace: " + context.getNamespace() + ", deployName: " + context.getDeployName() + "]");
+            } else {
+                limitRangeApi.update(limitRange);
+                log.info("limitRange更新成功: [namespace: " + context.getNamespace() + ", deployName: " + context.getDeployName() + "]");
+
+            }
+        } catch (K8sApiException e) {
+            context.appendErrorResult(PipelineException.LIMIT_RANGE_CREATE_ERROR, e);
+            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.LIMIT_RANGE_CREATE_ERROR, e);
+        }
+        context.appendSuccessResult("K8s limit range创建成功: [namespace: " + context.getNamespace() + ", deployName: " + context.getDeployName() + "]");
+
+
+        context.appendSuccessResult("成功开始执行K8sHandler");
+        // 0 变量
+        // imageName 是 namespace-deployName-sonar
+        // 例如 semobile id=248 项目的 imageName=semobile-248-haha-sonar
+        String imageName = context.getConfigs().get("imageName");
+        // imageTag 是镜像创建的时间
+        String imageTag = context.getConfigs().get("imageTag");
+        // namespace 是项目的 name 拼接上项目的 id
+        // 例如 semobile 项目 id 为 248, 则namespace = semobile-248
+        String namespace = context.getNamespace();
+        // deployName 是流水线的 name
+        String deployName = context.getDeployName();
+        // K8sConstants.JOB_SUFFIX = -job, 例如流水线叫做haha, jobname = haha-job
+        String jobName = String.format("%s%s", deployName, K8sConstants.JOB_SUFFIX);
+
+        context.appendSuccessResult("变量准备完成,开始配置容器");
+        // 1 配置容器
+        Container container = Container.builder()
+                .image(applicationProperties.getDocker().getRegistry() + "/" + imageName + ":" + imageTag)
+                .name(deployName + K8sConstants.CONTAINER_SUFFIX)
+                .build();
+
+        context.appendSuccessResult("容器配置完成,开始构建Job");
+        // 2 构建Job
+        Job job = Job.builder()
+                .containers(Collections.singletonList(container))
+                .build();
+        job.setName(jobName);
+        job.setNamespace(namespace);
+        job.setLabel(K8sConstants.JOB_LABEL, deployName);
+
+        context.appendSuccessResult("Job配置完成,开始检查仓库Secret");
+        try {
+            // 2 检查和配置 Secret
+            SecretVO secretVO = secretApi.getSecretByName(namespace, DEFAULT_IMAGE_PULL_SECRET_NAME);
+            if (secretVO == null) {
+                context.appendSuccessResult("secret null, 创建secret中");
+                secretApi.createPrivateRegistrySecret(
+                        namespace,
+                        DEFAULT_IMAGE_PULL_SECRET_NAME,
+                        applicationProperties.getDocker().getRegistry(),
+                        applicationProperties.getDocker().getRegistryUsername(),
+                        applicationProperties.getDocker().getRegistryPassword()
+                );
+            }
+            job.setImagePullSecrets(Collections.singletonList(DEFAULT_IMAGE_PULL_SECRET_NAME));
+        } catch (Exception e){
+            context.appendErrorResult("检查仓库Secret出错", e);
+        }
+
+        context.appendSuccessResult("Secret已确认,开始创建Job");
+        // 3 创建Job
+        try {
+            List<Job> jobs = jobApi.getByCondition(K8sObjectRequest.builder()
+                    .namespace(namespace)
+                    .name(jobName)
+                    .build());
+            if (jobs.size() == 0){
+                jobApi.create(job);
+                log.info(String.format("Job创建成功: [namespace: %s, jobName: %s]", namespace, jobName));
+                context.appendSuccessResult(String.format("Job创建成功: [namespace: %s, jobName: %s]", namespace, jobName));
+            } else {
+                log.info("job已存在!执行删除重建操作");
+                jobApi.update(job);
+                log.info(String.format("Job重建成功: [namespace: %s, jobName: %s]", namespace, jobName));
+                context.appendSuccessResult("Job已存在,已成功删除并重建");
+            }
+        } catch (K8sApiException e) {
+            context.appendErrorResult(PipelineException.JOB_CREATE_ERROR, e);
+            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.JOB_CREATE_ERROR, e);
+        }
+
+        // 4 确认Job情况
+        // 停用,如果镜像没有push成功的话,造成job的pod状态是 ImagePullBackOff 状态,但是pod是处于waiting状态,查不到
+        // 后续再调查怎么办吧
+//        try {
+//            Thread.sleep(3000L);
+//            List<Job> jobs = jobApi.getByCondition(K8sObjectRequest.builder()
+//                    .name(jobName)
+//                    .namespace(namespace)
+//                    .build());
+//            if (jobs.size() == 1){
+//                Job k8s_job = jobs.get(0);
+//                V1JobStatus status = k8s_job.getStatus();
+//                if (status != null && status.getFailed() != null && status.getFailed() > 0) {
+//                    throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.JOB_IMAGE_NOT_EXIST_ERROR);
+//                }
+//            }
+//        } catch (InterruptedException e) {
+//            e.printStackTrace();
+//            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.JOB_CREATE_ERROR);
+//        } catch (K8sApiException e) {
+//            context.appendErrorResult(PipelineException.JOB_CREATE_ERROR, e);
+//            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.JOB_CREATE_ERROR, e);
+//        } catch (PipelineException e) {
+//            context.appendErrorResult(PipelineException.JOB_IMAGE_NOT_EXIST_ERROR, e);
+//            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.JOB_IMAGE_NOT_EXIST_ERROR);
+//        }
+//        context.appendSuccessResult("Job 检查通过,运行正常");
+    }
+}

+ 175 - 0
web/src/main/java/cn/seecoder/web/core/pipeline/handler/SonarJavaImageBuildHandler.java

@@ -0,0 +1,175 @@
+package cn.seecoder.web.core.pipeline.handler;
+
+import cn.seecoder.api.ApplicationProperties;
+import cn.seecoder.api.docker.DockerApi;
+import cn.seecoder.api.git.GitApi;
+import cn.seecoder.common.util.SpringUtil;
+import cn.seecoder.web.core.pipeline.Context;
+import cn.seecoder.web.core.pipeline.PipelineException;
+import cn.seecoder.web.core.pipeline.template.PipelineTemplateTable;
+import cn.seecoder.web.model.vo.project.ProjectVO;
+import cn.seecoder.web.service.branch.BranchService;
+import cn.seecoder.web.service.project.ProjectService;
+import com.nju.edu.gitlab.vo.BranchVO;
+import com.spotify.docker.client.ProgressHandler;
+import com.spotify.docker.client.exceptions.DockerException;
+import com.spotify.docker.client.messages.ProgressMessage;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.StringUtils;
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.api.Git;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.List;
+
+@Slf4j
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class SonarJavaImageBuildHandler extends AbstractHandler {
+
+    private final GitApi gitApi;
+    private final DockerApi dockerApi;
+    private final ProjectService projectService;
+    private final BranchService branchService;
+    private final ApplicationProperties applicationProperties;
+
+    // properties from pipeline config json
+//    private String repoUrl;
+    private String branchOrHash;
+    private int projectId;
+//    private String commitHash;
+
+    public SonarJavaImageBuildHandler() {
+        this.gitApi = SpringUtil.getBean(GitApi.class);
+        this.dockerApi = SpringUtil.getBean(DockerApi.class);
+        this.projectService = SpringUtil.getBean(ProjectService.class);
+        this.branchService = SpringUtil.getBean(BranchService.class);
+        this.applicationProperties = SpringUtil.getBean(ApplicationProperties.class);
+    }
+
+    @Override
+    public void process(Context context) throws PipelineException {
+        // 0. prepare
+        String templateName = PipelineTemplateTable.SONAR_JAVA8;
+        String imageName = context.getNamespace() + '-' + context.getDeployName() + "-sonar";
+//        String imageTag = String.valueOf(new Date().getTime());
+        ProjectVO project = projectService.getProjectById(projectId);
+        if (project == null) {
+            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.PROJECT_NOT_FOUND_ERROR);
+        }
+        String repoUrl = project.getGitRemoteUrl();
+        // convert url for local test
+        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(repoUrl);
+        repoUrl = builder.scheme("https").host("seecoder-gitlab-gitlab.seec.seecoder.cn").path(".git/").toUriString();
+        BranchVO branch = branchService.getBranchByName(projectId, branchOrHash);
+        String commitHash;
+        boolean isBranch;
+        if (branch == null) {
+//            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.BRANCH_NOT_FOUND_ERROR);
+            context.appendResult("未找到对应分支,尝试当做commit hash处理");
+            commitHash = branchOrHash;
+            isBranch = false;
+        } else {
+             commitHash = branch.getCommit().getId();
+             isBranch = true;
+        }
+        String imageTag = commitHash;
+        String sonarProjectKey = String.format("%s-%s", imageName, commitHash); //TODO
+        String loginToken = applicationProperties.getSonar().getLoginToken();
+        String sonarHost = applicationProperties.getSonar().getHost();
+
+        // 1. 有git从git拿到地址仓库
+        Path directory;
+        try {
+            //注:temp文件/目录系统会定时删,实际文件名喂 prefix + 它生成的suffix随机数,所以不会命名冲突
+            directory = Files.createTempDirectory("seecoder-devcloud-");
+            Git git = gitApi.clone(repoUrl, directory.toString());
+            if (isBranch) {
+                git.checkout().setCreateBranch(false).setStartPoint("origin/" + branchOrHash).setName("origin/" + branchOrHash).call();
+            } else {
+                git.checkout().setCreateBranch(false).setStartPoint(branchOrHash).setName(branchOrHash).call();
+            }
+        } catch (Exception e) {
+            context.appendErrorResult(PipelineException.GIT_CLONE_ERROR, e);
+            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.GIT_CLONE_ERROR, e);
+        }
+
+        //2. 帮助学生创建dockerfile和maven的setting.xml文件, 读取学生项目architect名字
+        Path dockerfile = Paths.get(directory.toString(), "Dockerfile");
+        Path mavenSetting = Paths.get(directory.toString(), "settings.xml");
+        try {
+            Files.deleteIfExists(dockerfile);
+            Files.deleteIfExists(mavenSetting);
+            PipelineTemplateTable.copyTemplateDockerfile(templateName, dockerfile);
+            PipelineTemplateTable.copyTemplateMavenSetting(mavenSetting);
+        } catch (IOException e) {
+            context.appendErrorResult(PipelineException.DOCKERFILE_CREATE_ERROR, e);
+            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.DOCKERFILE_CREATE_ERROR, e);
+        }
+
+        //3. 修改dockerfile内部需要替换的内容
+        List<String> dockerfileLines = null;
+        try {
+            dockerfileLines = Files.readAllLines(dockerfile);
+            StringBuilder sb = new StringBuilder();
+            for (String line : dockerfileLines) {
+                line = line
+                        .replace("%commitHash%", commitHash)
+                        .replace("%projectId%", Integer.toString(projectId))
+                        .replace("%imageName%", imageName)
+                        .replace("%imageTag%", imageTag)
+                        .replace("%projectKey%", sonarProjectKey)
+                        .replace("%loginToken%", loginToken)
+                        .replace("%host%", sonarHost);
+
+                sb.append(line).append("\n");
+            }
+            Files.write(dockerfile, sb.toString().getBytes(), StandardOpenOption.WRITE);
+        } catch (IOException e) {
+            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.DOCKERFILE_MODIFY_ERROR, e);
+        }
+
+        //4. 镜像构建
+        StringBuilder sb = new StringBuilder();
+        final int DEFAULT_BUFFER_LENGTH = 200;
+        final long MAX_BUFFER_SIZE = 1 << 18;
+        try {
+            dockerApi.buildAndPush(directory.toString(), imageName, imageTag, new ProgressHandler() {
+                @Override
+                public void progress(ProgressMessage message) throws DockerException {
+                    String value = message.stream();
+                    if (value == null) {
+                        value = "";
+                    }
+                    if (!StringUtils.isEmpty(message.error())) {
+                        value += "\n" + message.error();
+                    }
+                    sb.append(value);
+                    if (sb.length() > DEFAULT_BUFFER_LENGTH) {
+                        context.appendResult(sb.toString());
+                        if (context.getResult().length() > MAX_BUFFER_SIZE) {
+                            context.setResult(context.getResult().substring((int) (context.getResult().length() - MAX_BUFFER_SIZE)));
+                        }
+                        sb.setLength(0);
+                    }
+                }
+            });
+        } catch (Exception e) {
+            log.error("镜像构建失败,项目目录为:{}", directory.toString());
+            context.appendErrorResult(PipelineException.IMAGE_BUILD_ERROR, e);
+            throw new PipelineException(HttpStatus.SC_SERVICE_UNAVAILABLE, PipelineException.IMAGE_BUILD_ERROR, e);
+        }
+
+        //5. 将镜像名字加入context
+        context.getConfigs().put("imageName", imageName);
+        context.getConfigs().put("imageTag", imageTag);
+        context.appendSuccessResult("镜像构建成功,已推送进仓库");
+    }
+}

+ 13 - 0
web/src/main/java/cn/seecoder/web/core/pipeline/handler/SonarPostProcessHandler.java

@@ -0,0 +1,13 @@
+package cn.seecoder.web.core.pipeline.handler;
+
+import cn.seecoder.web.core.pipeline.Context;
+import cn.seecoder.web.core.pipeline.PipelineException;
+
+public class SonarPostProcessHandler extends AbstractHandler{
+    private final static String SUCCESS_MESSAGE = "\n检查任务已提交,根据代码规模会需要等待一段时间。检查过程不会阻塞后续流水线进行\n请移步至\"提交记录\"界面查看!结果将直接标记在对应提交记录上。";
+
+    @Override
+    public void process(Context context) throws PipelineException {
+        context.appendSuccessResult(SUCCESS_MESSAGE);
+    }
+}

+ 2 - 1
web/src/main/java/cn/seecoder/web/core/pipeline/template/PipelineTemplateTable.java

@@ -31,6 +31,7 @@ public class PipelineTemplateTable {
     public static final String NODE_14 = "NODE_14";
     public static final String NODE_16 = "NODE_16";
     public static final String MYSQL = "MYSQL";
+    public static final String SONAR_JAVA8 = "SONAR_JAVA8";
 
 
     /**
@@ -44,7 +45,7 @@ public class PipelineTemplateTable {
         templateNames.add(NODE_16);
         templateNames.add(SPRINGBOOT_JAVA8);
         templateNames.add(MYSQL);
-
+        templateNames.add(SONAR_JAVA8);
     }
 
     public static void validateTemplateName(String name) throws PipelineException {

+ 2 - 2
web/src/main/java/cn/seecoder/web/dao/project/CommitMapper.java → web/src/main/java/cn/seecoder/web/dao/commit/CommitMapper.java

@@ -1,10 +1,10 @@
-package cn.seecoder.web.dao.project;
+package cn.seecoder.web.dao.commit;
 
 import cn.seecoder.web.model.enums.CommitRelatedEnum;
 import org.apache.ibatis.annotations.*;
 import org.springframework.stereotype.Repository;
 import cn.seecoder.web.dao.provider.GeneralInsertUpdateSqlProvider;
-import cn.seecoder.web.model.po.project.CommitPO;
+import cn.seecoder.web.model.po.commit.CommitPO;
 
 import java.util.List;
 

+ 36 - 0
web/src/main/java/cn/seecoder/web/dao/commit/CommitQualityMapper.java

@@ -0,0 +1,36 @@
+package cn.seecoder.web.dao.commit;
+
+import cn.seecoder.web.dao.provider.GeneralInsertUpdateSqlProvider;
+import cn.seecoder.web.model.po.commit.CommitQualityPO;
+import org.apache.ibatis.annotations.*;
+import org.springframework.security.core.parameters.P;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author claws
+ */
+@Repository
+public interface CommitQualityMapper {
+    @Select("select * from commit_quality where project_id = #{projectId}")
+    List<CommitQualityPO> selectByProjectId(@Param("projectId") int projectId);
+
+    // 这里的Map Key对应的是PO中的属性
+    @MapKey("commitHash")
+    @Select("select * from commit_quality where project_id = #{projectId}")
+    Map<String, CommitQualityPO> selectMapByProjectId(@Param("projectId") int projectId);
+
+    @Select("select * from commit_quality where commit_hash = #{commitHash}")
+    CommitQualityPO selectByCommitHash(@Param("commitHash") String commitHash);
+
+    @InsertProvider(type = GeneralInsertUpdateSqlProvider.class, method = "insert")
+    int insert(CommitQualityPO commitQualityPO, String... ignoredCols);
+
+    @InsertProvider(type = GeneralInsertUpdateSqlProvider.class, method = "updateById")
+    int update(CommitQualityPO commitQualityPO);
+
+    @Delete("delete from commit_quality where commit_hash = #{commitHash}")
+    int deleteByCommitHash(@Param("commitHash") String commitHash);
+}

+ 2 - 0
web/src/main/java/cn/seecoder/web/infrastructure/config/WebSecurityConfig.java

@@ -99,6 +99,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
                 .antMatchers(GET, "/commit").access("@authTools.checkProjOwnershipParam(request)")
                 .antMatchers(GET, "/commits/diff").access("@authTools.checkProjOwnershipParam(request)")
                 .antMatchers(GET, "/commits/link").access("@authTools.checkProjOwnershipParam(request)")
+                .antMatchers(GET, "/commit/quality").access("@authTools.checkProjOwnershipParam(request)")
                 // Branch Controller
                 .antMatchers(GET, "/branch/list").access("@authTools.checkProjOwnershipParam(request)")
                 // Deployment Controller
@@ -144,6 +145,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
                 .antMatchers("/**/*swagger*/**").permitAll()
                 .antMatchers("/**/*api-docs*/**").permitAll()
                 .antMatchers("/**/hook").permitAll()
+                .antMatchers("/**/sonar_hook").permitAll()
                 .antMatchers("/**/query/**").permitAll()
                 // 跨域的 Options 请求进行放行
                 .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()

+ 25 - 0
web/src/main/java/cn/seecoder/web/model/enums/CommitCheckResultEnum.java

@@ -0,0 +1,25 @@
+package cn.seecoder.web.model.enums;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * @author claws
+ */
+public enum CommitCheckResultEnum {
+    @JsonProperty("pass")
+    PASS("pass"),
+    @JsonProperty("fail")
+    FAIL("fail"),
+    @JsonProperty("none")
+    NONE("none");
+
+    private final String value;
+    CommitCheckResultEnum(String value) {
+        this.value = value;
+    }
+
+    @Override
+    public String toString(){
+        return value;
+    }
+}

+ 2 - 2
web/src/main/java/cn/seecoder/web/model/po/project/CommitPO.java → web/src/main/java/cn/seecoder/web/model/po/commit/CommitPO.java

@@ -1,4 +1,4 @@
-package cn.seecoder.web.model.po.project;
+package cn.seecoder.web.model.po.commit;
 
 import cn.seecoder.web.model.enums.CommitRelatedEnum;
 import lombok.AllArgsConstructor;
@@ -6,7 +6,7 @@ import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 import org.springframework.beans.BeanUtils;
-import cn.seecoder.web.model.vo.GitWebHookVO;
+import cn.seecoder.web.model.vo.commit.GitWebHookVO;
 
 /**
  * @author PuHong Weng

+ 28 - 0
web/src/main/java/cn/seecoder/web/model/po/commit/CommitQualityPO.java

@@ -0,0 +1,28 @@
+package cn.seecoder.web.model.po.commit;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Date;
+
+
+/**
+ * @author claws
+ * @description: 代码版本质量检查
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class CommitQualityPO {
+    private Integer id;
+    private Integer projectId;
+    private String commitHash;
+    private String sonarProjectKey;
+    private String checkResult;
+    private Date checkTime;
+    private String resultUrl;
+    private String qualityConditionsJson;
+}

+ 28 - 0
web/src/main/java/cn/seecoder/web/model/vo/commit/CommitQualityVO.java

@@ -0,0 +1,28 @@
+package cn.seecoder.web.model.vo.commit;
+
+import cn.seecoder.web.model.enums.CommitCheckResultEnum;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Date;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+public class CommitQualityVO {
+    private String commitHash;
+    private Date checkTime;
+    private String resultUrl;
+    private CommitCheckResultEnum checkResult;
+    private List<QualityCondition> qualityConditions;
+
+    @Data
+    @NoArgsConstructor
+    public static class QualityCondition {
+        private String metric;
+        private String operator;
+        private String value;
+        private String status;
+        private String errorThreshold;
+    }
+}

+ 5 - 2
web/src/main/java/cn/seecoder/web/model/vo/project/CommitVO.java → web/src/main/java/cn/seecoder/web/model/vo/commit/CommitVO.java

@@ -1,7 +1,8 @@
-package cn.seecoder.web.model.vo.project;
+package cn.seecoder.web.model.vo.commit;
 
+import cn.seecoder.web.model.enums.CommitCheckResultEnum;
 import cn.seecoder.web.model.enums.CommitRelatedEnum;
-import cn.seecoder.web.model.po.project.CommitPO;
+import cn.seecoder.web.model.po.commit.CommitPO;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
@@ -35,6 +36,8 @@ public class CommitVO {
     private Integer relatedId;
     @ApiModelProperty("commit parents")
     private List<String> parents;
+    @ApiModelProperty("commit check result enum")
+    private CommitCheckResultEnum checkResult;
 
     public CommitVO(CommitPO po) {
         BeanUtils.copyProperties(po,this);

+ 1 - 1
web/src/main/java/cn/seecoder/web/model/vo/GitWebHookVO.java → web/src/main/java/cn/seecoder/web/model/vo/commit/GitWebHookVO.java

@@ -1,4 +1,4 @@
-package cn.seecoder.web.model.vo;
+package cn.seecoder.web.model.vo.commit;
 
 import io.swagger.annotations.ApiModel;
 import lombok.Data;

+ 77 - 0
web/src/main/java/cn/seecoder/web/model/vo/hook/SonarResultHookVO.java

@@ -0,0 +1,77 @@
+package cn.seecoder.web.model.vo.hook;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+
+import java.util.List;
+
+@Data
+@ToString
+@ApiModel("sonar hook 传回结果VO")
+public class SonarResultHookVO {
+    private String serverUrl;
+    private String taskId;
+    private String status;
+    private String analysedAt;
+    private String revision;
+    private String changedAt;
+    private Project project;
+    private Branch branch;
+    private QualityGate qualityGate;
+    private Properties properties;
+
+    @Data
+    @NoArgsConstructor
+    public static class Project{
+        private String key;
+        private String name;
+        private String url;
+    }
+
+    @Data
+    @NoArgsConstructor
+    public static class Branch{
+        private String name;
+        private String type;
+        private Boolean isMain;
+        private String url;
+    }
+
+    @Data
+    @NoArgsConstructor
+    public static class QualityGate{
+        private String name;
+        private String status;
+        private List<Condition> conditions;
+
+        @Data
+        @NoArgsConstructor
+        public static class Condition{
+            private String metric;
+            private String operator;
+            private String value;
+            private String status;
+            private String errorThreshold;
+        }
+    }
+
+    @Data
+    @NoArgsConstructor
+    public static class Properties{
+        @JsonProperty("sonar.analysis.detectedscm")
+        private String detectedscm;
+        @JsonProperty("sonar.analysis.detectedci")
+        private String detectedci;
+        @JsonProperty("sonar.analysis.projectId")
+        private Integer projectId;
+        @JsonProperty("sonar.analysis.imageTag")
+        private String imageTag;
+        @JsonProperty("sonar.analysis.imageName")
+        private String imageName;
+        @JsonProperty("sonar.analysis.commitHash")
+        private String commitHash;
+    }
+}

+ 2 - 2
web/src/main/java/cn/seecoder/web/model/vo/tree/TreeNodeVO.java

@@ -9,9 +9,9 @@ import lombok.NoArgsConstructor;
 import org.springframework.beans.BeanUtils;
 import cn.seecoder.web.model.enums.NodeStateEnum;
 import cn.seecoder.web.model.enums.NodeTypeEnum;
-import cn.seecoder.web.model.po.project.CommitPO;
+import cn.seecoder.web.model.po.commit.CommitPO;
 import cn.seecoder.web.model.po.tree.TreeNodePO;
-import cn.seecoder.web.model.vo.project.CommitVO;
+import cn.seecoder.web.model.vo.commit.CommitVO;
 
 import java.util.List;
 import java.util.stream.Collectors;

+ 3 - 1
web/src/main/java/cn/seecoder/web/service/project/BranchService.java → web/src/main/java/cn/seecoder/web/service/branch/BranchService.java

@@ -1,4 +1,4 @@
-package cn.seecoder.web.service.project;
+package cn.seecoder.web.service.branch;
 
 import com.nju.edu.gitlab.vo.BranchVO;
 
@@ -6,4 +6,6 @@ import java.util.List;
 
 public interface BranchService {
     List<BranchVO> getAllBranches(Integer projectId);
+
+    BranchVO getBranchByName(Integer projectId, String branchName);
 }

+ 36 - 0
web/src/main/java/cn/seecoder/web/service/commit/CommitLinkService.java

@@ -0,0 +1,36 @@
+package cn.seecoder.web.service.commit;
+
+import cn.seecoder.web.model.enums.CommitRelatedEnum;
+import cn.seecoder.web.model.po.commit.CommitPO;
+import cn.seecoder.web.model.vo.commit.GitWebHookVO;
+import cn.seecoder.web.model.vo.commit.CommitVO;
+
+import java.util.List;
+
+public interface CommitLinkService {
+    void linkCommitByInfoFromGitlab(GitWebHookVO gitWebHookVO);
+
+    void saveLinkedCommit(CommitPO po);
+    /**
+     * 不同分支可能会push相同的commit,分析过的commit不应该再分析,这里是判断是否分析共没有
+     * @param id commit hash id
+     */
+    boolean isLinked(String id);
+
+    List<CommitVO> getTreeLinkedCommits(Integer treeId);
+
+    List<CommitVO> getBugLinkedCommits(Integer bugId);
+
+    List<CommitVO> getLinkedCommits(Integer projectId);
+
+    CommitPO getLinkedCommit(String hash);
+    /**
+     * link commit and tree node or bug
+     * @param projectId
+     * @param commitHash
+     * @param relatedType
+     * @param relatedId
+     * @return
+     */
+    boolean link(Integer projectId, String commitHash, CommitRelatedEnum relatedType, Integer relatedId);
+}

+ 28 - 0
web/src/main/java/cn/seecoder/web/service/commit/CommitQualityService.java

@@ -0,0 +1,28 @@
+package cn.seecoder.web.service.commit;
+
+import cn.seecoder.web.model.enums.CommitCheckResultEnum;
+import cn.seecoder.web.model.vo.commit.CommitQualityVO;
+import cn.seecoder.web.model.vo.hook.SonarResultHookVO;
+
+import java.util.Map;
+
+public interface CommitQualityService {
+
+    /**
+     * process the coming json data from sonarqube
+     * @param sonarResultHookVO
+     * @return
+     */
+    boolean processSonarResult(SonarResultHookVO sonarResultHookVO);
+
+    Map<String, CommitCheckResultEnum> getCommitsQualityMapByProjectId(Integer projectId);
+
+    CommitCheckResultEnum getQualityResultByCommit(String hash);
+    /**
+     * get commit quality by commit hash and project id
+     * @param projectId
+     * @param commitHash
+     * @return
+     */
+    CommitQualityVO getQualityByCommit(Integer projectId, String commitHash);
+}

+ 24 - 0
web/src/main/java/cn/seecoder/web/service/commit/CommitService.java

@@ -0,0 +1,24 @@
+package cn.seecoder.web.service.commit;
+
+import cn.seecoder.web.model.enums.CommitRelatedEnum;
+import cn.seecoder.web.model.po.commit.CommitPO;
+import cn.seecoder.web.model.vo.commit.CommitQualityVO;
+import cn.seecoder.web.model.vo.commit.CommitVO;
+import cn.seecoder.web.model.vo.hook.SonarResultHookVO;
+import com.nju.edu.gitlab.vo.DiffVO;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author PuHong Weng
+ * @date 2021/3/21
+ * @description: 项目相关的 git commit 记录分析功能
+ */
+public interface CommitService {
+    List<CommitVO> getCommitsByBranchName(Integer projectId, String branchName);
+
+    CommitVO getCommitByHash(Integer projectId, String hash);
+
+    Set<DiffVO> getDiff(Integer projectId, String fromHash, String toHash);
+}

+ 12 - 2
web/src/main/java/cn/seecoder/web/service/impl/project/BranchServiceImpl.java → web/src/main/java/cn/seecoder/web/service/impl/branch/BranchServiceImpl.java

@@ -1,6 +1,6 @@
-package cn.seecoder.web.service.impl.project;
+package cn.seecoder.web.service.impl.branch;
 
-import cn.seecoder.web.service.project.BranchService;
+import cn.seecoder.web.service.branch.BranchService;
 import com.nju.edu.gitlab.SeecoderGitlabApi;
 import com.nju.edu.gitlab.SeecoderGitlabException;
 import com.nju.edu.gitlab.vo.BranchVO;
@@ -26,4 +26,14 @@ public class BranchServiceImpl implements BranchService {
         }
         return branches;
     }
+
+    @Override
+    public BranchVO getBranchByName(Integer projectId, String branchName) {
+        for (BranchVO branchVO : this.getAllBranches(projectId)) {
+            if (branchVO.getName().equals(branchName)) {
+                return branchVO;
+            }
+        }
+        return null;
+    }
 }

+ 1 - 2
web/src/main/java/cn/seecoder/web/service/impl/bug_list/BugListServiceImpl.java

@@ -4,7 +4,7 @@ import cn.seecoder.common.util.LogTrackingUtil;
 import cn.seecoder.common.util.OpType;
 import cn.seecoder.web.core.commit.model.CommitState;
 import cn.seecoder.web.dao.bug_list.BugListMapper;
-import cn.seecoder.web.dao.project.CommitMapper;
+import cn.seecoder.web.dao.commit.CommitMapper;
 import cn.seecoder.web.dao.project.ProjectRelatedMapper;
 import cn.seecoder.web.dao.user.UserMapper;
 import cn.seecoder.web.model.enums.BugListStateEnum;
@@ -15,7 +15,6 @@ import cn.seecoder.web.model.vo.bug_list.BugListCreateVO;
 import cn.seecoder.web.model.vo.bug_list.BugListUpdateBasicVO;
 import cn.seecoder.web.model.vo.bug_list.BugListVO;
 import cn.seecoder.web.service.bug_list.BugListService;
-import net.sf.json.JSON;
 import net.sf.json.JSONObject;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;

+ 216 - 0
web/src/main/java/cn/seecoder/web/service/impl/commit/CommitLinkServiceImpl.java

@@ -0,0 +1,216 @@
+package cn.seecoder.web.service.impl.commit;
+
+import cn.seecoder.common.util.LogTrackingUtil;
+import cn.seecoder.common.util.OpType;
+import cn.seecoder.web.core.commit.CommitResolver;
+import cn.seecoder.web.core.commit.model.CommitRule;
+import cn.seecoder.web.dao.commit.CommitMapper;
+import cn.seecoder.web.dao.user.UserMapper;
+import cn.seecoder.web.model.enums.CommitRelatedEnum;
+import cn.seecoder.web.model.po.bug_list.BugListPO;
+import cn.seecoder.web.model.po.commit.CommitPO;
+import cn.seecoder.web.model.po.tree.TreeNodePO;
+import cn.seecoder.web.model.vo.commit.GitWebHookVO;
+import cn.seecoder.web.model.vo.commit.CommitVO;
+import cn.seecoder.web.service.bug_list.BugListService;
+import cn.seecoder.web.service.commit.CommitLinkService;
+import cn.seecoder.web.service.commit.CommitService;
+import cn.seecoder.web.service.tree.TreeNodeService;
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@Slf4j
+public class CommitLinkServiceImpl implements CommitLinkService {
+    @Autowired
+    private CommitMapper commitMapper;
+    @Autowired
+    private TreeNodeService treeNodeService;
+    @Autowired
+    private BugListService bugListService;
+    @Autowired
+    private CommitService commitService;
+    @Autowired
+    private UserMapper userMapper;
+
+    @Override
+    public void linkCommitByInfoFromGitlab(GitWebHookVO gitWebHookVO) {
+
+//
+//        List<String> emails = new ArrayList<>();
+//        emails.add(gitWebHookVO.getUser_email());
+//        List<GitlabUserEmailDTO> users;
+//        try {
+//            users = seecoderGitlabApi.findUserByEmails(emails);
+//        } catch (SeecoderGitlabException e) {
+//            throw new ServiceException("统一代码服务出现访问错误,无法正确分析webhook信息");
+//        }
+//        Integer userId = users.size()!=0 ? users.get(0).getUserId() : null;
+
+        Integer projectId = gitWebHookVO.getProject_id();
+        for (GitWebHookVO.Commit commit: gitWebHookVO.getCommits()){
+            if (this.isLinked(commit.getId())){
+                //已经解析过了
+                log.info("Commits 已经解析过了, id: {}", commit.getId());
+                continue;
+            }
+            //ANA 日志需要打出用户的commit信息
+            JSONObject object = new JSONObject();
+            try{
+                object.put("commit_id",commit.getId());
+                object.put("user_id",userMapper.selectIdByEmail(commit.getAuthor().getEmail()));
+                object.put("project_id",projectId);
+                object.put("commit_time",commit.getTimestamp());
+                object.put("message",commit.getMessage());
+            }catch (Exception ignored){}
+
+
+            log.info("resolving commit id : {}, title : {}, message : {}", commit.getId(), commit.getTitle(), commit.getMessage());
+            //todo 注意目前分析的是 message
+            // 应该分析 title 还是 message 与学生的提交规范有关
+            CommitRule rule = CommitResolver.resolve(commit.getMessage());
+            if (rule == null){
+                log.info("Commits 不符合规范, id: {}", commit.getId());
+                //ANA
+                try {
+                    object.put("type","不合规范");
+                    String data = JSONObject.toJSONString(object);
+                    LogTrackingUtil.log(data, OpType.COMMIT);
+                } catch (Exception ignored) {}
+                continue;
+            }
+            switch (rule.getType()){
+                case FEAT:
+                    treeNodeService.updateNodeState(rule.getId(),rule.getState());
+                    this.saveLinkedCommit(new CommitPO(commit,projectId, CommitRelatedEnum.TREE_ID, rule.getId()));
+                    log.info("Commits 关联tree节点, id: {}", commit.getId());
+                    //ANA
+                    try {
+                        object.put("type","关联tree");
+                        String data = JSONObject.toJSONString(object);
+                        LogTrackingUtil.log(data, OpType.COMMIT);
+                    } catch (Exception ignored) {}
+                    break;
+                case FIX:
+                    bugListService.updateState(rule.getState(), rule.getId());
+                    this.saveLinkedCommit(new CommitPO(commit,projectId, CommitRelatedEnum.BUG_ID, rule.getId()));
+                    log.info("Commits 关联bug节点, id: {}", commit.getId());
+                    //ANA
+                    try {
+                        object.put("type","关联bug");
+                        String data = JSONObject.toJSONString(object);
+                        LogTrackingUtil.log(data, OpType.COMMIT);
+                    } catch (Exception ignored) {}
+                    break;
+                case UNKNOWN:
+                    break;
+            }
+        }
+    }
+
+    @Override
+    public void saveLinkedCommit(CommitPO po) {
+
+        //ANA 日志需要打出用户commit的信息
+        try{
+            JSONObject object = new JSONObject();
+            object.put("commit_id",po.getId());
+            object.put("user_id",userMapper.selectIdByUserName(po.getGitlabUsername()));
+            object.put("project_id",po.getProjectId());
+            object.put("commit_name",po.getGitlabUsername());
+            object.put("commit_time",po.getTimestamp());
+            object.put("message",po.getMessage());
+            object.put("type",po.getRelatedType());
+
+            String data = JSONObject.toJSONString(object);
+            LogTrackingUtil.log(data, OpType.COMMIT);
+        }catch (Exception e){}
+
+        commitMapper.insert(po);
+    }
+
+    @Override
+    public boolean isLinked(String id) {
+        return 1 == commitMapper.isExist(id);
+    }
+
+    @Override
+    public List<CommitVO> getTreeLinkedCommits(Integer treeId) {
+        return commitMapper.selectByRelatedIdAndType(treeId,CommitRelatedEnum.TREE_ID).stream()
+                .map(CommitVO::new)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public List<CommitVO> getBugLinkedCommits(Integer bugId) {
+        return commitMapper.selectByRelatedIdAndType(bugId,CommitRelatedEnum.BUG_ID).stream()
+                .map(CommitVO::new)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public CommitPO getLinkedCommit(String hash) {
+        return commitMapper.selectByCommitId(hash);
+    }
+
+    @Override
+    public List<CommitVO> getLinkedCommits(Integer projectId) {
+        return commitMapper.selectByProjectId(projectId).stream()
+                .map(CommitVO::new)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public boolean link(Integer projectId, String commitHash, CommitRelatedEnum relatedType, Integer relatedId) {
+        if (relatedType == CommitRelatedEnum.NONE){
+            commitMapper.deleteByCommitId(commitHash);
+            return true;
+        }
+
+        // check if the related target exists
+        if (relatedType == CommitRelatedEnum.TREE_ID){
+            TreeNodePO node = treeNodeService.getNodeByNodeId(relatedId);
+            if (node == null){
+                return false;
+            } else if (! node.getProjectId().equals(projectId)) {
+                return false;
+            }
+        } else {
+            BugListPO node = bugListService.getBugById(relatedId);
+            if (node == null){
+                return false;
+            } else if (! node.getProjectId().equals(projectId)) {
+                return false;
+            }
+        }
+
+        // check if the commit is already exist, then update it. if not, insert a new one.
+        CommitPO commitPODB = commitMapper.selectByCommitId(commitHash);
+        if (commitPODB != null){
+            commitPODB.setRelatedType(relatedType);
+            commitPODB.setRelatedId(relatedId);
+            commitMapper.update(commitPODB);
+        } else {
+            CommitVO commit = commitService.getCommitByHash(projectId, commitHash);
+
+            CommitPO commitPO = new CommitPO();
+            commitPO.setId(commit.getId());
+            commitPO.setMessage(commit.getMessage());
+            commitPO.setTitle(commit.getTitle());
+            commitPO.setProjectId(projectId);
+            commitPO.setTimestamp(commit.getTimestamp());
+            commitPO.setGitlabUsername(commit.getGitlabUsername());
+            commitPO.setEmail(commit.getAuthorEmail());
+            commitPO.setRelatedType(relatedType);
+            commitPO.setRelatedId(relatedId);
+
+            this.saveLinkedCommit(commitPO);
+        }
+        return true;
+    }
+}

+ 126 - 0
web/src/main/java/cn/seecoder/web/service/impl/commit/CommitQualityServiceImpl.java

@@ -0,0 +1,126 @@
+package cn.seecoder.web.service.impl.commit;
+
+import cn.seecoder.api.ApplicationProperties;
+import cn.seecoder.web.dao.commit.CommitQualityMapper;
+import cn.seecoder.web.model.enums.CommitCheckResultEnum;
+import cn.seecoder.web.model.po.commit.CommitQualityPO;
+import cn.seecoder.web.model.vo.commit.CommitQualityVO;
+import cn.seecoder.web.model.vo.hook.SonarResultHookVO;
+import cn.seecoder.web.service.commit.CommitQualityService;
+import com.alibaba.fastjson.JSONArray;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+@Slf4j
+@Service
+public class CommitQualityServiceImpl implements CommitQualityService {
+    @Autowired
+    CommitQualityMapper commitQualityMapper;
+
+    @Autowired
+    private ApplicationProperties applicationProperties;
+
+    @Override
+    public boolean processSonarResult(SonarResultHookVO sonarResultHookVO) {
+        try {
+            SonarResultHookVO.Properties properties = sonarResultHookVO.getProperties();
+            String commitHash = properties.getCommitHash();
+            CommitQualityPO commitQualityPO = new CommitQualityPO();
+            commitQualityPO.setProjectId(properties.getProjectId());
+            commitQualityPO.setCommitHash(commitHash);
+            commitQualityPO.setCheckResult(sonarResultHookVO.getQualityGate().getStatus());
+
+            // convert time to java date
+            DateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+            simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+            Date date = simpleDateFormat.parse(sonarResultHookVO.getChangedAt());
+            commitQualityPO.setCheckTime(date);
+
+            // convert localhost url to available url
+            String url = sonarResultHookVO.getProject().getUrl();
+            // http://localhost:9000/dashboard?id\u003dsonart-4194-sonar-sonar-83700f2455cef5ca81ee0561d1a5509229ee0bcb
+            // to
+            // https://sonarqube-test.seec.seecoder.cn/dashboard?id=sonart-4194-sonar-sonar-83700f2455cef5ca81ee0561d1a5509229ee0bcb
+            String host = applicationProperties.getSonar().getHost();
+            url = url.replace("http://localhost:9000", host).replace("\\u003", "=");
+            commitQualityPO.setResultUrl(url);
+
+            commitQualityPO.setSonarProjectKey(sonarResultHookVO.getProject().getKey());
+
+            // set quality conditions json
+            String conditionsArrayJson = JSONArray.toJSONString(sonarResultHookVO.getQualityGate().getConditions());
+            commitQualityPO.setQualityConditionsJson(conditionsArrayJson);
+
+            // update to database
+            CommitQualityPO lastCommitQualityPO = commitQualityMapper.selectByCommitHash(commitHash);
+            if (lastCommitQualityPO != null) {
+                commitQualityPO.setId(lastCommitQualityPO.getId());
+                commitQualityMapper.update(commitQualityPO);
+            } else {
+                commitQualityMapper.insert(commitQualityPO);
+            }
+            return true;
+        } catch (NullPointerException e) {
+            log.error("传入的Sonar Result VO存在问题");
+            return false;
+        } catch (ParseException e) {
+            log.error("传入的时间格式不正确,期待格式:yyyy-MM-dd'T'HH:mm:ss");
+            return false;
+        }
+    }
+
+    @Override
+    public CommitQualityVO getQualityByCommit(Integer projectId, String commitHash) {
+        CommitQualityPO commitQualityPO = commitQualityMapper.selectByCommitHash(commitHash);
+        if (commitQualityPO != null) {
+            CommitQualityVO commitQualityVO = new CommitQualityVO();
+            commitQualityVO.setCommitHash(commitHash);
+            commitQualityVO.setCheckResult(convertCommitCheckResult(commitQualityPO.getCheckResult()));
+            commitQualityVO.setCheckTime(commitQualityPO.getCheckTime());
+            commitQualityVO.setResultUrl(commitQualityPO.getResultUrl());
+            List<CommitQualityVO.QualityCondition> qualityConditions = JSONArray.parseArray(commitQualityPO.getQualityConditionsJson(), CommitQualityVO.QualityCondition.class);
+            commitQualityVO.setQualityConditions(qualityConditions);
+            return commitQualityVO;
+        } else {
+            CommitQualityVO commitQualityVO = new CommitQualityVO();
+            commitQualityVO.setCheckResult(CommitCheckResultEnum.NONE);
+            return commitQualityVO;
+        }
+    }
+
+    @Override
+    public Map<String, CommitCheckResultEnum> getCommitsQualityMapByProjectId(Integer projectId) {
+        Map<String, CommitQualityPO> stringCommitQualityPOMap = commitQualityMapper.selectMapByProjectId(projectId);
+        Map<String, CommitCheckResultEnum> stringCommitCheckResultEnumHashMap = new HashMap<>();
+        for (Map.Entry<String, CommitQualityPO> entry :
+                stringCommitQualityPOMap.entrySet()) {
+            stringCommitCheckResultEnumHashMap.put(entry.getKey(), convertCommitCheckResult(entry.getValue().getCommitHash()));
+        }
+        return stringCommitCheckResultEnumHashMap;
+    }
+
+    @Override
+    public CommitCheckResultEnum getQualityResultByCommit(String hash) {
+        CommitQualityPO commitQualityPO = commitQualityMapper.selectByCommitHash(hash);
+        if (commitQualityPO != null) {
+            return convertCommitCheckResult(commitQualityPO.getCheckResult());
+        } else {
+            return CommitCheckResultEnum.NONE;
+        }
+    }
+
+    // convert String result to Enum result
+    private CommitCheckResultEnum convertCommitCheckResult(String rawResult) {
+        if (rawResult.equals("OK")) {
+            return CommitCheckResultEnum.PASS;
+        } else {
+            return CommitCheckResultEnum.FAIL;
+        }
+    }
+}

+ 124 - 0
web/src/main/java/cn/seecoder/web/service/impl/commit/CommitServiceImpl.java

@@ -0,0 +1,124 @@
+package cn.seecoder.web.service.impl.commit;
+
+import cn.seecoder.api.ApplicationProperties;
+import cn.seecoder.common.util.LogTrackingUtil;
+import cn.seecoder.common.util.OpType;
+import cn.seecoder.web.dao.commit.CommitQualityMapper;
+import cn.seecoder.web.dao.user.UserMapper;
+import cn.seecoder.web.model.enums.CommitCheckResultEnum;
+import cn.seecoder.web.model.enums.CommitRelatedEnum;
+import cn.seecoder.web.model.po.bug_list.BugListPO;
+import cn.seecoder.web.model.po.commit.CommitQualityPO;
+import cn.seecoder.web.model.po.tree.TreeNodePO;
+import cn.seecoder.web.model.vo.commit.CommitQualityVO;
+import cn.seecoder.web.model.vo.commit.CommitVO;
+import cn.seecoder.web.model.vo.hook.SonarResultHookVO;
+import cn.seecoder.web.service.bug_list.BugListService;
+import cn.seecoder.web.service.commit.CommitLinkService;
+import cn.seecoder.web.service.commit.CommitQualityService;
+import cn.seecoder.web.service.tree.TreeNodeService;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.nju.edu.gitlab.SeecoderGitlabApi;
+import com.nju.edu.gitlab.vo.DiffVO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import cn.seecoder.web.dao.commit.CommitMapper;
+import cn.seecoder.web.model.po.commit.CommitPO;
+import cn.seecoder.web.service.commit.CommitService;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author PuHong Weng
+ * @date 2021/3/23
+ * @description:
+ */
+@Service
+@Slf4j
+public class CommitServiceImpl implements CommitService {
+
+    private final SeecoderGitlabApi seecoderGitlabApi;
+
+    @Autowired
+    public CommitServiceImpl(SeecoderGitlabApi seecoderGitlabApi) {
+        this.seecoderGitlabApi = seecoderGitlabApi;
+    }
+
+    @Autowired
+    private CommitLinkService commitLinkService;
+    @Autowired
+    private CommitQualityService commitQualityService;
+
+
+    @Override
+    public List<CommitVO> getCommitsByBranchName(Integer projectId, String BranchName) {
+        List<CommitVO> allCommits = new ArrayList<CommitVO>();
+        Map<String, CommitCheckResultEnum> commitsQualityMapByProjectId = commitQualityService.getCommitsQualityMapByProjectId(projectId);
+
+        try{
+            List<com.nju.edu.gitlab.vo.commit.CommitVO> commitsResponse = seecoderGitlabApi.getCommitsByBranch(projectId, BranchName);
+            for (com.nju.edu.gitlab.vo.commit.CommitVO commit : commitsResponse){
+                CommitVO commitVO = new CommitVO(commit);
+                String hash = commitVO.getId();
+                CommitPO commitPO = commitLinkService.getLinkedCommit(hash);
+                if (commitPO != null){
+                    commitVO.setRelatedType(commitPO.getRelatedType());
+                    commitVO.setRelatedId(commitPO.getRelatedId());
+                } else {
+                    commitVO.setRelatedType(CommitRelatedEnum.NONE);
+                }
+
+                // deal with check result
+                CommitCheckResultEnum commitCheckResultEnum = commitsQualityMapByProjectId.getOrDefault(hash, CommitCheckResultEnum.NONE);
+                commitVO.setCheckResult(commitCheckResultEnum);
+
+                allCommits.add(commitVO);
+            }
+
+        } catch (Exception e){
+            e.printStackTrace();
+            return new ArrayList<>();
+        }
+
+        return allCommits;
+    }
+
+    @Override
+    public CommitVO getCommitByHash(Integer projectId, String hash) {
+        try {
+            com.nju.edu.gitlab.vo.commit.CommitVO commit = seecoderGitlabApi.getCommitByHash(projectId, hash);
+            CommitVO commitVO = new CommitVO(commit);
+            CommitPO commitPO = commitLinkService.getLinkedCommit(hash);
+            if (commitPO != null){
+                System.out.println(commitPO.getId());
+                commitVO.setRelatedType(commitPO.getRelatedType());
+                commitVO.setRelatedId(commitPO.getRelatedId());
+            } else {
+                commitVO.setRelatedType(CommitRelatedEnum.NONE);
+            }
+
+            commitVO.setCheckResult(commitQualityService.getQualityResultByCommit(hash));
+
+            return commitVO;
+        } catch (Exception e){
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    @Override
+    public Set<DiffVO> getDiff(Integer projectId, String fromHash, String toHash) {
+        try{
+            return seecoderGitlabApi.compare(projectId, fromHash, toHash);
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+        return new HashSet<>();
+    }
+}

+ 55 - 44
web/src/main/java/cn/seecoder/web/service/impl/pipeline/DeploymentServiceImpl.java

@@ -35,6 +35,7 @@ import java.util.List;
 import java.util.stream.Collectors;
 
 import static cn.seecoder.web.core.pipeline.template.PipelineTemplateTable.MYSQL;
+import static cn.seecoder.web.core.pipeline.template.PipelineTemplateTable.SONAR_JAVA8;
 
 /**
  * @author PuHong Weng
@@ -70,28 +71,27 @@ public class DeploymentServiceImpl implements DeploymentService {
     public List<DeploymentInfoVO> retrieveDeploymentInfos(Integer projectId) throws AccessDeniedException {
         //projectAuthentication(projectId);
         List<PipelinePO> pipelines = pipelineMapper.selectByProjectId(projectId);
-        if (pipelines.size()==0){
+        if (pipelines.size() == 0) {
             return new ArrayList<>();
         }
         List<Integer> pipelineIds = pipelines.stream().map(PipelinePO::getId).collect(Collectors.toList());
         List<DeploymentPO> deployments = deploymentMapper.selectByPipelineIds(pipelineIds);
-        return deployments.stream().map(x->{
-            PipelineInspector inspector = PipelineInspectorFactory.init(x.getNamespace(),x.getDeployName());
+        return deployments.stream().map(x -> {
+            PipelineInspector inspector = PipelineInspectorFactory.init(x.getNamespace(), x.getDeployName());
             //获取状态
             String status = null;
             V1DeploymentStatus deploymentStatus = inspector.inspectStatus();
-            if (deploymentStatus == null){
+            if (deploymentStatus == null) {
                 status = "部署不存在,请重新部署";
-            }
-            else if(deploymentStatus.getAvailableReplicas()!=null && deploymentStatus.getAvailableReplicas()>=1){
+            } else if (deploymentStatus.getAvailableReplicas() != null && deploymentStatus.getAvailableReplicas() >= 1) {
                 status = "正常运行";
-            } else{
+            } else {
                 status = "不可用";
             }
             return DeploymentInfoVO.builder()
                     .id(x.getId())
                     .pipelineId(x.getPipelineId())
-                    .name(x.getDeployName()+"."+x.getNamespace())
+                    .name(x.getDeployName() + "." + x.getNamespace())
                     .deployedTime(x.getDeployedTime())
                     .accessUrl(x.getAccessUrl())
                     .status(status)
@@ -115,31 +115,41 @@ public class DeploymentServiceImpl implements DeploymentService {
                 .build();
         pipelineRecordMapper.insert(record);
 
+        // 有一些流水线是不产生部署产物的
+        boolean hasDeployment = true;
+        switch (pipelinePO.getTemplateName()) {
+            case SONAR_JAVA8:
+                hasDeployment = false;
+            default:
+        }
 
-        DeploymentPO deployment = deploymentMapper.selectByPipelineId(pipelineId);
-        if (deployment == null){
-            //第一次部署
-            String accessUrl;
-            //根据模板类型设置起的访问路径
-            switch (pipelinePO.getTemplateName()){
-                case MYSQL:
-                    //集群无法暴露tcp端口,只提供了集群内部访问的url
-                    accessUrl = pipelinePO.getName() + K8sConstants.SERVICE_SUFFIX+"."+projectPO.getUniqueK8sNamespace() +".svc.cluster.local";
-                    break;
-                default:
-                    accessUrl = pipelinePO.getName()+"."+projectPO.getUniqueK8sNamespace()+ properties.getK8s().getIngressHostSuffix();
-                    break;
+        // 部署产物记录
+        if (hasDeployment) {
+            DeploymentPO deployment = deploymentMapper.selectByPipelineId(pipelineId);
+            if (deployment == null) {
+                //第一次部署
+                String accessUrl;
+                //根据模板类型设置起的访问路径
+                switch (pipelinePO.getTemplateName()) {
+                    case MYSQL:
+                        //集群无法暴露tcp端口,只提供了集群内部访问的url
+                        accessUrl = pipelinePO.getName() + K8sConstants.SERVICE_SUFFIX + "." + projectPO.getUniqueK8sNamespace() + ".svc.cluster.local";
+                        break;
+                    default:
+                        accessUrl = pipelinePO.getName() + "." + projectPO.getUniqueK8sNamespace() + properties.getK8s().getIngressHostSuffix();
+                        break;
+                }
+                deploymentMapper.insert(DeploymentPO.builder()
+                        .namespace(projectPO.getUniqueK8sNamespace())
+                        .deployName(pipelinePO.getName())
+                        .accessUrl(accessUrl)
+                        .pipelineId(pipelineId)
+                        .deployedTime(record.getStartTime())
+                        .build());
+            } else {
+                deployment.setDeployedTime(record.getStartTime());
+                deploymentMapper.update(deployment);
             }
-            deploymentMapper.insert(DeploymentPO.builder()
-                    .namespace(projectPO.getUniqueK8sNamespace())
-                    .deployName(pipelinePO.getName())
-                    .accessUrl(accessUrl)
-                    .pipelineId(pipelineId)
-                    .deployedTime(record.getStartTime())
-                    .build());
-        } else {
-            deployment.setDeployedTime(record.getStartTime());
-            deploymentMapper.update(deployment);
         }
 
         Pipeline pipeline = null;
@@ -153,12 +163,12 @@ public class DeploymentServiceImpl implements DeploymentService {
             record.setResult(PipelineRecordPO.DEPLOY_FAIL);
             record.setDetails(pipeline.getContext().getResult());
             log.error("流水线配置转换发生错误");
-            throw new ServiceException(HttpStatus.SC_INTERNAL_SERVER_ERROR,"应用部署失败",e);
-        } catch (RuntimeException e){
+            throw new ServiceException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "应用部署失败", e);
+        } catch (RuntimeException e) {
             pipeline.getContext().appendErrorResult("运行时错误: ", e);
             record.setResult(PipelineRecordPO.DEPLOY_FAIL);
             record.setDetails(pipeline.getContext().getResult());
-            throw new ServiceException(HttpStatus.SC_INTERNAL_SERVER_ERROR,"应用部署失败",e);
+            throw new ServiceException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "应用部署失败", e);
         } finally {
             pipelineRecordMapper.update(record);
         }
@@ -166,19 +176,20 @@ public class DeploymentServiceImpl implements DeploymentService {
         List<Integer> pipelineIds = new ArrayList<>();
         pipelineIds.add(pipelineId);
         PipelineRecordPO pipelineRecordPO = pipelineRecordMapper.selectPipelinesLatestRecord(pipelineIds);
-        try{
+        try {
             JSONObject object = new JSONObject();
-            object.put("project_id",projectId);
-            object.put("record_id",pipelineRecordPO.getId());
-            object.put("pipeline_id",pipelineId);
-            object.put("user_id",userId);
-            object.put("type",record.getResult());
+            object.put("project_id", projectId);
+            object.put("record_id", pipelineRecordPO.getId());
+            object.put("pipeline_id", pipelineId);
+            object.put("user_id", userId);
+            object.put("type", record.getResult());
             object.put("result", record.getResult());
-            object.put("success",record.getResult().equals(PipelineRecordPO.DEPLOY_SUCCESS));
+            object.put("success", record.getResult().equals(PipelineRecordPO.DEPLOY_SUCCESS));
             //success?
             String data = JSONObject.toJSONString(object);
             LogTrackingUtil.log(data, OpType.DEPLOY_PIPELINE);
-        }catch (Exception e){}
+        } catch (Exception e) {
+        }
     }
 
     @Override
@@ -187,7 +198,7 @@ public class DeploymentServiceImpl implements DeploymentService {
         //projectAuthentication(projectId);
         DeploymentPO deployment = deploymentMapper.selectByPipelineId(pipelineId);
         deploymentMapper.delete(pipelineId);
-        PipelineInspector inspector = PipelineInspectorFactory.init(deployment.getNamespace(),deployment.getDeployName());
+        PipelineInspector inspector = PipelineInspectorFactory.init(deployment.getNamespace(), deployment.getDeployName());
         inspector.delete();
 
     }
@@ -195,7 +206,7 @@ public class DeploymentServiceImpl implements DeploymentService {
     @Override
     public String log(Integer projectId, Integer pipelineId) {
         DeploymentPO deployment = deploymentMapper.selectByPipelineId(pipelineId);
-        PipelineInspector inspector = PipelineInspectorFactory.init(deployment.getNamespace(),deployment.getDeployName());
+        PipelineInspector inspector = PipelineInspectorFactory.init(deployment.getNamespace(), deployment.getDeployName());
         return inspector.log();
     }
 }

+ 0 - 210
web/src/main/java/cn/seecoder/web/service/impl/project/CommitServiceImpl.java

@@ -1,210 +0,0 @@
-package cn.seecoder.web.service.impl.project;
-
-import cn.seecoder.common.util.LogTrackingUtil;
-import cn.seecoder.common.util.OpType;
-import cn.seecoder.web.dao.user.UserMapper;
-import cn.seecoder.web.model.enums.CommitRelatedEnum;
-import cn.seecoder.web.model.po.bug_list.BugListPO;
-import cn.seecoder.web.model.po.tree.TreeNodePO;
-import cn.seecoder.web.model.vo.project.CommitVO;
-import cn.seecoder.web.service.bug_list.BugListService;
-import cn.seecoder.web.service.tree.TreeNodeService;
-import com.alibaba.fastjson.JSONObject;
-import com.nju.edu.gitlab.SeecoderGitlabApi;
-import com.nju.edu.gitlab.vo.DiffVO;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-import cn.seecoder.web.dao.project.CommitMapper;
-import cn.seecoder.web.model.po.project.CommitPO;
-import cn.seecoder.web.service.project.CommitService;
-
-import java.util.*;
-import java.util.stream.Collectors;
-
-/**
- * @author PuHong Weng
- * @date 2021/3/23
- * @description:
- */
-@Service
-public class CommitServiceImpl implements CommitService {
-
-    private final CommitMapper commitMapper;
-    private final SeecoderGitlabApi seecoderGitlabApi;
-    private final TreeNodeService treeNodeService;
-    private final BugListService bugListService;
-
-
-    @Autowired
-    public CommitServiceImpl(CommitMapper commitMapper, SeecoderGitlabApi seecoderGitlabApi, TreeNodeService treeNodeService, BugListService bugListService) {
-        this.commitMapper = commitMapper;
-        this.seecoderGitlabApi = seecoderGitlabApi;
-        this.treeNodeService = treeNodeService;
-        this.bugListService = bugListService;
-    }
-
-    @Autowired
-    private UserMapper userMapper;
-
-    @Override
-    public void save(CommitPO po) {
-
-        //ANA 日志需要打出用户commit的信息
-        try{
-            JSONObject object = new JSONObject();
-            object.put("commit_id",po.getId());
-            object.put("user_id",userMapper.selectIdByUserName(po.getGitlabUsername()));
-            object.put("project_id",po.getProjectId());
-            object.put("commit_name",po.getGitlabUsername());
-            object.put("commit_time",po.getTimestamp());
-            object.put("message",po.getMessage());
-            object.put("type",po.getRelatedType());
-
-            String data = JSONObject.toJSONString(object);
-            LogTrackingUtil.log(data, OpType.COMMIT);
-        }catch (Exception e){}
-
-        commitMapper.insert(po);
-    }
-
-    @Override
-    public List<CommitPO> listByProjectAndType(Integer projectId, CommitRelatedEnum type) {
-        return commitMapper.selectByProjectIdAndType(projectId,type);
-    }
-
-
-    @Override
-    public boolean isResolved(String id) {
-        return 1 == commitMapper.isExist(id);
-    }
-
-    @Override
-    public List<CommitVO> getTreeCommit(Integer treeId) {
-        return commitMapper.selectByRelatedIdAndType(treeId,CommitRelatedEnum.TREE_ID).stream()
-                .map(CommitVO::new)
-                .collect(Collectors.toList());
-    }
-
-    @Override
-    public List<CommitVO> getBugCommit(Integer bugId) {
-        return commitMapper.selectByRelatedIdAndType(bugId,CommitRelatedEnum.BUG_ID).stream()
-                .map(CommitVO::new)
-                .collect(Collectors.toList());
-    }
-
-    @Override
-    public List<CommitVO> getCommits(Integer projectId) {
-        return commitMapper.selectByProjectId(projectId).stream()
-                .map(CommitVO::new)
-                .collect(Collectors.toList());
-    }
-
-    @Override
-    public List<CommitVO> getCommitsByBranchName(Integer projectId, String BranchName) {
-        List<CommitVO> allCommits = new ArrayList<CommitVO>();
-
-        try{
-            List<com.nju.edu.gitlab.vo.commit.CommitVO> commitsResponse = seecoderGitlabApi.getCommitsByBranch(projectId, BranchName);
-            for (com.nju.edu.gitlab.vo.commit.CommitVO commit : commitsResponse){
-                CommitVO commitVO = new CommitVO(commit);
-                CommitPO commitPO = commitMapper.selectByCommitId(commitVO.getId());
-                if (commitPO != null){
-//                System.out.println(commitPO.getId());
-                    commitVO.setRelatedType(commitPO.getRelatedType());
-                    commitVO.setRelatedId(commitPO.getRelatedId());
-                } else {
-//                System.out.println("hh");
-                    commitVO.setRelatedType(CommitRelatedEnum.NONE);
-                }
-                allCommits.add(commitVO);
-            }
-
-        } catch (Exception e){
-            e.printStackTrace();
-            return new ArrayList<>();
-        }
-
-        return allCommits;
-    }
-
-    @Override
-    public CommitVO getCommitByHash(Integer projectId, String hash) {
-        try {
-            com.nju.edu.gitlab.vo.commit.CommitVO commit = seecoderGitlabApi.getCommitByHash(projectId, hash);
-            CommitVO commitVO = new CommitVO(commit);
-            CommitPO commitPO = commitMapper.selectByCommitId(hash);
-            if (commitPO != null){
-                System.out.println(commitPO.getId());
-                commitVO.setRelatedType(commitPO.getRelatedType());
-                commitVO.setRelatedId(commitPO.getRelatedId());
-            } else {
-                System.out.println("hh");
-                commitVO.setRelatedType(CommitRelatedEnum.NONE);
-            }
-            return commitVO;
-        } catch (Exception e){
-            e.printStackTrace();
-            return null;
-        }
-    }
-
-    @Override
-    public Set<DiffVO> getDiff(Integer projectId, String fromHash, String toHash) {
-        try{
-            Set<DiffVO> diff = seecoderGitlabApi.compare(projectId, fromHash, toHash);
-            return diff;
-        }catch (Exception e){
-            e.printStackTrace();
-        }
-        return new HashSet<>();
-    }
-
-    @Override
-    public boolean link(Integer projectId, String commitHash, CommitRelatedEnum relatedType, Integer relatedId) {
-        if (relatedType == CommitRelatedEnum.NONE){
-            commitMapper.deleteByCommitId(commitHash);
-            return true;
-        }
-
-        // check if the related target exists
-        if (relatedType == CommitRelatedEnum.TREE_ID){
-            TreeNodePO node = treeNodeService.getNodeByNodeId(relatedId);
-            if (node == null){
-                return false;
-            } else if (! node.getProjectId().equals(projectId)) {
-                return false;
-            }
-        } else {
-            BugListPO node = bugListService.getBugById(relatedId);
-            if (node == null){
-                return false;
-            } else if (! node.getProjectId().equals(projectId)) {
-                return false;
-            }
-        }
-
-        // check if the commit is already exist, then update it. if not, insert a new one.
-        CommitPO commitPODB = commitMapper.selectByCommitId(commitHash);
-        if (commitPODB != null){
-            commitPODB.setRelatedType(relatedType);
-            commitPODB.setRelatedId(relatedId);
-            commitMapper.update(commitPODB);
-        } else {
-            CommitVO commit = this.getCommitByHash(projectId, commitHash);
-
-            CommitPO commitPO = new CommitPO();
-            commitPO.setId(commit.getId());
-            commitPO.setMessage(commit.getMessage());
-            commitPO.setTitle(commit.getTitle());
-            commitPO.setProjectId(projectId);
-            commitPO.setTimestamp(commit.getTimestamp());
-            commitPO.setGitlabUsername(commit.getGitlabUsername());
-            commitPO.setEmail(commit.getAuthorEmail());
-            commitPO.setRelatedType(relatedType);
-            commitPO.setRelatedId(relatedId);
-
-            this.save(commitPO);
-        }
-        return true;
-    }
-}

+ 2 - 2
web/src/main/java/cn/seecoder/web/service/impl/tree/TreeNodeServiceImpl.java

@@ -2,11 +2,11 @@ package cn.seecoder.web.service.impl.tree;
 
 import cn.seecoder.common.util.LogTrackingUtil;
 import cn.seecoder.common.util.OpType;
-import cn.seecoder.web.dao.project.CommitMapper;
+import cn.seecoder.web.dao.commit.CommitMapper;
 import cn.seecoder.web.dao.tree.TreeNodeMapper;
 import cn.seecoder.web.dao.user.UserMapper;
 import cn.seecoder.web.model.enums.*;
-import cn.seecoder.web.model.po.project.CommitPO;
+import cn.seecoder.web.model.po.commit.CommitPO;
 import cn.seecoder.web.model.po.tree.TreeNodePO;
 import cn.seecoder.web.model.po.user.UserPO;
 import cn.seecoder.web.model.vo.tree.TreeNodeCreateVO;

+ 0 - 50
web/src/main/java/cn/seecoder/web/service/project/CommitService.java

@@ -1,50 +0,0 @@
-package cn.seecoder.web.service.project;
-
-import cn.seecoder.web.model.enums.CommitRelatedEnum;
-import cn.seecoder.web.model.po.project.CommitPO;
-import cn.seecoder.web.model.vo.project.CommitVO;
-import com.nju.edu.gitlab.vo.DiffVO;
-
-import java.util.List;
-import java.util.Set;
-
-/**
- * @author PuHong Weng
- * @date 2021/3/21
- * @description: 项目相关的 git commit 记录分析功能
- */
-public interface CommitService {
-
-    void save(CommitPO po);
-
-    List<CommitPO> listByProjectAndType(Integer projectId, CommitRelatedEnum type);
-
-
-    /**
-     * 不同分支可能会push相同的commit,分析过的commit不应该再分析,这里是判断是否分析共没有
-     * @param id commit hash id
-     */
-    boolean isResolved(String id);
-
-    List<CommitVO> getTreeCommit(Integer treeId);
-
-    List<CommitVO> getBugCommit(Integer bugId);
-
-    List<CommitVO> getCommits(Integer projectId);
-
-    List<CommitVO> getCommitsByBranchName(Integer projectId, String branchName);
-
-    CommitVO getCommitByHash(Integer projectId, String hash);
-
-    Set<DiffVO> getDiff(Integer projectId, String fromHash, String toHash);
-
-    /**
-     * link commit and tree node or bug
-     * @param projectId
-     * @param commitHash
-     * @param relatedType
-     * @param relatedId
-     * @return
-     */
-    boolean link(Integer projectId, String commitHash, CommitRelatedEnum relatedType, Integer relatedId);
-}

+ 7 - 4
web/src/main/resources/application-lzl.yml

@@ -35,7 +35,7 @@ seecoder:
   jwt:
     secret: UenM4KGb6DskRWUenM4KGb6DskRWUenM4KGb6DskRWUenM4KGb6DskRWUenM4KGb6DskRWUenM4KGb6DskRWUenM4KGb6DskRWUenM4KGb6DskRWUenM4KGb6DskRW
   k8s:
-    api-server: https://192.168.99.105:8443
+    api-server: https://101.133.232.246:8443
     token: eyJhbGciOiJSUzI1NiIsImtpZCI6IjBmSXc4RlRjNUpBYVpGYWc4QzRDY1MzUWJad1JMdFhKcDI1cngzWGtiRlkifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZXZjbG91ZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJkZWZhdWx0LXRva2VuLXhqYmR3Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImRlZmF1bHQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJlZWNmMjg5OS1jZjNmLTQ5ZTItOWZjMS1kZTM3ZGM1YmQ0YjIiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGV2Y2xvdWQ6ZGVmYXVsdCJ9.kKYqrK-Y31cG5r6oAPGsu1BqD4H40hdaFW5xoSvm9gGzO6XBwgznxXswhBpJFtAi3DVpfukgx1XXSdouB8-z9uA6wRKiBKqmbVYy3GjXfEh1WUlvKxbu-JSA3UisTFdJwfaP_mUjoMUWLgdNm2R2bW2qLMsT0fO54cQf8WSjX3lDW2h4CBec7rq3nVqC4zWn3VATpHYTJg7he-yNKbDvqJCgpNtmC-fWdXwPm2vTjB6sXYcCiDdspRAPB7yhU3T-up7AZPBpXdHiBPLvjTHEBHnnMDtgO-EN4e46KDaIa2STMgNTYm1to-rzxeUQCkxUjBRWNdkyozD_hOFih1ULrw
     debugging: true
     ingressHostSuffix: .devcloud.seec.seecoder.cn
@@ -45,13 +45,16 @@ seecoder:
     token: wXDeETv2Kmt-JhiApJ9H
   git:
     username: root
-    password: NJU67software
+    password: 1qaz0158Seecoder67
   docker:
-    host: https://192.168.99.105:2376
-    registry: 172.19.51.6:8092
+    host: https://localhost:2376
+    registry: 106.14.170.111:8092
     registryUsername: admin
     registryPassword: NJU67nexus
     tls: true
+  sonar:
+    host: https://sonarqube-test.seec.seecoder.cn
+    loginToken: 787658b11f0332f631bfb186aa1bfcd7898b0498
 
 
 

+ 3 - 0
web/src/main/resources/application-prod.yaml

@@ -40,6 +40,9 @@ seecoder:
     registryUsername: admin
     registryPassword: NJU67nexus
     tls: false
+  sonar:
+    host: https://sonarqube-test.seec.seecoder.cn
+    loginToken: 787658b11f0332f631bfb186aa1bfcd7898b0498
 
 logging:
   file:

+ 9 - 3
web/src/main/resources/sql/data_init.sql

@@ -1,5 +1,5 @@
-delete from devcloud.stage_config;
-delete from devcloud.stage;
+delete from devcloud.stage_config where true;
+delete from devcloud.stage where true;
 
 INSERT INTO devcloud.stage (id, title, description, name, skippable) VALUES (1, 'Java应用构建', '后端Java应用构建打包过程。项目需使用Maven作为项目管理工具。构建过程包含Maven test测试过程,需确保单元测试的100%正确性,单元测试结果将输出到打包日志中。此步骤将打包出一个可运行的服务器程序jar包,并构建生成一个可运行在平台内云服务器的docker镜像。请作为流水线的第一步。这里学生仅作粗略了解,无需深入配置。', 'java-image-build', 0);
 INSERT INTO devcloud.stage (id, title, description, name, skippable) VALUES (2, 'Node.js应用构建', '前端应用构建打包过程。需提供对应的Nginx配置文件,将请求转发给平台集群内的后端。请作为流水线的第一步。这里学生仅作粗略了解,无需深入配置。', 'node-image-build', 0);
@@ -10,6 +10,8 @@ INSERT INTO devcloud.stage (id, title, description, name, skippable) VALUES (6,
 INSERT INTO devcloud.stage (id, title, description, name, skippable) VALUES (7, '自动化api测试', '可指定自己在api测试界面所设定的api测试,并在流水线部署的末尾执行这些api测试用例,并自动记录结果。这个步骤可选择跳过。', 'api-test', 1);
 INSERT INTO devcloud.stage (id, title, description, name, skippable) VALUES (8, '应用部署', '可自动在平台部署一个服务实例,并对外开放访问方式供使用。', 'deploy', 0);
 INSERT INTO devcloud.stage (id, title, description, name, skippable) VALUES (9, 'Mysql部署', '可自动在平台部署一个Mysql服务实例。', 'mysql-deploy', 0);
+INSERT INTO devcloud.stage (id, title, description, name, skippable) VALUES (10, 'Java代码检查镜像创建', '后端Java代码静态质量检查,检查后结果会展示在构建详情和提交记录中。也可以在Java构建中选择此阶段进行检查。', 'sonar-java', 0);
+INSERT INTO devcloud.stage (id, title, description, name, skippable) VALUES (11, 'Java代码检查镜像创建', '后端Java代码静态质量检查,检查后结果会展示在构建详情和提交记录中。也可以单独创建质量检查流水线', 'sonar-java-deploy', 1);
 
 
 INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (1, 'repoUrl', '#todo', 1, 1);
@@ -29,4 +31,8 @@ INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (
 INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (15, 'access_url', '{pipelineName}.{projectName}-{projectId}.seec.seecoder.cn', 0, 8);
 INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (16, 'port', '3306', 0, 9);
 INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (17, 'username', 'root', 0, 9);
-INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (18, 'password', 'root', 1, 9);
+INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (18, 'password', 'root', 1, 9);
+INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (19, 'branchOrHash', '#todo', 1, 10);
+INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (20, 'projectId', '{projectId}', 0, 10);
+INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (21, 'branchOrHash', '#todo', 1, 11);
+INSERT INTO devcloud.stage_config (id, name, value, editable, stage_id) VALUES (22, 'projectId', '{projectId}', 0, 11);

+ 12 - 0
web/src/main/resources/sql/table_init.sql

@@ -215,3 +215,15 @@ create table if not exists devcloud.pipeline_record
 )
     charset = utf8;
 
+create table if not exists devcloud.commit_quality
+(
+    id                              int             auto_increment
+        primary key,
+    project_id                      int             not null,
+    commit_hash                     varchar(127)    not null,
+    sonar_project_key               varchar(127)    not null,
+    check_result                    varchar(31)     not null,
+    check_time                      datetime default CURRENT_TIMESTAMP not null,
+    result_url                      varchar(255)    null,
+    quality_conditions_json         varchar(1023)   null
+) CHARSET=utf8;

+ 10 - 0
web/src/main/resources/template/config/SONAR_JAVA8.json

@@ -0,0 +1,10 @@
+[
+  {
+    "name": "sonar-java",
+    "active": true,
+    "configs": {
+      "branchOrHash": "#todo",
+      "projectId": "#auto"
+    }
+  }
+]

+ 8 - 0
web/src/main/resources/template/config/SPRINGBOOT_JAVA8.json

@@ -1,4 +1,12 @@
 [
+  {
+    "name": "sonar-java",
+    "active": true,
+    "configs": {
+      "branchOrHash": "#todo",
+      "projectId": "#auto"
+    }
+  },
   {
     "name": "java-image-build",
     "active": true,

+ 6 - 0
web/src/main/resources/template/dockerfile/SONAR_JAVA8

@@ -0,0 +1,6 @@
+FROM maven:3.8-jdk-11
+ENV WORK_PATH /opt/sonar_test
+COPY settings.xml /root/.m2/settings.xml
+COPY . $WORK_PATH
+#RUN cd $WORK_PATH && mvn clean package -DskipTests
+CMD cd $WORK_PATH && mvn clean verify sonar:sonar  -Dsonar.projectKey=%projectKey%  -Dsonar.host.url=%host%  -Dsonar.login=%loginToken% -Dsonar.analysis.projectId=%projectId% -Dsonar.analysis.commitHash=%commitHash% -Dsonar.analysis.imageName=%imageName% -Dsonar.analysis.imageTag=%imageTag%

+ 72 - 0
web/src/test/java/cn/seecoder/web/dao/commit/CommitQualityMapperTest.java

@@ -0,0 +1,72 @@
+package cn.seecoder.web.dao.commit;
+
+import cn.seecoder.web.model.po.commit.CommitQualityPO;
+import org.junit.FixMethodOrder;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Map;
+import java.util.TimeZone;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author claws
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest
+@MapperScan(basePackages = {"cn.seecoder.web.dao"})
+@FixMethodOrder(MethodSorters.JVM)
+public class CommitQualityMapperTest {
+
+    @Autowired
+    private CommitQualityMapper commitQualityMapper;
+
+    // test insert
+    @Test
+    public void test1() throws ParseException {
+        CommitQualityPO commitQualityPO = new CommitQualityPO();
+        commitQualityPO.setCommitHash("iii");
+        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssz");
+        simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+        Date date = simpleDateFormat.parse("2022-04-25T08:16:28+0000");
+        commitQualityPO.setCheckTime(date);
+        commitQualityPO.setProjectId(999);
+        commitQualityPO.setSonarProjectKey("haha");
+        commitQualityPO.setCheckResult("OK");
+
+        commitQualityMapper.insert(commitQualityPO);
+        CommitQualityPO commitQualityPOResult = commitQualityMapper.selectByCommitHash("iii");
+        Assertions.assertNotNull(commitQualityPOResult);
+        Assertions.assertEquals(999, commitQualityPOResult.getProjectId());
+    }
+
+    // test select map
+    @Test
+    public void test2() {
+        Map<String, CommitQualityPO> stringCommitQualityPOMap = commitQualityMapper.selectMapByProjectId(999);
+        Assertions.assertNotNull(stringCommitQualityPOMap);
+        CommitQualityPO commitQualityPO = stringCommitQualityPOMap.getOrDefault("iii", null);
+        Assertions.assertNotNull(commitQualityPO);
+        Assertions.assertEquals(999, commitQualityPO.getProjectId());
+    }
+
+    // test delete
+    @Test
+    public void test3() {
+        commitQualityMapper.deleteByCommitHash("iii");
+        CommitQualityPO commitQualityPOResult = commitQualityMapper.selectByCommitHash("iii");
+        Assertions.assertNull(commitQualityPOResult);
+    }
+
+
+}

+ 2 - 1
web/src/test/java/cn/seecoder/web/dao/project/CommitMapperTest.java

@@ -1,5 +1,6 @@
 package cn.seecoder.web.dao.project;
 
+import cn.seecoder.web.dao.commit.CommitMapper;
 import cn.seecoder.web.model.enums.CommitRelatedEnum;
 import org.junit.jupiter.api.Test;
 import org.junit.runner.RunWith;
@@ -7,7 +8,7 @@ import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.test.context.junit4.SpringRunner;
-import cn.seecoder.web.model.po.project.CommitPO;
+import cn.seecoder.web.model.po.commit.CommitPO;
 
 /**
  * @author PuHong Weng