Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Empty endpoints bypass validation, and the README needs clarification.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Updates git_diff to accept two- and three-dot revision ranges while validating endpoints.
Changes:
- Adds range endpoint validation.
- Adds valid and invalid range tests.
- Documents revision-range support.
File summaries
| File | Summary |
|---|---|
src/git/src/mcp_server_git/server.py |
Adds range parsing and validation. Moderate (2 votes): reject empty endpoints such as ..HEAD, HEAD.., and .... |
src/git/tests/test_server.py |
Adds regression coverage for valid and invalid ranges. |
src/git/README.md |
Documents range support. Nit (2 votes): clarify behavior for revision ranges versus single revisions. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite (auto)
Note
Copilot is running an experiment and ran this review at Lite.
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Splitting the target on `..`/`...` and skipping empty pieces accepted `..HEAD`, `HEAD..` and `...`, which have no second endpoint, and `....`, which is not a range at all. `git diff` takes the first three as a range against HEAD and returns an empty diff, while `....` reached git and failed with a raw GitCommandError instead of BadName. `rev_parse` rejected all four before this change, so requiring both endpoints keeps the previous contract for everything except the two-endpoint ranges this PR adds. Ranges with more than one separator are rejected too. Also corrects the README: for a revision range, git compares the two endpoints rather than the current state with the target.
There was a problem hiding this comment.
🟡 Changes recommended
Three moderate validation issues remain unresolved in server.py.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
src/git/src/mcp_server_git/server.py:128
re.splitruns before validating the whole target, so this regresses valid single-revision selectors whose search expression contains..(for example, Git accepts:/fix..bugas a commit-message revision). The previousrepo.rev_parse(target)accepted that target, but this now validates:/fixandbugseparately and raisesBadName; try the whole target first and only fall back to endpoint validation when that fails.
# target may be a revision range (e.g. 'main..feature' or 'main...feature'),
# so validate each endpoint is a real git ref rather than the range as a whole
revisions = re.split(r"\.\.\.?", target)
src/git/src/mcp_server_git/server.py:136
- The loop only rejects empty endpoints before calling
rev_parse, so an option-like endpoint is still handed to Git's option parser. For example,rev-parse --allcan resolve to a commit when the repository has one ref, allowinggit_diff(repo, "HEAD..--all")past validation and then producing a raw ambiguous-argument error fromgit diffinstead ofBadName. Apply the--prefix guard to each extracted endpoint as well.
for revision in revisions:
if not revision:
raise BadName(f"Invalid target: '{target}' - empty range endpoint")
repo.rev_parse(revision)
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite (auto)
Note
Copilot is running an experiment and ran this review at Lite.
Trying the target unchanged first keeps single revisions that contain '..', such as the commit-message selector ':/fix..bug', from being split into the endpoints ':/fix' and 'bug' and rejected. rev_parse reports such a target as BadName, and rejects a spec its own parser cannot tokenize with ValueError, so the range fallback catches both. The range fallback also rejects a run of four or more dots outright: 'HEAD....' is not a range with two endpoints, and relying on the trailing '.' failing to resolve reports the wrong part of the input as the problem.
|
@/tmp/reply_issue.md |
There was a problem hiding this comment.
🔵 Needs a closer look
Range parsing must handle separators inside nested revision selectors.
Review details
Suppressed comments (1)
src/git/src/mcp_server_git/server.py:145
- The fallback splits on every
../..., including dots inside a nested revision selector. Git accepts ranges such asHEAD^{/fix..bug}..HEAD, where the first endpoint is the validHEAD^{/fix..bug}selector, but this produces three pieces and raisesBadNameinstead of diffing the range. Split only on a top-level range separator (and add a regression test for a dotted selector endpoint).
revisions = re.split(r"\.\.\.?", target)
if len(revisions) > 2:
raise BadName(
f"Invalid target: '{target}' - expected a revision or a single range"
- Files reviewed: 3/3 changed files
- Comments generated: 0 new
- Review effort level: Lite (auto)
Note
Copilot is running an experiment and ran this review at Lite.
Description
git_diffrejects revision ranges.git_diff(repo, "main..feature")raisesBadName: Ref 'main..range-feature' did not resolve to an object, even thoughgit diff main..featureis a valid command and the tool passestargetstraight through togit diff.Root cause: the CWE-88 flag-injection hardening in 9e5d5b8 added
repo.rev_parse(target)atsrc/git/src/mcp_server_git/server.py:125to confirm the target resolves to a git ref.rev_parseresolves a single object name, so a range such asmain..feature,main...feature, orHEAD~1..HEADis treated as one ref name and rejected before it ever reachesgit diff.The fix splits the target on
../...and validates each endpoint separately, so ranges are accepted while flag-like or unresolvable targets are still rejected.Server Details
Motivation and Context
A revision range is an ordinary single argument to
git diff(git diff main..feature), and thegit_difftool documentstargetas the branch or commit to compare with. Asking for a branch-to-branch comparison as a range is a natural thing for a client to do, and it worked before 9e5d5b8; the added ref validation silently broke it. Validating each endpoint keeps the CWE-88 protection intact: the-prefix check still runs first on the whole target, and every endpoint must resolve to a real git ref.How Has This Been Tested?
Added
test_git_diff_allows_revision_rangesinsrc/git/tests/test_server.py, which builds a repository with a divergent branch and asserts working diffs for:main..range-feature,range-feature..main(both directions)main...range-feature(three-dot)HEAD~1..HEADand<sha>..HEAD(commit ranges)and that invalid inputs still raise
BadName:nonexistent..HEADandmain..--output=/tmp/evil.The test fails on the unpatched tree (
gitdb.exc.BadName: Ref 'main..range-feature' did not resolve to an object) and passes with the fix.Commands run from
src/git:uv run pytest -quv run pytest tests/test_server.py -q -k test_git_diff_allows_revision_rangesuv run ruff check .uv run --frozen pyrightBreaking Changes
None. Targets that resolved before still resolve; the only change is that revision ranges are no longer rejected.
git_diffstill raisesBadNamefor flag-like targets and for targets whose endpoints are not real refs.Types of changes
Checklist
Additional context
The
-prefix guard and the per-endpointrev_parsecheck are the existing CWE-88 defense from 9e5d5b8, unchanged in spirit: a target may not start with-, and every component of the target must resolve to a real git ref. Endpoints are not forwarded to the git CLI individually — the originaltargetstring is still passed togit diffas a single argument, so no new flag-parsing surface is introduced. No new environment variables or configuration options are added.