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 | parent = "" | 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 | // Hash the new blob. | 229 | // Hash the new blob. |
| 215 | hb := exec.Command(toolpath.Look("git"), "-C", dir, "hash-object", "-w", "--stdin") | 230 | hb := exec.Command(toolpath.Look("git"), "-C", dir, "hash-object", "-w", "--stdin") |
| 216 | hb.Stdin = strings.NewReader(string(content)) | 231 | hb.Stdin = strings.NewReader(string(content)) |
| @@ -239,7 +254,7 @@ func CommitFileChange(dir, branch, path string, content []byte, name, email, mes | |||
| 239 | if out, err := rt.CombinedOutput(); err != nil { | 254 | if out, err := rt.CombinedOutput(); err != nil { |
| 240 | return "", fmt.Errorf("read-tree: %v\n%s", err, out) | 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 | ui.Env = env | 258 | ui.Env = env |
| 244 | if out, err := ui.CombinedOutput(); err != nil { | 259 | if out, err := ui.CombinedOutput(); err != nil { |
| 245 | return "", fmt.Errorf("update-index: %v\n%s", err, out) | 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 | if parent != "" { | 271 | if parent != "" { |
| 257 | parents = []string{parent} | 272 | parents = []string{parent} |
| 258 | } | 273 | } |
| 259 | sha, err := CommitTree(dir, tree, parents, name, email, message) | 274 | return 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 | ||
| 267 | } | 275 | } |
| 268 | 276 | ||
| 269 | // isEmptyRepo reports whether dir has no refs at all — a repository | 277 | // isEmptyRepo reports whether dir has no refs at all — a repository |
internal/hookd/hookd.go +4 −196
| @@ -15,12 +15,9 @@ import ( | |||
| 15 | "log/slog" | 15 | "log/slog" |
| 16 | "net" | 16 | "net" |
| 17 | "os" | 17 | "os" |
| 18 | "path" | ||
| 19 | "path/filepath" | 18 | "path/filepath" |
| 20 | "strings" | 19 | "strings" |
| 21 | "time" | ||
| 22 | 20 | ||
| 23 | "gitbay.org/gitbay/internal/ci" | ||
| 24 | "gitbay.org/gitbay/internal/config" | 21 | "gitbay.org/gitbay/internal/config" |
| 25 | "gitbay.org/gitbay/internal/control" | 22 | "gitbay.org/gitbay/internal/control" |
| 26 | "gitbay.org/gitbay/internal/gitutil" | 23 | "gitbay.org/gitbay/internal/gitutil" |
| @@ -275,204 +272,15 @@ func (s *Server) releaseAnchors(repo store.Repo, updates []policy.RefUpdate) str | |||
| 275 | return "" | 272 | return "" |
| 276 | } | 273 | } |
| 277 | 274 | ||
| 278 | // postReceive applies the cross-repo MR effect: a push to a source branch | 275 | // postReceive runs the ref-update work for a push; see |
| 279 | // refreshes refs/merge-requests/N/head in every target repo, by fetching — | 276 | // control.RefsUpdated, which server-side writes to a branch share. |
| 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. | ||
| 282 | func (s *Server) postReceive(req Request) { | 277 | func (s *Server) postReceive(req Request) { |
| 283 | pushedRepo, pushedRepoErr := s.st.RepoByID(req.RepoID) | 278 | control.RefsUpdated(s.st, s.cfg, req.RepoID, req.UserID, req.Scope, req.Updates) |
| 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()) | ||
| 433 | } | 279 | } |
| 434 | 280 | ||
| 435 | // queueTagBuilds runs the jobs whose tag pattern matches a pushed tag. | 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 | func (s *Server) queueTagBuilds(repo store.Repo, userID int64, tag, pushed string) { | 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) | 283 | control.QueueTagBuilds(s.st, s.cfg, repo, userID, tag, pushed) |
| 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 | ||
| 476 | } | 284 | } |
| 477 | 285 | ||
| 478 | // Ask sends one request from the hook process to the daemon. stream is | 286 | // Ask sends one request from the hook process to the daemon. stream is |