vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
Show what a merged change request landed
2841d922c15ce32fb600d992f9f5dc2d22c48f90 · Keenan Nemetz · 13d ago · parent c765ae2e
Show what a merged change request landed Landing moves the base to include the head, so the three-dot diff the Files tab reviews an open request with becomes empty by definition: the merge base of the base and the head is the head itself. A merged request therefore looked like it had never changed anything. The landing mark now records `merged_base`, the tip the base pointed at before it moved, and the Files tab diffs that against what landed: the head for a fast-forward, the merge commit otherwise, so conflict resolution is part of the review too. The record is written in the same commit as the mark, and a retry that finds the head already landed keeps the base it has instead of naming the moved tip. `merged_base` is optional on read because landings made before the field existed have none. Such a record falls back to the merge commit's first parent when the landing was a merge, and claims nothing when it was a fast-forward and the starting point is gone. The request page shows the file count and the review link for a merged request as well, with the merge named beside it, instead of replacing them with "Already merged".
metadata/requests.go +23−6
| @@ -54,6 +54,12 @@ type Request struct { | |||
| 54 | 54 | State string // open | merged | closed | |
| 55 | 55 | Created time.Time | |
| 56 | 56 | Description string | |
| 57 | // MergedBase is what Base pointed at before the landing. It is the "before" side | ||
| 58 | // of a merged request: once the head is in the base, a diff against the base as | ||
| 59 | // it is now is empty, so the record has to name where the branch started from. | ||
| 60 | // It is empty on a record written before this field existed and on a landing that | ||
| 61 | // found the head already in the base, which had no base tip of its own to name. | ||
| 62 | MergedBase string | ||
| 57 | 63 | // MergedHead is the commit that landed on Base, which is the head itself for a | |
| 58 | 64 | // fast-forward and a merge commit otherwise. It is empty unless State is | |
| 59 | 65 | // "merged": a request that claims a landing has to name one, so the file can | |
| @@ -106,6 +112,7 @@ type requestMeta struct { | |||
| 106 | 112 | State string `yaml:"state"` | |
| 107 | 113 | Closes []string `yaml:"closes"` | |
| 108 | 114 | Created string `yaml:"created"` | |
| 115 | MergedBase string `yaml:"merged_base"` | ||
| 109 | 116 | MergedHead string `yaml:"merged_head"` | |
| 110 | 117 | MergedBy string `yaml:"merged_by"` | |
| 111 | 118 | MergedAt string `yaml:"merged_at"` | |
| @@ -271,13 +278,23 @@ func applyRequest(req *Request, raw, path string, problems *[]string) { | |||
| 271 | 278 | closes, closeProblems := parseCloses(m.Closes, path) | |
| 272 | 279 | *problems = append(*problems, closeProblems...) | |
| 273 | 280 | req.Closes = closes | |
| 274 | // The merged fields are the record of a landing, so they are all-or-nothing and | ||
| 275 | // only exist on a merged request. A file that says "merged" without naming a | ||
| 276 | // commit is refused rather than shown as landed, and a stale merged_head left on | ||
| 277 | // a reopened request is refused too: the next landing writes a new one. | ||
| 281 | // The merged fields are the record of a landing, so they only exist on a merged | ||
| 282 | // request. A file that says "merged" without naming a commit is refused rather | ||
| 283 | // than shown as landed, and a stale merged_head left on a reopened request is | ||
| 284 | // refused too: the next landing writes a new one. merged_base is the one optional | ||
| 285 | // member: it was added after the first landings and a landing that found the head | ||
| 286 | // already in the base has no base tip of its own to name. | ||
| 287 | req.MergedBase = strings.TrimSpace(m.MergedBase) | ||
| 278 | 288 | req.MergedHead = strings.TrimSpace(m.MergedHead) | |
| 279 | 289 | req.MergedBy = strings.TrimSpace(m.MergedBy) | |
| 280 | 290 | if req.State == "merged" { | |
| 291 | // merged_base is optional because it was added after the first landings, so a | ||
| 292 | // file without one is an older record, not a broken one. A file that names one | ||
| 293 | // has to name a commit. | ||
| 294 | if req.MergedBase != "" && !shaRE.MatchString(req.MergedBase) { | ||
| 295 | *problems = append(*problems, fmt.Sprintf( | ||
| 296 | "%s: merged_base must be the full commit id the base pointed at before the landing", path)) | ||
| 297 | } | ||
| 281 | 298 | if !shaRE.MatchString(req.MergedHead) { | |
| 282 | 299 | *problems = append(*problems, fmt.Sprintf( | |
| 283 | 300 | "%s: state is merged, so merged_head must be the full commit id that landed", path)) | |
| @@ -294,9 +311,9 @@ func applyRequest(req *Request, raw, path string, problems *[]string) { | |||
| 294 | 311 | } else { | |
| 295 | 312 | req.MergedAt = t.UTC() | |
| 296 | 313 | } | |
| 297 | } else if req.MergedHead != "" || req.MergedBy != "" || strings.TrimSpace(m.MergedAt) != "" { | ||
| 314 | } else if req.MergedBase != "" || req.MergedHead != "" || req.MergedBy != "" || strings.TrimSpace(m.MergedAt) != "" { | ||
| 298 | 315 | *problems = append(*problems, fmt.Sprintf( | |
| 299 | "%s: merged_head, merged_by and merged_at only belong on a merged request (state is %q)", path, req.State)) | ||
| 316 | "%s: merged_base, merged_head, merged_by and merged_at only belong on a merged request (state is %q)", path, req.State)) | ||
| 300 | 317 | } | |
| 301 | 318 | req.Description = body | |
| 302 | 319 | } | |