Skip to content

fix: resolve _get_file_contents paths via a recursive tree, not ref:dir - #759

Closed
irfanuddinahmad wants to merge 1 commit into
openedx:feanil/large-uv-lock-blob-apifrom
irfanuddinahmad:irfan/fix-nested-uv-lock-blob-path
Closed

irfanuddinahmad wants to merge 1 commit into
openedx:feanil/large-uv-lock-blob-apifrom
irfanuddinahmad:irfan/fix-nested-uv-lock-blob-path

Conversation

@irfanuddinahmad

Copy link
Copy Markdown
Contributor

Summary

Targets #758's feanil/large-uv-lock-blob-api branch (not master).

_get_file_contents resolves a nested path with:

tree = self.repository.get_git_tree(f"{ref}:{directory}" if directory else ref)

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's
ref:path extended-SHA syntax the way the Contents API and git rev-parse do.
Confirmed against this repo directly:

$ gh api repos/openedx/repo-tools/git/trees/master              # root tree by branch name -> 200
$ gh api repos/openedx/repo-tools/git/trees/master:edx_repo_tools  # ref:path subdir -> 404 Not Found

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 real
call 404s — unhandled for the head/new ref, and silently misread as
"file didn't exist before" for the base/old ref.

uv.lock itself is effectively always at the repo root for a uv-managed
project, so this wouldn't currently bite on the actual call site, but
_get_file_contents is written and tested as a general-purpose helper, so
it's worth being correct rather than accidentally-correct.

Fix

Fetch the tree recursively from ref and match on the full path instead of
resolving a subdirectory as a tree SHA:

tree = self.repository.get_git_tree(ref, recursive=True)
entry = next((e for e in tree.tree if e.path == path), None)

Verified this resolves nested paths against the real API too:

$ gh api "repos/openedx/repo-tools/git/trees/master?recursive=true" \
    --jq '.tree[] | select(.path=="tests/test_pull_request_creator.py")'

Also updates _mock_repo_files and the subdirectory/missing-file tests to
match the new call shape.

Test plan

  • uv run pytest tests/test_pull_request_creator.py -q — 22 passed

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>
@openedx-webhooks openedx-webhooks added open-source-contribution PR author is not from Axim or 2U core contributor PR author is a Core Contributor (who may or may not have write access to this repo). labels Sep 23, 2026
@openedx-webhooks

Copy link
Copy Markdown

Thanks for the pull request, @irfanuddinahmad!

This repository is currently maintained by @openedx/axim-engineering.

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 approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To 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:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

Details
Where 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:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@feanil feanil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@irfanuddinahmad

Copy link
Copy Markdown
Contributor Author

You're right, I was wrong — my check was mangled by a zsh quirk ($VAR:e is a history modifier there, not concatenation), so the 404 I saw was from my own malformed request, not the API. Closing this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core contributor PR author is a Core Contributor (who may or may not have write access to this repo). open-source-contribution PR author is not from Axim or 2U

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants