GitBucket 4.48.0 REST API 使用详解
实测 GitBucket 4.48.0 的 /api/v3 GitHub 兼容接口:个人访问令牌生成与四种认证方式、用户/仓库/Issue/Webhook/提交/分支/文件内容端点、分页与限流行为、错误语义,含 /api/v3/user/repos 被路由遮挡返回 404、commits 分页第 1-2 页重复、Webhook url 缺 /repos 段等真实缺陷与 curl 响应原文。
目标:以 GitBucket 4.48.0 的
/api/v3GitHub 兼容接口为对象,实测个人访问令牌的生成与四种认证方式、用户/仓库/Issue/Webhook/提交/分支/文件内容接口、分页与限流行为、错误语义,给出可直接复制的 curl 请求与真实响应片段,并记录 4.48.0 中已确认的路由遮挡、分页错位等兼容性缺陷。
GitBucket 4.48.0 REST API 使用详解
GitBucket 自带一套以 GitHub REST API v3 为蓝本的 HTTP 接口,挂在 /api/v3 前缀下,认证、请求体、响应字段都尽量贴 GitHub,但实现只覆盖常用子集,且有几处被实测确认的行为偏差。本文在 Ubuntu 22.04 + OpenJDK 17.0.20.1 的 GitBucket 4.48.0 部署实例上(服务进程监听 127.0.0.1:8080,数据目录 /opt/gitbucket/data),把认证、仓库、Issue、Webhook、提交、分页逐项跑通,所有响应均为 2026-10-27 的真实抓取结果。
一、接口概览
| 项 | 值 |
|---|---|
| API 根 | http://127.0.0.1:8080/api/v3 |
| GitBucket 版本 | 4.48.0(2026-09-20 发布) |
| 运行时 | OpenJDK 17.0.20.1,单 JVM 进程 |
| 兼容目标 | GitHub REST API v3 子集 |
| 认证 | HTTP Basic、个人访问令牌(token / bearer / access_token 查询参数)、Web 会话 Cookie |
| 响应格式 | application/json; charset=utf-8 |
| 速率限制 | 未启用(/api/v3/rate_limit 固定返回 404) |
4.48.0 的 API 控制器共 16 个,位于源码 src/main/scala/gitbucket/core/controller/api/:
| 控制器 | 覆盖资源 |
|---|---|
ApiUserControllerBase | 当前用户、按用户名/ID 查用户、用户列表、创建用户(管理员) |
ApiRepositoryControllerBase | 仓库创建、查询、仓库列表、fork、tags、raw |
ApiIssueControllerBase | Issue 列表、单条、创建 |
ApiIssueCommentControllerBase | Issue 评论列表、单条、创建、编辑、删除 |
ApiIssueLabelControllerBase / ApiIssueMilestoneControllerBase | 标签、里程碑 |
ApiPullRequestControllerBase | Pull Request |
ApiRepositoryBranchControllerBase | 分支 |
ApiRepositoryCommitControllerBase | 提交列表与详情 |
ApiRepositoryContentsControllerBase | 目录、文件内容、README |
ApiRepositoryWebhookControllerBase | 仓库 Webhook 增删查改 |
ApiRepositoryCollaboratorControllerBase | 协作者 |
ApiRepositoryStatusControllerBase | 提交状态(commit status) |
ApiReleaseControllerBase / ApiGitReferenceControllerBase / ApiOrganizationControllerBase | Release、Git 引用、组织 |
官方没有提供 OpenAPI/Swagger 描述文件,实际可用路由以源码为唯一权威。所有未实现路径统一落到 ApiControllerBase 的兜底路由:
// ApiController.scala:64
get("/api/v3/*") { NotFound() }
因此未实现的路由返回的是 404 而不是 501,排查时不要把它当成"服务没起来"。
二、认证
2.1 四种凭据入口与优先级
认证逻辑全部集中在 ApiAuthenticationFilter,按顺序尝试:
// servlet/ApiAuthenticationFilter.scala:26-37
Option(request.getHeader("Authorization"))
.map {
case auth if auth.toLowerCase().startsWith("token ") || auth.toLowerCase().startsWith("bearer ") =>
AccessTokenService.getAccountByAccessToken(auth.substring(auth.indexOf(" ") + 1).trim).toRight(())
case auth if auth.startsWith("Basic ") => doBasicAuth(auth, loadSystemSettings(), request).toRight(())
case _ => Left(())
}
.orElse {
Option(req.getParameter("access_token")).map(AccessTokenService.getAccountByAccessToken(_).toRight(()))
}
.orElse {
Option(request.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account]).map(Right(_))
}
据此得到实测有效的四种方式,优先级自上而下:
| 方式 | 写法 | 适用场景 | 实测结果 |
|---|---|---|---|
| 令牌头 | Authorization: token <40位令牌> | 脚本、CI,最推荐 | 200 |
| 令牌头(Bearer 别名) | Authorization: bearer <40位令牌> | 同 token,大小写不敏感 | 200 |
| 基本认证 | Authorization: Basic base64(user:pass),等价 curl -u user:pass | 临时调试、首次引导 | 200 |
| 查询参数 | ?access_token=<40位令牌> | 无法设置请求头的环境(如 Web 图片链接) | 200 |
| 会话 Cookie | 先 POST /signin | 浏览器同源场景 | 200 |
查询参数方式会把令牌写进 URL,会落进访问日志和 Referer,生产环境只应作为最后手段。
四种方式的真实回显(令牌按实测值截断展示):
T=86b2878a…043a # 40 位十六进制,生成方式见 2.2
curl -s -H "Authorization: token $T" http://127.0.0.1:8080/api/v3/user
curl -s -H "Authorization: bearer $T" -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/api/v3/user
curl -s -u root:q1w2e3r4 http://127.0.0.1:8080/api/v3/user
curl -s "http://127.0.0.1:8080/api/v3/user?access_token=$T"
{"id":1,"login":"root","email":"root@localhost","type":"User","site_admin":true,"created_at":"2026-10-27T12:25:55Z","url":"http://localhost:8080/api/v3/users/root","html_url":"http://localhost:8080/root","avatar_url":"http://localhost:8080/root/_avatar"}
200
{"id":1,"login":"root", ... 同上 ...}
{"id":1,"login":"root", ... 同上 ...}
2.2 生成个人访问令牌
4.48.0 没有 POST /api/v3/authorizations 这类接口(实测返回 404),令牌只能从 Web 界面生成,服务端也只存哈希:
// service/AccessTokenService.scala:15-18, 25-36
def makeAccessTokenString: String = {
val bytes = new Array[Byte](20) // 20 字节 → 40 位十六进制
AccessTokenService.secureRandom.nextBytes(bytes)
bytes.map("%02x".format(_)).mkString
}
def tokenToHash(token: String): String = StringUtil.sha1(token)
// 落库的是 tokenHash(SHA-1),令牌原文只在生成当次返回
生成页的路由在 AccountController.scala:431(GET /:userName/_application)与 :448(POST /:userName/_personalToken,表单字段 note),令牌只在渲染当次出现在 <input id="generated-token"> 里,页面刷新即消失。全程可用 curl 完成:
# 1) 登录拿会话
curl -s -c /tmp/cj -o /dev/null \
-d "userName=root&password=q1w2e3r4&hash=" http://127.0.0.1:8080/signin
# 2) 生成令牌(note 是备注,不是令牌名)
curl -s -b /tmp/cj -c /tmp/cj -o /dev/null \
-w "gen=%{http_code} -> %{redirect_url}\n" \
-d "note=d5-rest-api-token-2" http://127.0.0.1:8080/root/_personalToken
# 3) 立刻取回明文(只显示一次)
curl -s -b /tmp/cj http://127.0.0.1:8080/root/_application > /tmp/app.html
grep -o 'value="[a-f0-9]\{40\}"' /tmp/app.html
实测回显:
gen=302 -> http://localhost:8080/root/_application
value="86b2878ad1c75cadda61267b78832f25b8b4043a"
要点:
- 令牌是 40 位十六进制(20 字节随机数),库里存的是它的 SHA-1,丢了只能重建,没有"再显示一次";
- 生成表单不需要 CSRF token,会话 Cookie 即可,脚本化没有额外障碍;
- 删除令牌是
GET /{user}/_personalToken/delete/{id}(源码AccountController.scala:457),注意是 GET 不是 DELETE。
2.3 令牌可以直接当 Git 密码
同一枚令牌不仅适用于 /api/v3,也能用于 Git over HTTP 的 Basic 认证(用户名任意、密码填令牌即可,这里用 root 作用户名)。实测 clone → commit → push 全程通过:
export GIT_TERMINAL_PROMPT=0
git clone -q "http://root:$T@127.0.0.1:8080/git/root/api-demo.git" /tmp/clone
cd /tmp/clone
git config user.email root@localhost && git config user.name root
echo "- webhook 实测提交" >> README.md
git commit -q -am "D5 webhook 触发测试" && git push -q origin main
remote: Updating references: 100% (1/1)
03adb0f D5 webhook 触发测试
8971cc4 Initial commit
2.4 三种 401/访问拒绝的语义不同
| 请求 | 状态码 | 响应体 |
|---|---|---|
| 带错误令牌 | 401 | { "message": "Bad credentials" } |
| 不带任何凭据 | 401 | {"message":"Requires authentication"} |
| 不带凭据访问私有仓库 | 401 | {"message":"Requires authentication"} |
注意第三行:GitHub 对无权访问的私有仓库返回 404(不泄露存在性),GitBucket 4.48.0 返回 401,等于告诉调用方"这个仓库存在,去认证"。公网部署时这一点要留意。
三、服务根与用户接口
3.1 根端点
curl -s -H "Authorization: token $T" http://127.0.0.1:8080/api/v3
{"rate_limit_url":"http://localhost:8080/api/v3/rate_limit"}
比 GitHub 的根端点(返回十来个 URL)精简得多,只给出限流地址,而限流本身并未实现:
curl -s -H "Authorization: token $T" http://127.0.0.1:8080/api/v3/rate_limit
{"message":"Rate limiting is not enabled."}
HTTP=404
3.2 当前用户
curl -s -H "Authorization: token $T" http://127.0.0.1:8080/api/v3/user
{"id":1,"login":"root","email":"root@localhost","type":"User","site_admin":true,"created_at":"2026-10-27T12:25:55Z","url":"http://localhost:8080/api/v3/users/root","html_url":"http://localhost:8080/root","avatar_url":"http://localhost:8080/root/_avatar"}
site_admin 是布尔值,可直接用来判断管理员;created_at 是账号建库时间(首次启动建库的时刻),不是 GitBucket 安装时间之外的业务时间。
3.3 按用户名与按 ID
curl -s -H "Authorization: token $T" http://127.0.0.1:8080/api/v3/users/root
curl -s -H "Authorization: token $T" http://127.0.0.1:8080/api/v3/user/1
两者返回同一份 ApiUser 结构。按 ID 查用户的路由是 /api/v3/user/:id(不是 /api/v3/users/:id),这是 GitBucket 自己的命名习惯,与 GitHub 的 /users/{id} 不一致。
四、仓库接口
4.1 创建仓库
请求体模型只有四个字段(api/CreateARepository.scala:7):
case class CreateARepository(
name: String,
description: Option[String],
`private`: Boolean = false,
auto_init: Boolean = false
)
| 字段 | 必填 | 说明 |
|---|---|---|
name | 是 | 长度 ≤ 100,须匹配 [a-zA-Z0-9\-+_.]+,不能以 _ 或 - 开头 |
description | 否 | 仓库描述,可为中文 |
private | 否 | 默认 false |
auto_init | 否 | true 时自动建初始提交与 README,否则是空仓库 |
curl -s -X POST -H "Authorization: token $T" -H "Content-Type: application/json" \
-d '{"name":"api-demo","description":"D5 REST API 实测仓库","private":false,"auto_init":true}' \
-w "\nHTTP=%{http_code}\n" \
http://127.0.0.1:8080/api/v3/user/repos
真实响应(HTTP 200,注意不是 GitHub 的 201):
{"id":2,"name":"api-demo","full_name":"root/api-demo","description":"D5 REST API 实测仓库","fork":false,"watchers":0,"forks":0,"private":false,"default_branch":"main","owner":{"id":1,"login":"root","email":"root@localhost","type":"User","site_admin":true,"created_at":"2026-10-27T12:25:55Z","url":"http://localhost:8080/api/v3/users/root","html_url":"http://localhost:8080/root","avatar_url":"http://localhost:8080/root/_avatar"},"has_issues":true,"forks_count":0,"watchers_count":0,"url":"http://localhost:8080/api/v3/repos/root/api-demo","clone_url":"http://localhost:8080/git/root/api-demo.git","html_url":"http://localhost:8080/root/api-demo"}
HTTP=200
default_branch 取自系统设置的默认分支(实例配置为 main),不是写死的 master;clone_url 的路径段是 /git/ 而不是 GitHub 的 /,脚本里拼接克隆地址时不要照抄 GitHub 习惯。
4.2 重复创建与非法名称
同账号下重名仓库不会返回 409/422,而是 HTTP 200 + 错误体:
curl -s -X POST -H "Authorization: token $T" -H "Content-Type: application/json" \
-d '{"name":"api-demo"}' -w "\nHTTP=%{http_code}\n" http://127.0.0.1:8080/api/v3/user/repos
{"message":"A repository with this name already exists on this account","documentation_url":"https://developer.github.com/v3/repos/#create"}
HTTP=200
非法仓库名(如 _bad)会直接落到兜底的 404:
{"message":"Not Found"}
HTTP=404
根因是控制器里的 for { data <- extractFromJsonBody[...] if data.isValid } ... getOrElse NotFound()(ApiRepositoryControllerBase.scala:79-108)——校验失败时 for 推导为空,最终走 NotFound()。任何参数校验失败在 4.48.0 都表现为 404,而不是 400,脚本必须同时判状态码和 message 字段。
4.3 仓库列表:/api/v3/user/repos 不可用
这是本轮实测最值得记录的一处缺陷。三个列表端点的真实结果:
| 端点 | 语义 | 实测 |
|---|---|---|
GET /api/v3/user/repos | 当前用户可见仓库 | 404 {"message":"Not Found"} |
GET /api/v3/users/root/repos | 指定用户的仓库 | 200,返回 root/api-demo、root/demo |
GET /api/v3/repositories | 全站公开仓库 | 200,不含私有仓库 |
curl -s -H "Authorization: token $T" -w "\nHTTP=%{http_code}\n" http://127.0.0.1:8080/api/v3/user/repos
curl -s -H "Authorization: token $T" http://127.0.0.1:8080/api/v3/users/root/repos
根因是 Scalatra 路由注册顺序被 trait 混入顺序决定:ApiController 最后混入 ApiUserControllerBase(ApiController.scala:27),它先于 ApiRepositoryControllerBase 注册 get("/api/v3/user/:id")(ApiUserControllerBase.scala:39);于是 /api/v3/user/repos 命中 :id 路由,params("id").toLongOption 对 "repos" 得到 None,返回 NotFound()。而 POST /api/v3/user/repos 没有被遮挡(用户控制器没有对应的 POST /api/v3/user/:id),所以建仓库能成功、列自己的仓库却 404。
规避方式:用 GET /api/v3/users/{用户名}/repos 代替,或直接用 GET /api/v3/repositories(仅公开仓库)。
4.4 单仓库与不存在
curl -s -H "Authorization: token $T" http://127.0.0.1:8080/api/v3/repos/root/api-demo
curl -s -H "Authorization: token $T" -w "\nHTTP=%{http_code}\n" http://127.0.0.1:8080/api/v3/repos/root/no-such-repo
{"id":2,"name":"api-demo", ... }
HTTP=200
{"message":"Not Found"}
HTTP=404
公开仓库可以不带任何凭据读取(实测 200),这也是把 GitBucket 直接暴露公网时最容易被爬的点。
4.5 其它仓库子资源
| 端点 | 说明 |
|---|---|
GET /api/v3/repos/{owner}/{repo}/branches | 分支列表 |
GET /api/v3/repos/{owner}/{repo}/commits | 提交列表(sha、page、per_page、author、path、since、until) |
GET /api/v3/repos/{owner}/{repo}/contents/{path} | 目录或文件内容 |
GET /api/v3/repos/{owner}/{repo}/readme | README |
GET /api/v3/repos/{owner}/{repo}/tags | 标签(tag) |
POST /api/v3/repos/{owner}/{repo}/forks | 创建 fork |
GET /api/v3/repos/{owner}/{repo}/hooks | Webhook 列表 |
GET /api/v3/repos/{owner}/{repo}/labels / milestones / pulls | 标签、里程碑、PR |
GET /api/v3/repos/{owner}/{repo}/collaborators | 协作者 |
GET /api/v3/repositories/{id} | GitBucket 私有扩展:按内部 ID 取仓库 |
五、Issue 与评论
5.1 创建 Issue
请求体模型(api/CreateAnIssue.scala:6)为 title、body、assignees、milestone、labels。
curl -s -X POST -H "Authorization: token $T" -H "Content-Type: application/json" \
-d '{"title":"D5 实测:REST API 创建 issue","body":"由 /api/v3 创建,验证 JSON 字段与返回值。","labels":[],"assignees":[]}' \
-w "\nHTTP=%{http_code}\n" \
http://127.0.0.1:8080/api/v3/repos/root/api-demo/issues
{"number":1,"title":"D5 实测:REST API 创建 issue","user":{"id":1,"login":"root","email":"root@localhost","type":"User","site_admin":true,"created_at":"2026-10-27T12:25:55Z","url":"http://localhost:8080/api/v3/users/root","html_url":"http://localhost:8080/root","avatar_url":"http://localhost:8080/root/_avatar"},"assignees":[],"labels":[],"state":"open","created_at":"2026-10-27T13:19:16Z","updated_at":"2026-10-27T13:19:16Z","body":"由 /api/v3 创建,验证 JSON 字段与返回值。","id":0,"comments_url":"http://localhost:8080/api/v3/repos/root/api-demo/issues/1/comments","html_url":"http://localhost:8080/root/api-demo/issues/1"}
HTTP=200
两个必须记住的差异:
"id":0—— GitBucket 的 Issue 主键就是自增的number,id字段恒为 0,不要拿id做后续请求的路径参数,/issues/0会 404;- 创建成功是 200,不是 201;
comments_url才是可用的评论端点。
缺 title 时不报 400,同样落到 404:
{"message":"Not Found"}
HTTP=404
5.2 列表与筛选
curl -s -H "Authorization: token $T" "http://127.0.0.1:8080/api/v3/repos/root/api-demo/issues?state=all"
curl -s -H "Authorization: token $T" "http://127.0.0.1:8080/api/v3/repos/root/api-demo/issues?state=open"
curl -s -H "Authorization: token $T" "http://127.0.0.1:8080/api/v3/repos/root/api-demo/issues?state=closed"
实测:state=all 返回 2 条,state=open 返回 2 条,state=closed 返回 0 条。列表项与创建响应结构一致,仍是 id: 0 + number。
5.3 评论
curl -s -X POST -H "Authorization: token $T" -H "Content-Type: application/json" \
-d '{"body":"D5 评论实测"}' -w "\nHTTP=%{http_code}\n" \
http://127.0.0.1:8080/api/v3/repos/root/api-demo/issues/1/comments
{"id":1,"user":{"id":1,"login":"root","email":"root@localhost","type":"User","site_admin":true,"created_at":"2026-10-27T12:25:55Z","url":"http://localhost:8080/api/v3/users/root","html_url":"http://localhost:8080/root","avatar_url":"http://localhost:8080/root/_avatar"},"body":"D5 评论实测","created_at":"2026-10-27T13:20:03Z","updated_at":"2026-10-27T13:20:03Z","html_url":"http://localhost:8080/root/api-demo/issues/1#comment-1"}
HTTP=200
评论的 id 是真实自增值(这里为 1),与 Issue 的 id: 0 不同。
5.4 没有 PATCH:改状态只能走 Web
ApiIssueControllerBase.scala 里只有三条路由:GET /issues、GET /issues/:id、POST /issues。4.48.0 没有 PATCH /api/v3/repos/{owner}/{repo}/issues/{id},实测 PATCH 会落到兜底路由返回 404。也就是说,用 API 关闭 Issue、改标题、改指派、打标签都做不到,只能进 Web 页面操作。同理,PATCH /api/v3/repos/{owner}/{repo}/pulls/{id} 之类也不在实现范围内。
六、Webhook
6.1 创建
curl -s -X POST -H "Authorization: token $T" -H "Content-Type: application/json" \
-d '{"name":"web","active":true,"events":["push","issues"],"config":{"url":"http://127.0.0.1:9999/hook","content_type":"json"}}' \
-w "\nHTTP=%{http_code}\n" \
http://127.0.0.1:8080/api/v3/repos/root/api-demo/hooks
{"type":"Repository","id":1,"name":"web","active":true,"events":["issues","push"],"config":{"content_type":"json","url":"http://127.0.0.1:9999/hook"},"url":"http://localhost:8080/api/v3/root/api-demo/hooks/1"}
HTTP=200
events 的合法取值来自 model/WebHook.scala:28-46:commit_comment、create、delete、deployment、deployment_status、fork、gollum、issue_comment、issues、member、page_build、public、pull_request、pull_request_review_comment、push、release、status、team_add、watch。传未定义值会落到 404(参数校验失败统一走兜底路由)。
6.2 列表、单条、删除
curl -s -H "Authorization: token $T" -w "\nHTTP=%{http_code}\n" http://127.0.0.1:8080/api/v3/repos/root/api-demo/hooks
curl -s -X DELETE -H "Authorization: token $T" -o /dev/null -w "HTTP=%{http_code}\n" \
http://127.0.0.1:8080/api/v3/repos/root/api-demo/hooks/1
[{"type":"Repository","id":1,"name":"web","active":true,"events":["issues","push"],"config":{"content_type":"json","url":"http://127.0.0.1:9999/hook"},"url":"http://localhost:8080/api/v3/root/api-demo/hooks/1"}]
HTTP=200
HTTP=204
删除后再查是 [](HTTP 200)。DELETE 返回 204 是这套 API 里少见的"标准"实现。
6.3 响应里的 url 字段不可用
创建和列表返回的 url 都是 http://localhost:8080/api/v3/root/api-demo/hooks/1,少了 /repos 段,拿它去请求直接 404;把 /repos 补回去才是有效路由:
curl -s -o /dev/null -w "wrong=%{http_code}\n" -H "Authorization: token $T" \
http://127.0.0.1:8080/api/v3/root/api-demo/hooks/1
curl -s -o /dev/null -w "right=%{http_code}\n" -H "Authorization: token $T" \
http://127.0.0.1:8080/api/v3/repos/root/api-demo/hooks/1
wrong=404
right=200
这是 ApiWebhook 拼 URL 时的固定写法缺陷,所有 Webhook 响应都受影响,且不能靠配置纠正。
6.4 投递实测
在实例内 127.0.0.1:9999 起一个 Python 监听器接 POST,向仓库推一次提交后,Webhook 立即命中。请求头:
Content-Type: application/json
X-Github-Event: push
X-Github-Delivery: ecf71974-524c-4e08-aca1-a35c08f6547a
Content-Length: 2293
Host: 127.0.0.1:9999
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.20.1)
Accept-Encoding: gzip,deflate
请求体(节选,2293 字节):
{"pusher":{"name":"root","email":"root@localhost"},
"sender":{"id":1,"login":"root","email":"root@localhost","type":"User","site_admin":true,...},
"ref":"refs/heads/main",
"before":"8971cc45c93483e88bf6fe798fec4a439742a90c",
"after":"03adb0f95d7b28104785e5d3487885ce2c650ad5",
"commits":[{"id":"03adb0f95d7b28104785e5d3487885ce2c650ad5",
"message":"D5 webhook 触发测试\n",
"timestamp":"2026-10-27T13:20:41Z",
"added":[],"removed":[],"modified":["README.md"],
"author":{"name":"root","email":"root@localhost","date":"2026-10-27T13:20:41Z"},
"committer":{"name":"root","email":"root@localhost","date":"2026-10-27T13:20:41Z"},
"url":"http://localhost:8080/api/v3/root/api-demo/commits/03adb0f...",
"html_url":"..."}]}
两个可操作的结论:
- 事件头是
X-Github-Event(不是X-Gitbucket-Event),接收端可以按 GitHub 的约定写; - payload 里
commits[].url同样缺/repos,实测该 URL 返回 404,正确的/api/v3/repos/root/api-demo/commits/{sha}返回 200。接收端若要用 payload 里的 URL 回查提交,必须自己重拼; User-Agent是Apache-HttpClient/4.5.14 (Java/17.0.20.1),可据此在网关侧识别 GitBucket 发起的回调。
七、提交、分支与文件内容
7.1 提交列表与分页错位
curl -s -H "Authorization: token $T" "http://127.0.0.1:8080/api/v3/repos/root/api-demo/commits?per_page=1&page=1"
curl -s -H "Authorization: token $T" "http://127.0.0.1:8080/api/v3/repos/root/api-demo/commits?per_page=1&page=2"
curl -s -H "Authorization: token $T" "http://127.0.0.1:8080/api/v3/repos/root/api-demo/commits?per_page=1&page=3"
仓库一共 2 个提交(03adb0f 新、8971cc4 旧),实测:
page=1 -> [('03adb0f', 'D5 webhook 触发测试')]
page=2 -> [('03adb0f', 'D5 webhook 触发测试')]
page=3 -> [('8971cc4', 'Initial commit')]
第 1 页与第 2 页返回完全相同的提交。根因在源码:
// ApiRepositoryCommitControllerBase.scala:62-65
if (page > 1) {
revfilters += SkipRevFilter.create(page * per_page - 2) // 应为 (page - 1) * per_page
}
revfilters += MaxCountRevFilter.create(per_page);
page * per_page - 2 少减了 per_page:per_page=1 时第 2 页偏移量算成 0,于是与第 1 页重复;第 3 页偏移 1,才跳过 1 条。分页遍历时若按"上一页/下一页"翻,会拿到重复数据,必须自己按 sha 去重。
per_page 本身是生效的(上限 100),sha、author、path、since、until 也都支持,参数缺省时 sha=HEAD、per_page=30。
7.2 分支
curl -s -H "Authorization: token $T" http://127.0.0.1:8080/api/v3/repos/root/api-demo/branches
[{"name":"main","commit":{"sha":"8971cc45c93483e88bf6fe798fec4a439742a90c"}}]
这条是列表返回历史遗留:分支接口只给出分支名与首个提交的 sha,没有 protected、_links 等 GitHub 字段;而且这里 sha 指向的是分支的起点提交,不是最新提交(最新是 03adb0f)。要用最新 sha 就查 GET /api/v3/repos/{owner}/{repo}/commits?per_page=1。
7.3 文件内容:目录与单文件的 sha 不是一回事
curl -s -H "Authorization: token $T" http://127.0.0.1:8080/api/v3/repos/root/api-demo/contents/
curl -s -H "Authorization: token $T" http://127.0.0.1:8080/api/v3/repos/root/api-demo/contents/README.md
目录列表(contents/)返回:
[{"type":"file","name":"README.md","path":"README.md","sha":"03adb0f95d7b28104785e5d3487885ce2c650ad5","download_url":"http://localhost:8080/api/v3/repos/root/api-demo/raw/03adb0f95d7b28104785e5d3487885ce2c650ad5/README.md"}]
单文件(contents/README.md)返回:
{"type":"file","name":"README.md","path":"README.md","sha":"de51446e271d59fbb9764b55a8d6a2984ee61589","content":"YXBpLWRlbW8KPT09PT09PT09PT09PT09CgpENSBSRVNUIEFQSSDlrp7mtYvku5PlupMtIHdlYmhvb2sg5a6e5rWL5o+Q5LqkCg==","encoding":"base64","download_url":"http://localhost:8080/api/v3/repos/root/api-demo/raw/…"}
同一个文件在两处返回不同 sha:目录列表给的是 commit sha(03adb0f…),单文件给的是 blob sha(de51446…)。GitHub 在目录列表里返回 blob sha,所以基于 GitHub 语义写的"依目录 sha 判文件变更"的逻辑在这里会失效。content 是 base64 编码(encoding 字段显式标注),解码即可得原文。
GET /api/v3/repos/{owner}/{repo}/readme 同样可用,返回结构与单文件一致。
八、分页与限流实测汇总
| 端点 | page | per_page | Link 响应头 | 实测结论 |
|---|---|---|---|---|
GET /repos/{o}/{r}/issues | 生效 | 被忽略,固定 25 条/页 | 无 | per_page=1 仍返回 2 条;page=2 返回 0 条(总数 2) |
GET /repos/{o}/{r}/commits | 生效但有偏移 bug | 生效,上限 100 | 无 | 第 1、2 页重复 |
GET /repos/{o}/{r}/forks | 生效 | 生效,上限 100 | 无 | 源码显式 slice 分页 |
GET /users/{u}/repos | 不生效 | 不生效 | 无 | per_page=1 返回全部 3 个仓库 |
GET /repositories | 不生效 | 不生效 | 无 | per_page=1 返回全部 2 个公开仓库 |
GET /repos/{o}/{r}/hooks、labels、branches | 不生效 | 不生效 | 无 | 全量返回 |
GET /rate_limit | —— | —— | —— | 404,限流未启用 |
Issue 列表的固定条数来自 PullRequestService.PullRequestLimit = 25(service/PullRequestService.scala:803),ApiIssueControllerBase.scala:24-49 直接用它做 limit,完全没读 per_page。所有端点都不返回 Link 头,翻页只能靠"本页不满即最后一页"的经验规则,或者改用 since/until 之类的时间过滤。
九、错误语义汇总
| 场景 | 状态码 | 响应体 |
|---|---|---|
| 令牌错误 | 401 | { "message": "Bad credentials" } |
| 无凭据访问需登录端点 | 401 | {"message":"Requires authentication"} |
| 无凭据访问私有仓库 | 401 | {"message":"Requires authentication"} |
| 仓库不存在 / Issue 不存在 | 404 | {"message":"Not Found"} |
| 未实现的路由(含所有 PATCH) | 404 | {"message":"Not Found"} |
| 请求体校验失败(非法仓库名、缺 title、非法事件名) | 404 | {"message":"Not Found"} |
| 重名仓库 | 200 | {"message":"...already exists...","documentation_url":"..."} |
| 限流查询 | 404 | {"message":"Rate limiting is not enabled."} |
| 删除 Webhook 成功 | 204 | 空响应体 |
写自动化脚本时,判断成功不能只看 2xx:重名仓库、参数非法都可能是 200 带错误体,或 404 带统一消息。稳妥的做法是"状态码 + message 字段 + 关键业务字段(full_name、number)是否存在"三重判定。
十、脚本化:一次建仓、开 Issue、挂 Webhook
把常用操作收成三个函数,令牌从环境变量读,避免写进脚本:
#!/usr/bin/env bash
set -euo pipefail
BASE="http://127.0.0.1:8080/api/v3"
TOKEN="${GITBUCKET_TOKEN:?请先导出 GITBUCKET_TOKEN}"
api() {
local method=$1 path=$2 data=${3:-}
if [ -n "$data" ]; then
curl -sS -X "$method" -H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" -d "$data" "$BASE$path"
else
curl -sS -X "$method" -H "Authorization: token $TOKEN" "$BASE$path"
fi
}
# 1) 建仓(重名会返回 200 + message,需自行判断)
api POST /user/repos '{"name":"ops-demo","description":"脚本创建","auto_init":true}' \
| python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("full_name") or d["message"])'
# 2) 列仓库——注意不能用 /user/repos(会 404)
api GET /users/root/repos | python3 -c 'import sys,json;print([r["full_name"] for r in json.load(sys.stdin)])'
# 3) 开 Issue(返回 200,issue 编号看 number)
api POST /repos/root/ops-demo/issues '{"title":"自动化巡检异常","body":"由脚本创建"}' \
| python3 -c 'import sys,json;d=json.load(sys.stdin);print("issue #",d["number"])'
# 4) 挂 Webhook(events 只接受 WebHook.Event 枚举值)
api POST /repos/root/ops-demo/hooks '{"name":"web","active":true,"events":["push"],"config":{"url":"http://127.0.0.1:9999/hook","content_type":"json"}}'
配套的接收端只需要一个 POST 处理函数(实测用 Python 标准库即可):
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers.get("Content-Length", 0)))
event = self.headers.get("X-Github-Event")
print(event, body[:200])
self.send_response(200)
self.end_headers()
def log_message(self, *args):
pass
HTTPServer(("127.0.0.1", 9999), Handler).serve_forever()
十一、常见问题
Q1:令牌页面上找不到了,还能再显示一次吗?
A:不能。库里只存 SHA-1(AccessTokenService.scala:16-18),明文只在生成当次的 id="generated-token" 输入框里出现。直接生成新令牌,旧的从 /{user}/_application 页面删除。
Q2:GET /api/v3/user/repos 一直 404,是权限不够吗?
A:不是。是被 GET /api/v3/user/:id 路由遮挡(见 4.3),与权限无关。改用 /api/v3/users/{用户名}/repos。
Q3:创建 Issue 返回的 id 是 0,能用 /issues/0 查吗?
A:不能,会 404。用返回体里的 number 作为路径参数,即 /issues/1。
Q4:能用 API 关闭 Issue 或改标签吗?
A:4.48.0 没有 PATCH /issues/:id,PATCH 全部落到兜底 404。Issue 状态变更只能进 Web 页面。标签、里程碑有独立端点可增删,但不提供 Issue 与标签的绑定接口。
Q5:Webhook 响应里的 url 打不开?
A:响应体拼的是 /api/v3/{owner}/{repo}/hooks/{id},缺了 /repos,属于实现缺陷。自行拼 /api/v3/repos/{owner}/{repo}/hooks/{id} 即可;payload 中 commits[].url 同理。
Q6:为什么翻页会看到重复的提交?
A:commits 接口偏移量算错(page * per_page - 2),第 1、2 页重复。按 sha 去重,或改用 sha + per_page 的游标式取法。
Q7:私有仓库匿名请求为什么返回 401 而不是 404? A:GitBucket 的鉴权中间件区分"未认证"与"无权限",未认证统一 401;这也是它和 GitHub 的语义差异,暴露了仓库存在性。
Q8:per_page 设了没用?
A:Issue 列表固定 25 条、仓库列表不分页,只有 commits 与 forks 真正实现了 per_page(见第八节表格)。不要假设所有列表端点都支持。
十二、参考链接
- 介绍与选型:《GitBucket 4.48.0 介绍与选型指南》
- 裸机安装:《Ubuntu 22.04 安装 GitBucket 4.48.0 指南》
- 配置参数详解:《GitBucket 4.48.0 配置参数详解》
- GitHub REST API 参考(GitBucket 的兼容蓝本):https://docs.github.com/en/rest
- 认证过滤器源码:https://github.com/gitbucket/gitbucket/blob/4.48.0/src/main/scala/gitbucket/core/servlet/ApiAuthenticationFilter.scala
- 仓库 API 源码:https://github.com/gitbucket/gitbucket/blob/4.48.0/src/main/scala/gitbucket/core/controller/api/ApiRepositoryControllerBase.scala
- 提交 API 源码(分页偏移 bug 所在):https://github.com/gitbucket/gitbucket/blob/4.48.0/src/main/scala/gitbucket/core/controller/api/ApiRepositoryCommitControllerBase.scala