Skip to content

Extend vk pr: implicit PR retrieval with branch/fragment handling#185

Merged
leynos merged 22 commits intomainfrom
terragon/extend-vk-pr-autodetect-a8kv2q
Jan 21, 2026
Merged

Extend vk pr: implicit PR retrieval with branch/fragment handling#185
leynos merged 22 commits intomainfrom
terragon/extend-vk-pr-autodetect-a8kv2q

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Jan 16, 2026

Summary

  • Extends vk pr to auto-detect the target PR when no reference is provided by deriving from the current Git branch and origin/FETCH_HEAD data.
  • Supports fragment-only references (#discussion_rID) to auto-detect the PR and scope to a specific discussion.
  • Adds a GraphQL query PR_FOR_BRANCH_QUERY to fetch PRs by headRef and wires up end-to-end resolution.
  • Refines ref parsing: moves ref_parser into a module; adds current_branch(), is_fragment_only(), parse_fragment_only() helpers and expands tests.
  • Updates CLI and PR resolution flow to handle implicit PR retrieval, including fork-disambiguation via origin and fetch-head data.
  • Introduces an asynchronous PR resolution flow and enhanced error handling for missing PRs on branch or detached HEAD states.
  • Includes unit and end-to-end tests for branch auto-detect, fragment handling, and error paths.
  • Documentation updated to explain PR reference resolution flow (branch auto-detection and fragment handling).

Changes

  • New: src/branch_pr.rs
    • Provides fetch_pr_for_branch to resolve a PR number for a given branch via the GitHub GraphQL API.
    • Uses PR_FOR_BRANCH_QUERY and returns VkError::NoPrForBranch when no PR is found.
    • Includes unit tests for deserialization of PR data.
  • New: src/graphql_queries.rs
    • Added PR_FOR_BRANCH_QUERY to fetch a PR by repository owner/name and headRef.
  • New: src/ref_parser/mod.rs
    • Replaces the previous monolithic ref_parser with a module-based structure.
    • Adds current_branch() to read the current branch from .git/HEAD.
    • Adds is_fragment_only() and parse_fragment_only() helpers to handle fragment-based references.
    • Expands tests to cover current branch detection and fragment parsing.
  • Updated: src/cli_args.rs
    • PrArgs.reference is now optional and supports a new description:
      • Pull request URL, number, or discussion fragment. When omitted, auto-detects the PR from the current Git branch.
      • Bare #discussion_r<ID> fragment still auto-detects PR and limits to that discussion; file filters are ignored when a fragment is provided.
  • Updated: src/commands.rs
    • Introduces resolve_pr_reference to handle three cases:
      1. No reference: detect PR from current branch and fetch PR number.
      2. Fragment-only (#discussion_r<ID>): detect PR from branch and extract comment ID.
      3. Full reference: delegate to existing parsing.
    • setup_pr_output now resolves PR reference asynchronously using the new logic.
  • Updated: src/main.rs
    • Exposes new branch_pr module.
  • Updated: src/ref_parser/tests.rs
    • Added tests for the new modular ref_parser API (branch detection and fragment parsing).
  • Updated: tests/e2e.rs
    • Expanded end-to-end testing scaffolding to simulate GraphQL responses and fetch-head data for implicit PR retrieval.
  • Updated: docs/vk-design.md
    • Expanded design documentation to illustrate PR reference resolution flow, including branch auto-detection and fragment handling.

How it works

  • When vk pr is invoked with no arguments, the tool reads the current branch from .git/HEAD, derives the repository information (via either an explicit repo, FETCH_HEAD, or fetch/origin data), and queries GitHub for a PR whose head ref matches the current branch using PR_FOR_BRANCH_QUERY.
  • If the user provides a fragment like #discussion_r123, the tool auto-detects the PR for the current branch and returns the specified discussion context.
  • If a full PR reference (URL/number) is provided, existing parsing and resolution remain unchanged.
  • Fork-disambiguation: the resolution logic uses fetch-head and origin remote information to disambiguate among PRs with the same head branch name across forks.
  • If no PR exists for the current branch, a VkError::NoPrForBranch is returned with the branch name. Detached HEAD states surface as VkError::DetachedHead.

Test plan

  • Unit tests for GraphQL PR data deserialization and fragment helpers:
    • Deserializing PR data with a single node
    • Handling empty PR node lists
    • Current branch extraction from symbolic HEAD and detached HEAD states
    • Fragment recognition and parsing (valid/invalid inputs)
  • End-to-end tests (e2e.rs):
    • Auto-detection from branch and subsequent PR rendering
    • Fragment-only auto-detection path
    • Behavior when HEAD is detached and no explicit PR is provided
    • Behavior when there is no PR for the current branch (proper error)
  • Additional tests for origin/fetch-head parsing and cross-fork resolution

Migration and usage

  • No breaking changes for existing workflows that pass a PR reference.
  • New behavior: running vk pr without arguments will attempt to auto-detect the PR for the current branch.
  • You can still specify vk pr <PR-URL-or-number> or vk pr #discussion_r<ID> to target a specific thread.

🌿 Generated by Terry

📎 Task: https://www.terragonlabs.com/task/48c367e5-f3d2-4172-840b-028b93e02129

Allow `vk pr` to be invoked without a URL or PR number. When no reference
is provided, the command detects the PR associated with the current Git
branch using the GitHub API.

This also supports passing only a comment fragment (`#discussion_r<ID>`)
without the full URL or PR number, in which case the PR is auto-detected
and the specified comment thread is displayed.

Changes:
- Add `NoPrForBranch` error variant to `VkError` for clearer error messages
- Add `PR_FOR_BRANCH_QUERY` GraphQL query to look up PRs by head ref
- Add `current_branch()` function to read branch from `.git/HEAD`
- Add `is_fragment_only()` and `parse_fragment_only()` for fragment parsing
- Make `parse_repo_str()` and `repo_from_fetch_head()` public
- Create `branch_pr` module with `fetch_pr_for_branch()` function
- Update `PrArgs.reference` to be optional (not required)
- Update `setup_pr_output()` to async with `resolve_pr_reference()` logic
- Add comprehensive unit tests for new parsing functions
- Add e2e tests for auto-detection scenarios

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Jan 16, 2026

Reviewer's Guide

Implements implicit PR resolution for the vk pr command by auto-detecting the pull request from the current Git branch (including fragment-only references) via a new GraphQL query and helper, and wires this into CLI parsing, PR resolution flow, and tests.

Sequence diagram for implicit vk pr resolution from current branch

sequenceDiagram
    actor User
    participant Cli as vk_pr_CLI
    participant Commands as run_pr
    participant Setup as setup_pr_output
    participant Resolver as resolve_pr_reference
    participant RefParser as ref_parser
    participant BranchPr as branch_pr
    participant Client as GraphQLClient
    participant GitHub as GitHub_API
    participant Git as Git_repo

    User->>Cli: invoke vk pr [optional reference]
    Cli->>Commands: parse_args -> run_pr(args, global)

    Commands->>Setup: setup_pr_output(args, global, token)
    Setup->>Client: build_graphql_client(token)
    Setup->>Resolver: resolve_pr_reference(args.reference, global.repo, client)

    alt no_reference_provided
        Resolver->>RefParser: current_branch()
        RefParser->>Git: read .git/HEAD
        Git-->>RefParser: HEAD_content
        RefParser-->>Resolver: Some(branch) or None
        alt detached_HEAD
            Resolver-->>Setup: Err(VkError_InvalidRef)
            Setup-->>Commands: Err(VkError_InvalidRef)
            Commands-->>Cli: print_error and exit
        else symbolic_HEAD
            Resolver->>RefParser: parse_repo_str(global.repo)
            alt repo_from_global
                RefParser-->>Resolver: Some(RepoInfo)
            else repo_from_fetch_head
                Resolver->>RefParser: repo_from_fetch_head()
                RefParser->>Git: read .git/FETCH_HEAD
                Git-->>RefParser: FETCH_HEAD_content
                RefParser-->>Resolver: Some(RepoInfo) or None
                alt repo_not_found
                    Resolver-->>Setup: Err(VkError_RepoNotFound)
                    Setup-->>Commands: Err(VkError_RepoNotFound)
                    Commands-->>Cli: print_error and exit
                end
            end
            Resolver->>BranchPr: fetch_pr_for_branch(client, RepoInfo, branch)
            BranchPr->>Client: run_query(PR_FOR_BRANCH_QUERY, vars)
            Client->>GitHub: POST GraphQL PR_FOR_BRANCH_QUERY
            GitHub-->>Client: PR list (nodes)
            Client-->>BranchPr: PrForBranchData
            alt pr_found
                BranchPr-->>Resolver: pr_number
                Resolver-->>Setup: (RepoInfo, pr_number, None)
            else no_pr_for_branch
                BranchPr-->>Resolver: Err(VkError_NoPrForBranch)
                Resolver-->>Setup: Err(VkError_NoPrForBranch)
                Setup-->>Commands: Err(VkError_NoPrForBranch)
                Commands-->>Cli: print_error and exit
            end
        end
    else fragment_only_reference
        Resolver->>RefParser: is_fragment_only(reference)
        RefParser-->>Resolver: true
        Resolver->>RefParser: parse_fragment_only(reference)
        RefParser-->>Resolver: Ok(comment_id) or Err(VkError_InvalidRef)
        alt invalid_fragment
            Resolver-->>Setup: Err(VkError_InvalidRef)
            Setup-->>Commands: Err(VkError_InvalidRef)
            Commands-->>Cli: print_error and exit
        else valid_fragment
            Resolver->>RefParser: current_branch()
            RefParser->>Git: read .git/HEAD
            Git-->>RefParser: HEAD_content
            RefParser-->>Resolver: Some(branch) or None
            alt detached_HEAD
                Resolver-->>Setup: Err(VkError_InvalidRef)
                Setup-->>Commands: Err(VkError_InvalidRef)
                Commands-->>Cli: print_error and exit
            else symbolic_HEAD
                Resolver->>RefParser: parse_repo_str(global.repo) or repo_from_fetch_head()
                RefParser-->>Resolver: Some(RepoInfo) or None
                alt repo_resolved
                    Resolver->>BranchPr: fetch_pr_for_branch(client, RepoInfo, branch)
                    BranchPr->>Client: run_query(PR_FOR_BRANCH_QUERY, vars)
                    Client->>GitHub: POST GraphQL PR_FOR_BRANCH_QUERY
                    GitHub-->>Client: PR list
                    Client-->>BranchPr: PrForBranchData
                    BranchPr-->>Resolver: pr_number or Err(VkError_NoPrForBranch)
                    alt pr_found
                        Resolver-->>Setup: (RepoInfo, pr_number, Some(comment_id))
                    else no_pr_for_branch
                        Resolver-->>Setup: Err(VkError_NoPrForBranch)
                        Setup-->>Commands: Err(VkError_NoPrForBranch)
                        Commands-->>Cli: print_error and exit
                    end
                else repo_not_found
                    Resolver-->>Setup: Err(VkError_RepoNotFound)
                    Setup-->>Commands: Err(VkError_RepoNotFound)
                    Commands-->>Cli: print_error and exit
                end
            end
        end
    else full_reference
        Resolver->>RefParser: parse_pr_thread_reference(reference, global.repo)
        RefParser-->>Resolver: (RepoInfo, pr_number, comment_id) or Err(VkError_InvalidRef)
        Resolver-->>Setup: result
    end

    Setup-->>Commands: Some(PrContext)
    Commands->>Cli: render_PR_output_and_threads
Loading

Class diagram for new branch_pr and updated ref_parser and VkError

classDiagram
    class RefParser {
        +current_branch() Option_String
        +parse_repo_str(repo String) Option_RepoInfo
        +repo_from_fetch_head() Option_RepoInfo
        +is_fragment_only(input String) bool
        +parse_fragment_only(input String) Result_u64_VkError
        +parse_pr_thread_reference(input String, default_repo Option_String) Result_PrRef_VkError
    }

    class RepoInfo {
        +owner String
        +name String
    }

    class BranchPr {
        +fetch_pr_for_branch(client GraphQLClient, repo RepoInfo, branch String) Result_u64_VkError
    }

    class PrForBranchData {
        +repository PrForBranchRepository
    }

    class PrForBranchRepository {
        +pull_requests PrConnection
    }

    class PrConnection {
        +nodes Vec_PrNode
    }

    class PrNode {
        +number u64
    }

    class GraphQLQueries {
        +PR_FOR_BRANCH_QUERY &str
    }

    class Commands {
        +resolve_pr_reference(reference Option_str, default_repo Option_str, client GraphQLClient) Result_PrThreadRef_VkError
        +setup_pr_output(args PrArgs, global GlobalArgs, cli_token Option_str) Result_Option_PrContext_VkError
    }

    class PrArgs {
        +reference Option_String
    }

    class VkError {
        +InvalidRef
        +RepoNotFound
        +NoPrForBranch branch Box_str
        +CommentNotFound comment_id u64
        +BadResponse message Box_str
    }

    class GraphQLClient {
        +run_query(query &str, vars Map_String_Value) Result_T_VkError
    }

    class PrContext {
        +repo RepoInfo
        +number u64
        +comment_id Option_u64
        +client GraphQLClient
    }

    RefParser --> RepoInfo
    BranchPr --> RepoInfo
    BranchPr --> GraphQLClient
    BranchPr --> PrForBranchData
    PrForBranchData --> PrForBranchRepository
    PrForBranchRepository --> PrConnection
    PrConnection --> PrNode

    GraphQLQueries <.. BranchPr

    Commands --> RefParser
    Commands --> BranchPr
    Commands --> GraphQLClient
    Commands --> PrArgs
    Commands --> PrContext

    VkError <.. RefParser
    VkError <.. BranchPr
    VkError <.. Commands

    GraphQLClient ..> VkError
    BranchPr ..> VkError
    RefParser ..> VkError

    PrArgs --> Commands
    RepoInfo --> PrContext
    GraphQLClient --> PrContext
Loading

Flow diagram for PR resolution from branch or reference

flowchart TD
    A["Start vk pr"] --> B{Reference provided?}

    B -->|No| C["Read current_branch from .git/HEAD"]
    C --> D{Branch found?}
    D -->|No| E["Error VkError::InvalidRef (detached HEAD)"]
    D -->|Yes| F["Resolve RepoInfo from global.repo via parse_repo_str"]
    F --> G{RepoInfo resolved?}
    G -->|No| H["Resolve RepoInfo from .git/FETCH_HEAD via repo_from_fetch_head"]
    H --> I{RepoInfo resolved?}
    I -->|No| J["Error VkError::RepoNotFound"]
    I -->|Yes| K["fetch_pr_for_branch via GraphQL PR_FOR_BRANCH_QUERY"]
    G -->|Yes| K
    K --> L{PR found for branch?}
    L -->|No| M["Error VkError::NoPrForBranch"]
    L -->|Yes| N["Use (RepoInfo, pr_number, None) as PR context"]

    B -->|Yes| O{"is_fragment_only(reference)?"}
    O -->|Yes| P["parse_fragment_only -> comment_id"]
    P --> Q{Valid comment_id?}
    Q -->|No| R["Error VkError::InvalidRef"]
    Q -->|Yes| C
    C --> S["After branch and repo resolution"]
    S --> T["fetch_pr_for_branch and use (RepoInfo, pr_number, Some(comment_id))"]

    O -->|No| U["parse_pr_thread_reference(reference, global.repo)"]
    U --> V{Parsed successfully?}
    V -->|No| W["Error VkError::InvalidRef"]
    V -->|Yes| X["Use parsed (RepoInfo, pr_number, optional_comment_id)"]

    N --> Y["Build PrContext and render PR"]
    T --> Y
    X --> Y
    Y --> Z["End"]

    E --> Z
    J --> Z
    M --> Z
    R --> Z
    W --> Z
Loading

File-Level Changes

Change Details Files
Introduce GraphQL-based helper to resolve a pull request number from a branch name and expose supporting query.
  • Add PR_FOR_BRANCH_QUERY to fetch PRs by owner/name and headRefName, returning the first open or merged PR number.
  • Create branch_pr module with fetch_pr_for_branch that builds query variables, invokes GraphQLClient::run_query, and maps missing nodes to VkError::NoPrForBranch.
  • Add unit tests ensuring PR_FOR_BRANCH_QUERY response structures deserialize correctly for non-empty and empty node lists.
src/graphql_queries.rs
src/branch_pr.rs
Extend reference parsing utilities to support repo/branch discovery, fragment-only detection, and current-branch resolution, plus tests.
  • Add current_branch() to read symbolic refs from .git/HEAD and return the branch name, returning None for detached HEAD.
  • Make parse_repo_str and repo_from_fetch_head public so commands can infer RepoInfo from config or .git/FETCH_HEAD.
  • Introduce is_fragment_only and parse_fragment_only to recognize bare #discussion_r fragments and extract numeric IDs with VkError::InvalidRef on malformed input.
  • Add helper with_git_head plus serial and rstest-backed tests to cover current_branch behaviors, FETCH_HEAD parsing, and fragment-only helpers.
src/ref_parser.rs
Update PR command flow to accept optional references and resolve implicit PRs from the current branch, including fragment-only references.
  • Make PrArgs.reference optional in CLI definition, update help text to describe auto-detection and fragment behavior, and remove Clap-required constraint.
  • Add async resolve_pr_reference to handle: no reference (branch-based PR lookup), fragment-only input (branch-based lookup plus comment_id extraction), and full references (delegate to existing parse_pr_thread_reference).
  • Change setup_pr_output to async, use resolve_pr_reference with GraphQLClient to obtain (RepoInfo, PR number, optional comment_id), and propagate through run_pr.
  • Document behavior and new error VkError::NoPrForBranch in main.rs, and extend VkError enum with this variant and its error message.
src/cli_args.rs
src/commands.rs
src/main.rs
Add end-to-end tests validating branch-based PR auto-detection, fragment-only behavior, and error handling for detached HEAD and missing PRs.
  • Extend tests/e2e.rs to import tempfile::tempdir and vk_cmd helper, and to use set_sequential_responder for ordered GraphQL responses.
  • Add pr_auto_detects_from_branch test that sets up .git/HEAD and FETCH_HEAD, stubs GraphQL responses for branch lookup and threads, and asserts vk pr succeeds with expected output.
  • Add pr_fragment_only_auto_detects_pr that resolves PR via branch then filters a specific discussion fragment, asserting fragment comment output.
  • Add pr_no_reference_fails_on_detached_head and pr_no_reference_fails_when_no_pr_for_branch to validate error messages for detached HEAD and absent PR for branch.
tests/e2e.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 16, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Enable PR auto‑detection when the CLI reference is omitted by making the reference optional; relocate and expand reference parsing into a ref_parser module with git helpers; add branch→PR GraphQL lookup with fork disambiguation; convert PR setup flow to async; add DetachedHead and NoPrForBranch error variants; and extend unit and e2e tests and test helpers.

Changes

Cohort / File(s) Summary
CLI argument changes
src/cli_args.rs
Make PrArgs.reference optional (#[arg(required = false)]) and expand docstring to cover URL/number/#discussion_r<ID> fragment handling and auto‑detection.
Reference parsing (new module)
src/ref_parser/mod.rs, src/ref_parser/parse.rs, src/ref_parser/git.rs
Add RepoInfo, DefaultRepo, parse helpers (parse_pr_reference, parse_issue_reference, parse_pr_thread_reference), fragment helpers (is_fragment_only, parse_fragment_only), and repo resolution from FETCH_HEAD/origin; re‑export git helpers and current branch detection.
Removed legacy parser
src/ref_parser.rs
Remove legacy flat parser file; functionality migrated and expanded under src/ref_parser/*.
Branch→PR lookup
src/branch_pr/mod.rs, src/branch_pr/tests.rs
Add async fetch_pr_for_branch using PR_FOR_BRANCH_QUERY, response types, optional head_owner filtering for fork disambiguation, and tests for filtering and error cases.
GraphQL query
src/graphql_queries.rs
Add public PR_FOR_BRANCH_QUERY constant that queries PRs by headRefName and includes head repository owner info.
Commands refactor & async flow
src/commands.rs
Add resolve_pr_reference and resolve_branch_and_repo helpers and BranchContext struct; integrate fetch_pr_for_branch; change setup_pr_output to async (signature updated) and adapt call sites.
Command tests (branch resolution)
src/commands/tests.rs
Add resolve_branch_and_repo_tests with GitRepoFixture exercising branch detection, FETCH_HEAD fallback and origin‑based head_owner extraction using real git repo fixtures.
Top-level errors & docs
src/main.rs
Add VkError::DetachedHead and VkError::NoPrForBranch { branch }; update Pr subcommand docs to describe auto‑detection and fragment behaviour.
E2E test infra & fixtures
tests/e2e/common.rs
Add GitRepoWithFetchHead, init_git_repo, payload builders (fork_disambiguation_responses, empty_comments_fallback), fixtures, and re‑exports used by e2e tests.
E2E tests
tests/e2e/pr_auto_detect.rs, tests/e2e/pr_basic.rs, tests/e2e/main.rs
Add comprehensive auto‑detection e2e tests (fork disambiguation, fragment‑only, detached HEAD, missing PR) and adjust basic PR tests and test entrypoint.
Test utilities
tests/utils/mod.rs
Add set_sequential_responder_with_assert to assert on JSON request bodies for sequential mock responders.
Ref parser unit tests
src/ref_parser/tests.rs
Add extensive unit tests covering URL parsing, FETCH_HEAD/origin extraction, fragment handling, and branch detection using temporary git fixtures.
Branch PR unit tests
src/branch_pr/tests.rs
Add deserialisation and mock‑server tests validating PR filtering and fetch behaviour.
Fixture embedding
src/diff.rs
Replace filesystem reads in tests with include_str! compile‑time embedding for fixture JSON.
Documentation
docs/vk-design.md
Append "PR Reference Resolution" sections with Mermaid diagrams (duplicate blocks present).

Sequence Diagram(s)

sequenceDiagram
    participant CLI
    participant Commands
    participant RefParser
    participant Git
    participant BranchPR as BranchPR (GraphQL)
    participant GitHub as GitHub API

    rect rgba(0, 100, 200, 0.5)
    note over CLI,GitHub: PR Auto-Detection Flow (no reference)
    end

    CLI->>Commands: setup_pr_output(args, global, token) [no reference]
    Commands->>Git: current_branch()
    Git-->>Commands: branch name or error (detached)
    Commands->>RefParser: resolve_branch_and_repo(branch)
    RefParser->>Git: repo_from_fetch_head() / repo_from_origin()
    Git-->>RefParser: RepoInfo, maybe head owner
    RefParser-->>Commands: BranchContext(repo, branch, head_owner?)
    Commands->>BranchPR: fetch_pr_for_branch(repo, branch, head_owner)
    BranchPR->>GitHub: PR_FOR_BRANCH_QUERY (headRefName, owner, name)
    GitHub-->>BranchPR: PR list with head repository owner info
    BranchPR->>BranchPR: filter by head_owner if provided
    BranchPR-->>Commands: PR number
    Commands-->>CLI: PrContext with PR number

    rect rgba(100, 200, 0, 0.5)
    note over CLI,Commands: Fragment-only input handling
    end

    CLI->>Commands: setup_pr_output(args, global, token) [fragment "#discussion_r123"]
    Commands->>RefParser: is_fragment_only("#discussion_r123")
    RefParser-->>Commands: true
    Commands->>Commands: parse fragment ID and short-circuit branch lookup
    Commands-->>CLI: PrContext with fragment-only selection
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Poem

✨ Missing refs now find their way,
Branches whisper what to say.
Forks and fragments fall in line,
Async paths and tests align. 🎋

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description comprehensively relates to the changeset, detailing the feature additions, architectural changes, testing approach, and migration guidance for the implicit PR resolution capability.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The pull request title accurately and concisely summarises the main change: extending vk pr to support implicit PR retrieval using branch and fragment references, with no reference provided.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch terragon/extend-vk-pr-autodetect-a8kv2q

Comment @coderabbitai help to get the list of available commands and usage tips.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 16, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

src/ref_parser.rs

Comment on lines +374 to +384

    fn current_branch_parses_symbolic_ref() {
        let dir = tempdir().expect("tempdir");
        let git_dir = dir.path().join(".git");
        fs::create_dir(&git_dir).expect("create git dir");
        fs::write(git_dir.join("HEAD"), "ref: refs/heads/feature-branch\n").expect("write HEAD");
        let cwd = std::env::current_dir().expect("cwd");
        std::env::set_current_dir(dir.path()).expect("chdir temp");
        let branch = current_branch().expect("branch from HEAD");
        std::env::set_current_dir(cwd).expect("restore cwd");
        assert_eq!(branch, "feature-branch");
    }

❌ New issue: Code Duplication
The module contains 2 functions with similar structure: tests.current_branch_parses_symbolic_ref,tests.current_branch_returns_none_for_detached_head

@coderabbitai

This comment was marked as resolved.

Extract common test setup for .git/HEAD file handling into a
`with_git_head` helper function. Add `#[serial]` attribute to
tests that change the current working directory to prevent
race conditions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@leynos leynos marked this pull request as ready for review January 16, 2026 21:01
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 10 issues, and left some high level feedback:

  • Both current_branch() and repo_from_fetch_head() assume .git is in the current working directory, which will fail when vk pr is run from a subdirectory of the repo; consider resolving paths relative to the repository root (e.g. via git rev-parse --show-toplevel or similar) instead of assuming ..
  • The resolve_pr_reference logic repeats the same default_repo.and_then(parse_repo_str).or_else(repo_from_fetch_head).ok_or(VkError::RepoNotFound)? chain in multiple match arms; factoring this into a small helper (e.g. resolve_repo(default_repo)) would reduce duplication and make future changes less error-prone.
  • When in a detached HEAD state, vk pr now returns VkError::InvalidRef with a generic "invalid reference" message; consider introducing a more specific error variant or message to indicate the detached HEAD condition explicitly, since this is a common scenario users may need guidance on.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Both `current_branch()` and `repo_from_fetch_head()` assume `.git` is in the current working directory, which will fail when `vk pr` is run from a subdirectory of the repo; consider resolving paths relative to the repository root (e.g. via `git rev-parse --show-toplevel` or similar) instead of assuming `.`.
- The `resolve_pr_reference` logic repeats the same `default_repo.and_then(parse_repo_str).or_else(repo_from_fetch_head).ok_or(VkError::RepoNotFound)?` chain in multiple match arms; factoring this into a small helper (e.g. `resolve_repo(default_repo)`) would reduce duplication and make future changes less error-prone.
- When in a detached HEAD state, `vk pr` now returns `VkError::InvalidRef` with a generic "invalid reference" message; consider introducing a more specific error variant or message to indicate the detached HEAD condition explicitly, since this is a common scenario users may need guidance on.

## Individual Comments

### Comment 1
<location> `src/ref_parser.rs:215-216` </location>
<code_context>
+/// assert!(!is_fragment_only("42#discussion_r123"));
+/// assert!(!is_fragment_only("https://github.com/o/r/pull/1#discussion_r123"));
+/// ```
+pub fn is_fragment_only(input: &str) -> bool {
+    input.starts_with("#discussion_r")
+}
+
</code_context>

<issue_to_address>
**suggestion:** Share the fragment prefix constant between `is_fragment_only` and `parse_fragment_only` to keep them in sync.

`is_fragment_only` hardcodes `"#discussion_r"` while `parse_fragment_only` uses a `FRAG` constant. Please reuse a single shared constant (e.g., a module-level `FRAG`) so changes to the fragment format are made in one place.

Suggested implementation:

```rust
pub fn is_fragment_only(input: &str) -> bool {
    input.starts_with(FRAG)
}

```

To fully apply the suggestion, make sure:

1. The `FRAG` constant used by `parse_fragment_only` is defined at module scope (e.g. near the top of `src/ref_parser.rs`), like:
   ```rust
   const FRAG: &str = "#discussion_r";
   ```
2. If `FRAG` is currently defined inside `parse_fragment_only`, move it out to module scope so both `parse_fragment_only` and `is_fragment_only` can reference the same constant.
</issue_to_address>

### Comment 2
<location> `tests/e2e.rs:237` </location>
<code_context>
+    })
+    .to_string();
+    let reviews_body = include_str!("fixtures/reviews_empty.json").to_string();
+    set_sequential_responder(&handler, vec![pr_lookup_body, threads_body, reviews_body]);
+
+    let dir = tempdir().expect("tempdir");
</code_context>

<issue_to_address>
**suggestion (testing):** Consider asserting the GraphQL request parameters (especially `headRef`) in the MITM handler to prove branch-based PR lookup wiring end-to-end.

Right now these e2e tests only verify behavior against canned GraphQL responses, not that the client sends the correct request. If `fetch_pr_for_branch` or `resolve_pr_reference` used the wrong branch or a hard-coded value, the tests could still pass. If the MITM helper supports it, consider extending `set_sequential_responder` (or adding a variant) to inspect the incoming GraphQL request body and assert that:

- `headRef` matches the branch name from `.git/HEAD` (e.g., `my-feature-branch`), and
- The repository owner/name match what was parsed from `FETCH_HEAD` or `--repo`.

That would turn these into true end-to-end checks of the branch-based lookup wiring to the API.

Suggested implementation:

```rust
    let threads_body = serde_json::json!({
        "data": {"repository": {"pullRequest": {"reviewThreads": {
            "nodes": [],
            "pageInfo": {"hasNextPage": false, "endCursor": null}
        }}}}
    })
    .to_string();
    let reviews_body = include_str!("fixtures/reviews_empty.json").to_string();

    // Assert GraphQL request variables to make this a true end-to-end wiring test.
    //
    // We expect:
    // - headRef to match the branch name from .git/HEAD
    // - owner/name to match what was parsed from FETCH_HEAD / --repo
    set_sequential_responder_with_assert(
        &handler,
        vec![pr_lookup_body, threads_body, reviews_body],
        |body: &serde_json::Value| {
            // Shape is expected to be: { "query": "...", "variables": { ... } }
            let vars = &body["variables"];

            // Branch-based lookup wiring
            assert_eq!(
                vars["headRef"],
                "my-feature-branch",
                "GraphQL headRef variable should match the branch from .git/HEAD"
            );

            // Repository owner/name wiring
            assert_eq!(
                vars["owner"],
                "owner",
                "GraphQL owner variable should match the repo owner parsed from FETCH_HEAD / --repo"
            );
            assert_eq!(
                vars["name"],
                "repo",
                "GraphQL name variable should match the repo name parsed from FETCH_HEAD / --repo"
            );
        },
    );


```

To fully implement this change, you will need to:

1. **Add a new helper** in your MITM / test HTTP helper module (where `set_sequential_responder` is defined), for example:

   ```rust
   pub fn set_sequential_responder_with_assert<F>(
       handler: &HandlerType,                    // use the actual handler type
       responses: Vec<String>,
       assert_body: F,
   )
   where
       F: Fn(&serde_json::Value) + Send + Sync + 'static,
   {
       // Pseudocode – adapt to your existing MITM implementation:
       //
       // 1. For each expected response, register a handler on `handler`
       //    that:
       //    - reads the incoming HTTP request body,
       //    - parses it as JSON (`serde_json::from_slice`),
       //    - calls `assert_body(&parsed_json)`,
       //    - returns the corresponding canned response.
       //
       // 2. Preserve any existing behavior from `set_sequential_responder`
       //    (status codes, headers, sequencing, etc.).
   }
   ```

2. **Ensure JSON parsing of GraphQL requests**:
   - The function must read the incoming request body and parse it as `serde_json::Value`.
   - Call the provided `assert_body` closure before returning the canned response.

3. **Update call sites as needed**:
   - Keep the existing `set_sequential_responder` for tests that don’t care about inspecting the request.
   - Use `set_sequential_responder_with_assert` where you want end-to-end wiring assertions like in this test.

4. If your actual GraphQL variables use different keys (e.g. `headRefName`, `repositoryOwner`, `repositoryName`), adjust the `vars["..."]` lookups in the test and the assertions to match the real schema.
</issue_to_address>

### Comment 3
<location> `src/commands.rs:174` </location>
<code_context>
+/// 1. No reference: detect PR from current branch
+/// 2. Fragment only (`#discussion_r<ID>`): detect PR from branch, extract comment ID
+/// 3. Full reference: use existing parsing
+async fn resolve_pr_reference(
+    reference: Option<&str>,
+    default_repo: Option<&str>,
</code_context>

<issue_to_address>
**issue (complexity):** Consider extracting the shared branch-and-repo resolution into a helper to simplify and de-duplicate `resolve_pr_reference`.

You can reduce the new complexity by factoring out the shared “branch + repo” resolution and keeping `resolve_pr_reference` focused on classifying the reference and wiring calls together.

Specifically, the `None` and `Some(input) if is_fragment_only(input)` arms duplicate:

- `current_branch().ok_or(VkError::InvalidRef)?`
- `default_repo.and_then(parse_repo_str).or_else(repo_from_fetch_head).ok_or(VkError::RepoNotFound)?`
- `fetch_pr_for_branch(client, &repo, &branch).await?`

Refactor that into a small helper and reuse it:

```rust
fn resolve_branch_and_repo(
    default_repo: Option<&str>,
) -> Result<(RepoInfo, String), VkError> {
    let branch = current_branch().ok_or(VkError::InvalidRef)?;
    let repo = default_repo
        .and_then(parse_repo_str)
        .or_else(repo_from_fetch_head)
        .ok_or(VkError::RepoNotFound)?;
    Ok((repo, branch))
}
```

Then simplify `resolve_pr_reference` to:

```rust
async fn resolve_pr_reference(
    reference: Option<&str>,
    default_repo: Option<&str>,
    client: &GraphQLClient,
) -> Result<(RepoInfo, u64, Option<u64>), VkError> {
    match reference {
        None => {
            let (repo, branch) = resolve_branch_and_repo(default_repo)?;
            let number = fetch_pr_for_branch(client, &repo, &branch).await?;
            Ok((repo, number, None))
        }
        Some(input) if is_fragment_only(input) => {
            let comment_id = parse_fragment_only(input)?;
            let (repo, branch) = resolve_branch_and_repo(default_repo)?;
            let number = fetch_pr_for_branch(client, &repo, &branch).await?;
            Ok((repo, number, Some(comment_id)))
        }
        Some(input) => parse_pr_thread_reference(input, default_repo),
    }
}
```

This:

- Removes duplicated logic and ensures the “detect PR from branch” path stays consistent.
- Keeps `resolve_pr_reference` focused on reference classification + orchestration, with repo/branch resolution in a single, testable helper.
- Preserves all current behavior, including the new “no reference” and fragment-only cases.
</issue_to_address>

### Comment 4
<location> `src/commands.rs:174` </location>
<code_context>
+/// 1. No reference: detect PR from current branch
+/// 2. Fragment only (`#discussion_r<ID>`): detect PR from branch, extract comment ID
+/// 3. Full reference: use existing parsing
+async fn resolve_pr_reference(
+    reference: Option<&str>,
+    default_repo: Option<&str>,
</code_context>

<issue_to_address>
**issue (review_instructions):** Add both unit and behavioural tests for the new PR auto-detection and fragment-only resolution logic.

Exercise `resolve_pr_reference` via tests that cover all three match arms:
- `None` reference: PR auto-detected from the current branch (happy path and error cases such as missing `.git/HEAD`, no repo, or no PR for branch).
- Fragment-only reference: `#discussion_r<ID>` combined with branch auto-detection, ensuring the comment ID is correctly propagated.
- Full reference: regression tests verifying existing behaviour is preserved.
These should include at least one higher-level/behavioural test (e.g., integration or e2e) for the CLI flow, not just pure unit tests.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 5
<location> `src/commands.rs:180` </location>
<code_context>
+    client: &GraphQLClient,
+) -> Result<(RepoInfo, u64, Option<u64>), VkError> {
+    match reference {
+        None => {
+            let branch = current_branch().ok_or(VkError::InvalidRef)?;
+            let repo = default_repo
</code_context>

<issue_to_address>
**suggestion (review_instructions):** Refactor the duplicated branch and repo resolution logic in `resolve_pr_reference` to keep the code DRY while remaining readable.

The `None` and `Some(input) if is_fragment_only(input)` arms both perform:
- `current_branch().ok_or(VkError::InvalidRef)?`
- the same `default_repo.and_then(parse_repo_str).or_else(repo_from_fetch_head).ok_or(VkError::RepoNotFound)?` pipeline.
Extract this into a small helper (e.g., `fn resolve_branch_and_repo(...)`) to avoid duplication while keeping the control flow clear.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
Keep code DRY, but readable. Use refactoring approaches best suited for the language in question. Context managers and generators for Python; RAII and macros for Rust.

</details>
</issue_to_address>

### Comment 6
<location> `src/branch_pr.rs:43` </location>
<code_context>
+///
+/// Returns [`VkError::NoPrForBranch`] if no PR exists for the branch, or
+/// propagates API errors from the underlying request.
+pub async fn fetch_pr_for_branch(
+    client: &GraphQLClient,
+    repo: &RepoInfo,
</code_context>

<issue_to_address>
**issue (review_instructions):** Add unit tests for `fetch_pr_for_branch` covering both successful and `NoPrForBranch` cases.

Currently the tests only cover deserialization helpers, not the behaviour of `fetch_pr_for_branch` itself. Add unit tests using a stub or mock `GraphQLClient` (or a test double) to verify that:
- When the underlying query returns at least one node, the function returns the correct PR number.
- When `nodes` is empty, the function returns `VkError::NoPrForBranch` with the expected branch value.
This satisfies the requirement for unit tests on new functionality.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 7
<location> `src/main.rs:57` </location>
<code_context>
-    /// thread starting from the referenced comment. When a fragment is
-    /// provided, both resolved and unresolved threads are searched.
-    /// Without a fragment, only unresolved threads are shown.
+    /// When invoked without arguments, detects the PR associated with the
+    /// current Git branch. Passing a `#discussion_r<ID>` fragment shows only
+    /// that discussion thread, auto-detecting the PR when no number or URL
</code_context>

<issue_to_address>
**issue (review_instructions):** Add behavioural tests to cover the new CLI behaviour where `pr` without arguments auto-detects the PR from the current branch and supports bare `#discussion_r<ID>` fragments.

You changed the `pr` command semantics so that:
- Invoking `pr` with no reference auto-detects the PR from the current branch.
- Passing a bare `#discussion_r<ID>` fragment auto-detects the PR and filters to that thread.
Add behavioural/e2e tests (e.g., in `tests/e2e.rs`) that invoke the binary or integration layer to validate these flows end-to-end, including error paths (no PR for branch, invalid fragment, etc.). Unit tests alone are not sufficient per the review instructions.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 8
<location> `src/cli_args.rs:59` </location>
<code_context>
+    /// A bare `#discussion_r<ID>` fragment shows only that discussion thread
+    /// (auto-detecting the PR). File filters are ignored when a fragment is
+    /// provided; unresolved filtering still applies.
+    #[arg(required = false)]
+    // The argument is optional; when absent the PR is auto-detected from the
+    // current branch. The `Option` allows `PrArgs::default()` and config
</code_context>

<issue_to_address>
**issue (review_instructions):** Ensure there are tests exercising the new optional `reference` argument semantics for the `pr` subcommand.

Changing `reference` from required to optional fundamentally alters how argument parsing behaves. Add tests that:
- Verify `pr` with no positional reference parses successfully and flows into the new auto-detection logic.
- Verify `pr` with a number, URL, and bare `#discussion_r<ID>` still parse as expected.
These can be CLI-level tests (e.g., invoking `PrArgs::try_parse_from` or higher-level behavioural tests) to demonstrate the new behaviour is correct.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 9
<location> `src/ref_parser.rs:253` </location>
<code_context>
+    /// Creates a temporary directory with a `.git` subdirectory and writes the
+    /// provided `head_content` to `.git/HEAD`. Changes to that directory,
+    /// executes the closure, then restores the original working directory.
+    fn with_git_head<F>(head_content: &str, test_fn: F)
+    where
+        F: FnOnce(),
</code_context>

<issue_to_address>
**suggestion (review_instructions):** Shared test setup in `with_git_head` is implemented as a helper function rather than an `rstest` fixture as requested.

The `with_git_head` helper encapsulates shared setup for multiple tests, but the review guidelines request using `rstest` fixtures for shared setup. Consider turning this into a fixture (e.g. a function or async fixture returning a temp repo with `.git/HEAD` configured) and injecting it into the tests via `#[rstest]` instead of calling a helper directly.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.rs`

**Instructions:**
Use `rstest` fixtures for shared setup.

</details>
</issue_to_address>

### Comment 10
<location> `src/ref_parser.rs:99` </location>
<code_context>
+///
+/// ```
+/// # use vk::ref_parser::parse_repo_str;
+/// let repo = parse_repo_str("owner/repo").unwrap();
+/// assert_eq!(repo.owner, "owner");
+/// assert_eq!(repo.name, "repo");
</code_context>

<issue_to_address>
**suggestion (review_instructions):** The doctest uses `.unwrap()` instead of `.expect()` which goes against the error-handling guideline.

The instructions ask to prefer `.expect()` over `.unwrap()`. In the doctest, `parse_repo_str("owner/repo").unwrap()` should be changed to something like `parse_repo_str("owner/repo").expect("valid owner/repo string")` so that failures carry an explicit message.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.rs`

**Instructions:**
Prefer `.expect()` over `.unwrap()`

</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/ref_parser.rs Outdated
Comment thread tests/e2e.rs Outdated
Comment thread src/commands.rs
Comment thread src/commands.rs
Comment thread src/ref_parser.rs Outdated
Comment thread src/ref_parser.rs Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dc3b318fbe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/graphql_queries.rs Outdated
- Fix .git path resolution to work from subdirectories by using
  `git rev-parse --show-toplevel` instead of assuming `.git` is in cwd
- Add `DetachedHead` error variant for clearer error messages when
  repository is in detached HEAD state
- Extract `resolve_branch_and_repo` helper to reduce duplication in
  `resolve_pr_reference` function
- Share `DISCUSSION_FRAGMENT` constant between `is_fragment_only` and
  `parse_fragment_only` functions
- Add request assertion support to e2e tests via
  `set_sequential_responder_with_assert` helper
- Add unit tests for `resolve_branch_and_repo` helper
- Add CLI argument parsing tests for optional reference semantics
- Convert `with_git_head` test helper to rstest `GitRepoFixture` struct
- Replace `.unwrap()` with `.expect()` in doctests per style guidelines
- Update e2e tests to initialize real git repositories for path
  resolution to work correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@coderabbitai coderabbitai Bot added the codex label Jan 17, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@src/commands/tests.rs`:
- Around line 75-98: The test returns non-deterministic results because it
relies on the current working directory's git state; change the test
returns_repo_from_default_repo_when_provided to use an rstest temp-repo fixture
that creates a fresh git repo (git init), writes a reachable HEAD and FETCH_HEAD
(or creates an initial commit and sets HEAD to a known branch), then call
resolve_branch_and_repo(Some("owner/repo")) from that temp repo and assert the
single expected outcome (e.g., Ok((repo, branch)) with repo.owner == "owner" and
repo.name == "repo"); reference the test function
returns_repo_from_default_repo_when_provided and the resolve_branch_and_repo
function when adding the fixture and assertions so the test no longer varies by
CI/dev cwd.

In `@src/ref_parser.rs`:
- Around line 5-8: Reorder the imports in src/ref_parser.rs to satisfy rustfmt:
combine and order std imports canonically (e.g., use std::{fs, process::Command,
sync::LazyLock};) and keep external imports like use url::Url; after them, then
run cargo fmt (or cargo fmt --check) to ensure the formatting passes CI.
- Around line 60-82: current_branch() and repo_from_fetch_head() incorrectly
read .git/HEAD and .git/FETCH_HEAD directly, which breaks for worktrees/linked
gitdirs where .git is a file pointing to a gitdir; instead invoke git to resolve
the correct paths and branch: call `git rev-parse --abbrev-ref HEAD` to get the
branch name (return None when it yields "HEAD" or failure) in current_branch(),
and call `git rev-parse --git-path FETCH_HEAD` to obtain the actual FETCH_HEAD
file path before reading it in repo_from_fetch_head(); replace the direct
fs::read_to_string(root.join(".git/HEAD")) and root.join(".git/FETCH_HEAD")
usages with these git-resolved results and propagate errors as Option::None on
failure.

In `@tests/e2e.rs`:
- Around line 289-296: Extract the repeated tempdir + git setup into an rstest
fixture: create a #[fixture] (e.g., git_repo_with_fetch_head) that calls
init_git_repo(dir.path(), head_content) and writes FETCH_HEAD into
dir.path().join(".git/FETCH_HEAD") using the fetch_head parameter (provide
defaults for head_content and fetch_head), then update the tests that currently
call tempdir()/init_git_repo()/fs::write(FETCH_HEAD) (the blocks around
init_git_repo and writing FETCH_HEAD) to accept and use this fixture via
#[rstest] and pass custom head_content/fetch_head only when needed.
- Around line 30-45: The test helper init_git_repo currently passes the
--initial-branch=main flag
(StdCommand::new("git").args(["init","--initial-branch=main"])...) which
requires Git >=2.28; make it backward-compatible by removing the hard flag and
instead run git with a per-invocation config (use git -c init.defaultBranch=main
init) or attempt the current command and on failure fall back to
StdCommand::new("git").args(["init"]) and then set the branch/HEAD explicitly;
update the init_git_repo logic to prefer the -c init.defaultBranch=main style or
implement the try-with-fallback approach so older Git versions still succeed.

In `@tests/utils/mod.rs`:
- Around line 226-227: Replace the lint suppression on the helper function
set_sequential_responder_with_assert: change the attribute from
#[allow(dead_code, reason = "...")] to #[expect(dead_code, reason = "...")] so
the test-only helper uses the expected-lint annotation; keep the same reason
string and location on the function signature to preserve intent and scope.
- Around line 237-242: The request body parsing inside the handler closure
currently silently skips assertions on malformed JSON because it uses if let
Ok(...); change the parsing in the closure created at
handler.lock().expect("lock handler") (the closure taking req: &Request<Bytes>)
to fail fast: replace the conditional parse with a direct parse that panics on
error (e.g., let json =
serde_json::from_slice::<serde_json::Value>(body_bytes).expect("invalid JSON
request body");) and then call assert_fn(&json) so malformed payloads do not
bypass assertions.
♻️ Duplicate comments (1)
src/graphql_queries.rs (1)

64-71: Disambiguate branch PR lookup across forks.

Filter by head repository owner (or use a search query with head:owner:branch) and add explicit ordering; headRefName plus first: 1 can return the wrong PR when multiple forks share a branch name.

GitHub GraphQL pullRequests headRefName headRepositoryOwner orderBy arguments

Comment thread src/commands/tests.rs
Comment thread src/ref_parser.rs Outdated
Comment thread src/ref_parser.rs Outdated
Comment thread tests/e2e.rs Outdated
Comment thread tests/e2e.rs Outdated
Comment thread tests/utils/mod.rs Outdated
Comment thread tests/utils/mod.rs
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 17, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 17, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

leynos and others added 2 commits January 17, 2026 13:49
Add a Mermaid flowchart documenting the three supported input modes
for `vk pr` reference resolution: no reference (auto-detect from
branch), fragment-only (#discussion_r...), and full reference
(URL or owner/repo#N format).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add a Mermaid sequence diagram showing the module interactions
during branch-based PR auto-detection: CLI → ref_parser →
branch_pr → GitHub GraphQL API, illustrating the flow from
user invocation through to PR data output.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@docs/vk-design.md`:
- Around line 248-253: The FRAG_ONLY/AUTO2 path oversimplifies branch detection;
change the diagram so FRAG_ONLY routes into the existing branch-detection flow
(e.g., FRAG_ONLY --> GET_BRANCH) instead of directly to FETCH_WITH_FRAG, and
update the PR_FOUND node to conditionally split to FETCH_WITH_FRAG when a
fragment is present and to FETCH_THREADS when not (use PR_FOUND -->|Yes,
fragment present| FETCH_WITH_FRAG and PR_FOUND -->|Yes, no fragment|
FETCH_THREADS); alternatively you may implement this by referencing a shared
Mermaid subgraph for GET_BRANCH → BRANCH_OK → GET_REPO → REPO_OK → QUERY_PR →
PR_FOUND and reuse it from both AUTO and FRAG_ONLY paths so the fragment-only
resolution is explicit and unambiguous.

Comment thread docs/vk-design.md Outdated
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 17, 2026

@coderabbitai Have the following now been resolved?

Please address the comments from this code review:

## Overall Comments
- Both `current_branch()` and `repo_from_fetch_head()` assume `.git` is in the current working directory, which will fail when `vk pr` is run from a subdirectory of the repo; consider resolving paths relative to the repository root (e.g. via `git rev-parse --show-toplevel` or similar) instead of assuming `.`.
- The `resolve_pr_reference` logic repeats the same `default_repo.and_then(parse_repo_str).or_else(repo_from_fetch_head).ok_or(VkError::RepoNotFound)?` chain in multiple match arms; factoring this into a small helper (e.g. `resolve_repo(default_repo)`) would reduce duplication and make future changes less error-prone.
- When in a detached HEAD state, `vk pr` now returns `VkError::InvalidRef` with a generic "invalid reference" message; consider introducing a more specific error variant or message to indicate the detached HEAD condition explicitly, since this is a common scenario users may need guidance on.

## Individual Comments

### Comment 1
<location> `src/ref_parser.rs:215-216` </location>
<code_context>
+/// assert!(!is_fragment_only("42#discussion_r123"));
+/// assert!(!is_fragment_only("https://github.com/o/r/pull/1#discussion_r123"));
+/// ```
+pub fn is_fragment_only(input: &str) -> bool {
+    input.starts_with("#discussion_r")
+}
+
</code_context>

<issue_to_address>
**suggestion:** Share the fragment prefix constant between `is_fragment_only` and `parse_fragment_only` to keep them in sync.

`is_fragment_only` hardcodes `"#discussion_r"` while `parse_fragment_only` uses a `FRAG` constant. Please reuse a single shared constant (e.g., a module-level `FRAG`) so changes to the fragment format are made in one place.

Suggested implementation:

```rust
pub fn is_fragment_only(input: &str) -> bool {
    input.starts_with(FRAG)
}

```

To fully apply the suggestion, make sure:

1. The `FRAG` constant used by `parse_fragment_only` is defined at module scope (e.g. near the top of `src/ref_parser.rs`), like:
   ```rust
   const FRAG: &str = "#discussion_r";
   ```
2. If `FRAG` is currently defined inside `parse_fragment_only`, move it out to module scope so both `parse_fragment_only` and `is_fragment_only` can reference the same constant.
</issue_to_address>

### Comment 2
<location> `tests/e2e.rs:237` </location>
<code_context>
+    })
+    .to_string();
+    let reviews_body = include_str!("fixtures/reviews_empty.json").to_string();
+    set_sequential_responder(&handler, vec![pr_lookup_body, threads_body, reviews_body]);
+
+    let dir = tempdir().expect("tempdir");
</code_context>

<issue_to_address>
**suggestion (testing):** Consider asserting the GraphQL request parameters (especially `headRef`) in the MITM handler to prove branch-based PR lookup wiring end-to-end.

Right now these e2e tests only verify behavior against canned GraphQL responses, not that the client sends the correct request. If `fetch_pr_for_branch` or `resolve_pr_reference` used the wrong branch or a hard-coded value, the tests could still pass. If the MITM helper supports it, consider extending `set_sequential_responder` (or adding a variant) to inspect the incoming GraphQL request body and assert that:

- `headRef` matches the branch name from `.git/HEAD` (e.g., `my-feature-branch`), and
- The repository owner/name match what was parsed from `FETCH_HEAD` or `--repo`.

That would turn these into true end-to-end checks of the branch-based lookup wiring to the API.

Suggested implementation:

```rust
    let threads_body = serde_json::json!({
        "data": {"repository": {"pullRequest": {"reviewThreads": {
            "nodes": [],
            "pageInfo": {"hasNextPage": false, "endCursor": null}
        }}}}
    })
    .to_string();
    let reviews_body = include_str!("fixtures/reviews_empty.json").to_string();

    // Assert GraphQL request variables to make this a true end-to-end wiring test.
    //
    // We expect:
    // - headRef to match the branch name from .git/HEAD
    // - owner/name to match what was parsed from FETCH_HEAD / --repo
    set_sequential_responder_with_assert(
        &handler,
        vec![pr_lookup_body, threads_body, reviews_body],
        |body: &serde_json::Value| {
            // Shape is expected to be: { "query": "...", "variables": { ... } }
            let vars = &body["variables"];

            // Branch-based lookup wiring
            assert_eq!(
                vars["headRef"],
                "my-feature-branch",
                "GraphQL headRef variable should match the branch from .git/HEAD"
            );

            // Repository owner/name wiring
            assert_eq!(
                vars["owner"],
                "owner",
                "GraphQL owner variable should match the repo owner parsed from FETCH_HEAD / --repo"
            );
            assert_eq!(
                vars["name"],
                "repo",
                "GraphQL name variable should match the repo name parsed from FETCH_HEAD / --repo"
            );
        },
    );


```

To fully implement this change, you will need to:

1. **Add a new helper** in your MITM / test HTTP helper module (where `set_sequential_responder` is defined), for example:

   ```rust
   pub fn set_sequential_responder_with_assert<F>(
       handler: &HandlerType,                    // use the actual handler type
       responses: Vec<String>,
       assert_body: F,
   )
   where
       F: Fn(&serde_json::Value) + Send + Sync + 'static,
   {
       // Pseudocode – adapt to your existing MITM implementation:
       //
       // 1. For each expected response, register a handler on `handler`
       //    that:
       //    - reads the incoming HTTP request body,
       //    - parses it as JSON (`serde_json::from_slice`),
       //    - calls `assert_body(&parsed_json)`,
       //    - returns the corresponding canned response.
       //
       // 2. Preserve any existing behavior from `set_sequential_responder`
       //    (status codes, headers, sequencing, etc.).
   }
   ```

2. **Ensure JSON parsing of GraphQL requests**:
   - The function must read the incoming request body and parse it as `serde_json::Value`.
   - Call the provided `assert_body` closure before returning the canned response.

3. **Update call sites as needed**:
   - Keep the existing `set_sequential_responder` for tests that don’t care about inspecting the request.
   - Use `set_sequential_responder_with_assert` where you want end-to-end wiring assertions like in this test.

4. If your actual GraphQL variables use different keys (e.g. `headRefName`, `repositoryOwner`, `repositoryName`), adjust the `vars["..."]` lookups in the test and the assertions to match the real schema.
</issue_to_address>

### Comment 3
<location> `src/commands.rs:174` </location>
<code_context>
+/// 1. No reference: detect PR from current branch
+/// 2. Fragment only (`#discussion_r<ID>`): detect PR from branch, extract comment ID
+/// 3. Full reference: use existing parsing
+async fn resolve_pr_reference(
+    reference: Option<&str>,
+    default_repo: Option<&str>,
</code_context>

<issue_to_address>
**issue (complexity):** Consider extracting the shared branch-and-repo resolution into a helper to simplify and de-duplicate `resolve_pr_reference`.

You can reduce the new complexity by factoring out the shared “branch + repo” resolution and keeping `resolve_pr_reference` focused on classifying the reference and wiring calls together.

Specifically, the `None` and `Some(input) if is_fragment_only(input)` arms duplicate:

- `current_branch().ok_or(VkError::InvalidRef)?`
- `default_repo.and_then(parse_repo_str).or_else(repo_from_fetch_head).ok_or(VkError::RepoNotFound)?`
- `fetch_pr_for_branch(client, &repo, &branch).await?`

Refactor that into a small helper and reuse it:

```rust
fn resolve_branch_and_repo(
    default_repo: Option<&str>,
) -> Result<(RepoInfo, String), VkError> {
    let branch = current_branch().ok_or(VkError::InvalidRef)?;
    let repo = default_repo
        .and_then(parse_repo_str)
        .or_else(repo_from_fetch_head)
        .ok_or(VkError::RepoNotFound)?;
    Ok((repo, branch))
}
```

Then simplify `resolve_pr_reference` to:

```rust
async fn resolve_pr_reference(
    reference: Option<&str>,
    default_repo: Option<&str>,
    client: &GraphQLClient,
) -> Result<(RepoInfo, u64, Option<u64>), VkError> {
    match reference {
        None => {
            let (repo, branch) = resolve_branch_and_repo(default_repo)?;
            let number = fetch_pr_for_branch(client, &repo, &branch).await?;
            Ok((repo, number, None))
        }
        Some(input) if is_fragment_only(input) => {
            let comment_id = parse_fragment_only(input)?;
            let (repo, branch) = resolve_branch_and_repo(default_repo)?;
            let number = fetch_pr_for_branch(client, &repo, &branch).await?;
            Ok((repo, number, Some(comment_id)))
        }
        Some(input) => parse_pr_thread_reference(input, default_repo),
    }
}
```

This:

- Removes duplicated logic and ensures the “detect PR from branch” path stays consistent.
- Keeps `resolve_pr_reference` focused on reference classification + orchestration, with repo/branch resolution in a single, testable helper.
- Preserves all current behavior, including the new “no reference” and fragment-only cases.
</issue_to_address>

### Comment 4
<location> `src/commands.rs:174` </location>
<code_context>
+/// 1. No reference: detect PR from current branch
+/// 2. Fragment only (`#discussion_r<ID>`): detect PR from branch, extract comment ID
+/// 3. Full reference: use existing parsing
+async fn resolve_pr_reference(
+    reference: Option<&str>,
+    default_repo: Option<&str>,
</code_context>

<issue_to_address>
**issue (review_instructions):** Add both unit and behavioural tests for the new PR auto-detection and fragment-only resolution logic.

Exercise `resolve_pr_reference` via tests that cover all three match arms:
- `None` reference: PR auto-detected from the current branch (happy path and error cases such as missing `.git/HEAD`, no repo, or no PR for branch).
- Fragment-only reference: `#discussion_r<ID>` combined with branch auto-detection, ensuring the comment ID is correctly propagated.
- Full reference: regression tests verifying existing behaviour is preserved.
These should include at least one higher-level/behavioural test (e.g., integration or e2e) for the CLI flow, not just pure unit tests.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 5
<location> `src/commands.rs:180` </location>
<code_context>
+    client: &GraphQLClient,
+) -> Result<(RepoInfo, u64, Option<u64>), VkError> {
+    match reference {
+        None => {
+            let branch = current_branch().ok_or(VkError::InvalidRef)?;
+            let repo = default_repo
</code_context>

<issue_to_address>
**suggestion (review_instructions):** Refactor the duplicated branch and repo resolution logic in `resolve_pr_reference` to keep the code DRY while remaining readable.

The `None` and `Some(input) if is_fragment_only(input)` arms both perform:
- `current_branch().ok_or(VkError::InvalidRef)?`
- the same `default_repo.and_then(parse_repo_str).or_else(repo_from_fetch_head).ok_or(VkError::RepoNotFound)?` pipeline.
Extract this into a small helper (e.g., `fn resolve_branch_and_repo(...)`) to avoid duplication while keeping the control flow clear.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
Keep code DRY, but readable. Use refactoring approaches best suited for the language in question. Context managers and generators for Python; RAII and macros for Rust.

</details>
</issue_to_address>

### Comment 6
<location> `src/branch_pr.rs:43` </location>
<code_context>
+///
+/// Returns [`VkError::NoPrForBranch`] if no PR exists for the branch, or
+/// propagates API errors from the underlying request.
+pub async fn fetch_pr_for_branch(
+    client: &GraphQLClient,
+    repo: &RepoInfo,
</code_context>

<issue_to_address>
**issue (review_instructions):** Add unit tests for `fetch_pr_for_branch` covering both successful and `NoPrForBranch` cases.

Currently the tests only cover deserialization helpers, not the behaviour of `fetch_pr_for_branch` itself. Add unit tests using a stub or mock `GraphQLClient` (or a test double) to verify that:
- When the underlying query returns at least one node, the function returns the correct PR number.
- When `nodes` is empty, the function returns `VkError::NoPrForBranch` with the expected branch value.
This satisfies the requirement for unit tests on new functionality.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 7
<location> `src/main.rs:57` </location>
<code_context>
-    /// thread starting from the referenced comment. When a fragment is
-    /// provided, both resolved and unresolved threads are searched.
-    /// Without a fragment, only unresolved threads are shown.
+    /// When invoked without arguments, detects the PR associated with the
+    /// current Git branch. Passing a `#discussion_r<ID>` fragment shows only
+    /// that discussion thread, auto-detecting the PR when no number or URL
</code_context>

<issue_to_address>
**issue (review_instructions):** Add behavioural tests to cover the new CLI behaviour where `pr` without arguments auto-detects the PR from the current branch and supports bare `#discussion_r<ID>` fragments.

You changed the `pr` command semantics so that:
- Invoking `pr` with no reference auto-detects the PR from the current branch.
- Passing a bare `#discussion_r<ID>` fragment auto-detects the PR and filters to that thread.
Add behavioural/e2e tests (e.g., in `tests/e2e.rs`) that invoke the binary or integration layer to validate these flows end-to-end, including error paths (no PR for branch, invalid fragment, etc.). Unit tests alone are not sufficient per the review instructions.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 8
<location> `src/cli_args.rs:59` </location>
<code_context>
+    /// A bare `#discussion_r<ID>` fragment shows only that discussion thread
+    /// (auto-detecting the PR). File filters are ignored when a fragment is
+    /// provided; unresolved filtering still applies.
+    #[arg(required = false)]
+    // The argument is optional; when absent the PR is auto-detected from the
+    // current branch. The `Option` allows `PrArgs::default()` and config
</code_context>

<issue_to_address>
**issue (review_instructions):** Ensure there are tests exercising the new optional `reference` argument semantics for the `pr` subcommand.

Changing `reference` from required to optional fundamentally alters how argument parsing behaves. Add tests that:
- Verify `pr` with no positional reference parses successfully and flows into the new auto-detection logic.
- Verify `pr` with a number, URL, and bare `#discussion_r<ID>` still parse as expected.
These can be CLI-level tests (e.g., invoking `PrArgs::try_parse_from` or higher-level behavioural tests) to demonstrate the new behaviour is correct.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural *and* unit tests are required.

</details>
</issue_to_address>

### Comment 9
<location> `src/ref_parser.rs:253` </location>
<code_context>
+    /// Creates a temporary directory with a `.git` subdirectory and writes the
+    /// provided `head_content` to `.git/HEAD`. Changes to that directory,
+    /// executes the closure, then restores the original working directory.
+    fn with_git_head<F>(head_content: &str, test_fn: F)
+    where
+        F: FnOnce(),
</code_context>

<issue_to_address>
**suggestion (review_instructions):** Shared test setup in `with_git_head` is implemented as a helper function rather than an `rstest` fixture as requested.

The `with_git_head` helper encapsulates shared setup for multiple tests, but the review guidelines request using `rstest` fixtures for shared setup. Consider turning this into a fixture (e.g. a function or async fixture returning a temp repo with `.git/HEAD` configured) and injecting it into the tests via `#[rstest]` instead of calling a helper directly.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.rs`

**Instructions:**
Use `rstest` fixtures for shared setup.

</details>
</issue_to_address>

### Comment 10
<location> `src/ref_parser.rs:99` </location>
<code_context>
+///
+/// ```
+/// # use vk::ref_parser::parse_repo_str;
+/// let repo = parse_repo_str("owner/repo").unwrap();
+/// assert_eq!(repo.owner, "owner");
+/// assert_eq!(repo.name, "repo");
</code_context>

<issue_to_address>
**suggestion (review_instructions):** The doctest uses `.unwrap()` instead of `.expect()` which goes against the error-handling guideline.

The instructions ask to prefer `.expect()` over `.unwrap()`. In the doctest, `parse_repo_str("owner/repo").unwrap()` should be changed to something like `parse_repo_str("owner/repo").expect("valid owner/repo string")` so that failures carry an explicit message.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.rs`

**Instructions:**
Prefer `.expect()` over `.unwrap()`

</details>
</issue_to_address>

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 17, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

Code Duplication

src/main.rs:

What lead to degradation?

introduced similar code in: tests.pr_subcommand_parses_fragment_only,tests.pr_subcommand_parses_without_reference

Why does this problem occur?

Duplicated code often leads to code that's harder to change since the same logical change has to be done in multiple functions. More duplication gives lower code health.

How to fix it?

A certain degree of duplicated code might be acceptable. The problems start when it is the same behavior that is duplicated across the functions in the module, ie. a violation of the Don't Repeat Yourself (DRY) principle. DRY violations lead to code that is changed together in predictable patterns, which is both expensive and risky. DRY violations can be identified using CodeScene's X-Ray analysis to detect clusters of change coupled functions with high code similarity. Read More
Once you have identified the similarities across functions, look to extract and encapsulate the concept that varies into its own function(s). These shared abstractions can then be re-used, which minimizes the amount of duplication and simplifies change.

- Route FRAG_ONLY through shared branch detection subgraph instead
  of oversimplified direct path to FETCH_WITH_FRAG
- Add subgraph to encapsulate branch-based PR detection flow
- Track fragment state with SET_FRAG/SET_NO_FRAG nodes
- Split PR_FOUND into conditional paths based on fragment presence
- Both AUTO and FRAG_ONLY now use identical branch detection logic

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

- Reorder imports in ref_parser.rs for rustfmt compliance
- Use git symbolic-ref and git rev-parse --git-path for worktree/linked
  gitdir support in current_branch() and repo_from_fetch_head()
- Make init_git_repo backward-compatible with Git < 2.28 using
  -c init.defaultBranch=main instead of --initial-branch
- Create GitRepoWithFetchHead fixture in e2e.rs to reduce test setup
  duplication
- Make resolve_branch_and_repo tests deterministic using rstest fixtures
  with proper git symbolic-ref setup
- Make JSON parsing fail fast in set_sequential_responder_with_assert

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@docs/vk-design.md`:
- Around line 245-246: The flowchart node GET_BRANCH currently says "Get current
branch from .git/HEAD" which is inaccurate; update the GET_BRANCH label to
describe the git-based branch resolution (worktree-safe lookup) instead of a
direct .git/HEAD read so it matches the implementation, and ensure the
downstream BRANCH_OK decision text remains consistent with that change.
♻️ Duplicate comments (2)
tests/utils/mod.rs (1)

226-227: Replace forbidden #[allow] with #[expect] on the new helper.

Swap the lint suppression to #[expect(dead_code, reason = "...")] so the policy is honoured. As per coding guidelines, avoid #[allow] and use narrowly scoped #[expect].

♻️ Proposed fix
-#[allow(dead_code, reason = "helper used in some tests only")]
+#[expect(dead_code, reason = "helper used in some tests only")]
tests/e2e.rs (1)

95-126: Convert shared git-repo setup into an rstest fixture.

Replace the shared setup with an #[fixture] that yields the repo instance so duplicated wiring is removed and the tests align with the required fixture style. As per coding guidelines, use rstest fixtures for shared setup.

♻️ Fixture sketch
+use rstest::fixture;
+
 const DEFAULT_FETCH_HEAD: &str =
     "deadbeef\tnot-for-merge\tbranch 'main' of https://github.com/owner/repo.git";
+
+#[fixture]
+fn git_repo_with_fetch_head(
+    #[default("ref: refs/heads/main\n")] head: &str,
+    #[default(DEFAULT_FETCH_HEAD)] fetch_head: &str,
+) -> GitRepoWithFetchHead {
+    GitRepoWithFetchHead::new(head, fetch_head)
+}
-    let repo = GitRepoWithFetchHead::new(
+    let repo = git_repo_with_fetch_head(
         "ref: refs/heads/my-feature-branch\n",
         "deadbeef\tnot-for-merge\tbranch 'main' of https://github.com/owner/repo.git",
     );

Comment thread docs/vk-design.md Outdated
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 20, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

src/branch_pr.rs

Comment on lines +381 to +412

        async fn filters_by_head_owner_when_provided(upstream_repo: RepoInfo) {
            let body = json!({
                "data": {
                    "repository": {
                        "pullRequests": {
                            "nodes": [
                                {
                                    "number": 100,
                                    "headRepository": {
                                        "owner": { "login": "other-fork" }
                                    }
                                },
                                {
                                    "number": 200,
                                    "headRepository": {
                                        "owner": { "login": "my-fork" }
                                    }
                                }
                            ]
                        }
                    }
                }
            })
            .to_string();
            let server = start_mock_server(body);

            let result =
                fetch_pr_for_branch(server.client(), &upstream_repo, "feature", Some("my-fork"))
                    .await;

            assert_eq!(result.expect("success"), 200);
        }

❌ New issue: Code Duplication
The module contains 3 functions with similar structure: tests.fetch_pr_for_branch_tests.filters_by_head_owner_when_provided,tests.fetch_pr_for_branch_tests.returns_first_pr_when_head_owner_is_none,tests.fetch_pr_for_branch_tests.skips_pr_with_null_head_repository

@coderabbitai

This comment was marked as resolved.

- Deleted large src/branch_pr.rs and moved its contents to new
  src/branch_pr/mod.rs namespace
- Encapsulated PR-related data structures and fetch_pr_for_branch function
  within branch_pr module
- Added comprehensive tests for the branch_pr logic to src/branch_pr/tests.rs
- Minor adjustments to ref_parser module for attribute style (allow to expect)

This refactor improves module organization by isolating PR lookup logic in its
own module, improving code clarity and maintainability without functional changes.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@src/branch_pr/mod.rs`:
- Around line 96-99: The Map insertion uses unnecessary clones inside the json!
macro; change the inserts to pass references (e.g. json!(&repo.owner),
json!(&repo.name), and json!(branch) as-is) so you don't clone repo.owner/name
unnecessarily—update the three vars.insert calls (the Map named vars, and the
json! usage) to use references instead of .clone().

In `@src/branch_pr/tests.rs`:
- Around line 125-187: The mock server (MockServer / start_mock_server) should
capture and assert incoming request bodies so tests verify GraphQL variables;
inside the service_fn handler used in start_mock_server, read the request body
(e.g., hyper::body::to_bytes(req.into_body()).await), parse it to
serde_json::Value, and assert the expected fields like
json["variables"]["owner"], json["variables"]["name"], and
json["variables"]["headRef"] before returning the mocked response; keep the
existing RAII cleanup and client creation (GraphQLClient::with_endpoint_retry)
but add these request-inspection assertions to validate the client sends the
correct query/variables.
- Around line 189-203: The two test fixtures basic_repo and upstream_repo use
the fully-qualified attribute #[rstest::fixture]; change both to the imported
attribute #[fixture] to match the existing rstest import and project
style—update the attributes on the functions basic_repo() and upstream_repo() to
#[fixture] so the crate import is used consistently.

In `@src/ref_parser/mod.rs`:
- Around line 278-290: Add Rust doc comments (///) to the public functions
parse_issue_reference and parse_pr_reference describing their purpose (parsing a
string into (RepoInfo, u64) for issue and PR references respectively),
documenting the parameters (input: &str, default_repo: impl
Into<DefaultRepo<'a>>), the return value, and possible error VkError conditions;
place the comments immediately above each function signature and keep them
concise so cargo doc picks them up (mention ResourceType mapping to Issues and
PullRequest for clarity).

Comment thread src/branch_pr/mod.rs
Comment thread src/branch_pr/tests.rs
Comment thread src/branch_pr/tests.rs Outdated
Comment thread src/ref_parser/mod.rs
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 20, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

src/branch_pr/tests.rs

Comment on lines +257 to +287

    async fn filters_by_head_owner_when_provided(upstream_repo: RepoInfo) {
        let body = json!({
            "data": {
                "repository": {
                    "pullRequests": {
                        "nodes": [
                            {
                                "number": 100,
                                "headRepository": {
                                    "owner": { "login": "other-fork" }
                                }
                            },
                            {
                                "number": 200,
                                "headRepository": {
                                    "owner": { "login": "my-fork" }
                                }
                            }
                        ]
                    }
                }
            }
        })
        .to_string();
        let server = start_mock_server(body);

        let result =
            fetch_pr_for_branch(server.client(), &upstream_repo, "feature", Some("my-fork")).await;

        assert_eq!(result.expect("success"), 200);
    }

❌ New issue: Code Duplication
The module contains 3 functions with similar structure: fetch_pr_for_branch_tests.filters_by_head_owner_when_provided,fetch_pr_for_branch_tests.returns_first_pr_when_head_owner_is_none,fetch_pr_for_branch_tests.skips_pr_with_null_head_repository

@coderabbitai

This comment was marked as resolved.

- Enhance fetch_pr_for_branch to filter PRs by head owner when provided.
- Add comprehensive tests covering filtering logic and null headRepository cases.
- Introduce helpers to build mock PR data for testing.
- Minor code fixes: use references when inserting JSON values for owner and name.

Also add parsing helpers in ref_parser for issue and pull request references.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/ref_parser/mod.rs`:
- Around line 395-403: The module exceeds the 400-line cap; split out URL
parsing and git helper logic into one or more submodules (e.g., create new
submodules like url.rs and git.rs and declare them in mod.rs), move related
functions such as parse_fragment_only plus constants like DISCUSSION_FRAGMENT
and error type references (VkError) that belong to URL parsing into the new url
submodule, update mod.rs to pub use the moved symbols or adjust call sites to
reference ref_parser::url::parse_fragment_only, and ensure internal imports and
visibility (pub/pub(crate)) are corrected so existing callers compile.
- Around line 278-323: Add runnable doc examples for both parse_issue_reference
and parse_pr_reference by inserting a fenced code block under their /// #
Examples sections that imports the function (e.g. use
vk::ref_parser::parse_issue_reference;), calls it with a full GitHub URL (e.g.
"https://github.com/o/r/issues/42" and "https://github.com/o/r/pull/42"),
unwraps the Result, and asserts repo.owner == "o", repo.name == "r", and number
== 42; ensure the examples compile as doctests by using the same argument type
expected for default_repo (convert or pass the appropriate DefaultRepo/None
value used in project docs) and keeping the code minimal so it runs as-is in CI.
- Around line 76-78: The static GITHUB_RE currently uses .expect() and may panic
at startup; change its type to LazyLock<Result<Regex, regex::Error>> and
construct it without .expect() so compilation errors are stored, then update
parse_repo_str to retrieve and propagate that Result (e.g., map_err/and_then or
?-propagate) before using the regex — reference GITHUB_RE and parse_repo_str to
locate the change, return or propagate the regex::Error from parse_repo_str (or
convert to the function's error type) instead of letting the program panic.
♻️ Duplicate comments (1)
src/branch_pr/tests.rs (1)

153-187: Assert GraphQL variables in the mock server handler.

Validate owner, name, and headRef in the incoming JSON so the tests verify the client’s query wiring, not just responses.

♻️ Proposed refactor
+    #[derive(Clone, Copy)]
+    struct ExpectedVars {
+        owner: &'static str,
+        name: &'static str,
+        head_ref: &'static str,
+    }
+
-    fn start_mock_server(body: String) -> MockServer {
+    fn start_mock_server(body: String, expected: ExpectedVars) -> MockServer {
         let body = Arc::new(body);
+        let expected = Arc::new(expected);
         let svc = third_wheel::hyper::service::make_service_fn(move |_conn| {
             let body = Arc::clone(&body);
+            let expected = Arc::clone(&expected);
             async move {
-                Ok::<_, Infallible>(service_fn(move |_req| {
+                Ok::<_, Infallible>(service_fn(move |req| {
                     let body = Arc::clone(&body);
+                    let expected = Arc::clone(&expected);
                     async move {
+                        let bytes = third_wheel::hyper::body::to_bytes(req.into_body())
+                            .await
+                            .expect("request body");
+                        let json: serde_json::Value =
+                            serde_json::from_slice(&bytes).expect("request json");
+                        assert_eq!(json["variables"]["owner"].as_str(), Some(expected.owner));
+                        assert_eq!(json["variables"]["name"].as_str(), Some(expected.name));
+                        assert_eq!(json["variables"]["headRef"].as_str(), Some(expected.head_ref));
                         Ok::<_, Infallible>(
                             Response::builder()
                                 .status(StatusCode::OK)
                                 .header("Content-Type", "application/json")
                                 .body(Body::from(body.as_ref().clone()))
                                 .expect("response"),
                         )
                     }
                 }))
             }
         });

Update call sites to pass ExpectedVars per test.

Comment thread src/ref_parser/mod.rs Outdated
Comment thread src/ref_parser/mod.rs
Comment thread src/ref_parser/mod.rs
- Introduced new `git` module in `ref_parser` for extracting repository info and current branch
- Support fetching origin remote URL, current branch, and parsing `FETCH_HEAD`, compatible with worktrees
- Moved existing git detection code from `mod.rs` to `git.rs` and re-exported functions
- Updated tests to reflect changes and improve PR lookup JSON construction

This enhances repository context detection by interfacing directly with git commands, improving robustness for fork detection and branch identification.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@src/branch_pr/tests.rs`:
- Around line 75-123: The test duplicates the predicate logic from
fetch_pr_for_branch; extract that predicate into a reusable helper in mod.rs
(e.g., pub(crate) fn head_owner_matches(pr: &PrNode, owner: &str) -> bool) that
encapsulates the head_repository/as_ref/is_some_and and eq_ignore_ascii_case
check, update fetch_pr_for_branch to call this helper, and change this test to
call the new helper (or remove it and rely on fetch_pr_for_branch_tests) so the
behavior is asserted via the single implementation rather than duplicated logic.
- Around line 228-229: Swap the attribute order so rstest runs before tokio's
test macro for each async test: place #[rstest] above #[tokio::test] for the
functions returns_pr_number_on_success, returns_no_pr_for_branch_when_empty,
head_owner_filtering, and returns_no_pr_when_head_owner_not_found; update the
attribute ordering on those function declarations so rstest's macro expansion is
applied first.

In `@src/ref_parser/mod.rs`:
- Around line 118-147: The short-ref branch in parse_repo_str currently accepts
any string containing '/' which lets inputs with extra path segments (e.g.,
"o/r/extra" or full URLs not matched by GITHUB_RE) pass; change that branch to
only accept exactly one '/' and non-empty owner and repo parts: replace the
repo.contains('/') check with a check that the slash count == 1 (or split into
parts and ensure parts.len() == 2), then validate both owner and name_part are
non-empty before returning RepoInfo and still call strip_git_suffix(name_part);
keep behavior of the GITHUB_RE branch and use the same RepoInfo construction
(refer to parse_repo_str, GITHUB_RE, strip_git_suffix, RepoInfo).
- Around line 243-280: Extract the three-branch match that parses
DISCUSSION_FRAGMENT out of parse_pr_thread_reference into a small helper (e.g.,
parse_discussion_fragment) that takes the input &str and returns Result<(&str,
Option<u64>), VkError>; inside the helper mirror the current logic:
split_once(DISCUSSION_FRAGMENT), return Err(VkError::InvalidRef) for empty
fragments or non-numeric ids and Ok((input, None)) when no fragment; then
simplify parse_pr_thread_reference to call this helper, pass the returned base
into parse_pr_reference along with default_repo, and return Ok((repo, number,
comment)). Ensure the helper uses the same symbols (DISCUSSION_FRAGMENT,
VkError::InvalidRef) and accepts default_repo only in parse_pr_reference as
before.
♻️ Duplicate comments (2)
src/branch_pr/tests.rs (1)

125-187: MockServer lacks request inspection.

The MockServer correctly handles RAII cleanup and dynamic port allocation. However, per prior feedback, the mock server does not inspect incoming GraphQL request variables to verify the client sends the expected headRef, owner, and name values. This remains unaddressed.

src/ref_parser/mod.rs (1)

80-82: Remove panic on regex initialization.

Line 80 calls .expect() outside tests, which can panic at start-up and breaches the no-expect rule. Store the regex initialization result and short-circuit in parse_repo_str.

♻️ Proposed fix
-static GITHUB_RE: LazyLock<Regex> = LazyLock::new(|| {
-    Regex::new(r"github\.com[/:](?P<owner>[^/]+)/(?P<repo>[^/]+)").expect("valid regex")
-});
+static GITHUB_RE: LazyLock<Result<Regex, regex::Error>> = LazyLock::new(|| {
+    Regex::new(r"github\.com[/:](?P<owner>[^/]+)/(?P<repo>[^/]+)")
+});
@@
-pub fn parse_repo_str(repo: &str) -> Option<RepoInfo> {
-    if let Some(caps) = GITHUB_RE.captures(repo) {
+pub fn parse_repo_str(repo: &str) -> Option<RepoInfo> {
+    let re = GITHUB_RE.as_ref().ok()?;
+    if let Some(caps) = re.captures(repo) {
As per coding guidelines.

Comment thread src/branch_pr/tests.rs
Comment thread src/branch_pr/tests.rs Outdated
Comment thread src/ref_parser/mod.rs
Comment thread src/ref_parser/mod.rs
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 21, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

src/ref_parser/mod.rs

Comment on file

//! Parse pull request and issue references into repository and number pairs, optionally including discussion comment IDs.

❌ New issue: String Heavy Function Arguments
In this module, 45.0% of all arguments to its 15 functions are strings. The threshold for string arguments is 39.0%

@coderabbitai

This comment was marked as resolved.

… and enhance PR reference parsing with discussion fragments

- Introduced `head_owner_matches` helper in branch_pr to simplify and unify case-insensitive owner comparison.
- Updated `fetch_pr_for_branch` and tests to use the new helper.
- Improved parse_repo_str to only accept owner/repo with exactly one slash and non-empty parts.
- Added support for parsing PR references with optional discussion fragments (`#discussion_r<id>`) in ref_parser.
- Refactored PR thread parsing to use a helper function for discussion fragment extraction.

These changes improve code clarity, correctness, and extend PR reference parsing capabilities.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

- Extract parsing logic and helper types (ResourceType, parse_github_url, etc.) from mod.rs into a new internal parse.rs module
- Remove redundant code and imports from mod.rs
- Introduce LazyLock for regex compilation in parse.rs
- Keep API surface in mod.rs minimal by re-exporting only needed items

This improves code organization by separating core parsing implementation details from the public API.

Additionally, enhance branch_pr tests:
- Add capturing of GraphQL request variables in mock server
- Verify sent request variables in tests for more robust validation.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 21, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

src/ref_parser/mod.rs

Comment on file

//! Parse pull request and issue references into repository and number pairs, optionally including discussion comment IDs.

❌ New issue: String Heavy Function Arguments
In this module, 50.0% of all arguments to its 12 functions are strings. The threshold for string arguments is 39.0%

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 21, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

src/branch_pr/tests.rs

Comment on lines +172 to +174

                        if let Some(bytes) = bytes
                            && let Ok(json) = serde_json::from_slice::<Value>(&bytes)
                            && let Some(vars) = json.get("variables").cloned()

❌ New issue: Complex Conditional
fetch_pr_for_branch_tests.start_mock_server has 1 complex conditionals with 2 branches, threshold = 2

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

Refactor test code in src/branch_pr/tests.rs by extracting the logic for
parsing GraphQL variables from the request body into a dedicated
helper function `extract_graphql_variables`. This improves code
readability and reusability in the test suite.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title Extend vk pr: implicit PR retrieval with branch/fragment support Extend vk pr: implicit PR retrieval with branch/fragment handling Jan 21, 2026
@leynos leynos merged commit a9e7495 into main Jan 21, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant