任务

实验室开发官网,简化纳新流程,减少纳新负担。在系统后端(项目地址)需要实现的功能之一就是通过JGit从Github及Gitee拉取代码。

该部分第一阶段要求如下:

本系统需要从远程仓库上拉取代码,进行拉取、查看、构建、运行,并进行管理。需要支持

  • 远程仓库:GitHub、Gitee

  • 支持Clone方式:HTTPS、SSH

  • 可以通过Github / Gitee用户信息,克隆public或有权限的private仓库

  • 通过配置代理或者其他方案,加速与GitHub的连接速度

完成之后,需要:

  • 一个完善的工具类和静态方法,用于完成以上逻辑

  • 一个RestFul Api接口,访问该接口即可以执行拉取代码逻辑(之后会安排前端对接口)

  • 必须有完善的代码规范和异常处理,需要把异常处理情景返回给前端。(如仓库不存在 / 没有访问权限 / 网络问题clone失败 / 登录用户信息错误 / 连接超时……)

实现

config.GitHubProxySelector

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;


public class GitHubProxySelector extends ProxySelector {
    private static final Logger LOGGER = Logger.getLogger(GitHubProxySelector.class.getName());


    private final Proxy githubProxy;

    public GitHubProxySelector(String proxyHost, int proxyPort) {
        this.githubProxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));
    }

    @Override
    public List<Proxy> select(URI uri) {
        // 检查 URI 是否指向 GitHub
        if (isGitHubUri(uri)) {
            // 如果是,则返回 GitHub 代理
            return Collections.singletonList(githubProxy);
        } else {
            // 否则,返回默认的代理(通常是无代理)
            return Collections.singletonList(Proxy.NO_PROXY);
        }
    }

    @Override
    public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
        // 连接失败时的处理逻辑(可选)

        LOGGER.log(Level.SEVERE, "Failed to connect to " + uri + " via " + sa);
    }

    // 私有方法,用于检查 URI 是否指向 GitHub
    private boolean isGitHubUri(URI uri) {
        // 这里只是一个简单的示例,只检查主机名是否包含 "github.com"
        // 在实际应用中,你可能需要更精确的检查逻辑
        return uri.getHost() != null && uri.getHost().contains("github.com");
    }

    // 静态方法,用于设置默认的 ProxySelector
    public static void setDefaultGitHubProxy(String proxyHost, int proxyPort) {
        ProxySelector.setDefault(new GitHubProxySelector(proxyHost, proxyPort));
    }
}

controller.JGitController

import com.example.jgitdemo.entity.CloneRequest;
import com.example.jgitdemo.utils.JGitUtils;
import com.example.jgitdemo.utils.MavenUtils;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.net.MalformedURLException;

@RestController
@RequestMapping
public class JGitController {
    @PostMapping("/clone")
    public String cloneRepo(@RequestBody CloneRequest cloneRequest) throws GitAPIException, IOException {
        JGitUtils jGitUtils = new JGitUtils();
        return jGitUtils.cloneRepository(cloneRequest.getRepoUrl(), cloneRequest.getLocalPath(), cloneRequest.getUsername(), cloneRequest.getPassword(), cloneRequest.isUseSsh(), cloneRequest.getPrivateKeyPath());
    }

    @PostMapping("/build")
    public String buildAndRun(@RequestBody CloneRequest cloneRequest)
    {
        MavenUtils mavenUtils = new MavenUtils();
        return mavenUtils.buildAndRun(cloneRequest.getLocalPath());
    }
}

entity.CloneRequest

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CloneRequest {
    private String repoUrl;
    private String localPath;
    private String username;
    private String password;
    private boolean useSsh;
    private String privateKeyPath;
}

utils.JGitUtils

import com.example.jgitdemo.config.GitHubProxySelector;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.TransportException;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.transport.*;

import org.eclipse.jgit.util.FS;


import java.io.File;
import java.io.IOException;

@Slf4j
public class JGitUtils {
//    private final String repoUrl = "https://github.com/Werun-backend/anser-demo.git";
//    private final String localPath = "D:\\IJ\\IJProjects\\anser\\demosPull";

    public String cloneRepository(String repoUrl, String localPath, String username, String password, boolean useSsh,String privateKeyPath) throws GitAPIException, IOException {
//        System.setProperty("https.proxyHost", "140.82.113.4");
        File localDir = new File(localPath);
        if (localDir.exists()) {
            return "本地仓库已存在";
        }
        try {
            Git git ;
            if (useSsh) {
                log.info("cloning from use ssh :{}", repoUrl);
                // 使用 SSH 克隆仓库
                git = Git.cloneRepository()
                        .setURI(repoUrl)
                        .setTransportConfigCallback(transport -> {
                            SshTransport sshTransport = (SshTransport) transport;
                            sshTransport.setSshSessionFactory(createSshSessionFactory(privateKeyPath));
                        })
                        .setDirectory(localDir)
                        .setCloneAllBranches(true)
                        .call();
                log.info("clone success");
                git.close();
            } else {
                log.info("cloning from use username and password:{}", repoUrl);


                //TODO:这里配代理,由于暂时没有我在这里把他设置为本机的Clash默认端口
                GitHubProxySelector.setDefaultGitHubProxy("127.0.0.1",7890);

                // 使用 HTTPS 克隆仓库
                if (username != null && !username.isEmpty() && password != null && !password.isEmpty()) {
                    // 用户名和密码提供时,设置认证
                    git = Git.cloneRepository()
                            .setURI(repoUrl)
                            .setCredentialsProvider(new UsernamePasswordCredentialsProvider(username, password))
                            .setDirectory(localDir)
                            .call();
                    log.info("clone success");
                } else {
                    log.info("cloning from public repo:{}", repoUrl);
                    // 公共仓库,不需要认证
                    git = Git.cloneRepository()
                            .setURI(repoUrl)
                            .setDirectory(localDir)
                            .call();
                    log.info("clone success");
                }
            }
            return "克隆成功:" + git.getRepository().getDirectory().getAbsolutePath();
        } catch (TransportException e) {
            log.error("克隆失败 - 网络问题或仓库不可达: {}", e.getMessage());
            return "克隆失败 - 网络问题或仓库不可达";
        } catch (GitAPIException e) {
            log.error("克隆失败 - Git操作错误: {}", e.getMessage());
            return "克隆失败 - Git操作错误";
        } catch (UnsupportedCredentialItem e) {
            log.error("克隆失败 - 登录信息错误: {}", e.getMessage());
            return "克隆失败 - 登录信息错误";
        } catch (Exception e) {
            log.error("克隆失败 - 其他错误: {}", e.getMessage());
            return "克隆失败 - 其他错误";
        }
    }

    private static SshSessionFactory createSshSessionFactory(String privateKeyPath) {
        return new JschConfigSessionFactory() {
            @Override
            protected void configure(OpenSshConfig.Host hc, Session session) {
                session.setConfig("StrictHostKeyChecking", "no");
//                session.setProxy(new ProxySOCKS5("your.proxy.host", proxy_port));
            }

            @Override
            protected JSch createDefaultJSch(FS fs) throws JSchException {
                JSch jSch = super.createDefaultJSch(fs);
                jSch.addIdentity(privateKeyPath);
                   return jSch;
            }
        };
    }
}

utils.MavenUtils

import lombok.extern.slf4j.Slf4j;
import org.apache.maven.shared.invoker.*;

import java.io.File;
import java.util.Collections;

@Slf4j
public class MavenUtils {
    public String buildAndRun(String localPath){
        Invoker invoker = new DefaultInvoker();
        invoker.setMavenHome(new File("D:\\maven\\apache-maven-3.6.1"));
        InvocationRequest request = new DefaultInvocationRequest();
        request.setPomFile(new File(localPath + "\\pom.xml"));
        request.setGoals(Collections.singletonList("clean install"));

        try {
            InvocationResult result = invoker.execute(request);
            if (result.getExitCode() == 0) {
                log.info("Maven build successful.");
                // 运行 Spring Boot 应用
                return runSpringBoot(localPath);
            } else {
                return "Maven build failed.";
            }
        } catch (MavenInvocationException e) {
            return "Maven invocation failed: " + e.getMessage();
        }
    }

JGitDemoApplication

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

import java.io.IOException;
import java.net.*;
import java.util.Arrays;
import java.util.List;

@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
public class JGitDemoApplication {

    public static void main(String[] args) {
        ProxySelector.setDefault(new ProxySelector() {
            final ProxySelector delegate = ProxySelector.getDefault();

            @Override
            public List<Proxy> select(URI uri) {
                // Filter the URIs to be proxied
                if (uri.toString().contains("github")
                        && uri.toString().contains("https")) {
                    return Arrays.asList(new Proxy(Proxy.Type.HTTP, InetSocketAddress
                            .createUnresolved("207.148.73.152", 443)));
                }
                // revert to the default behaviour
                return delegate == null ? Arrays.asList(Proxy.NO_PROXY)
                        : delegate.select(uri);
            }

            @Override
            public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
                if (uri == null || sa == null || ioe == null) {
                    throw new IllegalArgumentException(
                            "Arguments can't be null.");
                }
            }
        });
        SpringApplication.run(JGitDemoApplication.class, args);
    }

}

注意/问题

问题一

由于Github不支持rsa1加密的key,且jsch不能够解析高版本ssh,所以ssh的生成必须使用以下命令

ssh-keygen -t ecdsa -m PEM

见博客:https://www.cnblogs.com/jjjhs/p/17503077.html

问题二

由于github不支持远程密码登陆,所以需要使用token,先在github中生成token令牌,复制并将其设置为git密码

git config --global user.password "token"