摘要
- 本文档介绍 GitLab REST API 的 v3 与 v4 差异,并结合本仓库迁移工具的实际调用场景说明:如何判断版本、常见接口对比、v4 独有能力,以及排障要点。
- 官方背景:GitLab 自 9.0 起引入并推荐 API v4;9.5 起 v3 停止支持;11.0 起 v3 被完全移除。现代实例(11.0+)只能使用 v4。
版本生命周期
| GitLab 版本 |
API v3 |
API v4 |
| 8.x |
默认 API |
不可用 |
| 9.0 |
仍可用,但不推荐 |
引入并推荐使用 |
| 9.5(2017-08-22) |
停止支持(Unsupported) |
推荐使用 |
| 11.0+ |
已删除(Removed) |
唯一可用 REST API |
如何判断实例使用 v3 还是 v4
在浏览器或终端分别探测:
1 2 3 4 5 6 7
| curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/version" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v3/version" | jq .
|
能返回 JSON(含 version 字段)的路径,即为该实例应使用的 API 版本。
共同基础
下文示例统一使用以下变量,复制前请替换为你的实际值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| GITLAB="https://gitlab.example.com" TOKEN="your_personal_access_token"
GROUP="android" PROJECT="tool" PROJECT_PATH="android/tool" USER_PROJECT_PATH="hanqunfeng/wifitest" USERNAME="hanqunfeng" USER_ID="42" NAMESPACE_ID="123"
ENCODED_PATH=$(jq -nr --arg v "$PROJECT_PATH" '$v|@uri') ENCODED_USER_PATH=$(jq -nr --arg v "$USER_PROJECT_PATH" '$v|@uri')
|
基础 URL 格式
1 2
| https://<gitlab-host>/api/v3/<endpoint> # 旧版 https://<gitlab-host>/api/v4/<endpoint> # 现代
|
认证方式(v3 / v4 相同)
新版本使用 Personal Access Token(PAT),老版本使用 Private Token,通过请求头传递:
1 2 3 4 5 6 7
| curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects?per_page=10" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v3/projects?per_page=10" | jq .
|
创建Personal Access Token(PAT)时分配的常见权限(迁移相关), Private Token 拥有全部权限:
| 操作 |
建议 scope |
| 读取项目/用户/成员 |
api 或 read_api |
| 创建 Group/Project/用户 |
api |
| git clone |
read_repository |
| git push |
write_repository |
路径编码(v3 / v4 均需注意)
项目路径 group/subgroup/project 中的 / 必须 URL 编码为 %2F:
1 2 3 4 5
| /api/v4/projects/group/subgroup/project
/api/v4/projects/group%2Fsubgroup%2Fproject
|
bash 中可用 jq 编码:
1 2 3 4
| ENCODED_PATH=$(jq -nr --arg v "$PROJECT_PATH" '$v|@uri')
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects/${ENCODED_PATH}" | jq .
|
分页(基本相同)
1 2 3 4 5 6 7
| curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects?per_page=100&page=1"
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v3/projects?per_page=100&page=1"
|
v3 部分旧端点可能忽略 page 参数或分页行为不一致;本仓库在拉取 members 时会检测“不足一页即停止”,避免死循环。
迁移相关接口对比
以下按本仓库脚本实际用到的 API 进行对比。
1)获取版本信息
| 项目 |
v3 |
v4 |
| 端点 |
GET /api/v3/version |
GET /api/v4/version |
| 用途 |
探测 API 是否可用 |
探测 API 是否可用 |
1 2 3 4 5 6 7
| curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/version" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v3/version" | jq .
|
2)项目列表
| 项目 |
v3 |
v4 |
| 端点 |
GET /api/v3/projects |
GET /api/v4/projects |
| 分页 |
per_page + page |
per_page + page |
| 简化字段 |
simple=true |
simple=true |
namespace 类型识别差异(重要):
| 字段 |
v3 |
v4 |
namespace.kind |
通常不存在 |
存在:group / user |
| 推断个人项目 |
看 namespace.owner_id 是否有值 |
优先用 namespace.kind == "user" |
本仓库步骤 1 的逻辑:
1 2 3 4 5 6 7 8
| if namespace.kind kind = namespace.kind elsif namespace.owner_id == null kind = "group" else kind = "user" end
|
1 2 3 4 5 6 7 8 9 10 11 12
| curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects?per_page=100&page=1&simple=true" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v3/projects?per_page=100&page=1&simple=true" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects?per_page=100&page=1&simple=true" \ | jq '.[] | {path, namespace: {path: .namespace.path, kind: .namespace.kind, owner_id: .namespace.owner_id}}'
|
3)获取单个项目
| 项目 |
v3 |
v4 |
| 端点 |
GET /api/v3/projects/:id_or_encoded_path |
GET /api/v4/projects/:id_or_encoded_path |
| 路径编码 |
需要 |
需要 |
v4 返回的 namespace 信息更完整(含 kind),便于区分 Group 项目与个人项目。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects/${ENCODED_PATH}" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects/${ENCODED_USER_PATH}" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects/123" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v3/projects/${ENCODED_PATH}" | jq .
|
4)Group 管理
| 操作 |
v3 |
v4 |
| 搜索 Group |
GET /api/v3/groups?search=<name> |
GET /api/v4/groups?search=<name> |
| 获取 Group |
GET /api/v3/groups/:id_or_path |
GET /api/v4/groups/:id_or_path |
| 创建 Group |
POST /api/v3/groups |
POST /api/v4/groups |
创建参数(本仓库使用):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/groups?search=$GROUP" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/groups/$GROUP" | jq '{id, name, path, full_path}'
curl -s --request POST \ --header "PRIVATE-TOKEN: $TOKEN" \ --data "name=$GROUP&path=$GROUP" \ "$GITLAB/api/v4/groups" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v3/groups?search=$GROUP" | jq .
curl -s --request POST \ --header "PRIVATE-TOKEN: $TOKEN" \ --data "name=$GROUP&path=$GROUP" \ "$GITLAB/api/v3/groups" | jq .
|
5)Project 管理
| 操作 |
v3 |
v4 |
| 检查是否存在 |
GET /api/v3/projects/:encoded_path |
GET /api/v4/projects/:encoded_path |
| 创建 Project |
POST /api/v3/projects |
POST /api/v4/projects |
创建参数(本仓库使用):
1
| name=<project>&namespace_id=<namespace_id>
|
v4 通过 namespace_id 明确指定项目归属(Group 或用户个人命名空间),是本仓库创建个人项目的关键。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| curl -s -o /dev/null -w "%{http_code}\n" \ --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects/${ENCODED_PATH}"
NAMESPACE_ID=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/groups/$GROUP" | jq -r '.id')
curl -s --request POST \ --header "PRIVATE-TOKEN: $TOKEN" \ --data "name=$PROJECT&namespace_id=$NAMESPACE_ID" \ "$GITLAB/api/v4/projects" | jq .
USER_NAMESPACE_ID=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/namespaces?search=$USERNAME" \ | jq -r --arg u "$USERNAME" '[.[] | select(.path == $u and .kind == "user") | .id] | first')
curl -s --request POST \ --header "PRIVATE-TOKEN: $TOKEN" \ --data "name=wifitest&namespace_id=$USER_NAMESPACE_ID" \ "$GITLAB/api/v4/projects" | jq .
curl -s -o /dev/null -w "%{http_code}\n" \ --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v3/projects/${ENCODED_PATH}"
curl -s --request POST \ --header "PRIVATE-TOKEN: $TOKEN" \ --data "name=$PROJECT&namespace_id=$NAMESPACE_ID" \ "$GITLAB/api/v3/projects" | jq .
|
6)用户管理
| 操作 |
v3 |
v4 |
| 按 username 查找 |
GET /api/v3/users?username=<name> |
GET /api/v4/users?username=<name> |
| 按 search 查找 |
GET /api/v3/users?search=<q> |
GET /api/v4/users?search=<q> |
| 获取用户详情 |
GET /api/v3/users/:id |
GET /api/v4/users/:id |
| 创建用户 |
POST /api/v3/users |
POST /api/v4/users |
创建用户时本仓库使用的 v4 参数:
1
| email=...&username=...&name=...&reset_password=true&skip_confirmation=true
|
| 参数 |
v3 |
v4 |
说明 |
skip_confirmation |
可能不支持或行为不同 |
支持 |
跳过邮箱确认,便于批量迁移 |
reset_password |
视版本而定 |
支持 |
发送密码重置邮件 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/users?username=$USERNAME" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/users?search=han@example.com" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/users/$USER_ID" | jq .
curl -s --request POST \ --header "PRIVATE-TOKEN: $TOKEN" \ --data "email=han@example.com&username=$USERNAME&name=Han%20Qunfeng&reset_password=true&skip_confirmation=true" \ "$GITLAB/api/v4/users" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v3/users?username=$USERNAME" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v3/users/$USER_ID" | jq .
curl -s --request POST \ --header "PRIVATE-TOKEN: $TOKEN" \ --data "email=han@example.com&username=$USERNAME&name=Han%20Qunfeng&password=TempPass123&confirmed_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ "$GITLAB/api/v3/users" | jq .
|
7)成员(Members)——差异最大
| 端点 |
v3 |
v4 |
返回范围 |
.../members |
支持 |
支持 |
仅直接成员 |
.../members/all |
不存在 |
支持 |
直接成员 + 继承成员 |
适用资源:
对本仓库迁移的影响:
| 场景 |
v3 做法 |
v4 做法 |
| Group 继承成员 |
必须单独拉 Group members;项目 members 拿不到继承权限 |
可用 members/all 一次拿到继承成员 |
| 个人项目 members 为空 |
正常;权限在 owner 上,需从 project.owner / namespace.owner_id 收集 |
同样可能为空;也可用 owner 信息补充 |
本仓库用户步骤 1 的策略:
access_level 常用值:10=Guest,20=Reporter,30=Developer,40=Maintainer,50=Owner
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/groups/$GROUP/members?per_page=100&page=1" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/groups/$GROUP/members/all?per_page=100&page=1" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v3/groups/$GROUP/members?per_page=100&page=1" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects/${ENCODED_PATH}/members?per_page=100&page=1" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects/${ENCODED_PATH}/members/all?per_page=100&page=1" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v3/projects/${ENCODED_PATH}/members?per_page=100&page=1" | jq .
curl -s --request POST \ --header "PRIVATE-TOKEN: $TOKEN" \ --data "user_id=$USER_ID&access_level=30" \ "$GITLAB/api/v4/groups/$GROUP/members" | jq .
curl -s --request POST \ --header "PRIVATE-TOKEN: $TOKEN" \ --data "user_id=$USER_ID&access_level=40" \ "$GITLAB/api/v4/projects/${ENCODED_PATH}/members" | jq .
curl -s --request PUT \ --header "PRIVATE-TOKEN: $TOKEN" \ --data "access_level=40" \ "$GITLAB/api/v4/projects/${ENCODED_PATH}/members/$USER_ID" | jq .
|
8)Namespaces(v4 独有,迁移关键)
| 项目 |
v3 |
v4 |
| 端点 |
无独立 Namespaces API |
GET /api/v4/namespaces |
| 搜索 |
— |
?search=<username> |
返回 kind |
— |
group / user |
本仓库用 v4 /namespaces 解析用户个人 namespace_id,以便在个人命名空间下创建项目:
1 2 3 4 5 6 7 8 9
| curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/namespaces?search=$USERNAME" \ | jq --arg u "$USERNAME" '[.[] | select(.path == $u and .kind == "user")]'
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/users?username=$USERNAME" \ | jq '.[0] | {id, username, namespace_id}'
|
回退方案:GET /api/v4/users/:id 读取 namespace_id 字段。
v4 独有能力概览
除上述迁移相关差异外,v4 相对 v3 还引入/完善了大量能力。以下按类别列举,并给出 v4 完整 curl 示例(v3 均不可用或极不完整)。
REST API 设计与一致性
-
更规范的 REST 资源命名与 HTTP 动词
-
更统一的错误响应格式(message / error 字段)
-
更完善的分页头(X-Total、X-Total-Pages、X-Page 等,视端点而定)
-
支持用 numeric id 或 URL 编码 path 访问多数资源
1 2 3 4 5
| curl -s -D - -o /dev/null \ --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects?per_page=10&page=1" \ | grep -i '^X-'
|
成员与权限
1 2 3 4 5
| curl -s --request POST \ --header "PRIVATE-TOKEN: $TOKEN" \ --data "user_id=$USER_ID&access_level=30&expires_at=2026-12-31" \ "$GITLAB/api/v4/projects/${ENCODED_PATH}/members" | jq .
|
命名空间与组织结构
-
/namespaces API:统一查询 Group 与用户命名空间(见上文 8)Namespaces)
-
namespace.kind 字段:明确区分 group / user
-
更完善的 Subgroup、共享 Group 等管理能力
1 2 3 4 5 6 7 8 9
| curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/namespaces?per_page=100&page=1" | jq .
curl -s --request POST \ --header "PRIVATE-TOKEN: $TOKEN" \ --data "name=mobile&path=mobile&parent_id=12" \ "$GITLAB/api/v4/groups" | jq .
|
CI/CD 与 DevOps
-
Pipeline / Job / Artifact / Runner 等现代 CI API
-
环境(Environments)、部署(Deployments)
-
容器镜像仓库(Container Registry)API
-
Package Registry(Maven/NuGet 等)API
1 2 3 4 5 6 7 8 9 10 11 12
| curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects/${ENCODED_PATH}/pipelines?per_page=20" | jq .
PIPELINE_ID=100 curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects/${ENCODED_PATH}/pipelines/${PIPELINE_ID}/jobs" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects/${ENCODED_PATH}/runners" | jq .
|
安全与合规
1 2 3
| curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects/${ENCODED_PATH}/vulnerabilities?per_page=20" | jq .
|
集成与自动化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects/${ENCODED_PATH}/hooks" | jq .
curl -s --request POST \ --header "PRIVATE-TOKEN: $TOKEN" \ --header "Content-Type: application/json" \ --data '{"url":"https://example.com/hook","push_events":true,"merge_requests_events":true}' \ "$GITLAB/api/v4/projects/${ENCODED_PATH}/hooks" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects/${ENCODED_PATH}/deploy_keys" | jq .
curl -s --request POST \ --header "PRIVATE-TOKEN: $TOKEN" \ --header "Content-Type: application/json" \ --data '{"name":"ci-token","scopes":["read_repository","read_api"],"expires_at":"2026-12-31"}' \ "$GITLAB/api/v4/projects/${ENCODED_PATH}/access_tokens" | jq .
|
其他现代能力
-
GraphQL API(/api/graphql):v4 时代主推的查询方式,适合复杂聚合查询
-
Issue/MR 高级操作(时间跟踪、评审规则、审批规则等)
-
Wiki、Snippets、Milestones、Labels 等资源的完整 CRUD
-
审计事件(Audit Events,EE)
-
Geo 复制相关 API(EE)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| curl -s --request POST \ --header "PRIVATE-TOKEN: $TOKEN" \ --header "Content-Type: application/json" \ --data '{"query":"{ currentUser { id username name } }"}' \ "$GITLAB/api/graphql" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects/${ENCODED_PATH}/issues?per_page=20&state=opened" | jq .
curl -s --header "PRIVATE-TOKEN: $TOKEN" \ "$GITLAB/api/v4/projects/${ENCODED_PATH}/merge_requests?per_page=20&state=opened" | jq .
curl -s --request POST \ --header "PRIVATE-TOKEN: $TOKEN" \ --data "title=v1.0&description=First%20release&due_date=2026-12-31" \ "$GITLAB/api/v4/projects/${ENCODED_PATH}/milestones" | jq .
|
完整端点列表以官方文档为准:GitLab REST API
常见问题
API 返回 HTML 重定向(You are being redirected)
常见原因:
-
路径未 URL 编码(group/project 须写成 group%2Fproject)
-
API 版本与实例不匹配(如对 11.0+ 实例访问 v3)
-
Token 无效或权限不足,被重定向到登录页
v3 拉取成员为空
-
/projects/:id/members 只返回直接成员,不含 Group 继承
-
个人项目 members 常为 [],需从 owner / namespace.owner_id 收集
-
v3 无 members/all,需单独拉 Group members
创建用户报 username 已被使用
GitLab 的 username 与 Group 顶级 path 共享命名空间。若误将个人 namespace 建成了 Group(如 Group hanqunfeng),则无法创建同名用户。需在 Admin 删除误建 Group 后重试。
如何判断该用哪个版本
1 2
| curl -s --header "PRIVATE-TOKEN: $TOKEN" "$GITLAB/api/v4/version" | jq . curl -s --header "PRIVATE-TOKEN: $TOKEN" "$GITLAB/api/v3/version" | jq .
|
参考链接