Skip to content

feat: infer repo from git remote (origin) when not passed#235

Merged
sachiniyer merged 6 commits intomainfrom
devin/1775768856-infer-repo-from-git-remote
Apr 9, 2026
Merged

feat: infer repo from git remote (origin) when not passed#235
sachiniyer merged 6 commits intomainfrom
devin/1775768856-infer-repo-from-git-remote

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot commented Apr 9, 2026

Summary

Makes the repo positional argument optional across all commands that accept it (bugs list, scans list, rules create/propose/list, rules requests list). When omitted, the CLI infers owner/repo from the current git repository's origin remote.

If origin does not exist or does not point to a recognised GitHub URL, the CLI errors with a message telling the user to pass the repo explicitly.

New module: src/utils/git.rs with:

  • parse_github_remote_url — extracts owner/repo from HTTPS, SSH-colon, and ssh:// GitHub URLs (uses a prefix list + find_map + bool::then)
  • infer_repo_from_git_remote — runs git remote get-url origin and parses the result
  • resolve_repo_arg — bridge: uses explicit arg if provided, otherwise infers

15 unit tests cover URL parsing edge cases (.git suffix, trailing slashes, non-GitHub URLs, malformed input, extra path segments).

Review & Testing Checklist for Human

  • Optional positional arg behaviour in clap: repo is now Option<String> as a positional (no #[arg(long)]). Confirm that flag arguments (e.g. --status, --limit) are not accidentally consumed as the repo positional when repo is omitted. Try: detail bugs list --status pending --limit 5 (no repo).
  • Error message clarity: All inference failures (not in a git repo, no origin remote, non-GitHub origin) now produce the same error: "Could not infer repository from git remotes. Please pass a repo argument explicitly (e.g. owner/repo)." Verify this is acceptable vs. having distinct messages.
  • Manual end-to-end test: cd into a repo with a GitHub origin, run detail bugs list (no repo arg) and confirm it infers correctly. Then run from a non-git directory and confirm the error message appears.

Notes

  • Only the origin remote is checked. This matches the most common setup (git clone creates origin).
  • GitHub-only: non-GitHub remotes (GitLab, Bitbucket, self-hosted) will fall through to the error. This is intentional.
  • The is_silent pattern matching in lib.rs uses .. wildcards, so no changes were needed there despite the type change.
  • URL prefix matching is case-sensitive; git normalises remote URLs to lowercase hostnames so this is unlikely to be an issue in practice.
  • git remote get-url origin is called via std::process::Command (blocking) on the tokio runtime thread. This is a fast local operation.

Link to Devin session: https://app.devin.ai/sessions/9cd8d459be13437587ce77146aa4ce17
Requested by: @sachiniyer

Make the repo positional argument optional across all commands that
accept it (bugs list, scans list, rules create/propose/list, rules
requests list).

When omitted, the CLI checks git remotes in order: upstream, then
origin. If a recognisable GitHub URL is found, the owner/repo slug
is extracted automatically.

If neither a repo argument nor a usable git remote is available, a
clear error message tells the user to pass the repo explicitly.

Co-Authored-By: Sachin Iyer <siyer@detail.dev>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

Original prompt from Sachin

Can you add a feature where we infer the github repo from upstream/origin and use that for the repo. Then if we are not in git/we could not infer the repo throw an error and say that we must pass a repo in

@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Co-Authored-By: Sachin Iyer <siyer@detail.dev>
cubic-dev-ai[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor Author

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 6 potential issues.

Open in Devin Review

Comment thread src/utils/git.rs Outdated
Comment thread src/utils/git.rs Outdated
Comment thread src/utils/git.rs Outdated
Comment thread src/utils/git.rs
Comment on lines +66 to +93
pub fn infer_repo_from_git_remote() -> Result<String> {
// Make sure we are inside a git work-tree.
let in_git = Command::new("git")
.args(["rev-parse", "--is-inside-work-tree"])
.output()
.context("Failed to run git")?;

if !in_git.status.success() {
bail!(
"Not inside a git repository. Please pass a repo argument explicitly \
(e.g. owner/repo)."
);
}

// Try upstream first, then origin.
for remote in &["upstream", "origin"] {
if let Some(url) = get_remote_url(remote) {
if let Some(owner_repo) = parse_github_remote_url(&url) {
return Ok(owner_repo);
}
}
}

bail!(
"Could not infer repository from git remotes. \
Please pass a repo argument explicitly (e.g. owner/repo)."
)
}
Copy link
Copy Markdown
Contributor Author

@devin-ai-integration devin-ai-integration Bot Apr 9, 2026

Choose a reason for hiding this comment

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

📝 Info: Synchronous process spawn inside async context

infer_repo_from_git_remote() at src/utils/git.rs:30-32 uses std::process::Command::output() which blocks the current thread. This is called from async handler functions (e.g., src/commands/bugs.rs:285). For a single-threaded CLI application this is acceptable and not a bug, but if the runtime were ever changed to multi-threaded with concurrent tasks, this could starve the executor. For now this is fine given the CLI's architecture.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/utils/git.rs
Comment on lines +97 to +99
pub fn resolve_repo_arg(explicit: Option<&str>) -> Result<String> {
explicit.map_or_else(infer_repo_from_git_remote, |r| Ok(r.to_string()))
}
Copy link
Copy Markdown
Contributor Author

@devin-ai-integration devin-ai-integration Bot Apr 9, 2026

Choose a reason for hiding this comment

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

📝 Info: Consistent transformation across all affected commands

Verified that all commands that previously took a required repo: String positional argument have been updated to repo: Option<String> with the resolve_repo_arg call pattern. The transformation is applied consistently in: bugs list (src/commands/bugs.rs:82,285), rules create (src/commands/rules.rs:24,121), rules propose (src/commands/rules.rs:43,256), rules list (src/commands/rules.rs:54,149), rules requests list (src/commands/rules.rs:84,278), and scans list (src/commands/scans.rs:15,41). Commands that don't take a repo argument (e.g. bugs show, bugs close, rules show, rules persist, rules requests show) are correctly left unchanged.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/utils/git.rs Outdated
Comment on lines +8 to +39
fn parse_github_remote_url(url: &str) -> Option<String> {
// HTTPS: https://github.com/owner/repo or https://github.com/owner/repo.git
if let Some(rest) = url
.strip_prefix("https://github.com/")
.or_else(|| url.strip_prefix("http://github.com/"))
{
let rest = rest.trim_end_matches(".git").trim_end_matches('/');
let parts: Vec<&str> = rest.splitn(3, '/').collect();
if parts.len() >= 2 && !parts[0].is_empty() && !parts[1].is_empty() {
return Some(format!("{}/{}", parts[0], parts[1]));
}
}

// SSH: git@github.com:owner/repo.git
if let Some(rest) = url.strip_prefix("git@github.com:") {
let rest = rest.trim_end_matches(".git").trim_end_matches('/');
let parts: Vec<&str> = rest.splitn(3, '/').collect();
if parts.len() >= 2 && !parts[0].is_empty() && !parts[1].is_empty() {
return Some(format!("{}/{}", parts[0], parts[1]));
}
}

// SSH with ssh:// scheme: ssh://git@github.com/owner/repo.git
if let Some(rest) = url.strip_prefix("ssh://git@github.com/") {
let rest = rest.trim_end_matches(".git").trim_end_matches('/');
let parts: Vec<&str> = rest.splitn(3, '/').collect();
if parts.len() >= 2 && !parts[0].is_empty() && !parts[1].is_empty() {
return Some(format!("{}/{}", parts[0], parts[1]));
}
}

None
Copy link
Copy Markdown
Contributor Author

@devin-ai-integration devin-ai-integration Bot Apr 9, 2026

Choose a reason for hiding this comment

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

📝 Info: GitHub-only URL parsing may surprise users with other Git hosts

The parse_github_remote_url function only recognizes GitHub URLs (github.com). If a user's upstream or origin remote points to GitLab, Bitbucket, or a self-hosted GitHub Enterprise instance (e.g. github.mycompany.com), the inference silently fails and falls through to the error message at src/utils/git.rs:89-92. The error message says "Could not infer repository from git remotes" which is reasonable, but doesn't hint that only GitHub URLs are supported. This is a design choice rather than a bug, but could cause confusion for GitHub Enterprise users whose URLs differ only in the hostname.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Co-Authored-By: Sachin Iyer <siyer@detail.dev>
Copy link
Copy Markdown
Contributor Author

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

Open in Devin Review

Comment thread src/utils/git.rs Outdated
.strip_prefix("https://github.com/")
.or_else(|| url.strip_prefix("http://github.com/"))
{
let rest = rest.trim_end_matches('/').trim_end_matches(".git");
Copy link
Copy Markdown
Contributor Author

@devin-ai-integration devin-ai-integration Bot Apr 9, 2026

Choose a reason for hiding this comment

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

📝 Info: trim_end_matches order handles .git/ URLs correctly

The order trim_end_matches('/').trim_end_matches(".git") at src/utils/git.rs:18 is intentional — commit a4c8c9b explicitly swapped the order to handle URLs ending in .git/. First stripping trailing slashes then stripping .git correctly normalizes repo.git/repo.gitrepo. The reverse order would fail on .git/ since .git wouldn't be a suffix when / is still present. Note that trim_end_matches with a &str pattern removes repeatedly, so repo.git.gitrepo, which is benign for real-world URLs.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

…:then

Co-Authored-By: Sachin Iyer <siyer@detail.dev>
Copy link
Copy Markdown
Contributor Author

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

Open in Devin Review

Comment thread src/utils/git.rs
Comment on lines +10 to +15
const PREFIXES: &[&str] = &[
"https://github.com/",
"http://github.com/",
"git@github.com:",
"ssh://git@github.com/",
];
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

📝 Info: SSH URLs with non-standard ports are not handled

The parse_github_remote_url function matches against a fixed set of prefixes (src/utils/git.rs:10-15). An SSH URL with an explicit port like ssh://git@github.com:22/owner/repo.git would not match the prefix ssh://git@github.com/ due to the :22 port segment. This is a reasonable limitation since GitHub doesn't use non-standard ports, and the error message from infer_repo_from_git_remote clearly directs the user to pass the repo argument explicitly. Not a bug, just an edge case to be aware of.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

Testing Results

Tested the CLI locally after building from the feature branch. Devin session

Results (4/5 passed, 1 untestable)
  • Test 1: Explicit repo argument (regression) — PASSED. detail bugs list usedetail/cli --limit 3 returned 3 bugs, exit code 0.
  • Test 2: Inference happy path — UNTESTED. Devin git proxy rewrites all remote URLs to git-manager.devin.ai/proxy/..., preventing creation of a real github.com remote. Covered by 15 unit tests in src/utils/git.rs.
  • Test 3: Error outside git repo — PASSED. From /tmp: Error: Not inside a git repository. Please pass a repo argument explicitly (e.g. owner/repo). Exit code 1.
  • Test 4: Error with non-GitHub remotes — PASSED. From CLI repo (proxy URL origin): Error: Could not infer repository from git remotes. Please pass a repo argument explicitly (e.g. owner/repo). Exit code 1.
  • Test 5: Help text — PASSED. Shows [REPO] (optional) with: "If omitted, inferred from the git remote (upstream, then origin)".
Test 1 output
1. CLI: Repository identifiers with leading/trailing whitespace pass validation...
    Bug ID   bug_14a49a44-895a-4523-a6bc-d0a5c89fe83a
    Created  2026-04-03
2. CLI config updates can unlock mid-write...
    Bug ID   bug_e79362bd-5504-4465-9dfc-ed6b564cd07b
    Created  2026-04-03
3. SatisfyingSort animation shows holes in triangle...
    Bug ID   bug_468fc2a0-d06e-4f51-a627-be2481e9a65d
    Created  2026-04-03

Page: 1 of 4
EXIT_CODE=0
Test 3 output (from /tmp)
Error: Not inside a git repository. Please pass a repo argument explicitly (e.g. owner/repo).
EXIT_CODE=1
Test 4 output (from CLI repo with proxy remote)
Error: Could not infer repository from git remotes. Please pass a repo argument explicitly (e.g. owner/repo).
EXIT_CODE=1
Test 5 output
Usage: detail bugs list [OPTIONS] [REPO]

Arguments:
  [REPO]  Repository by owner/repo (e.g., usedetail/cli) or repo (e.g., cli).
          If omitted, inferred from the git remote (upstream, then origin)

@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

Updated Testing Results — All 5 tests passed

Re-tested inference happy path by cloning a fresh copy and using a PATH wrapper to undo the Devin git proxy URL rewrite.

Results (5/5 passed)
  • Test 1: Explicit repo argument (regression) — PASSED. detail bugs list usedetail/cli --limit 3 returned 3 bugs, exit code 0.
  • Test 2: Inference happy path — PASSED. detail bugs list (no repo arg) from inside the cloned repo successfully inferred usedetail/cli from origin and returned 3 bugs, exit code 0.
  • Test 3: Error outside git repo — PASSED. From /tmp: Error: Not inside a git repository. Please pass a repo argument explicitly (e.g. owner/repo). Exit code 1.
  • Test 4: Error with non-GitHub remotes — PASSED. From CLI repo with proxy URL origin: Error: Could not infer repository from git remotes. Please pass a repo argument explicitly (e.g. owner/repo). Exit code 1.
  • Test 5: Help text — PASSED. Shows [REPO] (optional) with: "If omitted, inferred from the git remote (upstream, then origin)".
Test 2 output (inference happy path — no repo argument passed)
$ detail bugs list --limit 3
1. CLI: Repository identifiers with leading/trailing whitespace pass validation but fail lookup with misleading "not found" error
    Bug ID   bug_14a49a44-895a-4523-a6bc-d0a5c89fe83a
    Created  2026-04-03
2. CLI config updates can unlock mid-write, allowing concurrent reads of truncated/corrupted config
    Bug ID   bug_e79362bd-5504-4465-9dfc-ed6b564cd07b
    Created  2026-04-03
3. SatisfyingSort animation shows holes in triangle during shuffle due to region-crossing pixel culling
    Bug ID   bug_468fc2a0-d06e-4f51-a627-be2481e9a65d
    Created  2026-04-03

Page: 1 of 4
EXIT_CODE=0

Devin session

Co-Authored-By: Sachin Iyer <siyer@detail.dev>
Copy link
Copy Markdown
Contributor Author

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

Open in Devin Review

Comment thread src/utils/git.rs Outdated
Comment on lines +62 to +66
if let Some(url) = get_remote_url("origin") {
if let Some(owner_repo) = parse_github_remote_url(&url) {
return Ok(owner_repo);
}
}
Copy link
Copy Markdown
Contributor Author

@devin-ai-integration devin-ai-integration Bot Apr 9, 2026

Choose a reason for hiding this comment

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

📝 Info: Only the 'origin' remote is checked for inference

The infer_repo_from_git_remote function at src/utils/git.rs:62 only checks the origin remote. If a user's GitHub remote is configured under a different name (e.g., upstream, github), inference will fail with a generic error. This is a reasonable default — origin is by far the most common convention — and the error message clearly instructs the user to pass the argument explicitly. Worth noting in documentation if users report confusion.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/utils/git.rs
Comment on lines +10 to +17
const PREFIXES: &[&str] = &[
"https://github.com/",
"http://github.com/",
"git@github.com:",
"ssh://git@github.com/",
];

let rest = PREFIXES.iter().find_map(|p| url.strip_prefix(p))?;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

📝 Info: URL prefix matching is case-sensitive

The prefix list at src/utils/git.rs:10-15 uses case-sensitive matching via strip_prefix. A URL like https://GitHub.com/owner/repo would not match. In practice, git normalizes remote URLs to lowercase hostnames, so this is unlikely to be an issue. If it ever becomes one, the fix would be to lowercase the URL before matching.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration devin-ai-integration Bot changed the title feat: infer repo from git remote (upstream/origin) when not passed feat: infer repo from git remote (origin) when not passed Apr 9, 2026
Co-Authored-By: Sachin Iyer <siyer@detail.dev>
@sachiniyer sachiniyer merged commit 575962a into main Apr 9, 2026
13 checks passed
@sachiniyer sachiniyer deleted the devin/1775768856-infer-repo-from-git-remote branch April 9, 2026 22:54
tjdammann pushed a commit that referenced this pull request Apr 10, 2026
## Summary

Makes the `repo` positional argument **optional** across all commands
that accept it (`bugs list`, `scans list`, `rules create/propose/list`,
`rules requests list`). When omitted, the CLI infers `owner/repo` from
the current git repository's `origin` remote.

If `origin` does not exist or does not point to a recognised GitHub URL,
the CLI errors with a message telling the user to pass the repo
explicitly.

New module: `src/utils/git.rs` with:
- `parse_github_remote_url` — extracts `owner/repo` from HTTPS,
SSH-colon, and `ssh://` GitHub URLs (uses a prefix list + `find_map` +
`bool::then`)
- `infer_repo_from_git_remote` — runs `git remote get-url origin` and
parses the result
- `resolve_repo_arg` — bridge: uses explicit arg if provided, otherwise
infers

15 unit tests cover URL parsing edge cases (`.git` suffix, trailing
slashes, non-GitHub URLs, malformed input, extra path segments).

## Review & Testing Checklist for Human

- [ ] **Optional positional arg behaviour in clap**: `repo` is now
`Option<String>` as a positional (no `#[arg(long)]`). Confirm that flag
arguments (e.g. `--status`, `--limit`) are not accidentally consumed as
the repo positional when repo is omitted. Try: `detail bugs list
--status pending --limit 5` (no repo).
- [ ] **Error message clarity**: All inference failures (not in a git
repo, no origin remote, non-GitHub origin) now produce the same error:
*"Could not infer repository from git remotes. Please pass a repo
argument explicitly (e.g. owner/repo)."* Verify this is acceptable vs.
having distinct messages.
- [ ] **Manual end-to-end test**: `cd` into a repo with a GitHub origin,
run `detail bugs list` (no repo arg) and confirm it infers correctly.
Then run from a non-git directory and confirm the error message appears.

### Notes
- Only the `origin` remote is checked. This matches the most common
setup (`git clone` creates `origin`).
- GitHub-only: non-GitHub remotes (GitLab, Bitbucket, self-hosted) will
fall through to the error. This is intentional.
- The `is_silent` pattern matching in `lib.rs` uses `..` wildcards, so
no changes were needed there despite the type change.
- URL prefix matching is case-sensitive; git normalises remote URLs to
lowercase hostnames so this is unlikely to be an issue in practice.
- `git remote get-url origin` is called via `std::process::Command`
(blocking) on the tokio runtime thread. This is a fast local operation.

Link to Devin session:
https://app.devin.ai/sessions/9cd8d459be13437587ce77146aa4ce17
Requested by: @sachiniyer

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Sachin Iyer <siyer@detail.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant