Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,22 @@ customizations.
let g:vimini_review_max_turns = 30
```

8. **Context Files (`g:context_files`)**:
8. **Default Git Remote (`g:vimini_default_remote`)**:
Set a default git remote to fetch GitHub PRs or GitLab MRs from when using `:ViminiReview` without the `--remote` flag.
If not configured, Vimini will check available remotes and use `upstream` (preferred for fork workflows), or the
canonical remote for the current branch, falling back to default branches (`main`/`master`) or `origin`.
```vim
let g:vimini_default_remote = 'gh'
```

9. **Context Files (`g:context_files`)**:
In addition to all files currently open in Vim buffers, you can specify a list of files to always include as context for AI commands like `:ViminiCode`. This is useful for providing the AI with important project files (like configurations, core utilities, or type definitions) without needing to have them open. Note: The total size of all uploaded context files combined is strictly limited to 1MB; if this limit is exceeded, the largest files will automatically be excluded.
```vim
" Always include these files as context for code generation tasks
let g:context_files = ['package.json', 'src/main.js', 'src/utils/api.js']
```

9. **Project Configuration (`ViminiConfig`)**:
10. **Project Configuration (`ViminiConfig`)**:
Configure per-project settings such as whether compilation is needed, shell commands for compiling/building code and running tests when invoked by chat tools (`build_code` and `test_code`), and the directory path where temporary git worktrees can be created. Project configuration is stored in `~/.var/vimini/projects/<project_name>`:
```json
{
Expand Down Expand Up @@ -264,15 +272,15 @@ Sends content to the Gemini model for a code review. This command has three main

1. **Current Buffer Review**: If no `-c`, `--pr`, or `--mr` option is provided, it sends the content of the current buffer for review.
2. **Git Object Review**: If the `-c <git_objects>` option is provided, it reviews the changes specified by the git objects. It sends the `git show` output (the diff) to the AI, and critically, it also uploads the full content of all changed files to provide complete context for the review. `<git_objects>` can be any valid git object reference, like a commit hash, branch name, or a range like `HEAD~3..HEAD`.
3. **GitHub PR / GitLab MR Review**: Use `--pr <number>` (or `--github <number>`) or `--mr <number>` (or `--gitlab <number>`) to automatically fetch the branch from the remote repository and run the review on its commits. You can optionally specify `--remote <remote>` (default: `origin`).
3. **GitHub PR / GitLab MR Review**: Use `--pr <number>` (or `--github <number>`) or `--mr <number>` (or `--gitlab <number>`) to automatically fetch the branch from the remote repository and run the review on its commits. You can optionally specify `--remote <remote>` (defaults to `g:vimini_default_remote`, `upstream`, the canonical remote for the current branch, or `origin`).

You can add an optional `{prompt}` to guide the AI's review. The review will be displayed in a new vertical split buffer. If `g:vimini_thinking` is `on`, an additional buffer showing the AI's thought process will also be opened.

**Additional Options:**

* `--pr <number>` / `--github <number>`: Fetches and reviews the specified GitHub Pull Request number.
* `--mr <number>` / `--gitlab <number>`: Fetches and reviews the specified GitLab Merge Request number.
* `--remote <remote>`: Remote git repository to fetch PR/MR from (defaults to `origin`).
* `--remote <remote>`: Remote git repository to fetch PR/MR from (defaults to `g:vimini_default_remote`, `upstream`, the canonical remote for the current branch, or `origin`).
* `--security`: Narrows the scope of the review to focus exclusively on security vulnerabilities, insecure coding practices, and potential attack vectors.
* `--save[=<path>]`: Used with `-c`, `--pr`, or `--mr`. This option reviews each commit in the given range individually and saves each review to a separate file. If a path is provided (e.g., `--save=./reviews`), files are saved there. Otherwise, they are saved in the root of the git repository (e.g., `0001-fix-login-bug.review.txt`).

Expand Down
5 changes: 4 additions & 1 deletion plugin/vimini.vim
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ let g:vimini_review_path = get(g:, 'vimini_review_path', '')
" Configuration: Maximum review tool iterations
let g:vimini_review_max_turns = get(g:, 'vimini_review_max_turns', 30)

" Configuration: Default git remote for PR/MR reviews
let g:vimini_default_remote = get(g:, 'vimini_default_remote', '')

" Configuration: Agent Channel
let g:vimini_channel = get(g:, 'vimini_channel', v:null)

Expand Down Expand Up @@ -234,7 +237,7 @@ function! ViminiReview(args)
let l:save_path = ''
let l:pr_arg = v:null
let l:mr_arg = v:null
let l:remote_arg = 'origin'
let l:remote_arg = v:null
let l:args = a:args

" Handle key=value options like --save=path, --pr=123, --mr=123, --remote=origin
Expand Down
2 changes: 1 addition & 1 deletion python3/vimini/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ def help(command_name=None):
" -c <ref>: Review changes in git ref (can be a range for batch review).",
" --pr <number>: Fetch and review GitHub Pull Request.",
" --mr <number>: Fetch and review GitLab Merge Request.",
" --remote <remote>: Remote repository to fetch from (defaults to origin).",
" --remote <remote>: Remote repository to fetch from (defaults to g:vimini_default_remote, upstream, branch remote, or origin).",
" --security: Focus on security vulnerabilities.",
" --save: Save reviews to text files (useful with batch review).",
" --save=path: Save reviews to a specific directory (defaults to g:vimini_review_path).",
Expand Down
179 changes: 165 additions & 14 deletions python3/vimini/review.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,158 @@ def send_review_termination(req_id):
util.send_channel_request(req, True)


def resolve_remote(repo_path, remote=None):
"""
Determines the git remote to use.
If 'remote' is explicitly specified and non-empty, returns it.
If 'g:vimini_default_remote' is configured and non-empty, returns it (or warns if invalid).
Otherwise, inspects available remotes in repo_path:
- If 'upstream' is present in remotes, returns 'upstream' (preferred for fork workflows).
- Finds the canonical remote for the current branch:
1. branch.<current_branch>.remote
2. Upstream tracking branch remote (@{upstream})
- Falls back to default branch configurations:
3. Default branch remote (branch.main.remote, branch.master.remote)
- If no configured remote is found, falls back to available remotes:
4. 'origin' (if present in remotes)
5. First available remote in remotes
6. Fallback to 'origin' if no remotes available.
"""
if remote is not None and str(remote).strip() != "":
return str(remote).strip()

try:
cfg_remote = vim.eval("get(g:, 'vimini_default_remote', '')")
if isinstance(cfg_remote, str) and cfg_remote.strip():
candidate = cfg_remote.strip()
if not candidate.startswith("-") and re.match(
r"^[a-zA-Z0-9_\-\./]+$", candidate
):
return candidate
util.display_message(
f"Warning: Invalid 'g:vimini_default_remote' setting '{candidate}'.",
error=True,
)
except Exception:
pass

if not repo_path:
return "origin"

# Find available remotes
remotes = []
try:
res = subprocess.run(
["git", "-C", repo_path, "remote"],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
check=False,
)
if res.returncode == 0:
remotes = [line.strip() for line in res.stdout.splitlines() if line.strip()]
except Exception:
pass

# In fork setups, PRs/MRs reside on upstream; prefer it early to avoid overhead
if "upstream" in remotes:
return "upstream"

# Find canonical remote for current branch
try:
branch_res = subprocess.run(
["git", "-C", repo_path, "symbolic-ref", "--short", "-q", "HEAD"],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
check=False,
)
current_branch = branch_res.stdout.strip() if branch_res.returncode == 0 else ""
if current_branch:
# 1. branch.<name>.remote
cfg_res = subprocess.run(
[
"git",
"-C",
repo_path,
"config",
f"branch.{current_branch}.remote",
],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
check=False,
)
branch_remote = cfg_res.stdout.strip() if cfg_res.returncode == 0 else ""
if (
branch_remote
and branch_remote != "."
and (not remotes or branch_remote in remotes)
):
return branch_remote

# 2. @{upstream} tracking remote
u_res = subprocess.run(
[
"git",
"-C",
repo_path,
"rev-parse",
"--abbrev-ref",
"--symbolic-full-name",
"@{upstream}",
],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
check=False,
)
if u_res.returncode == 0 and u_res.stdout.strip():
u_remote = u_res.stdout.strip().split("/")[0]
if u_remote and u_remote in remotes:
return u_remote
except Exception:
pass

# 3. Fallback to main/master branch remote config
try:
for def_branch in ["main", "master"]:
def_res = subprocess.run(
[
"git",
"-C",
repo_path,
"config",
f"branch.{def_branch}.remote",
],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
check=False,
)
def_remote = def_res.stdout.strip() if def_res.returncode == 0 else ""
if (
def_remote
and def_remote != "."
and (not remotes or def_remote in remotes)
):
return def_remote
except Exception:
pass

if "origin" in remotes:
return "origin"
if remotes:
return remotes[0]

return "origin"


def review(
prompt,
git_objects=None,
Expand All @@ -99,7 +251,7 @@ def review(
save_path=None,
pr=None,
mr=None,
remote="origin",
remote=None,
):
"""
Sends content to the Gemini API for a code review via the background agent.
Expand All @@ -110,18 +262,6 @@ def review(
f"review({prompt}, git_objects='{git_objects}', security_focus={security_focus}, verbose={verbose}, save={save}, save_path='{save_path}', pr={pr}, mr={mr}, remote='{remote}')"
)
try:
# Validate remote
if remote is None or str(remote).strip() == "":
remote = "origin"
else:
remote = str(remote).strip()

if remote.startswith("-") or not re.match(r"^[a-zA-Z0-9_\-\./]+$", remote):
util.display_message(
f"Security error: Invalid remote name '{remote}'.", error=True
)
return

# Handle GitHub PR or GitLab MR fetching
pr_id = str(pr).strip() if pr is not None else None
mr_id = str(mr).strip() if mr is not None else None
Expand All @@ -144,6 +284,17 @@ def review(
if not repo_path:
return

remote = resolve_remote(repo_path, remote)
if (
not remote
or remote.startswith("-")
or not re.match(r"^[a-zA-Z0-9_\-\./]+$", remote)
):
util.display_message(
f"Security error: Invalid remote name '{remote}'.", error=True
)
return

if pr_id:
refspec = f"+refs/pull/{pr_id}/head:refs/vimini/pr/{pr_id}"
local_ref = f"refs/vimini/pr/{pr_id}"
Expand All @@ -161,7 +312,7 @@ def review(
worktree_ref = local_ref

util.display_message(f"Fetching {service_desc} from {remote}...")
fetch_cmd = ["git", "-C", repo_path, "fetch", remote, refspec]
fetch_cmd = ["git", "-C", repo_path, "fetch", "--", remote, refspec]
res = subprocess.run(
fetch_cmd,
capture_output=True,
Expand Down
5 changes: 2 additions & 3 deletions tests/test_commit_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,10 @@ def test_toggle_file():
]

with (
patch.object(vim.current, "buffer", mock_buffer),
patch.object(vim, "eval") as mock_eval,
patch.object(vim, "command") as mock_cmd,
):
vim.current.buffer = mock_buffer
vim.current.window.cursor = (4, 4) # Row 4 is "# src/app.py"

mock_eval.side_effect = lambda expr: (
Expand Down Expand Up @@ -119,9 +119,8 @@ def test_handle_commit_response_adds_files_and_mapping(tmp_path):
}

mock_buffer = []
vim.current.buffer = mock_buffer

with (
patch.object(vim.current, "buffer", mock_buffer),
patch("vimini.util.new_split") as mock_split,
patch.object(vim, "command") as mock_cmd,
patch("vimini.util.display_message") as mock_display,
Expand Down
Loading
Loading