TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Checked 45 open PRs at once: Five merge states, three conflicts, and rebasing without a clone

Side note: This is the procedure for tending to open PRs, not merged ones. All PRs mentioned in this article are in progress as of 2026-11-13. In fact, these were taken from GitHub's PR and check logs, documentation, original contribution rules for each project, and raw query data and measured records from contribution logs. Times are in Korea Standard Time.

Table of Contents

One-line summary

Target 45 open PRs in the upstream project. Submitted between mid and late October, none of which are drafts. 7 older PRs from my own repository and community repositories are counted separately.
Query gh pr view --json mergeable,mergeStateStatus,reviewDecision once per PR
Distribution BLOCKED 24 · UNSTABLE 9 · CLEAN 7 · BEHIND 3 · DIRTY 2. First query had DIRTY 3 and UNKNOWN 4.
Handling Out of the three conflicts, one was resolved by rebasing onto the new base. The other two need to be rewritten and are in progress.
열린 PR 45건의 병합 상태를 한 번에 조회했다 2026-11-13 재조회 · gh pr view --json mergeStateStatus ① 상태별 건수: 충돌은 DIRTY 뿐이다 BLOCKED 24 리뷰 대기 UNSTABLE 9 검사 미통과 CLEAN 7 병합 가능 BEHIND 3 뒤처짐 DIRTY 2 충돌 ② 충돌 원인 셋과 처리(첫 조회의 DIRTY 는 3건) 이웃 줄이 고쳐짐 dynamo#16465 · 문서 2개 새 main 위로 재기준 +15/−4 그대로 · 해소 문서를 통째로 다시 씀 dcgm-exporter#760 새 문서에 맞춰 다시 쓰기 새 판 4.9.0 미실측 · 진행 중 주 버전 개편(v3) catwalk#676 새 형식으로 다시 쓰기 새 Go 도구로 빌드 · 진행 중 BEHIND 3건은 충돌이 아니다: 기준 브랜치만 앞서 있다 첫 조회의 UNKNOWN 4건은 다시 조회해야 값이 나온다
Figure: Distribution of merge states for 45 open PRs and causes/handling of the three conflicts

1. Why check: Open PRs grow stale even when left alone

On the day you submit a PR, there are no conflicts and checks pass. However, as you wait for reviews over several weeks, the base branch keeps moving. Other people modify the same files, the documentation structure changes, and the environment of the runner executing checks changes. Our PR hasn't changed a single character, yet one day a conflict label appears or tests that used to pass start failing.

From a maintainer's perspective, a PR with conflicts is a PR that cannot be reviewed. When your turn for review comes up and there are conflicts, that turn simply gets passed by. Therefore, after open PRs grew to dozens, I decided not to just look when notifications arrive, but to query the whole set at once and categorize them by status.

This article is a record of that check performed in the early morning of 2026-11-13. Unlike parts 1–6, it does not follow a single merged PR, but rather covers the procedure of tending to multiple open PRs. Since these are PRs with no final outcome yet, none of them are described as accepted.

2. Querying: Five status values and UNKNOWN

First, I created a list of open PRs (gh search prs --author <계정> --state open), and ran the following command once for each PR.

gh pr view <번호> --repo <소유자>/<저장소> --json mergeable,mergeStateStatus,reviewDecision

mergeable only tells whether there is a conflict (MERGEABLE·CONFLICTING·UNKNOWN), mergeStateStatus breaks down "can the merge button be pressed right now?" into finer details. The meanings intended by GitHub's GraphQL documentation and the counts found this time are as follows.

Value Documentation meaning (summary) Count How we interpreted it
BLOCKED Merge is blocked 24 Mostly waiting for review. Review decisions: Review required 17, No decision 5, Approved 1, Changes requested 1
UNSTABLE Mergeable, but some checks are not passing 9 Open check logs to verify first whether the cause is our change
CLEAN Can be merged and checks pass 7 Nothing for us to do. 1 of them has even received approval
BEHIND PR branch is behind the base 3 Not a conflict. Only the base branch is ahead
DIRTY Merge commit cannot be cleanly created 2 Conflict. Trace the cause down to the PR level

We learned two things from this.

The UNKNOWN of the first query is not a final result. In the first query, four items came out as UNKNOWN . GitHub's REST documentation states that if this value is empty, it means the background job calculating mergeability has just started, so you should request again after some time. Indeed, when I queried again with the same command about 4 minutes later, three came out as BLOCKED, and one came out as UNSTABLE . The longer a PR went unqueried, the more likely this value appeared on the first fetch, so batch checking scripts should be made to collect only UNKNOWN and query them once more.

CLEAN does not mean "will be merged soon". The seven items simply mean there are no technical blocks, and merges are decided by maintainers. Conversely, the 24 BLOCKED items were mostly in a state of waiting for their review turn rather than having things to fix. Among the 45 items, the only ones we need to take action on are DIRTY and those among UNSTABLE where the cause is on our end. UNSTABLE The reasons for the nine items varied individually, so they are not categorized in this article.

There were also cases where check failures were not caused by our changes. Seen a day prior on 11-12, containers/kubernetes-mcp-server#1518 had its browser test changed to a failure while approved by one reviewer. The same test failed only on the stale base, and the cause appeared to be a change in the check runner's image. We rebased it onto the new base while keeping the change contents intact. Afterward, a second approval was added, and in this query it was CLEAN . This PR is also in progress.

3. The three conflicts had different causes

There were three DIRTY items in the first query. Knowing just the fact that a conflict occurred does not tell us what needs to be done, so I found out which merged PR caused the conflict for all three. The causes differed, and thus the handling differed as well.

Our PR Conflicting merge Size of that merge Cause Handling
ai-dynamo/dynamo#16465 4 docs, +15/−4 #17229, 11-06 04:34 29 files, +97/−3068 Neighboring lines of lines we modified were changed Rebased onto new base. Resolved
NVIDIA/dcgm-exporter#760 1 doc, +16/−7 #768, 11-07 03:45 961 files, +161632/−27808 The document containing the modified paragraph was completely rewritten Must be rewritten to match the new doc. In progress
charmbracelet/catwalk#676 3 files, +63/−0 #611, 10-29 23:22 73 files, +39578/−18179 Major version overhaul changed module paths and configuration format Must be rewritten in the new format. In progress

When neighboring lines are modified. The dynamo PR is a change to add missing deprecation notices to four documents for deprecated features. Meanwhile, upstream merged a PR stripping out related integration code and modified the immediate neighboring lines of two of the same documents. What I checked first before rebasing was "is our change still needed?". Opening the four files in the new base branch, the deprecation notices were still missing. So I re-applied the same changes to the files in the new base, and the change volume remained +15/−4. Querying again, the conflict was resolved and it returned to waiting for review (BLOCKED).

When documentation is completely rewritten. The dcgm-exporter PR modifies paragraphs on the procedure to extract debug dumps. However, a PR releasing a new version (4.9.0) modified 961 files and completely rewrote that document. This PR was merged just 30 minutes after creation, so there was no way to know in advance. In the new document, the paragraph we modified has disappeared, and the location to place dumps has also changed. This is not a conflict that can be resolved simply by rebasing. We first need to measure whether a single copy command left in the new document fails on the new image as well, but the new version hasn't been measured yet (unmeasured). After measuring, it is correct to either rewrite it as a single-line PR or close it if the issue has disappeared.

When the major version is overhauled. The catwalk PR is a change adding provider configurations. Meanwhile, v3 was merged, changing module paths and configuration file formats. Files in the old format cannot simply be rebased as-is; they must be rewritten in the new format and built with the new version's tools. This is also in progress.

BEHIND is not a conflict. For these three items, the base branch is simply ahead and merge commits can be created cleanly. They were not touched this time. Since there are no conflicts, I judged there is no reason to rush a rebase before reviews start. Some projects discourage force pushes after a review has started, so it is better to minimize unnecessary rebases.

Looking at these three cases, conflicts were divided not by "the degree to which our changes and others' changes overlapped," but by "how much the ground our changes stood on has changed." If it's just neighboring lines, rebasing is enough; if paragraphs or formats have disappeared, the change itself must be re-evaluated.

4. Rebasing without a clone: Git Data API and SSH signatures

Downloading dozens of repositories entirely just to rebase is burdensome. Downloading a multi-gigabyte repository for a PR that modifies just a few lines of documentation is also wasteful. Therefore, rebasing is done without downloading the repository, using GitHub's Git Data API. Locally, no commits are created and no branches are switched. The sequence is as follows.

  1. Align the fork's default branch with upstream. The new base commit must be in the fork so that commits can be created on top of it.
  2. Download only the files to be changed from the new base, re-apply the same modifications, and run tests that can be executed.
  3. Create blobs for each file (POST git/blobs), and specify the new base tree as base_tree to create a tree (POST git/trees).
  4. ). Assemble the commit object's body directly and sign it with an SSH key.
  5. Attach the signature to create a commit (POST git/commits), and verify that the response's verification.verified is true.
  6. Only when true, move the PR branch ref to the new commit. The target being moved is only the branch in my fork.

The target signed in step 4 is exactly the text git uses when storing a commit.

tree <새 트리 SHA>
parent <새 기준 커밋 SHA>
author <이름> <메일> <epoch 초> +0900
committer <이름> <메일> <epoch 초> +0900

<커밋 메시지>
ssh-keygen -Y sign -n git -f <개인 키 파일> <위 내용을 담은 파일>

Here are the hurdles encountered along the way.

The most expensive lesson learned was about file modes. The initial tool we built fixed the mode of uploaded files to 100644 . When re-applying containers/podman-compose#1585 on October 31, scripts that used to be executables were turned into regular files, and 30 integration tests failed with PermissionError . Since not a single character of the content differed, this was a change that could not be noticed by looking only at the body of the diff. The next day, we fixed the tool to follow the base tree's mode and recreated the commits with the same content. Since then, whenever we use the signing tool, we always verify the following. This PR is also still in progress.

gh pr diff <번호> --repo <소유자>/<저장소> | grep -n 'mode'

It is normal for nothing to be output. old mode 100755 and new mode 100644 appearing are unintended changes.

5. Things Not Submitted: Different Doors for Each Project

While reviewing, we also checked if any of our registered issues could be connected to PRs. The conclusion was "nothing to submit right now." This was not because we couldn't make patches, but because the project rules dictated a strict sequence.

We did submit one thing. It is kubevirt/kubevirt#19889 , which fixes KubeVirt's virt-operator repeatedly printing logs saying "dropping old CA" while discarding no certificates. KubeVirt's AI contribution policy requires reviewing and understanding every line of AI-generated code before submission and encourages disclosing the use of tools. Therefore, we attached the Assisted-by trailer and submitted it as a draft PR . After a human reads through the diff entirely, it will be changed to a review-requested state. This PR is also in progress, and once merged, we will write a separate post about it.

6. How to Measure Tests That Cannot Be Run on a Mac

The unit tests for the KubeVirt change above could not be run on a Mac because the package containing the tests depends on Linux-only packages. Instead of submitting them with the thought that "CI will run the tests," we measured them directly using the following method.

  1. Cross-compile only the test binaries for Linux on the Mac. Since it uses only the repository's vendor directory, nothing new is downloaded.
  2. Place those binaries into a temporary pod in the TT Lab cluster and run them. The pod uses an image already pulled to the node, drops all capabilities, runs as a non-root user, has no service account token, and is launched with a 1Gi memory limit. It is deleted upon completion.
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOTOOLCHAIN=local GOPROXY=off \
  GOFLAGS="-mod=vendor -buildvcs=false" go test -c -o <시험 바이너리> <패키지 경로>
What Result
2 newly written tests, pre-fix code 0 passed, 2 failed
2 newly written tests, fixed code 2 passed
CA bundle-related tests 27 out of 27 passed
Entire package (3 iterations with shuffled order) 160 out of 160 passed, approx. 0.53 seconds
Neighboring apply entire package 219 out of 219 passed

The environment is Linux kernel 7.0, x86_64, Go 1.26.6. Symptoms before the fix were also measured. Counting the leader operator's logs for 24 minutes in a TT Lab cluster running KubeVirt v1.9.0 yielded 711 lines of the same log. This matches the rate of one line per CA bundle set every 237 adjustments.

There was one trap here as well. The initially written tests passed even with the pre-fix code. The test environment's default logger filtered out info-level logs, causing the test asserting "logs should not be printed" to pass thanks to the log filtering. Only after modifying the test to increase log verbosity and revert it afterward did both tests fail on the pre-fix code. If we had skipped the principle of first confirming that new tests fail on the pre-fix code, we would have submitted tests that verify nothing.

7. Lessons Learned: Tips for Maintaining Open PRs

Titi's Note. I put several "Please buy this book" slips into the library. When I went back a long while later, some slips had to be rewritten because the shelf numbers had changed, and others required getting a librarian's stamp first. So now, I don't just put slips in and forget about them; I occasionally take them out all at once and read through them to see if they are still correct.

Links

All materials were accessed on 2026-11-13.

Sign in to like

Comments

No comments yet.

Sign in to leave a comment