-
Notifications
You must be signed in to change notification settings - Fork 0
No issue reference is appended when committing through the editor (#679) #684
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
trixy-the-ai-bot
wants to merge
12
commits into
main
from
issue-679/no-issue-reference-is-appended-when-committing-thr
Closed
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
3624270
fix(githooks): append issue reference on the editor commit path (#679)
f87df37
fix(githooks): accept chained autosquash prefixes and make append bes…
e26d6c3
test(githooks): isolate ambient git config and add discriminating cas…
55252dc
ci: install zsh so the hook test matrix actually runs it (#679)
8e590e1
docs(adr): trim ADR-0048 to the decision and fix no-op wording (#679)
647b1e4
fix(githooks): keep commit.template quit-to-cancel intact on the edit…
4cda570
fix(githooks): preserve the commit message file's mode across the app…
2d12c17
docs(adr): enumerate the -m/reuse behaviour deltas versus main (#679)
b2eedb6
docs(adr): describe the trailing-reference invariant accurately (#679)
2da7ca0
fix(githooks): catch untouched CLI templates in the quit-to-cancel gu…
f921004
docs(adr): note the template guard covers all spellings via a snapsho…
0f26b8c
test(githooks): pin the unconditional snapshot-clear invariant (#679)
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,141 @@ | ||
| #!/bin/sh | ||
|
|
||
| # Regex to check for conventional commits (wip allowed locally, blocked on push) | ||
| commit_regex='^((fixup|squash)! )?(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|wip)(\([a-zA-Z0-9_-]+\))?: .+' | ||
| # commit-msg hook | ||
| # | ||
| # Two responsibilities, in this order: | ||
| # | ||
| # 1. Reject a subject that is not in Conventional Commits format. | ||
| # 2. Append the branch's issue reference on the EDITOR path — the commit | ||
| # prepare-commit-msg cannot reach, because it runs before the editor opens | ||
| # and there is no subject to append to yet (#679). | ||
| # | ||
| # Validation runs first and needs no shared library, so a missing lib can never | ||
| # silently disable it — the check every push depends on keeps working even if | ||
| # the issue-reference feature cannot load. | ||
|
|
||
| if ! grep -iqE "$commit_regex" "$1"; then | ||
| # --- 1. Conventional Commits validation ------------------------------------- | ||
| # (wip allowed locally, blocked on push) | ||
| # | ||
| # Validate the SUBJECT — the first non-blank line, the same line the reference | ||
| # logic below treats as the subject — not the whole file. A grep over the whole | ||
| # file passes a non-conventional subject on the strength of a | ||
| # Conventional-Commits-shaped line in the BODY (`git commit -m 'wip' -m 'fix: x'` | ||
| # recorded `wip` as the subject); reading only the first non-blank line closes | ||
| # that. On the editor-quit path that first non-blank line is one of git's own | ||
| # comment lines, which never matches the regex, so the commit still aborts — | ||
| # under the default comment character and under core.commentChar / | ||
| # core.commentString alike. | ||
| # | ||
| # `amend!` joins `fixup!`/`squash!` in the accepted prefixes, and the `*` | ||
| # quantifier accepts them REPEATED: git prepends another prefix when the fixup | ||
| # target is itself a fixup (`git commit --fixup` of a `fixup!` commit yields | ||
| # `fixup! fixup! <subject>`), so a chained subject is just as git-authored as a | ||
| # single one. append_issue_reference leaves all of them verbatim (its guard | ||
| # matches the leading prefix), so they must pass validation to reach it — with | ||
| # `?` a `fixup! fixup!` subject was rejected and the commit blocked outright. | ||
| commit_regex='^((fixup|squash|amend)! )*(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|wip)(\([a-zA-Z0-9_-]+\))?: .+' | ||
|
|
||
| subject=$(awk 'NF { print; exit }' "$1") | ||
| if ! printf '%s\n' "$subject" | grep -iqE "$commit_regex"; then | ||
| echo "❌ Error: Commit message does not follow Conventional Commits format." | ||
| echo "Expected format: type(scope): subject" | ||
| echo "Examples: " | ||
| echo " feat: add user login" | ||
| echo " fix(api): resolve null pointer exception" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Everything from here down is the issue-reference append. It must never fabricate | ||
| # a subject out of a commit the user is trying to CANCEL, which git detects two | ||
| # ways on the editor path — and both are honoured before anything is written: | ||
| # | ||
| # * A comments-only file (quit without saving, no template) fails the | ||
| # conventional-format validation above and is rejected there — the first | ||
| # non-blank line is one of git's own comment lines, which never matches. | ||
| # * An untouched template — under ANY spelling (`commit.template`, | ||
| # `--template`, `-t`, or a CLI template overriding a config one) — is caught | ||
| # by the guard just below. git's own "did not edit the message" abort | ||
| # compares the final message to the template, but that comparison runs AFTER | ||
| # this hook, so appending here would convince git the user edited it. The | ||
| # guard skips the append when the message still equals the template | ||
| # (snapshotted by prepare-commit-msg, the hook git tells the spelling to), | ||
| # leaving git's abort intact. | ||
|
|
||
| # --- 2. Issue reference on the editor path ---------------------------------- | ||
| # | ||
| # commit-msg is handed only the message file; git does not pass $COMMIT_SOURCE | ||
| # here. So the cases where prepare-commit-msg would have skipped are | ||
| # reconstructed from intrinsic signals instead: | ||
| # | ||
| # * merge — MERGE_HEAD exists. Guarded here. | ||
| # * squash — SQUASH_MSG exists (git merge --squash, rebase squash). | ||
| # Guarded here. | ||
| # * autosquash — the subject begins fixup!/squash!/amend!. In the lib. | ||
| # * rebase reword/edit — HEAD is detached, so no branch name and no number. | ||
| # In the lib, via git symbolic-ref. | ||
| # * already referenced / already trailing a (#N) — in the lib. | ||
| # | ||
| # The merge and squash signals live here because they are reasons to skip the | ||
| # append outright; the per-subject guards live in append_issue_reference so | ||
| # both hooks share them. A conventional merge subject (e.g. `chore: merge x`) | ||
| # passes the validation above and would otherwise gain a reference — this is | ||
| # what keeps it byte-identical, matching the `-m`/prepare behaviour. A | ||
| # non-conventional `Merge branch ...` was already rejected by validation, as it | ||
| # is today. | ||
| if git rev-parse -q --verify MERGE_HEAD >/dev/null 2>&1 || | ||
| [ -f "$(git rev-parse --git-path SQUASH_MSG 2>/dev/null)" ]; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Locate the shared contract. Same candidate list as prepare-commit-msg: a | ||
| # `.git/hooks/* -> ../../.githooks/*` symlink layout runs this file with $0 in | ||
| # .git/hooks, where lib/ does not exist, so `dirname "$0"` alone is not enough. | ||
| # First readable candidate wins. | ||
| lib="" | ||
| for candidate in \ | ||
| "$( unset CDPATH; cd -- "$(dirname -- "$0")" 2>/dev/null && pwd )/lib/issue-reference.sh" \ | ||
| "$(git rev-parse --git-path hooks 2>/dev/null)/lib/issue-reference.sh" \ | ||
| "$(git rev-parse --show-toplevel 2>/dev/null)/.githooks/lib/issue-reference.sh"; do | ||
| if [ -r "$candidate" ]; then | ||
| lib=$candidate | ||
| break | ||
| fi | ||
| done | ||
|
|
||
| # Guard clause: degrade to a silent no-op if the lib is missing. This must come | ||
| # BEFORE the `.` below, because `.` is a POSIX special builtin whose failure | ||
| # aborts the script under sh/dash — and here that would reject a | ||
| # validation-passing commit. Validation has already run, so bailing out only | ||
| # forgoes the issue reference, never the format check. | ||
| if [ -z "$lib" ]; then | ||
| echo "commit-msg: shared issue-reference lib not found; skipping issue reference" >&2 | ||
| exit 0 | ||
| fi | ||
|
|
||
| # shellcheck source=.githooks/lib/issue-reference.sh | ||
| . "$lib" | ||
|
|
||
| # Guard: an untouched template must still abort a cancelled commit, under any | ||
| # spelling. prepare-commit-msg snapshots the pristine template into | ||
| # COMMIT_TEMPLATE_PRISTINE (it is the hook git tells which template it loaded — | ||
| # config, --template, -t, or a CLI override of a config template); if the message | ||
| # still equals that snapshot, skip the append so git's own "did not edit the | ||
| # message" abort still fires. Both sides go through message_body, so comments, | ||
| # the scissors line and core.commentChar are stripped identically. On the -m path | ||
| # prepare-commit-msg removed the snapshot and already appended, so a -m subject | ||
| # that happens to match the template text is still (correctly) referenced. The | ||
| # snapshot is consumed here so it never lingers into a later commit. | ||
| pristine=$(git rev-parse --git-path COMMIT_TEMPLATE_PRISTINE 2>/dev/null) | ||
| if [ -n "$pristine" ] && [ -f "$pristine" ]; then | ||
| if [ "$(message_body "$1")" = "$(message_body "$pristine")" ]; then | ||
| rm -f "$pristine" | ||
| exit 0 | ||
| fi | ||
| rm -f "$pristine" | ||
| fi | ||
|
|
||
| # The append is best-effort: exit 0 regardless of its status, so a failed write | ||
| # inside append_issue_reference (e.g. a failed mv) can never reject a commit that | ||
| # already passed validation. | ||
| append_issue_reference "$1" | ||
| exit 0 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,227 @@ | ||
| # shellcheck shell=sh | ||
|
|
||
| # Shared issue-reference contract for the commit-message hooks. | ||
| # | ||
| # This library is the one place the repo's branch-name -> issue-reference | ||
| # contract is written down. Two hooks source it, on the two paths git can take | ||
| # to build a subject: | ||
| # | ||
| # * prepare-commit-msg — the `-m`/`-F` path, where git already holds the | ||
| # message before any editor opens ($COMMIT_SOURCE = "message"). | ||
| # * commit-msg — the editor path, where the subject exists only AFTER the | ||
| # editor closes, and $COMMIT_SOURCE is not passed to the hook at all. | ||
| # | ||
| # Because both callers land in append_issue_reference, the accepted branch | ||
| # shapes and the bare-digits output are ONE contract, not two drifting ones | ||
| # (#679). Every reason NOT to append that can be read from the file or the | ||
| # branch lives in this function, so a caller only decides whether its path is | ||
| # append-eligible at all (the merge/squash question commit-msg answers from | ||
| # MERGE_HEAD / SQUASH_MSG), never how to append. | ||
|
|
||
| # Prints a bare GitHub issue number, or nothing at all. Accepted branch shapes: | ||
| # | ||
| # <type>/<number>[-<slug>] fix/605-converge-duplicate-tags, reviews/586 | ||
| # issue-<number>/<slug> issue-458/global-capture-fab | ||
| # JVS-<number>[-<slug>] JVS-123 | ||
| # <number>[-<slug>] 605-add-login | ||
| # | ||
| # The number is always anchored to the *start* of the last path segment, and | ||
| # what this prints is always bare digits — no prefix ever reaches the message, | ||
| # which is what makes `(#review-655)` and `(#proxy-3048)` structurally | ||
| # impossible rather than merely unlikely (#666). | ||
| # | ||
| # Three rules that look like they could be simplified away, and must not be: | ||
| # | ||
| # * A leading zero disqualifies. GitHub issue numbers never carry one; this | ||
| # repo's migration and ADR numbers always do, so `fix/0028-ambiguous- | ||
| # parameter` would otherwise link commits to issue #28. It has to be an | ||
| # anchor-adjacent example: `fix/alembic-0028-...` and `docs/...-adr-0044` | ||
| # are already rejected by the anchor, before this rule is ever consulted. | ||
| # * The `issue-*/*` arm must stay ABOVE the `*/*` arm. Swap them and | ||
| # `issue-458/global-capture-fab` silently yields nothing. | ||
| # * A trailing number is deliberately NOT accepted (`feat/login-ui-94`). | ||
| # That convention was abandoned in April 2026, and the tail of a branch | ||
| # name is exactly where versions, ADR numbers and dependabot bumps live. | ||
| # | ||
| # Known residuals, accepted and unguarded: | ||
| # | ||
| # * A date-prefixed branch such as `chore/2026-08-audit` is structurally | ||
| # identical to `<type>/<number>-<slug>` and appends `(#2026)`. | ||
| # Distinguishing it needs a heuristic worse than the disease. Don't name | ||
| # branches that way. | ||
| # * The `-m` cleanup-mode residual, described at message_body below, is a | ||
| # property of the `prepare-commit-msg` (`$COMMIT_SOURCE`=message) caller | ||
| # only. On the editor path git cleans up with `strip`, which drops comment | ||
| # lines exactly as this hook does, so the mismatch cannot arise there. | ||
| issue_number_from_branch() { | ||
| branch=$1 | ||
| case $branch in | ||
| JVS-*) rest=${branch#JVS-} ;; | ||
| issue-*/*) rest=${branch#issue-} ;; | ||
| */*) rest=${branch##*/} ;; | ||
| *) rest=$branch ;; | ||
| esac | ||
| number=${rest%%[!0-9]*} | ||
| [ -n "$number" ] || return 1 | ||
| case $number in 0*) return 1 ;; esac | ||
| # The digits must be followed by a separator or nothing — never by more | ||
| # branch name, so `fix/605_underscore` is rejected rather than truncated. | ||
| case ${rest#"$number"} in ''|-*|/*) ;; *) return 1 ;; esac | ||
| printf '%s' "$number" | ||
| } | ||
|
|
||
| # Everything in the message file that git will keep as the message, given as an | ||
| # argument so either hook can pass its own $1. On the `-m` path a plain -m | ||
| # leaves the file holding nothing but the message, but `-m ... -e` (and the | ||
| # editor path) has git append its status block and, under commit.verbose, the | ||
| # staged diff. Two things are dropped: | ||
| # | ||
| # * Everything from the scissors line down. Under `commit.verbose` git puts | ||
| # the staged diff there, un-commented — so without this a `#605` inside the | ||
| # diff reads as an existing reference and suppresses a legitimate append, | ||
| # which is the one plausible way to lose a reference on this path. | ||
| # | ||
| # Git's scissors line is comment-PREFIXED, so anchoring the trim on `#` | ||
| # loses it the moment the comment character changes — and then the entire | ||
| # diff is scanned. The pattern therefore matches the `---- >8 ----` run and | ||
| # ignores whatever precedes it, which holds identically under `#`, | ||
| # `core.commentChar`, `core.commentString` and `core.commentChar=auto`. | ||
| # * Comment lines, so git's own `# On branch fix/605-...` status block is not | ||
| # scanned for a reference. `git stripspace --strip-comments` is git's own | ||
| # plumbing for exactly this: run in the repository, it reads | ||
| # `core.commentChar` / `core.commentString` itself. Nothing is interpolated | ||
| # into a pattern and no prefix is escaped — a hand-rolled | ||
| # `grep -v "^$prefix"` would need both, because git's candidate list | ||
| # contains `$`, which raw in a BRE is the end-of-line anchor and would | ||
| # silently strip every blank line instead. | ||
| # | ||
| # What that buys, concretely: under `core.commentChar=';'` on a branch | ||
| # named `fix/605-#605`, git's own status line `; On branch fix/605-#605` | ||
| # survived a hard-coded `^#` filter, and the guard below read the `#605` | ||
| # in it as an existing reference — silently suppressing a legitimate | ||
| # append. That needed no unusual message, only an unusual config. | ||
| # | ||
| # `core.commentChar=auto` is the one spelling stripspace does not resolve: | ||
| # it falls back to the default `#` rather than running git-commit's | ||
| # per-message candidate scan. Measured on git 2.55: `-m '#605 subject' -e` | ||
| # under `auto` makes git write its status block with `;`, while stripspace | ||
| # strips the `#` line. So under `auto` this behaves exactly as the old | ||
| # hard-coded `^#` did — no better, no worse — and reimplementing the | ||
| # candidate scan is the only exact fix. `auto` is not a value anyone here | ||
| # sets, and the residual runs the safe way (below). | ||
| # | ||
| # If stripspace exits non-zero, this yields NO text, so the guard finds no | ||
| # reference and the append proceeds. That direction is deliberate: this | ||
| # function feeds only the already-referenced guard and never the message | ||
| # that gets written, so its worst case is a visible duplicate `(#605)` in | ||
| # the subject. Failing the other way would silently drop the reference the | ||
| # hook exists to add, and silence is the failure mode #666 was filed for. | ||
| # | ||
| # The residual runs one way: scanning text git will discard can only ever | ||
| # SUPPRESS an append, never fabricate a commit. On the editor path the file | ||
| # holds only comments before the subject is typed, so its worst case there is | ||
| # the same suppression — and commit-msg never even reaches this function on a | ||
| # comments-only file, because the conventional-format validation rejects it | ||
| # first. | ||
| message_body() { | ||
| stripped=$(sed -e '/-\{8,\} >8 -\{8,\}$/,$d' "$1" \ | ||
| | git stripspace --strip-comments 2>/dev/null) || return 0 | ||
| printf '%s\n' "$stripped" | ||
| } | ||
|
|
||
| # Appends `(#NNN)` to the commit subject when the branch names an issue and the | ||
| # message does not already carry the reference. Idempotent and caller-agnostic: | ||
| # every guard reads intrinsic state (the file's first non-blank line, the | ||
| # branch, the existing message), so prepare-commit-msg and commit-msg get the | ||
| # same answer on the same commit. | ||
| append_issue_reference() { | ||
| file=$1 | ||
|
|
||
| # The subject is the first NON-BLANK line, not line 1. On the `-m` path | ||
| # line 1 is always the subject, so this is line 1 there and prepare's | ||
| # output is byte-for-byte what it always was. On the editor path a | ||
| # developer may leave blank lines above the subject; git takes the first | ||
| # non-blank line as the subject, and appending to line 1 there would strand | ||
| # the reference on the blank line and corrupt the recorded subject (#679). | ||
| subject=$(awk 'NF { print; exit }' "$file") | ||
|
|
||
| # Guard: leave git's autosquash subjects byte-identical. | ||
| # | ||
| # `git commit --fixup <sha>`, `--squash <sha>`, `--fixup=amend:<sha>` and | ||
| # `--fixup=reword:<sha>` reach prepare-commit-msg with $COMMIT_SOURCE = | ||
| # "message" — the very value a plain `-m` produces — so its guard lets them | ||
| # through. But git has built these subjects itself as `fixup! <target>` / | ||
| # `squash! <target>` / `amend! <target>` (both amend: and reword: emit | ||
| # `amend!`), and `git rebase --autosquash` matches them against the | ||
| # target's subject byte-for-byte. Appending ` (#NNN)` breaks that match, so | ||
| # the fixup never squashes and survives the rebase as a standalone commit | ||
| # (#675). These are exactly the prefixes git's own autosquash recognises; a | ||
| # subject shaped like one is git's to own, so leave it verbatim. Neither | ||
| # caller's out-of-band signals ($COMMIT_SOURCE for prepare, none at all for | ||
| # commit-msg) can tell these apart from `-m`/a hand-typed subject, so the | ||
| # subject prefix is the only signal — and matching only these three | ||
| # git-authored prefixes means a non-match costs nothing. | ||
| case $subject in | ||
| 'fixup! '*|'squash! '*|'amend! '*) return 0 ;; | ||
| esac | ||
|
|
||
| # Branch -> issue number. On a detached HEAD — which is where git parks you | ||
| # mid-rebase, including autosquash — `git symbolic-ref` fails, the branch is | ||
| # empty, and no number is produced. That accident is what keeps a | ||
| # rebase-reword/autosquash-combine from acquiring a spurious reference, so a | ||
| # future git that stopped detaching HEAD there would need a real guard here. | ||
| branch=$(git symbolic-ref --short HEAD 2>/dev/null) | ||
| number=$(issue_number_from_branch "$branch") | ||
|
|
||
| # Guard: the branch name carries no issue number. This is the silent, | ||
| # successful no-op path — most ad-hoc and generated branches land here. | ||
| [ -n "$number" ] || return 0 | ||
|
|
||
| # Guard: the message already references this issue. `#` is its own left | ||
| # boundary and the right boundary is a non-digit or end of line, so `#605` | ||
| # does not match `#6051`, and a bare `605` in prose does not match at all. | ||
| # $number is digits by construction, so nothing regex-significant is | ||
| # interpolated. | ||
| if message_body "$file" | grep -qE "#${number}([^0-9]|$)"; then | ||
| return 0 | ||
| fi | ||
|
|
||
| # Guard: the subject already ends with a reference of this hook's own | ||
| # shape. This is what stops a second `(#NNN)` stacking on when a commit | ||
| # authored on one numbered branch is amended — or otherwise re-committed — | ||
| # on another: the already-referenced guard above only looks for THIS | ||
| # branch's number, so `feat: work (#605)` amended on `fix/999-b` would | ||
| # otherwise become `feat: work (#605) (#999)` (#679). A `-m` subject that | ||
| # genuinely ends in a hand-written `(#N)` is treated the same way, and not | ||
| # doubled. | ||
| if printf '%s\n' "$subject" | grep -qE ' \(#[0-9]+\)$'; then | ||
| return 0 | ||
| fi | ||
|
|
||
| # Append the reference to the end of the first non-blank line. A single awk | ||
| # pass copies the file verbatim except for that one line; `d` fires once so | ||
| # only the subject is touched. The temp file is created BESIDE the target, | ||
| # not in $TMPDIR: $TMPDIR is often a different filesystem, where `mv` is a | ||
| # copy-plus-unlink rather than an atomic rename — so a same-directory temp is | ||
| # what makes the swap atomic and keeps a half-written file from ever being | ||
| # seen. If the awk fails, or the mv fails, the temp is removed so a failed | ||
| # write leaves nothing behind. | ||
| # | ||
| # The rename replaces the inode, and mktemp makes the temp 0600, so capture | ||
| # the target's mode first and restore it after the swap — otherwise the | ||
| # message file (git's COMMIT_EDITMSG, normally 0644) would be left 0600. | ||
| # stat's flags differ across GNU/BSD; if neither answers, the mode is left | ||
| # as-is (harmless — the file is transient and lives in .git). | ||
| mode=$(stat -c '%a' "$file" 2>/dev/null || stat -f '%Lp' "$file" 2>/dev/null) | ||
| tmp=$(mktemp "$file.XXXXXX") || return 0 | ||
| if awk -v ref=" (#${number})" '!d && NF { print $0 ref; d=1; next } { print }' \ | ||
| "$file" > "$tmp"; then | ||
| if mv "$tmp" "$file"; then | ||
| [ -n "$mode" ] && chmod "$mode" "$file" 2>/dev/null | ||
| else | ||
| rm -f "$tmp" | ||
| fi | ||
| else | ||
| rm -f "$tmp" | ||
| fi | ||
|
trixy-the-ai-bot marked this conversation as resolved.
|
||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.