From 4ff0b0bff15937606106b52b837c51398edcf1b9 Mon Sep 17 00:00:00 2001 From: Dragon ze <52615460+Aoisop@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:38:13 +0800 Subject: [PATCH] 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 --- README.md | 2 + .../scripts/github_api.py | 18 ++++----- tests/skills/test_github_deep_research.py | 40 +++++++++++++++++++ 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d93ec0610c..bb78f03d3a 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/skills/public/github-deep-research/scripts/github_api.py b/skills/public/github-deep-research/scripts/github_api.py index 2e9b4dfdf9..dad88ec94a 100644 --- a/skills/public/github-deep-research/scripts/github_api.py +++ b/skills/public/github-deep-research/scripts/github_api.py @@ -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.""" diff --git a/tests/skills/test_github_deep_research.py b/tests/skills/test_github_deep_research.py index 2168e12443..d89683227c 100644 --- a/tests/skills/test_github_deep_research.py +++ b/tests/skills/test_github_deep_research.py @@ -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"]