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.
|
Addressed the three points in
Test state: 51 passed. The |
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.
Probing the target unchanged relies on rev_parse saying no in every way it can. It raises BadName for a revision that does not resolve and ValueError for a spec its own parser cannot tokenize, but a 'rev:path' target whose revision resolves and whose path is not in the tree raises KeyError from the tree lookup, which escaped git_diff and reached the caller as a raw KeyError. 'HEAD:missing/path' leaked that way before this branch, and probing the whole target first extended it to 'HEAD:missing..path..HEAD'. All three now read as one refusal, so an unusable target is a BadName as documented.
|
The selector is valid on its own, but the range around it is not one — git rejects it too, so there is nothing to accept here. Checking the claim did turn up a real bug of mine, fixed in
So What the check did find. Probing the target unchanged first assumed That leaked before this branch, and trying the whole target first extended it to |
There was a problem hiding this comment.
🟢 Approval recommended
Tests and validation cover the change; the remaining documentation nit is non-blocking.
Review details
Suppressed comments (1)
src/git/README.md:41
- The MCP-facing
Tooldescription inserver.pystill says only “Shows differences between branches or commits,” while this README now documents revision ranges. Clients receive the runtime tool description rather than this README, so they will not discover the newly supportedtargetform; update that description alongside this documentation change.
- `target` (string): Target branch, commit, or revision range (e.g. `main..feature`) to compare with
- 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.