fix: resolve _get_file_contents paths via a recursive tree, not ref:dir - #759
irfanuddinahmad wants to merge 1 commit into
Conversation
GitHub's Git Trees API only accepts an actual tree/commit SHA or a
branch/tag name pointing at the repo root as `tree_sha`. It doesn't
support git's `ref:path` extended-SHA syntax the way the Contents API
and `git rev-parse` do, so `get_git_tree(f"{ref}:{directory}")` 404s
for any path outside the repo root:
$ gh api repos/openedx/repo-tools/git/trees/master:edx_repo_tools
{"message":"Not Found", ...}
Fetch the tree recursively from `ref` instead and match the full
path, which resolves nested files the same as root-level ones.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Thanks for the pull request, @irfanuddinahmad! This repository is currently maintained by Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review. 🔘 Get product approvalIf you haven't already, check this list to see if your contribution needs to go through the product review process.
🔘 Provide contextTo help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:
🔘 Get a green buildIf one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green. DetailsWhere can I find more information?If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources: When can I expect my changes to be merged?Our goal is to get community contributions seen and reviewed as efficiently as possible. However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:
💡 As a result it may take up to several weeks or months to complete a review and merge your PR. |
feanil
left a comment
There was a problem hiding this comment.
I wasn't able to re-produce the failure. ref:path works on the trees API at every depth I tried, against this repo:
$ gh api "repos/openedx/repo-tools/git/trees/master:edx_repo_tools" --jq .sha
a259a822b46c8f6c6886c395239527e6cb16248a
$ gh api "repos/openedx/repo-tools/git/trees/master:.github/workflows" --jq .sha
d8c1babbb2292ed32c30cc3d7cdb06a7728398b2
$ gh api "repos/openedx/repo-tools/git/trees/master:edx_repo_tools/codemods/django2" --jq .sha
f07a1719d38ecea87b30184c0f6d2ae8024342d9
$ gh api "repos/openedx/repo-tools/git/trees/master:tests/fake_repos/repo_with_nvmrc/.github/workflows" --jq .sha
aecac4f83a78185b0603a995050ca1299511dea5
A directory that isn't there still 404s, which is the shape _parse_uv depends on:
$ gh api "repos/openedx/repo-tools/git/trees/master:nosuchdir"
{"message":"Not Found","status":"404"}
The helper reads every depth too. This against openedx/repo-tools:
helper = GitHubHelper.__new__(GitHubHelper)
helper.repository = Github(os.environ["GITHUB_TOKEN"]).get_repo("openedx/repo-tools")
for path in [
"pyproject.toml",
"edx_repo_tools/__init__.py",
".github/workflows/add-depr-ticket-to-depr-board.yml",
"edx_repo_tools/codemods/django2/README",
"tests/fake_repos/repo_with_nvmrc/.github/workflows/release.yml",
]:
print(path.count("/"), len(helper._get_file_contents(path, "master")))gives identical output on this branch and on its base:
depth 0 5251 bytes pyproject.toml
depth 1 22 bytes edx_repo_tools/__init__.py
depth 2 617 bytes .github/workflows/add-depr-ticket-to-depr-board.yml
depth 3 710 bytes edx_repo_tools/codemods/django2/README
depth 5 1055 bytes tests/fake_repos/repo_with_nvmrc/.github/workflows/release.yml
You're right that ref:path is undocumented. The docs describe tree_sha as "The SHA1 value or ref (branch or tag) name of the tree" and say nothing about the extended-SHA form, so not wanting to lean on it is reasonable. If we want to avoid this, I think the fix is to walk the tree a level at a time by SHA, not to fetch the whole thing recursively. See the inline comment for what recursive costs.
Do you want to re-work this to do the manual walking? or are you good using the code that's already here.
| """ | ||
| directory, _, filename = path.rpartition("/") | ||
| tree = self.repository.get_git_tree(f"{ref}:{directory}" if directory else ref) | ||
| tree = self.repository.get_git_tree(ref, recursive=True) |
There was a problem hiding this comment.
Recursive pulls the whole repo tree to find one entry. On openedx-platform master that is 11442 entries and 3.0 MB, against 11 KB for the root tree this replaces, and _parse_uv calls it twice per run, once for pr.head.sha and once for pr.base.sha.
It also adds a failure mode that isn't there now. The recursive response caps at 100,000 entries / 7 MB and sets truncated: true past it. Nothing here checks truncated, so a truncated tree fails this lookup and raises the 404 below, and _parse_uv reads that 404 as "no previous uv.lock" and reports every package as new. openedx-platform is at 3.0 MB of the 7 MB cap today.
|
You're right, I was wrong — my check was mangled by a zsh quirk ( |
Summary
Targets #758's
feanil/large-uv-lock-blob-apibranch (notmaster)._get_file_contentsresolves a nested path with:GitHub's Git Trees API only accepts an actual tree/commit SHA, or a branch/tag
name for the repo root tree, as
tree_sha. It does not support git'sref:pathextended-SHA syntax the way the Contents API andgit rev-parsedo.Confirmed against this repo directly:
So the helper only works for files at the repo root. For any nested path
(e.g.
requirements/edx-sandbox/uv.lock, the PR's own test case), the realcall 404s — unhandled for the head/new ref, and silently misread as
"file didn't exist before" for the base/old ref.
uv.lockitself is effectively always at the repo root for auv-managedproject, so this wouldn't currently bite on the actual call site, but
_get_file_contentsis written and tested as a general-purpose helper, soit's worth being correct rather than accidentally-correct.
Fix
Fetch the tree recursively from
refand match on the full path instead ofresolving a subdirectory as a tree SHA:
Verified this resolves nested paths against the real API too:
Also updates
_mock_repo_filesand the subdirectory/missing-file tests tomatch the new call shape.
Test plan
uv run pytest tests/test_pull_request_creator.py -q— 22 passed