Skip to content
Open
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
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,26 @@ jobs:

- name: Run Rust test suite
run: cargo test --workspace --locked

# Runs after the Rust suite so an E2E flake cannot stop it reporting.
# CI uses npm's prebuilt browsers, which are fine on ubuntu; the nix
# devshell instead supplies pkgs.playwright-driver.browsers, because the
# npm builds are unpatched and will not launch on NixOS. Both paths need
# the browser revision to match the pinned @playwright/test version.
- name: Install Playwright browser
working-directory: crates/diffcore-tauri/ui
run: npx playwright install --with-deps chromium

- name: Run Playwright E2E suite
working-directory: crates/diffcore-tauri/ui
run: npx playwright test

- name: Upload Playwright report on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: |
crates/diffcore-tauri/ui/playwright-report/
crates/diffcore-tauri/ui/test-results/
retention-days: 7
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ diffcore analyze --base main --refine --refine-model gpt-4o
# Analyze a different repo
diffcore analyze --base main --repo /path/to/repo

# Analyze a pull/merge request by URL — clones into ~/.diffcore/cache/repos and
# resolves base/head from the provider's PR refs. GitHub, GitLab, Gitea/Forgejo,
# Pagure, Bitbucket Data Center, Azure DevOps and Gerrit; see
# docs/pr-url-providers.md for URL shapes and the forges that cannot work.
diffcore analyze --repo https://github.com/BurntSushi/ripgrep/pull/2900

# Open a flow group in an external diff tool
diffcore launch --tool bcompare --group group_1 --input review.json
```
Expand Down
8 changes: 6 additions & 2 deletions TODOs.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ Replace GitHub's PR review tab entirely — do the full review in Diffcore, push

### Phase 1: GitHub Login + PR Fetching
- GitHub OAuth login flow in Tauri app
- `diffcore review <pr-url>` — fetch PR diff directly from GitHub API
- Store GitHub token securely (keychain / 1Password)
- ~~Fetch a PR diff by URL~~ — done without an API: the repository field and
`diffcore analyze --repo` take PR/MR URLs across forges and resolve them from
the provider's git refs. See [docs/pr-url-providers.md](./docs/pr-url-providers.md).
- Store GitHub token securely (keychain / 1Password). An API token would also
close the gaps git refs cannot cover: Bitbucket Cloud (no PR refs at all) and
fast-forward-merged PRs (no recoverable base branch).

### Phase 2: Bidirectional Comment Sync
- Push review comments from Diffcore back to GitHub as a PR review
Expand Down
46 changes: 43 additions & 3 deletions crates/diffcore-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use diffcore_core::llm;
use diffcore_core::llm::refinement;
use diffcore_core::output::{self, build_analysis_output};
use diffcore_core::pipeline;
use diffcore_core::pr_url;
use diffcore_core::rank;
use diffcore_core::types::AnalysisOutput;

Expand Down Expand Up @@ -111,7 +112,11 @@ struct AnalyzeArgs {
#[arg(long)]
no_cache: bool,

/// Path to the git repository (defaults to current directory)
/// Path to the git repository, or a pull/merge request URL
/// (GitHub, GitLab, Gitea/Forgejo, Pagure, Bitbucket DC, Azure DevOps, Gerrit).
/// URLs are cloned into ~/.diffcore/cache/repos (override with
/// DIFFCORE_REPO_CACHE_DIR) and resolved to base/head refs, overriding
/// --base/--head.
#[arg(long, default_value = ".")]
repo: PathBuf,
}
Expand Down Expand Up @@ -316,8 +321,42 @@ fn main() {
}
}

/// When `--repo` is a pull/merge request URL, clone (or reuse) the repository in
/// the diffcore cache, fetch the PR refs, and rewrite `--repo`/`--base`/`--head`
/// to point at the resolved local checkout.
fn resolve_pr_url_args(args: &mut AnalyzeArgs) -> Result<(), Box<dyn std::error::Error>> {
let Some(pr) = args.repo.to_str().and_then(pr_url::parse) else {
return Ok(());
};
info!(
"Resolving {} #{} on {} ({})",
pr.provider.unit(),
pr.number,
pr.host,
pr.provider.name()
);
let resolved = pr_url::resolve(&pr)?;
info!("Using cached checkout at {}", resolved.path);
// Override rather than fill in: the resolved refs are the whole point of
// passing a PR URL, and a stale `--base main` would otherwise be diffed
// against the cached clone's own default branch and silently wrong.
for (flag, supplied) in [("--base", &args.base), ("--head", &args.head)] {
if let Some(v) = supplied {
warn!("ignoring {flag} {v}: refs come from {}", pr.provider.unit());
}
}
args.repo = PathBuf::from(&resolved.path);
args.base = Some(resolved.base);
args.head = Some(resolved.head);
// The checkout is detached at the PR head; never mix in working-tree state.
args.include_uncommitted = false;
args.no_include_uncommitted = true;
Ok(())
}

/// Run analysis and return the output (without writing or LLM steps).
fn run_analyze_and_return(args: AnalyzeArgs) -> Result<AnalysisOutput, Box<dyn std::error::Error>> {
fn run_analyze_and_return(mut args: AnalyzeArgs) -> Result<AnalysisOutput, Box<dyn std::error::Error>> {
resolve_pr_url_args(&mut args)?;
let repo_path = std::fs::canonicalize(&args.repo)?;
let repo =
Repository::discover(&repo_path).map_err(|e| format!("Not a git repository: {}", e))?;
Expand Down Expand Up @@ -404,7 +443,8 @@ fn run_analyze_and_return(args: AnalyzeArgs) -> Result<AnalysisOutput, Box<dyn s
Ok(analysis_output)
}

fn run_analyze(args: AnalyzeArgs) -> Result<(), Box<dyn std::error::Error>> {
fn run_analyze(mut args: AnalyzeArgs) -> Result<(), Box<dyn std::error::Error>> {
resolve_pr_url_args(&mut args)?;
// Resolve repo path
let repo_path = std::fs::canonicalize(&args.repo)?;
let repo =
Expand Down
1 change: 1 addition & 0 deletions crates/diffcore-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ tree-sitter-rust = "0.24.1"
tree-sitter-java = "0.23.5"
toml = "0.8"
glob = "0.3"
url = "2"
reqwest = { version = "0.12", default-features = false, features = ["json", "http2", "charset", "rustls-tls-native-roots", "macos-system-configuration"] }
tokio = { version = "1", features = ["full"] }
async-trait = "0.1"
Expand Down
2 changes: 1 addition & 1 deletion crates/diffcore-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ impl DiffcoreConfig {
}
}

fn diffcore_config_home() -> Option<PathBuf> {
pub(crate) fn diffcore_config_home() -> Option<PathBuf> {
env::var_os("DIFFCORE_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".diffcore")))
Expand Down
1 change: 1 addition & 0 deletions crates/diffcore-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub mod llm;
pub mod logging;
pub mod output;
pub mod pipeline;
pub mod pr_url;
pub mod query_engine;
pub mod manifest;
pub mod rank;
Expand Down
Loading