Gitlab镜像自动同步

GitLab镜像同步实践指南

背景

Gitlab代码服务器开发给第三方使用,为了保证内部资源安全,做了一个镜像gitlab,内部gitlab同步部分仓库代码到镜像gitlab,镜像gitlab开放外部网络

整体架构流程

本地仓库(以下简称gitlab1),镜像仓库(以下简称gitlab2)

Y
Y
N
Y
N
根group
判断是否有Projects
Creat project
设置mirror
上传临时文件触发push
校验commits
判断是否有SubGroup
Create group
End
检查原因后手动处理

技术名词解释

GitLab

GitLab 是一个基于 Git 的代码托管和协作平台,提供代码仓库管理、CI/CD(持续集成与持续交付)、问题跟踪等功能。支持自托管或云端部署,适用于团队协作开发。

Group

在 GitLab 中,Group 是组织项目和其他子组的容器,用于权限管理和团队协作。Group 可以嵌套,支持多层级结构,便于分部门或分模块管理代码仓库。

Project

Project 是 GitLab 中的基本代码仓库单元,包含代码文件、分支、提交记录等。每个 Project 归属于一个 Group 或用户,支持独立的权限设置、Wiki、Issue 跟踪等功能。

Mirror

Mirror 是 GitLab 的仓库镜像功能,允许将外部仓库(如 GitHub、Bitbucket)同步到 GitLab 中。支持单向(只拉取)或双向同步,便于统一管理分散的代码库。

Push

Push 是将本地代码变更上传到远程仓库的操作。例如,通过 git push origin main 将本地 main 分支的提交推送到远程仓库的 main 分支。

Branch

Branch 是代码仓库中的独立开发线,用于隔离不同功能或修复的开发。常见的分支策略包括 main(主分支)、feature(功能分支)和 hotfix(紧急修复分支)。

Commit

Commit 是代码变更的最小单位,包含一组文件的修改记录。每次提交需附带描述信息(Commit Message),用于追溯变更目的。提交哈希(如 a1b2c3d)唯一标识一个提交。


关键操作示例

推送分支到远程仓库
git push origin <branch-name>
克隆镜像仓库
git clone --mirror <repository-url>
查看提交历史
git log --oneline
创建新分支
git checkout -b <new-branch-name>

GitLab AccessToken 名词解释

AccessToken(访问令牌)是GitLab中用于身份验证和授权的凭证,允许用户或应用程序通过API或其他方式访问GitLab资源,而无需直接使用用户名和密码。以下是其核心特性:

功能与用途
  • API访问:用于程序化调用GitLab REST或GraphQL API,例如自动化脚本或第三方工具集成。
  • 替代密码:避免在代码或配置中硬编码敏感密码,提升安全性。
  • 权限控制:支持为不同用途创建独立令牌,并分配细粒度权限(如read_repositorywrite_repository)。
类型与区别
  • Personal Access Token:用户个人生成,用于自身账户的操作,权限由用户自定义。
  • Project Access Token:绑定到特定项目,以项目身份执行操作(需GitLab Premium及以上版本)。
  • Group Access Token:以组身份执行操作,适用于组级别自动化(需GitLab Premium及以上版本)。
创建与管理
  • 生成路径:User Settings -> Access Tokens(个人令牌)或项目/组的Settings -> Access Tokens
  • 生命周期:可设置过期时间,并随时撤销令牌。
  • 权限范围:需明确勾选apiread_repositorywrite_repository等所需权限。
安全实践
  • 保密性:令牌等同于密码,需存储在安全位置(如环境变量或密钥管理器)。
  • 最小权限原则:仅授予必要权限,避免使用sudo等高危权限。
  • 定期轮换:建议设置较短有效期并及时更新。
示例代码(使用令牌调用API)
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects"

注意:GitLab的免费版和付费版对令牌类型的支持可能不同,需根据实际版本选择合适类型。

技术细节

这里给出JAVA代码实现,基于maven项目,jdk1.8以上支持

pom.xml配置

			<dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>5.7.22</version>
            </dependency>
			<!-- fastjson  -->
			<dependency>
				<groupId>com.alibaba</groupId>
				<artifactId>fastjson</artifactId>
				<version>1.2.83</version>
			</dependency>
			<dependency>
				<groupId>org.projectlombok</groupId>
				<artifactId>lombok</artifactId>
				<optional>true</optional>
			</dependency>
			<dependency>
				<groupId>junit</groupId>
				<artifactId>junit</artifactId>
				<version>4.12</version>
			</dependency>

java代码实现,如下代码可以帮助你把gitlab1上指定group下所有的子group,project都在镜像上创建,并且配置好mirror地址,并且触发一次主动同步,并且比较最后一次提交记录是否一致。
正常需要运行2次,第一次创建group和project,设置mirror,触发push,第二次检查最后提交记录,若发现错误,需要去页面查看push error原因,手动处理

package com.gitlab;

import cn.hutool.http.HttpUtil;
import cn.hutool.http.Method;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import lombok.SneakyThrows;

import java.util.HashMap;
import java.util.Map;

/**
 * 应用场景
 * gitlab1的group project自动同步到gitlab2,gitlab2开放为外部可访问环境
 * 建议局域网环境下,大文件传输更稳定
 */
public class DemoGitlabMirror {

    /**
     * gitlab1 ip: 192.168.0.1 port:7800
     * gitlab2 ip: 192.168.0.2 port:7800
     * 这里gitlab是15.11版本
     */
    @SneakyThrows
    @org.junit.Test
    public void partGroupsCreate(){
        //gitlab1的根目录groupId
        int sepcGroupId = 5;
        //gitlab2的根目录groupId
        int destGroupId = 6;
        //gitlab1的subgroup api
        String group1 = "http://192.168.0.1:7800/api/v4/groups/:id/subgroups";
        //gitlab1的token,页面新建
        String accessToken1 = "glpat-111111";
        //gitlab2的groups api
        String group2 = "http://192.168.0.2:7800/api/v4/groups/";
        //gitlab2的token,页面新建
        String accessToken2 = "glpat-2222";

        //gitlab2的subgroup api
        String group2Subgroup = "http://192.168.0.2:7800/api/v4/groups/:id/subgroups";

        //gitlab1的指定groupid下的所有projects api
        String project1 = "http://192.168.0.1:7800/api/v4/groups/:id/projects/";
        //gitlab2的所有projects api
        String project2 = "http://192.168.0.2:7800/api/v4/projects/";

        //gitlab1的指定project的mirror api
        String remote_mirrors = "http://192.168.0.1:7800/api/v4/projects/:id/remote_mirrors";
        //gitlab1的mirror配置的url 这里是root和密码
        String targetMirrorUrlPre = "http://root:333333=@192.168.0.2:7800";
        //gitlab1的mirror配置中的替换ulr
        String targetReplaceUrl = "http://192.168.0.1:7800";

        //gitlab1的指定project的提交记录 api
        String targetCommits1 = "http://192.168.0.1:7800/api/v4/projects/:id/repository/commits";

        //是否在gitlab2创建group
        boolean isAddGroup = false;
        //是否在gitlab2创建project
        boolean isAddProject = false;

        //gitlab1的projects api
        String project1Projects = "http://192.168.0.1:7800/api/v4/projects/";

        //gitlab2的指定groupId下所有projects api
        String project2WithSubGroup = "http://192.168.0.2:7800/api/v4/groups/:id/projects/";

        //#1 获取源gitlab上groups,在目标gitlab上批量新建group
        gitLabPointGroups(sepcGroupId, group1, accessToken1, group2, accessToken2, destGroupId,
                project1, project2, remote_mirrors, targetMirrorUrlPre,
                targetReplaceUrl, group2Subgroup, targetCommits1, isAddGroup,
                isAddProject, project1Projects, project2WithSubGroup);
    }

    /**
     * 指定源groupid 目标groupid,批量新建groups projects
     * @param sepcGroupId
     * @param group1
     * @param accessToken1
     * @param group2
     * @param accessToken2
     * @param destGroupId
     * @param project1
     * @param project2
     * @param remote_mirrors
     * @param targetMirrorUrlPre
     * @param targetReplaceUrl
     * @param group2Subgroup
     * @param targetCommits1
     * @param isAddGroup
     * @param isAddProject
     * @param project1Projects
     * @param project2WithSubGroup
     */
    @SneakyThrows
    private void gitLabPointGroups(int sepcGroupId, String group1, String accessToken1, String group2,
                                   String accessToken2, int destGroupId, String project1, String project2,
                                   String remote_mirrors, String targetMirrorUrlPre, String targetReplaceUrl,
                                   String group2Subgroup, String targetCommits1, boolean isAddGroup,
                                   boolean isAddProject, String project1Projects, String project2WithSubGroup)
    {
        // #2 目标gitlab 新建project
        HashMap<String, String> headers3 = new HashMap<>();//存放请求头,可以存放多个请求头
        headers3.put("PRIVATE-TOKEN", accessToken1);
        //发送get请求并接收响应数据
        int page3 = 1;
        String url3 = project1.replace(":id", sepcGroupId+"");
        while (true) {
            Map<String, Object> map3 = new HashMap<>();//存放参数
            map3.put("order_by", "id");
            map3.put("sort", "desc");
            map3.put("page", page3);
            map3.put("per_page", 100);
            String result3 = HttpUtil.createGet(url3).addHeaders(headers3).form(map3).execute().body();
            //System.out.println(result3);
            JSONArray jsonArray3 = JSONArray.parseArray(result3);
            source_projectCount += jsonArray3.size();
            if(jsonArray3.size() == 0) {
                break;
            }
            for (int i1 = 0; i1 < jsonArray3.size(); i1++) {
                JSONObject jsonObject1 = jsonArray3.getJSONObject(i1);
                int proid = jsonObject1.getIntValue("id");
                
                if(isAddProject) {
                    HashMap<String, String> headers4 = new HashMap<>();//存放请求头,可以存放多个请求头
                    headers4.put("PRIVATE-TOKEN", accessToken2);
                    Map<String, Object> map4 = new HashMap<>();//存放参数
                    map4.put("name", jsonObject1.getString("name"));
                    map4.put("description", jsonObject1.getString("description"));
                    map4.put("path", jsonObject1.getString("path"));
                    JSONObject namespace = jsonObject1.getJSONObject("namespace");
                    String full_path = jsonObject1.getString("full_path");
                    map4.put("namespace_id", destGroupId);
                    map4.put("initialize_with_readme", true);
                    String result4 = HttpUtil.createPost(project2).addHeaders(headers4).form(map4).execute().body();
                    dest_projectCount++;
                    //System.out.println(result4);
                    String path_with_namespace = jsonObject1.getString("path_with_namespace");
                    sourceProjectSet.add(path_with_namespace);

                    // #3 设置mirror
                    String remote_mirrors_url = remote_mirrors.replace(":id", jsonObject1.getIntValue("id")+"");
                    HashMap<String, String> headers5 = new HashMap<>();//存放请求头,可以存放多个请求头
                    headers5.put("PRIVATE-TOKEN", accessToken1);
                    Map<String, Object> map5 = new HashMap<>();//存放参数
                    String http_url_to_repo = jsonObject1.getString("http_url_to_repo");
                    http_url_to_repo = http_url_to_repo.replace(targetReplaceUrl,"");
                    String targetUrl = targetMirrorUrlPre + http_url_to_repo;
                    map5.put("url", targetUrl);
                    map5.put("enabled", true);
                    String result5 = HttpUtil.createPost(remote_mirrors_url).addHeaders(headers5).form(map5).execute().body();
                    //System.out.println(result5);
                }

                int targetProjectId = 0;
                String targetBranch = "master";
                HashMap<String, String> headers9 = new HashMap<>();//存放请求头,可以存放多个请求头
                headers9.put("PRIVATE-TOKEN", accessToken2);
                String url9 = project2WithSubGroup.replace(":id", destGroupId+"");
                int page9 = 1;
                out:while (true) {
                    Map<String, Object> map9 = new HashMap<>();//存放参数
                    map9.put("order_by", "id");
                    map9.put("sort", "desc");
                    map9.put("page", page9);
                    map9.put("per_page", 100);
                    String result9 = HttpUtil.createGet(url9).addHeaders(headers9).form(map9).execute().body();
                    JSONArray jsonArray9 = JSONArray.parseArray(result9);
                    if(jsonArray9.size() == 0){
                        break out;
                    }
                    for (int i = 0; i < jsonArray9.size(); i++) {
                        JSONObject jsonObject = jsonArray9.getJSONObject(i);
                        String name = jsonObject.getString("name");
                        if (name.equals(jsonObject1.getString("name"))) {
                            targetProjectId = jsonObject.getInteger("id");
                            targetBranch = jsonObject.getString("default_branch");
                            break out;
                        }
                    }
                    page9++;
                }

                // #4 设置master 发现暂不处理

                // #5 比较最后一次commit,不一致触发push
                String default_branch = jsonObject1.getString("default_branch");
                String commmit1 = getCommitId(accessToken1, project1Projects, jsonObject1.getIntValue("id"), default_branch);
                String commmit2 = getCommitId(accessToken2, project2, targetProjectId, default_branch);
                if(null == commmit1 || !commmit1.equals(commmit2)) {
                    System.out.println(jsonObject1.getString("http_url_to_repo"));

                    // #6 主动触发push
                    HashMap<String, String> headers6 = new HashMap<>();//存放请求头,可以存放多个请求头
                    headers6.put("PRIVATE-TOKEN", accessToken1);
                    headers6.put("Content-Type", "application/json");
                    JSONObject body = new JSONObject();
                    body.put("branch",default_branch);
                    body.put("commit_message","some commit message");
                    JSONObject action = new JSONObject();
                    //action.put("action","create");
                    action.put("action","update");
                    action.put("file_path","test.log");
                    action.put("content","");
                    JSONArray actions = new JSONArray();
                    actions.add(action);
                    body.put("actions", actions);
                    String url6 = targetCommits1.replace(":id", jsonObject1.getIntValue("id")+"");
                    String result6 = HttpUtil.createPost(url6).addHeaders(headers6).body(body.toJSONString()).execute().body();
                    JSONObject jsonObject = null;
                    try {
                        jsonObject = JSONObject.parseObject(result6);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    if(null != jsonObject) {
                        String error = jsonObject.getString("error");
                        if (null != error && error.startsWith("404")) {
                            System.out.println(jsonObject1.getString("http_url_to_repo"));
                        }
                    }
                    //System.out.println(result6);
                    //Thread.sleep(600000L);
                }
            }
            page3++;
        }

        String url = group1.replace(":id", sepcGroupId+"");
        HashMap<String, String> headers = new HashMap<>();//存放请求头,可以存放多个请求头
        headers.put("PRIVATE-TOKEN", accessToken1);
        //发送get请求并接收响应数据
        //JSONArray jsonArrayAll = new JSONArray();
        int page = 1;
        while (true) {
            Map<String, Object> map = new HashMap<>();//存放参数
            map.put("order_by", "id");
            map.put("sort", "asc");
            map.put("page", page);
            map.put("per_page", 100);
            String result = HttpUtil.createGet(url).addHeaders(headers).form(map).execute().body();
            JSONArray jsonArray = JSONArray.parseArray(result);
            source_groupCount += jsonArray.size();
            if(jsonArray.size() == 0) {
                break;
            }else{
                for (int i = 0; i < jsonArray.size(); i++) {
                    JSONObject jsonObject = jsonArray.getJSONObject(i);
                    int sourceGroupId = jsonObject.getIntValue("id");

                    int destGroupId2 = 0;
                    if(isAddGroup) {
                        // #1 目标gitlab 新建group
                        HashMap<String, String> headers2 = new HashMap<>();//存放请求头,可以存放多个请求头
                        headers2.put("PRIVATE-TOKEN", accessToken2);
                        Map<String, Object> map2 = new HashMap<>();//存放参数
                        map2.put("name", jsonObject.getString("name"));
                        map2.put("path", jsonObject.getString("path"));
                        map2.put("description", jsonObject.getString("description"));
                        map2.put("parent_id", destGroupId);
                        String result2 = HttpUtil.createPost(group2).addHeaders(headers2).form(map2).execute().body();
                        JSONObject destGroupJsonObj = JSONObject.parseObject(result2);
                        destGroupId2 = destGroupJsonObj.getIntValue("id");

                    }
                    //已新建,根据名称获取
                    if(destGroupId2 == 0) {
                        String url7 = group2Subgroup.replace(":id", destGroupId+"");
                        HashMap<String, String> headers7 = new HashMap<>();//存放请求头,可以存放多个请求头
                        headers7.put("PRIVATE-TOKEN", accessToken2);
                        Map<String, Object> map7 = new HashMap<>();//存放参数
                        map7.put("search", jsonObject.getString("name"));
                        String result7 = HttpUtil.createGet(url7).addHeaders(headers7).form(map7).execute().body();
                        JSONArray jsonArray1 = JSONArray.parseArray(result7);
                        for (int i1 = 0; i1 < jsonArray1.size(); i1++) {
                            JSONObject jsonObject1 = jsonArray1.getJSONObject(i1);
                            int parent_id = jsonObject1.getIntValue("parent_id");
                            if(parent_id == destGroupId) {
                                destGroupId2 = jsonObject1.getIntValue("id");
                                dest_groupCount++;
                                break;
                            }
                        }
                    }
                    //System.out.println(result2);
                    gitLabPointGroups(sourceGroupId, group1, accessToken1, group2, accessToken2, destGroupId2, project1, project2,
                            remote_mirrors, targetMirrorUrlPre, targetReplaceUrl, group2Subgroup, targetCommits1,
                            isAddGroup, isAddProject, project1Projects, project2WithSubGroup);
                }

                //jsonArrayAll.addAll(jsonArray);
                page++;
            }
        }
    }
	
    private static String getCommitId(String accessToken2, String project2, int id, String default_branch) {
        String url8 = project2 + id + "/repository/commits/"+default_branch;
        HashMap<String, String> headers8 = new HashMap<>();//存放请求头,可以存放多个请求头
        headers8.put("PRIVATE-TOKEN", accessToken2);
        String result8 = HttpUtil.createRequest(Method.GET, url8).addHeaders(headers8).execute().body();
        JSONObject jsonObject8 = JSONObject.parseObject(result8);
        String tarCommitId = null;
        if(null != jsonObject8) {
            tarCommitId = jsonObject8.getString("id");
        }
        return tarCommitId;
    }
}

踩坑实践

  1. mirror push报错
13:push to mirror: git push: exit status 1, stderr: "remote: GitLab: LFS objects are missing. Ensure LFS is properly set up or try a manual \"git lfs push --all\".\nTo http://192.168.0.2:7800/xx.git\n ! [remote rejected] master -> master (pre-receive hook declined)\nerror: failed to push some refs to 'http://192.168.0.2:7800/xx.git'\n".

解决:去镜像gitlab页面去掉LFS
关闭LFS

  1. gitlab mirror respiratory配置错误
13:get remote references: create git ls-remote: exit status 128, stderr: "ssh Could not resolve hostname root:xxx: Name or service not known\r\nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\nand the repository exists.\n".

解决:mirror地址配置错误,可以如上提供的代码执行一次即可

  1. mirror push 页面error报错
13:push to mirror: git push: exit status 1, stderr: "remote: GitLab: You are not allowed to force push code to a protected branch on this project.\nTo http://192.168.0.2:7800/xx.git\n ! [remote rejected] master -> master (pre-receive hook declined)\n ! [remote rejected] main -> main (pre-receive hook declined)\nerror: failed to push some refs to 'http://192.168.0.2:7800/xx.git'\n".

解决:去镜像gitlab页面去掉protected保护
解除保护

  1. mirror push页面error错误
badFilemode: contains bad file modes \nremote: warning: object 8ff31038cc910
13:push to mirror: git push: exit status 1, stderr: "error: RPC failed; HTTP 500 curl 22 The requested URL returned error: 500\nsend-pack: unexpected disconnect while reading sideband packet\nfatal: the remote end hung up unexpectedly\nEverything up-to-date\n".

以上错误检查如下几点

  1. gitlab1 gitlab2是否正常运行
  2. gitlab1 gitlab2上磁盘空间是否满了
  3. 频繁push大数据导致push超时,可以在手动推送或者上述推送代码部分加等待时间

参考链接

https://archives.docs.gitlab.com/15.11/ee/api/rest/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瑞瑞绮绮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值