使用GitLabApi获取远程仓库中的文件内容

在一些实际情况中,希望能够直接像读取本地文件一样读取远程仓库中的文件内容,避免git操作失败的情况下读取的本地缓存的文件内容。由于项目使用gitLab管理配置文件,查询了GitLabApi,其提供了诸多API接口,包括常见的git操作、项目管理以及我们需要的获取文件内容等接口。

1.接口分析

查询GitLab api,可以容易找到获取文件内容的API文档: GitLab获取仓库中文件内容 ,可以发现,其格式要求为:

GET /projects/:id/repository/files/:file_path
curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fmodels%2Fkey%2Erb?ref=master'

通过分析可以发现,如果想获取仓库中文件内容,需要以下几个要素:

  • 用户的private token
  • 经过url编码的文件全路径
  • 文件所在的分支
    由此,我在实现时将API接口整理为一个常量:
  • private static String GITLAB_FILECONTENT_API = "http://#{REPO_IP}/api/v3/projects/#{PROJECT_ID}/repository/files?private_token=#{PRIVATE_TOKEN}&file_path=#{FILE_PATH}&ref=#{BRANCH_NAME}";
    

    2.获取用户的private token

    GitLabApi关于获取个人口令的API可以整理为常量类:

    private static String GITLAB_SESSION_API = "http://#{REPO_IP}/api/v3/session?login=#{USER_NAME}&password=#{PASSWORD}";
    

    提供如下3个变量即可获取指定用户的token:

    经过简单的json解析就能获取到结果,代码如下: * 根据用户名称和密码获取gitlab的private token,为Post请求 * @param ip gitlab仓库的ip * @param userName 登陆gitlab的用户名 * @param password 登陆gitlab的密码 * @return 返回该用户的private token public static String getPrivateTokenByPassword(String ip, String userName, String password) { /** 1.参数替换,生成获取指定用户privateToken地址 */ // 校验参数 Objects.requireNonNull(ip, "参数ip不能为空!"); Objects.requireNonNull(userName, "参数userName不能为空!"); Objects.requireNonNull(password, "参数password不能为空!"); // 参数准备,存入map Map<String, String> params = new HashMap<String, String>(4); params.put("REPO_IP", ip); params.put("USER_NAME", userName); params.put("PASSWORD", password); // 调用工具类替换,得到具体的调用地址 String reqUserTokenUrl = PlaceholderUtil.anotherReplace(GITLAB_SESSION_API, params); sysLogger.debug(String.format("获取用户:%s的private token地址为:%s", userName,reqUserTokenUrl)); /** 2.访问url,获取指定用户的信息 */ RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String> response = restTemplate.postForEntity(reqUserTokenUrl, null,String.class); sysLogger.debug(String.format("响应头为:%s,响应体为:%s", response.getHeaders(), response.getBody())); /** 3.解析结果 */ String body = response.getBody(); JSONObject jsonBody = JsonUtil.parseObjectToJSONObject(body); String privateToken =jsonBody.getString("private_token"); sysLogger.debug(String.format("获取到用户:%s的privateToken为:%s", userName, privateToken)); /** 4.返回privateToken */ return privateToken;

    注意,这个接口时POST请求,正确的HTTP响应码是201,而非200.

    3.获取项目的projectId

    projectId可以直接到gitlab具体项目信息中查找,但是也有相关的api可以依据项目名称查询。从使用的角度讲,配置项目的名称更加方便,配置项目的projectId增加配置项,且如果提供获取projectId的方法,内部调用可以去掉该项的配置。
    获取projectId的api参考:获取指定项目的projectId,需要如下3个要素:

  • 项目id:The ID or URL-encoded path of the project,即提供id或者是项目path,需要经url编码(namespace + projectName),参见:项目path的url编码
  • private token
    提取出常量类:
  • private static String GITLAB_SINGLE_PROJECT_API = "http://#{REPO_IP}/api/v3/projects/#{PROJECT_PATH}?private_token=#{PRIVATE_TOKEN}";
    

    然后对响应码为200的正确json返回结果信息解析即可得到,代码如下:

    * 使用gitLab api获取指定项目的projectId,为Get请求 * @param ip 项目仓库的ip地址 * @param projectPath 项目的path,如:http://192.168.59.185/acountting/dispatcher-cloud.git,则projectPath为:acountting/dispatcher-cloud * @param privateToken 用户个人访问gitlab库时的privateToken,可以通过{@link GitLabAPIUtils#getPrivateTokenByPassword}获取 * @return 返回目的projectId public static String getProjectId(String ip, String projectPath, String privateToken) { /** 1.参数替换,生成访问获取project信息的uri地址 */ // 校验参数 Objects.requireNonNull(ip, "参数ip不能为空!"); Objects.requireNonNull(projectPath, "参数projectPath不能为空!"); Objects.requireNonNull(privateToken, "参数privateToken不能为空!"); // 参数准备,存入map Map<String, String> params = new HashMap<String, String>(4); params.put("REPO_IP", ip); params.put("PRIVATE_TOKEN", privateToken); // gitlab api要求项目的path需要安装uri编码格式进行编码,比如"/"编码为"%2F" try { params.put("PROJECT_PATH", URLEncoder.encode(projectPath, "UTF-8")); } catch (UnsupportedEncodingException e) { throw new RuntimeException(String.format("对%s进行URI编码出错!", projectPath)); // 调用工具类替换,得到具体的调用地址 String getSingleProjectUrl = PlaceholderUtil.anotherReplace(GITLAB_SINGLE_PROJECT_API, params); sysLogger.debug(String.format("获取projectId的url:%s", getSingleProjectUrl)); // 创建URI对象 URI url = null; try { url = new URI(getSingleProjectUrl); } catch (URISyntaxException e) { throw new RuntimeException(String.format("使用%s创建URI出错!", getSingleProjectUrl)); /** 2.访问url,获取制定project的信息 */ RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String> reslut = restTemplate.getForEntity(url, String.class); sysLogger.debug(String.format("响应头为:%s,响应体为:%s", reslut.getHeaders(), reslut.getBody())); /** 3.解析结果 */ if (reslut.getStatusCode() != HttpStatus.OK ) { throw new RuntimeException(String.format("请求%s出错!错误码为:%s", url, reslut.getStatusCode())); // 如果响应码是200,说明正常拿到响应结果,解析出projectId返回即可 JSONObject responBody = JsonUtil.parseObjectToJSONObject(reslut.getBody()); String projectRepo = responBody.getString("http_url_to_repo"); String projectId = responBody.getString("id"); sysLogger.info(String.format("获取到项目:%s的projectId为:%s", projectRepo, projectId)); /** 4.返回projectId */ return projectId;

    需要特别注意,在方法中对配置好的project path进行url编码后,没有直接使用RestTemplate创建get请求获取项目信息,因为实践中发现会出现将本义编码好的如:src%2FHelloWorld.java变为:src%256FHelloWorld.java,具体没有深入RestTemplate源码,所以直接创建URI对象,避免这种情况出现。

    4 获取仓库文件内容

    api参考:gitlab获取仓库文件内容,需要提供3个参数:

  • private token
  • projectId
  • 文件全路径,需经过url编码,如: main%2Fclass%HelloWorld.java
  • 文件所在分支branch
    整理出具体api的常量类:
  • private static String GITLAB_FILECONTENT_API = "http://#{REPO_IP}/api/v3/projects/#{PROJECT_ID}/repository/files?private_token=#{PRIVATE_TOKEN}&file_path=#{FILE_PATH}&ref=#{BRANCH_NAME}";
    

    返回的内容json格式如下:

    "file_name": "HelloWorld.java", "file_path": "main/class/HelloWorld.java", "size": 1476, "encoding": "base64", "content": "IyA9PSBTY2hlbWEgSW5mb3...", "content_sha256": "4c294617b60715c1d218e61164a3abd4808a4284cbc30e6728a01ad9aada4481", "ref": "master", "blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83", "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50", "last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"

    特别需要注意的是返回的文件内容是base64编码,拿到结果后需要解码才能获取原始内容。如果想直接获取原始文件内容:获取原始文件内容
    所以获取方法如下:

    public static String getFileContentFromRepository(String ip,String projectPath,String userName,String password,
                String fileFullPath, String branchName) throws Exception {
            //  校验参数
            Objects.requireNonNull(ip, "参数ip不能为空!");
            Objects.requireNonNull(projectPath, "参数projectPath不能为空!");
            Objects.requireNonNull(userName, "参数userName不能为空!");
            Objects.requireNonNull(password, "参数password不能为空!");
            Objects.requireNonNull(fileFullPath, "参数fileFullPath不能为空!");
            Objects.requireNonNull(branchName, "参数branchName不能为空!");
            /** 1.依据用户名、密码获取到用户的privateToken */
            String privateToken = getPrivateTokenByPassword(ip, userName, password);
            /** 2.使用privateToken获取项目的projectId */
            String projectId = getProjectId(ip, projectPath, privateToken);
            /** 3.使用参数替换形成请求git库中文件内容的uri */
            //  参数准备,存入map
            Map<String, String> params = new HashMap<String, String>(4);
            params.put("REPO_IP", ip);
            params.put("PROJECT_ID", projectId);    
            params.put("PRIVATE_TOKEN", privateToken);  
            params.put("FILE_PATH", fileFullPath);      
            params.put("BRANCH_NAME", branchName);  
            //  使用工具类替换参数
            String reqFileCotnetUri = PlaceholderUtil.anotherReplace(GITLAB_FILECONTENT_API, params);
            sysLogger.debug(String.format("获取文件:%s的uri:%s", fileFullPath, reqFileCotnetUri));       
            /** 4.请求gitlab获取文件内容 */
            RestTemplate restTemplate = new RestTemplate();
            ResponseEntity<String> response = restTemplate.getForEntity(reqFileCotnetUri, String.class);
            sysLogger.debug(String.format("响应头为:%s,响应体为:%s", response.getHeaders(), response.getBody()));
            /** 5.解析响应结果内容 */
            String body = response.getBody();
            JSONObject jsonBody = JsonUtil.parseObjectToJSONObject(body);
            String fileName = jsonBody.getString("file_name");
            String filePath = jsonBody.getString("file_path");
            String encoding = jsonBody.getString("encoding");
            String content = jsonBody.getString("content");
            String commitId = jsonBody.getString("commit_id");
            String lastCommitId = jsonBody.getString("last_commit_id");
            //  内容已经base64编码,如果需要获取原始文件内容可以参看api:https://docs.gitlab.com/ee/api/repository_files.html#get-raw-file-from-repository
            content = new String(Base64.decode(content), "UTF-8");
            sysLogger.debug(String.format(
                    "获取http://%s 上%s项目 %s分支的%s文件,响应信息是:fileName :%s ,filePath:%s , 编码:%s ,内容:%s , commitId:%s ,lastCommitId :%s",
                    ip,projectPath, branchName, fileFullPath, fileName, filePath, encoding, content, commitId,
                    lastCommitId));
            /** 6.返回指定文件的内容 */
            sysLogger.debug(String.format("解析得到文件内容为:%s", content));
            return content;
    

    大致经过如下过程:

  • 使用用户名、密码获取private token
  • 使用private token,project path获取projectId
  • 使用private token,projectId,结合file path、分支branch参数,获取文件base64编码内容,然后解码即可
  • 此外,考虑到项目中实际获取的是配置文件内容,为了剔除不必要的空行、注释行,提供了工具类方法对解码的原始文件内容进行处理:

    public static String getCleanFileContentFromRepository(String ip,String projectPath,String userName,String password,
                String fileFullPath, String branchName) throws Exception{
            /**  1.获取到原始文件内容 */
            String fileContent = getFileContentFromRepository(ip, projectPath, userName, password, fileFullPath, branchName);
            if (fileContent.isEmpty()) {
                return fileContent;
            /** 2.所有行转换为list,并过滤掉空行、#开头的注释行 */
            List<String> list = Arrays.asList(fileContent.split("\n")).stream().filter(a-> (!a.trim().isEmpty()&&!a.trim().startsWith("#"))).collect(Collectors.toList());
            /** 3.转为一整行字符串返回 */
            StringBuilder sb = new StringBuilder();
            int size =list.size();
            for (int i = 0; i < size; i++) {
                sb.append(list.get(i).trim());
            return sb.toString();
    

    5 其它相关

    5.1 替换参数工具类

    对#{}包裹的参数进行替换,代码如下:

    public class PlaceholderUtil {
        /** 默认替换形如#{param}的占位符 */
        private static Pattern pattern = Pattern.compile("\\#\\{.*?\\}");
         * 替换字符串中形如#{}的占位符
         * @param src
         * @param parameters
         * @return
        public static String replace(String src, Map<String, Object> parameters) {
            Matcher paraMatcher = pattern.matcher(src);
            // 存储参数名
            String paraName = "";
            String result = new String(src);
            while (paraMatcher.find()) {
                paraName = paraMatcher.group().replaceAll("\\#\\{", "").replaceAll("\\}", "");
                Object objParam = parameters.get(paraName);
                if(objParam!=null){
                    result = result.replace(paraMatcher.group(), objParam.toString());
            return result;
         * 替换字符串中形如#{}的占位符
         * @param src
         * @param parameters
         * @return
        public static String anotherReplace(String str, Map<String, String> params) {
            Map<String, Object> newParams = new HashMap<>(params);
            return replace(str, newParams);
    

    5.2 测试类:

    public class GitLabAPIUtilsTest {
        String repoIp; 
        String privateToken;
        String projectPath1;
        String projectPath2;
        String userName;
        String password;
        String fileFullPath;
        String branchName;
        @Before
        public void setUp() throws Exception {
            repoIp = "gitlab仓库ip";
            projectPath1 = "acountting/accounting-config-repo";
            projectPath2 = "acountting/csv-filefront-cloud";
            userName = "用户名";
            password="密码";
            fileFullPath = "/apps/cmup-clearing/0055-account.config";
            branchName = "develop";
        @After
        public void tearDown() throws Exception {
            repoIp = null;
            projectPath1  = null;
            projectPath2  = null;       
            userName = null;
            password = null;        
            fileFullPath = null;
            branchName = null;
        @Test
        public void testGetProjectId() {
            String privateToken = GitLabAPIUtils.getPrivateTokenByPassword(repoIp, userName, password);
            String projectId = GitLabAPIUtils.getProjectId(repoIp, projectPath1, privateToken);
            System.out.println("projectId = " + projectId);
        @Test
        public void testGetPrivateToken() {
            String privateToken = GitLabAPIUtils.getPrivateTokenByPassword(repoIp, userName, password);
            System.out.println("projectId = " + privateToken);
        @Test
        public void testGetFileContent() {
            String fileContent;
            try {
                fileContent = GitLabAPIUtils.getCleanFileContentFromRepository(repoIp, projectPath1, userName, password, fileFullPath, branchName);
                System.out.println("fileContent = " + fileContent);
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();