fix(github-research): resolve the tree against the repository's default branch (#7074)

* fix(github-research): resolve the tree against the repository's default branch

The tree command asked for "main" and, when that was missing, for "master".
A repository whose default branch is neither gets a name-guessing fallback, and
when the stale "master" still resolves GitHub returns it happily: the agent is
handed the tree of an abandoned branch and reports it as the current structure.

Ask for HEAD instead, which GitHub resolves to the repository's own default
branch, so no candidate names are guessed and no extra request is added.

* test(github-research): clarify stale branch tree comment

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Dragon ze 2026-10-19 11:38:13 +08:00 committed by GitHub
parent a1f23b294a
commit 4ff0b0bff1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 51 additions and 9 deletions

View File

@ -1492,6 +1492,8 @@ Python's standard-library HTTP client. Its fallback preserves spaces, Unicode, a
reserved characters in search queries and label filters.
File reads accept raw repository-relative paths and preserve spaces, Unicode,
`#`, `?`, and literal percent signs with either HTTP client.
Directory listings ask for the repository's own default branch, so a repository that
uses neither `main` nor `master` reports its current tree instead of a stale branch.
Users can explicitly activate an enabled skill for a single turn by starting the request with `/skill-name`, for example `/data-analysis analyze uploads/foo.csv`. DeerFlow loads that skill's `SKILL.md` as hidden current-turn context while leaving the base prompt limited to skill metadata. Slash activation respects disabled skills, custom-agent skill whitelists, and existing channel commands such as `/new` and `/help`.

View File

@ -101,17 +101,17 @@ class GitHubAPI:
return f"[README not found: {e}]"
def get_tree(
self, owner: str, repo: str, branch: str = "main", recursive: bool = True
self, owner: str, repo: str, branch: str = "HEAD", recursive: bool = True
) -> Dict:
"""Get repository directory tree."""
"""Get repository directory tree.
``HEAD`` is resolved by GitHub to the repository's default branch, so a
repository that uses neither ``main`` nor ``master`` gets its current tree.
Guessing those two names instead served the tree of an abandoned branch
whenever the repository kept a stale ``master`` around.
"""
params = {"recursive": "1"} if recursive else {}
try:
return self._get(f"/repos/{owner}/{repo}/git/trees/{branch}", params)
except Exception:
# Try 'master' if 'main' fails
if branch == "main":
return self._get(f"/repos/{owner}/{repo}/git/trees/master", params)
raise
return self._get(f"/repos/{owner}/{repo}/git/trees/{branch}", params)
def get_file_content(self, owner: str, repo: str, path: str) -> str:
"""Get content of a file using its raw repository-relative path."""

View File

@ -130,3 +130,43 @@ def test_fallback_without_parameters(fallback_client):
assert client.get_repo_info("owner", "repo") == {"ok": True}
assert captured["url"] == "https://api.github.com/repos/owner/repo"
def test_tree_asks_for_the_repository_head(file_content_client):
client, captured = file_content_client
assert client.get_tree("owner", "repo") == {"ok": True}
url = urlsplit(captured["url"])
assert url.path == "/repos/owner/repo/git/trees/HEAD"
assert parse_qs(url.query) == {"recursive": ["1"]}
def test_tree_keeps_an_explicit_branch(file_content_client):
client, captured = file_content_client
assert client.get_tree("owner", "repo", branch="dev", recursive=False) == {
"ok": True
}
url = urlsplit(captured["url"])
assert url.path == "/repos/owner/repo/git/trees/dev"
assert url.query == ""
def test_tree_does_not_guess_another_branch(monkeypatch, fallback_client):
client, _ = fallback_client
asked: list[str] = []
def fail_after_recording(endpoint, params=None, accept=None):
asked.append(endpoint)
raise RuntimeError("HTTP 404")
monkeypatch.setattr(client, "_get", fail_after_recording)
with pytest.raises(RuntimeError):
client.get_tree("owner", "repo")
# A stale `master` still resolves as a branch, so retrying names here silently
# returns the stale branch's tree instead of failing.
assert asked == ["/repos/owner/repo/git/trees/HEAD"]