Commit 6b8e159687
Verified · cmc
Layout: unified · split
internal/control/refsupdated.go added +212
| @@ -0,0 +1,212 @@ | ||
| 1 | package control | |
| 2 | ||
| 3 | import ( | |
| 4 | "encoding/json" | |
| 5 | "fmt" | |
| 6 | "log/slog" | |
| 7 | "path" | |
| 8 | "strings" | |
| 9 | "time" | |
| 10 | ||
| 11 | "gitbay.org/gitbay/internal/ci" | |
| 12 | "gitbay.org/gitbay/internal/config" | |
| 13 | "gitbay.org/gitbay/internal/gitutil" | |
| 14 | "gitbay.org/gitbay/internal/policy" | |
| 15 | "gitbay.org/gitbay/internal/store" | |
| 16 | ) | |
| 17 | ||
| 18 | // RefsUpdated is the work that follows a ref update in repoID by userID, | |
| 19 | // with a key or token of scope: post-receive runs it for every push, and | |
| 20 | // a server-side write to a branch (an applied suggestion) runs it after | |
| 21 | // its own ref update, so nothing a push triggers is skipped. A push to a | |
| 22 | // source branch refreshes refs/merge-requests/N/head in every target | |
| 23 | // repo, by fetching — the target owns the objects, so the MR outlives the | |
| 24 | // fork. This is the only place a push writes outside its own repository. | |
| 25 | func RefsUpdated(st *store.Store, cfg config.Config, repoID, userID int64, scope string, updates []policy.RefUpdate) { | |
| 26 | pushedRepo, pushedRepoErr := st.RepoByID(repoID) | |
| 27 | if pushedRepoErr == nil { | |
| 28 | adoptDefaultBranch(st, cfg, &pushedRepo, updates) | |
| 29 | } | |
| 30 | for _, u := range updates { | |
| 31 | // Every ref update is an event webhooks can subscribe to. | |
| 32 | st.RecordEvent(repoID, userID, "push", fmt.Sprintf( | |
| 33 | `{"ref":%q,"old":%q,"new":%q,"forced":%v,"deleted":%v}`, | |
| 34 | u.Ref, u.Old, u.New, u.IsForce, u.IsDelete)) | |
| 35 | ||
| 36 | // Any ref update — branch or tag — schedules the push mirrors. | |
| 37 | st.MarkMirrorsDirty(repoID, "push") | |
| 38 | ||
| 39 | // Tag pushes run the tag-triggered CI jobs. | |
| 40 | if tag, ok := strings.CutPrefix(u.Ref, "refs/tags/"); ok && !u.IsDelete && pushedRepoErr == nil { | |
| 41 | QueueTagBuilds(st, cfg, pushedRepo, userID, tag, u.New) | |
| 42 | } | |
| 43 | ||
| 44 | branch, ok := cutHeads(u.Ref) | |
| 45 | if !ok { | |
| 46 | continue | |
| 47 | } | |
| 48 | // Commits landing on the default branch act on issue references | |
| 49 | // in their messages (closes #N, plain #N). | |
| 50 | if pushedRepoErr == nil && branch == pushedRepo.DefaultBranch && !u.IsDelete { | |
| 51 | dir := RepoDir(cfg.Server.Root, pushedRepo.OwnerName, pushedRepo.Name) | |
| 52 | ProcessCommitMessages(st, dir, pushedRepo, userID, scope, u.Old, u.New) | |
| 53 | RecordLandedCommits(st, dir, pushedRepo, u.Old, u.New) | |
| 54 | } | |
| 55 | // A branch push with a .gitbay/ci.yml queues one build per job. | |
| 56 | if pushedRepoErr == nil && !u.IsDelete { | |
| 57 | QueueBranchBuilds(st, cfg.Server.Root, cfg.Server.SiteURL, | |
| 58 | pushedRepo, userID, branch, u.Old, u.New, time.Now()) | |
| 59 | } | |
| 60 | if u.IsForce { | |
| 61 | st.Audit(userID, "push.forced", map[string]any{ | |
| 62 | "repo": repoID, "ref": u.Ref, "old": u.Old, "new": u.New}) | |
| 63 | } | |
| 64 | mrs, err := st.OpenMRsBySource(repoID, branch) | |
| 65 | if err != nil { | |
| 66 | slog.Error("post-receive: listing MRs", "err", err) | |
| 67 | continue | |
| 68 | } | |
| 69 | srcRepo, err := st.RepoByID(repoID) | |
| 70 | if err != nil { | |
| 71 | continue | |
| 72 | } | |
| 73 | srcDir := RepoDir(cfg.Server.Root, srcRepo.OwnerName, srcRepo.Name) | |
| 74 | for _, mr := range mrs { | |
| 75 | target, err := st.RepoByID(mr.RepoID) | |
| 76 | if err != nil { | |
| 77 | continue | |
| 78 | } | |
| 79 | if u.IsDelete { | |
| 80 | if mr.State == "open" { | |
| 81 | st.SetMRState(mr.ID, "source_gone") | |
| 82 | } | |
| 83 | if mr.QueuedAt != "" { | |
| 84 | TryQueuedMerge(st, cfg, mr.ID) // dequeues: the source is gone | |
| 85 | } | |
| 86 | continue // head ref retained: the diff stays viewable | |
| 87 | } | |
| 88 | dstDir := RepoDir(cfg.Server.Root, target.OwnerName, target.Name) | |
| 89 | headRef := fmt.Sprintf("refs/merge-requests/%d/head", mr.Number) | |
| 90 | if err := gitutil.FetchInto(dstDir, srcDir, u.New, headRef); err != nil { | |
| 91 | slog.Error("post-receive: refreshing MR head", "mr", mr.Number, "err", err) | |
| 92 | continue | |
| 93 | } | |
| 94 | // The merge base as it stands now, so a later range-diff | |
| 95 | // compares each revision against the target it was written | |
| 96 | // on rather than against today's. Best-effort: a base that | |
| 97 | // cannot be worked out costs precision, not the record. | |
| 98 | base, err := gitutil.MergeBase(dstDir, "refs/heads/"+mr.TargetRef, headRef) | |
| 99 | if err != nil { | |
| 100 | base = "" | |
| 101 | } | |
| 102 | if err := st.UpdateMRHead(mr.ID, u.New, base, sameChange(dstDir, mr, base, u.New)); err != nil { | |
| 103 | slog.Error("post-receive: recording MR head", "mr", mr.Number, "err", err) | |
| 104 | } | |
| 105 | if srcRepo.ID != target.ID { | |
| 106 | QueueMRBuilds(st, cfg.Server.Root, cfg.Server.SiteURL, | |
| 107 | target, userID, mr.Number, u.New) | |
| 108 | } | |
| 109 | if mr.State == "source_gone" { | |
| 110 | st.SetMRState(mr.ID, "open") // branch came back | |
| 111 | } | |
| 112 | // A queued merge stays queued across a push by someone who can | |
| 113 | // merge it, and the new head has to pass the gates on its own. | |
| 114 | if mr.QueuedAt != "" { | |
| 115 | QueuedMergePushed(st, cfg, mr.ID, userID, scope) | |
| 116 | } | |
| 117 | } | |
| 118 | } | |
| 119 | } | |
| 120 | ||
| 121 | // adoptDefaultBranch moves an unborn HEAD to the first branch a push | |
| 122 | // creates. A repository is initialised with HEAD at the stored default, | |
| 123 | // and a first push of master or trunk left HEAD naming a branch that did | |
| 124 | // not exist: clones checked out nothing and every surface asked git for | |
| 125 | // a branch that was not there (#189). A push that includes the default | |
| 126 | // branch itself needs nothing. | |
| 127 | func adoptDefaultBranch(st *store.Store, cfg config.Config, repo *store.Repo, updates []policy.RefUpdate) { | |
| 128 | dir := RepoDir(cfg.Server.Root, repo.OwnerName, repo.Name) | |
| 129 | if _, err := gitutil.ResolveRef(dir, "refs/heads/"+repo.DefaultBranch); err == nil { | |
| 130 | return | |
| 131 | } | |
| 132 | for _, u := range updates { | |
| 133 | branch, ok := cutHeads(u.Ref) | |
| 134 | if !ok || u.IsDelete || !gitutil.ZeroSHA(u.Old) { | |
| 135 | continue | |
| 136 | } | |
| 137 | if err := gitutil.SetHead(dir, branch); err != nil { | |
| 138 | slog.Error("post-receive: moving HEAD", "repo", repo.Path(), "err", err) | |
| 139 | return | |
| 140 | } | |
| 141 | if err := st.UpdateDefaultBranch(repo.ID, branch); err != nil { | |
| 142 | slog.Error("post-receive: recording default branch", "repo", repo.Path(), "err", err) | |
| 143 | return | |
| 144 | } | |
| 145 | repo.DefaultBranch = branch | |
| 146 | return | |
| 147 | } | |
| 148 | } | |
| 149 | ||
| 150 | // sameChange reports whether the new head proposes the diff the old one | |
| 151 | // did: the patch-id of each revision against its own merge base. A | |
| 152 | // rebase onto a moved target changes every sha and nothing about the | |
| 153 | // change, and the reviews of it should not go stale for that (#198). | |
| 154 | // Any doubt answers false, which is the old behaviour. | |
| 155 | func sameChange(dir string, mr store.MR, newBase, newHead string) bool { | |
| 156 | if mr.HeadSHA == "" || newBase == "" || mr.HeadSHA == newHead { | |
| 157 | return false | |
| 158 | } | |
| 159 | oldBase, err := gitutil.MergeBase(dir, "refs/heads/"+mr.TargetRef, mr.HeadSHA) | |
| 160 | if err != nil { | |
| 161 | return false | |
| 162 | } | |
| 163 | oldID, err := gitutil.PatchID(dir, oldBase, mr.HeadSHA) | |
| 164 | if err != nil || oldID == "" { | |
| 165 | return false | |
| 166 | } | |
| 167 | newID, err := gitutil.PatchID(dir, newBase, newHead) | |
| 168 | return err == nil && newID == oldID | |
| 169 | } | |
| 170 | ||
| 171 | // QueueTagBuilds runs the jobs whose tag pattern matches a pushed tag. | |
| 172 | // The build records the tag as its ref and the peeled commit as its sha, | |
| 173 | // so statuses land on the commit, not an annotated tag object. | |
| 174 | func QueueTagBuilds(st *store.Store, cfg config.Config, repo store.Repo, userID int64, tag, pushed string) { | |
| 175 | dir := RepoDir(cfg.Server.Root, repo.OwnerName, repo.Name) | |
| 176 | sha, err := gitutil.PeelToCommit(dir, pushed) | |
| 177 | if err != nil { | |
| 178 | return | |
| 179 | } | |
| 180 | raw, err := gitutil.ReadBlob(dir, sha, ci.ConfigPath, 1<<16) | |
| 181 | if err != nil { | |
| 182 | return | |
| 183 | } | |
| 184 | jobs, err := ci.Parse(raw) | |
| 185 | if err != nil { | |
| 186 | return // the branch push already reported ci/config | |
| 187 | } | |
| 188 | for _, j := range jobs { | |
| 189 | if j.Tags == "" { | |
| 190 | continue | |
| 191 | } | |
| 192 | if ok, _ := path.Match(j.Tags, tag); !ok { | |
| 193 | continue | |
| 194 | } | |
| 195 | steps, _ := json.Marshal(j.Steps) | |
| 196 | n, err := st.CreateBuild(repo.ID, j.Name, sha, tag, string(steps), j.Image, "", true) | |
| 197 | if err != nil { | |
| 198 | slog.Error("queueing tag build", "repo", repo.Path(), "job", j.Name, "err", err) | |
| 199 | continue | |
| 200 | } | |
| 201 | url := fmt.Sprintf("%s/%s/builds/%d", cfg.Server.SiteURL, repo.Path(), n) | |
| 202 | st.SetCommitStatus(repo.ID, sha, "ci/"+j.Name, "pending", "tag "+tag, url, userID) | |
| 203 | } | |
| 204 | } | |
| 205 | ||
| 206 | func cutHeads(ref string) (string, bool) { | |
| 207 | const p = "refs/heads/" | |
| 208 | if len(ref) > len(p) && ref[:len(p)] == p { | |
| 209 | return ref[len(p):], true | |
| 210 | } | |
| 211 | return "", false | |
| 212 | } | |
internal/gitutil/merge.go +17 −9
| @@ -211,6 +211,21 @@ func CommitFileChange(dir, branch, path string, content []byte, name, email, mes | ||
| 211 | 211 | parent = "" |
| 212 | 212 | } |
| 213 | 213 | |
| 214 | sha, err := CommitWithFile(dir, parent, path, "100644", content, name, email, message) | |
| 215 | if err != nil { | |
| 216 | return "", err | |
| 217 | } | |
| 218 | if err := UpdateRefCAS(dir, branchRef, sha, parent); err != nil { | |
| 219 | return "", fmt.Errorf("branch moved during edit; reload and retry: %w", err) | |
| 220 | } | |
| 221 | return sha, nil | |
| 222 | } | |
| 223 | ||
| 224 | // CommitWithFile writes a commit on parent ("" for a root commit) whose | |
| 225 | // tree is parent's with content at path, as a file of mode (100644 or | |
| 226 | // 100755), authored and committed as name <email>. No ref moves: the | |
| 227 | // caller updates one, and enforces policy, since no hook runs. | |
| 228 | func CommitWithFile(dir, parent, path, mode string, content []byte, name, email, message string) (string, error) { | |
| 214 | 229 | // Hash the new blob. |
| 215 | 230 | hb := exec.Command(toolpath.Look("git"), "-C", dir, "hash-object", "-w", "--stdin") |
| 216 | 231 | hb.Stdin = strings.NewReader(string(content)) |
| @@ -239,7 +254,7 @@ func CommitFileChange(dir, branch, path string, content []byte, name, email, mes | ||
| 239 | 254 | if out, err := rt.CombinedOutput(); err != nil { |
| 240 | 255 | return "", fmt.Errorf("read-tree: %v\n%s", err, out) |
| 241 | 256 | } |
| 242 | ui := exec.Command(toolpath.Look("git"), "-C", dir, "update-index", "--add", "--cacheinfo", "100644,"+blob+","+path) | |
| 257 | ui := exec.Command(toolpath.Look("git"), "-C", dir, "update-index", "--add", "--cacheinfo", mode+","+blob+","+path) | |
| 243 | 258 | ui.Env = env |
| 244 | 259 | if out, err := ui.CombinedOutput(); err != nil { |
| 245 | 260 | return "", fmt.Errorf("update-index: %v\n%s", err, out) |
| @@ -256,14 +271,7 @@ func CommitFileChange(dir, branch, path string, content []byte, name, email, mes | ||
| 256 | 271 | if parent != "" { |
| 257 | 272 | parents = []string{parent} |
| 258 | 273 | } |
| 259 | sha, err := CommitTree(dir, tree, parents, name, email, message) | |
| 260 | if err != nil { | |
| 261 | return "", err | |
| 262 | } | |
| 263 | if err := UpdateRefCAS(dir, branchRef, sha, parent); err != nil { | |
| 264 | return "", fmt.Errorf("branch moved during edit; reload and retry: %w", err) | |
| 265 | } | |
| 266 | return sha, nil | |
| 274 | return CommitTree(dir, tree, parents, name, email, message) | |
| 267 | 275 | } |
| 268 | 276 | |
| 269 | 277 | // isEmptyRepo reports whether dir has no refs at all — a repository |
internal/hookd/hookd.go +4 −196
| @@ -15,12 +15,9 @@ import ( | ||
| 15 | 15 | "log/slog" |
| 16 | 16 | "net" |
| 17 | 17 | "os" |
| 18 | "path" | |
| 19 | 18 | "path/filepath" |
| 20 | 19 | "strings" |
| 21 | "time" | |
| 22 | 20 | |
| 23 | "gitbay.org/gitbay/internal/ci" | |
| 24 | 21 | "gitbay.org/gitbay/internal/config" |
| 25 | 22 | "gitbay.org/gitbay/internal/control" |
| 26 | 23 | "gitbay.org/gitbay/internal/gitutil" |
| @@ -275,204 +272,15 @@ func (s *Server) releaseAnchors(repo store.Repo, updates []policy.RefUpdate) str | ||
| 275 | 272 | return "" |
| 276 | 273 | } |
| 277 | 274 | |
| 278 | // postReceive applies the cross-repo MR effect: a push to a source branch | |
| 279 | // refreshes refs/merge-requests/N/head in every target repo, by fetching — | |
| 280 | // the target owns the objects, so the MR outlives the fork. This is the only | |
| 281 | // place a hook writes outside its own repository. | |
| 275 | // postReceive runs the ref-update work for a push; see | |
| 276 | // control.RefsUpdated, which server-side writes to a branch share. | |
| 282 | 277 | func (s *Server) postReceive(req Request) { |
| 283 | pushedRepo, pushedRepoErr := s.st.RepoByID(req.RepoID) | |
| 284 | if pushedRepoErr == nil { | |
| 285 | s.adoptDefaultBranch(&pushedRepo, req.Updates) | |
| 286 | } | |
| 287 | for _, u := range req.Updates { | |
| 288 | // Every ref update is an event webhooks can subscribe to. | |
| 289 | s.st.RecordEvent(req.RepoID, req.UserID, "push", fmt.Sprintf( | |
| 290 | `{"ref":%q,"old":%q,"new":%q,"forced":%v,"deleted":%v}`, | |
| 291 | u.Ref, u.Old, u.New, u.IsForce, u.IsDelete)) | |
| 292 | ||
| 293 | // Any ref update — branch or tag — schedules the push mirrors. | |
| 294 | s.st.MarkMirrorsDirty(req.RepoID, "push") | |
| 295 | ||
| 296 | // Tag pushes run the tag-triggered CI jobs. | |
| 297 | if tag, ok := strings.CutPrefix(u.Ref, "refs/tags/"); ok && !u.IsDelete && pushedRepoErr == nil { | |
| 298 | s.queueTagBuilds(pushedRepo, req.UserID, tag, u.New) | |
| 299 | } | |
| 300 | ||
| 301 | branch, ok := cutHeads(u.Ref) | |
| 302 | if !ok { | |
| 303 | continue | |
| 304 | } | |
| 305 | // Commits landing on the default branch act on issue references | |
| 306 | // in their messages (closes #N, plain #N). | |
| 307 | if pushedRepoErr == nil && branch == pushedRepo.DefaultBranch && !u.IsDelete { | |
| 308 | dir := control.RepoDir(s.cfg.Server.Root, pushedRepo.OwnerName, pushedRepo.Name) | |
| 309 | control.ProcessCommitMessages(s.st, dir, pushedRepo, req.UserID, req.Scope, u.Old, u.New) | |
| 310 | control.RecordLandedCommits(s.st, dir, pushedRepo, u.Old, u.New) | |
| 311 | } | |
| 312 | // A branch push with a .gitbay/ci.yml queues one build per job. | |
| 313 | if pushedRepoErr == nil && !u.IsDelete { | |
| 314 | s.queueBuilds(pushedRepo, req.UserID, branch, u.Old, u.New) | |
| 315 | } | |
| 316 | if u.IsForce { | |
| 317 | s.st.Audit(req.UserID, "push.forced", map[string]any{ | |
| 318 | "repo": req.RepoID, "ref": u.Ref, "old": u.Old, "new": u.New}) | |
| 319 | } | |
| 320 | mrs, err := s.st.OpenMRsBySource(req.RepoID, branch) | |
| 321 | if err != nil { | |
| 322 | slog.Error("post-receive: listing MRs", "err", err) | |
| 323 | continue | |
| 324 | } | |
| 325 | srcRepo, err := s.st.RepoByID(req.RepoID) | |
| 326 | if err != nil { | |
| 327 | continue | |
| 328 | } | |
| 329 | srcDir := control.RepoDir(s.cfg.Server.Root, srcRepo.OwnerName, srcRepo.Name) | |
| 330 | for _, mr := range mrs { | |
| 331 | target, err := s.st.RepoByID(mr.RepoID) | |
| 332 | if err != nil { | |
| 333 | continue | |
| 334 | } | |
| 335 | if u.IsDelete { | |
| 336 | if mr.State == "open" { | |
| 337 | s.st.SetMRState(mr.ID, "source_gone") | |
| 338 | } | |
| 339 | if mr.QueuedAt != "" { | |
| 340 | control.TryQueuedMerge(s.st, s.cfg, mr.ID) // dequeues: the source is gone | |
| 341 | } | |
| 342 | continue // head ref retained: the diff stays viewable | |
| 343 | } | |
| 344 | dstDir := control.RepoDir(s.cfg.Server.Root, target.OwnerName, target.Name) | |
| 345 | headRef := fmt.Sprintf("refs/merge-requests/%d/head", mr.Number) | |
| 346 | if err := gitutil.FetchInto(dstDir, srcDir, u.New, headRef); err != nil { | |
| 347 | slog.Error("post-receive: refreshing MR head", "mr", mr.Number, "err", err) | |
| 348 | continue | |
| 349 | } | |
| 350 | // The merge base as it stands now, so a later range-diff | |
| 351 | // compares each revision against the target it was written | |
| 352 | // on rather than against today's. Best-effort: a base that | |
| 353 | // cannot be worked out costs precision, not the record. | |
| 354 | base, err := gitutil.MergeBase(dstDir, "refs/heads/"+mr.TargetRef, headRef) | |
| 355 | if err != nil { | |
| 356 | base = "" | |
| 357 | } | |
| 358 | if err := s.st.UpdateMRHead(mr.ID, u.New, base, sameChange(dstDir, mr, base, u.New)); err != nil { | |
| 359 | slog.Error("post-receive: recording MR head", "mr", mr.Number, "err", err) | |
| 360 | } | |
| 361 | if srcRepo.ID != target.ID { | |
| 362 | control.QueueMRBuilds(s.st, s.cfg.Server.Root, s.cfg.Server.SiteURL, | |
| 363 | target, req.UserID, mr.Number, u.New) | |
| 364 | } | |
| 365 | if mr.State == "source_gone" { | |
| 366 | s.st.SetMRState(mr.ID, "open") // branch came back | |
| 367 | } | |
| 368 | // A queued merge stays queued across a push by someone who can | |
| 369 | // merge it, and the new head has to pass the gates on its own. | |
| 370 | if mr.QueuedAt != "" { | |
| 371 | control.QueuedMergePushed(s.st, s.cfg, mr.ID, req.UserID, req.Scope) | |
| 372 | } | |
| 373 | } | |
| 374 | } | |
| 375 | } | |
| 376 | ||
| 377 | // adoptDefaultBranch moves an unborn HEAD to the first branch a push | |
| 378 | // creates. A repository is initialised with HEAD at the stored default, | |
| 379 | // and a first push of master or trunk left HEAD naming a branch that did | |
| 380 | // not exist: clones checked out nothing and every surface asked git for | |
| 381 | // a branch that was not there (#189). A push that includes the default | |
| 382 | // branch itself needs nothing. | |
| 383 | func (s *Server) adoptDefaultBranch(repo *store.Repo, updates []policy.RefUpdate) { | |
| 384 | dir := control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name) | |
| 385 | if _, err := gitutil.ResolveRef(dir, "refs/heads/"+repo.DefaultBranch); err == nil { | |
| 386 | return | |
| 387 | } | |
| 388 | for _, u := range updates { | |
| 389 | branch, ok := cutHeads(u.Ref) | |
| 390 | if !ok || u.IsDelete || !gitutil.ZeroSHA(u.Old) { | |
| 391 | continue | |
| 392 | } | |
| 393 | if err := gitutil.SetHead(dir, branch); err != nil { | |
| 394 | slog.Error("post-receive: moving HEAD", "repo", repo.Path(), "err", err) | |
| 395 | return | |
| 396 | } | |
| 397 | if err := s.st.UpdateDefaultBranch(repo.ID, branch); err != nil { | |
| 398 | slog.Error("post-receive: recording default branch", "repo", repo.Path(), "err", err) | |
| 399 | return | |
| 400 | } | |
| 401 | repo.DefaultBranch = branch | |
| 402 | return | |
| 403 | } | |
| 404 | } | |
| 405 | ||
| 406 | // sameChange reports whether the new head proposes the diff the old one | |
| 407 | // did: the patch-id of each revision against its own merge base. A | |
| 408 | // rebase onto a moved target changes every sha and nothing about the | |
| 409 | // change, and the reviews of it should not go stale for that (#198). | |
| 410 | // Any doubt answers false, which is the old behaviour. | |
| 411 | func sameChange(dir string, mr store.MR, newBase, newHead string) bool { | |
| 412 | if mr.HeadSHA == "" || newBase == "" || mr.HeadSHA == newHead { | |
| 413 | return false | |
| 414 | } | |
| 415 | oldBase, err := gitutil.MergeBase(dir, "refs/heads/"+mr.TargetRef, mr.HeadSHA) | |
| 416 | if err != nil { | |
| 417 | return false | |
| 418 | } | |
| 419 | oldID, err := gitutil.PatchID(dir, oldBase, mr.HeadSHA) | |
| 420 | if err != nil || oldID == "" { | |
| 421 | return false | |
| 422 | } | |
| 423 | newID, err := gitutil.PatchID(dir, newBase, newHead) | |
| 424 | return err == nil && newID == oldID | |
| 425 | } | |
| 426 | ||
| 427 | // queueBuilds queues the push jobs for a branch update. The work is | |
| 428 | // shared with the merge path, which moves a ref without reaching a hook. | |
| 429 | func (s *Server) queueBuilds(repo store.Repo, userID int64, branch, old, sha string) { | |
| 430 | control.QueueBranchBuilds( | |
| 431 | s.st, s.cfg.Server.Root, s.cfg.Server.SiteURL, | |
| 432 | repo, userID, branch, old, sha, time.Now()) | |
| 278 | control.RefsUpdated(s.st, s.cfg, req.RepoID, req.UserID, req.Scope, req.Updates) | |
| 433 | 279 | } |
| 434 | 280 | |
| 435 | 281 | // queueTagBuilds runs the jobs whose tag pattern matches a pushed tag. |
| 436 | // The build records the tag as its ref and the peeled commit as its sha, | |
| 437 | // so statuses land on the commit, not an annotated tag object. | |
| 438 | 282 | func (s *Server) queueTagBuilds(repo store.Repo, userID int64, tag, pushed string) { |
| 439 | dir := control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name) | |
| 440 | sha, err := gitutil.PeelToCommit(dir, pushed) | |
| 441 | if err != nil { | |
| 442 | return | |
| 443 | } | |
| 444 | raw, err := gitutil.ReadBlob(dir, sha, ci.ConfigPath, 1<<16) | |
| 445 | if err != nil { | |
| 446 | return | |
| 447 | } | |
| 448 | jobs, err := ci.Parse(raw) | |
| 449 | if err != nil { | |
| 450 | return // the branch push already reported ci/config | |
| 451 | } | |
| 452 | for _, j := range jobs { | |
| 453 | if j.Tags == "" { | |
| 454 | continue | |
| 455 | } | |
| 456 | if ok, _ := path.Match(j.Tags, tag); !ok { | |
| 457 | continue | |
| 458 | } | |
| 459 | steps, _ := json.Marshal(j.Steps) | |
| 460 | n, err := s.st.CreateBuild(repo.ID, j.Name, sha, tag, string(steps), j.Image, "", true) | |
| 461 | if err != nil { | |
| 462 | slog.Error("queueing tag build", "repo", repo.Path(), "job", j.Name, "err", err) | |
| 463 | continue | |
| 464 | } | |
| 465 | url := fmt.Sprintf("%s/%s/builds/%d", s.cfg.Server.SiteURL, repo.Path(), n) | |
| 466 | s.st.SetCommitStatus(repo.ID, sha, "ci/"+j.Name, "pending", "tag "+tag, url, userID) | |
| 467 | } | |
| 468 | } | |
| 469 | ||
| 470 | func cutHeads(ref string) (string, bool) { | |
| 471 | const p = "refs/heads/" | |
| 472 | if len(ref) > len(p) && ref[:len(p)] == p { | |
| 473 | return ref[len(p):], true | |
| 474 | } | |
| 475 | return "", false | |
| 283 | control.QueueTagBuilds(s.st, s.cfg, repo, userID, tag, pushed) | |
| 476 | 284 | } |
| 477 | 285 | |
| 478 | 286 | // Ask sends one request from the hook process to the daemon. stream is |