Skip to content

Commit d721da2

Browse files
committed
refactor: split review pattern repository helpers
Made-with: Cursor
1 parent 3ea22b5 commit d721da2

File tree

6 files changed

+158
-137
lines changed

6 files changed

+158
-137
lines changed

TODO.md

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,15 @@
1010

1111
## Immediate Queue
1212

13-
- [ ] `src/review/context_helpers/pattern_repositories.rs`
14-
- Split local path resolution from git-source resolution.
15-
- Split git URL safety validation from clone/pull cache management.
16-
- Isolate checkout cache hashing/path construction and shell-backed update helpers.
17-
18-
## Review Backlog
19-
2013
- [ ] `src/review/filters.rs`
2114
- Split comment-type classification and comment-type filtering.
2215
- Split vague-comment detection heuristics from vague-comment filtering.
2316
- Split explicit/adaptive feedback suppression decisions from filter application.
2417
- Split feedback-confidence lookup fallback order from confidence mutation.
2518
- Keep `apply_review_filters()` as a thin composition root.
19+
20+
## Review Backlog
21+
2622
- [ ] `src/review/feedback.rs`
2723
- Split feedback stats/data models from file-pattern derivation.
2824
- Split JSON persistence and atomic-write helpers from store mutation helpers.

src/review/context_helpers/pattern_repositories.rs

Lines changed: 11 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,141 +1,22 @@
11
use std::collections::HashMap;
2-
use std::hash::{Hash, Hasher};
3-
use std::path::{Path, PathBuf};
4-
use tracing::warn;
2+
use std::path::PathBuf;
53

6-
use crate::config;
4+
#[path = "pattern_repositories/checkout.rs"]
5+
mod checkout;
6+
#[path = "pattern_repositories/git.rs"]
7+
mod git;
8+
#[path = "pattern_repositories/local.rs"]
9+
mod local;
10+
#[path = "pattern_repositories/run.rs"]
11+
mod run;
712

813
pub type PatternRepositoryMap = HashMap<String, PathBuf>;
914

10-
pub fn resolve_pattern_repositories(
11-
config: &config::Config,
12-
repo_root: &Path,
13-
) -> PatternRepositoryMap {
14-
let mut resolved = HashMap::new();
15-
if config.pattern_repositories.is_empty() {
16-
return resolved;
17-
}
18-
19-
for repo in &config.pattern_repositories {
20-
if resolved.contains_key(&repo.source) {
21-
continue;
22-
}
23-
24-
let source_path = Path::new(&repo.source);
25-
if source_path.is_absolute() && source_path.is_dir() {
26-
if let Ok(path) = source_path.canonicalize() {
27-
resolved.insert(repo.source.clone(), path);
28-
}
29-
continue;
30-
}
31-
32-
let repo_relative = repo_root.join(&repo.source);
33-
if repo_relative.is_dir() {
34-
if let Ok(path) = repo_relative.canonicalize() {
35-
resolved.insert(repo.source.clone(), path);
36-
}
37-
continue;
38-
}
39-
40-
if is_git_source(&repo.source) {
41-
if let Some(path) = prepare_pattern_repository_checkout(&repo.source) {
42-
resolved.insert(repo.source.clone(), path);
43-
continue;
44-
}
45-
}
46-
47-
warn!(
48-
"Skipping pattern repository '{}' (not a readable local path or cloneable git source)",
49-
repo.source
50-
);
51-
}
52-
53-
resolved
54-
}
55-
56-
/// Check whether a source string looks like a git URL and uses an allowed scheme.
57-
/// Accepts `https://`, `ssh://`, `git@` (SSH shorthand), and `http://` (with `.git` suffix).
58-
fn is_safe_git_url(source: &str) -> bool {
59-
source.starts_with("https://")
60-
|| source.starts_with("ssh://")
61-
|| source.starts_with("git@")
62-
|| (source.starts_with("http://") && source.ends_with(".git"))
63-
}
64-
65-
fn is_git_source(source: &str) -> bool {
66-
is_safe_git_url(source)
67-
}
68-
69-
fn prepare_pattern_repository_checkout(source: &str) -> Option<PathBuf> {
70-
use std::process::Command;
71-
72-
if !is_safe_git_url(source) {
73-
warn!(
74-
"Rejecting pattern repository '{}': only https://, ssh://, and git@ URLs are allowed",
75-
source
76-
);
77-
return None;
78-
}
79-
80-
let home_dir = dirs::home_dir()?;
81-
let cache_root = home_dir.join(".diffscope").join("pattern_repositories");
82-
if std::fs::create_dir_all(&cache_root).is_err() {
83-
return None;
84-
}
85-
86-
let mut hasher = std::collections::hash_map::DefaultHasher::new();
87-
source.hash(&mut hasher);
88-
let repo_dir = cache_root.join(format!("{:x}", hasher.finish()));
89-
90-
if repo_dir.is_dir() {
91-
let pull_result = Command::new("git")
92-
.arg("-C")
93-
.arg(&repo_dir)
94-
.arg("pull")
95-
.arg("--ff-only")
96-
.output();
97-
if let Err(err) = pull_result {
98-
warn!(
99-
"Unable to update cached pattern repository {}: {}",
100-
source, err
101-
);
102-
}
103-
} else {
104-
let clone_result = Command::new("git")
105-
.arg("clone")
106-
.arg("--depth")
107-
.arg("1")
108-
.arg(source)
109-
.arg(&repo_dir)
110-
.output();
111-
match clone_result {
112-
Ok(output) if output.status.success() => {}
113-
Ok(output) => {
114-
let stderr = String::from_utf8_lossy(&output.stderr);
115-
warn!(
116-
"Failed to clone pattern repository {}: {}",
117-
source,
118-
stderr.trim()
119-
);
120-
return None;
121-
}
122-
Err(err) => {
123-
warn!("Failed to clone pattern repository {}: {}", source, err);
124-
return None;
125-
}
126-
}
127-
}
128-
129-
if repo_dir.is_dir() {
130-
Some(repo_dir)
131-
} else {
132-
None
133-
}
134-
}
15+
pub use run::resolve_pattern_repositories;
13516

13617
#[cfg(test)]
13718
mod tests {
138-
use super::*;
19+
use super::git::{is_git_source, is_safe_git_url};
13920

14021
#[test]
14122
fn test_is_git_source_https() {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use std::hash::{Hash, Hasher};
2+
use std::path::PathBuf;
3+
4+
use tracing::warn;
5+
6+
use super::git::is_safe_git_url;
7+
8+
pub(super) fn prepare_pattern_repository_checkout(source: &str) -> Option<PathBuf> {
9+
use std::process::Command;
10+
11+
if !is_safe_git_url(source) {
12+
warn!(
13+
"Rejecting pattern repository '{}': only https://, ssh://, and git@ URLs are allowed",
14+
source
15+
);
16+
return None;
17+
}
18+
19+
let repo_dir = pattern_repository_cache_dir(source)?;
20+
if repo_dir.is_dir() {
21+
let pull_result = Command::new("git")
22+
.arg("-C")
23+
.arg(&repo_dir)
24+
.arg("pull")
25+
.arg("--ff-only")
26+
.output();
27+
if let Err(err) = pull_result {
28+
warn!(
29+
"Unable to update cached pattern repository {}: {}",
30+
source, err
31+
);
32+
}
33+
} else {
34+
let clone_result = Command::new("git")
35+
.arg("clone")
36+
.arg("--depth")
37+
.arg("1")
38+
.arg(source)
39+
.arg(&repo_dir)
40+
.output();
41+
match clone_result {
42+
Ok(output) if output.status.success() => {}
43+
Ok(output) => {
44+
let stderr = String::from_utf8_lossy(&output.stderr);
45+
warn!(
46+
"Failed to clone pattern repository {}: {}",
47+
source,
48+
stderr.trim()
49+
);
50+
return None;
51+
}
52+
Err(err) => {
53+
warn!("Failed to clone pattern repository {}: {}", source, err);
54+
return None;
55+
}
56+
}
57+
}
58+
59+
if repo_dir.is_dir() {
60+
Some(repo_dir)
61+
} else {
62+
None
63+
}
64+
}
65+
66+
fn pattern_repository_cache_dir(source: &str) -> Option<PathBuf> {
67+
let home_dir = dirs::home_dir()?;
68+
let cache_root = home_dir.join(".diffscope").join("pattern_repositories");
69+
std::fs::create_dir_all(&cache_root).ok()?;
70+
71+
let mut hasher = std::collections::hash_map::DefaultHasher::new();
72+
source.hash(&mut hasher);
73+
Some(cache_root.join(format!("{:x}", hasher.finish())))
74+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
pub(super) fn is_safe_git_url(source: &str) -> bool {
2+
source.starts_with("https://")
3+
|| source.starts_with("ssh://")
4+
|| source.starts_with("git@")
5+
|| (source.starts_with("http://") && source.ends_with(".git"))
6+
}
7+
8+
pub(super) fn is_git_source(source: &str) -> bool {
9+
is_safe_git_url(source)
10+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use std::path::{Path, PathBuf};
2+
3+
pub(super) fn resolve_local_repository_path(source: &str, repo_root: &Path) -> Option<PathBuf> {
4+
let source_path = Path::new(source);
5+
if source_path.is_absolute() && source_path.is_dir() {
6+
return source_path.canonicalize().ok();
7+
}
8+
9+
let repo_relative = repo_root.join(source);
10+
if repo_relative.is_dir() {
11+
return repo_relative.canonicalize().ok();
12+
}
13+
14+
None
15+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use std::path::Path;
2+
3+
use tracing::warn;
4+
5+
use crate::config;
6+
7+
use super::checkout::prepare_pattern_repository_checkout;
8+
use super::git::is_git_source;
9+
use super::local::resolve_local_repository_path;
10+
use super::PatternRepositoryMap;
11+
12+
pub fn resolve_pattern_repositories(
13+
config: &config::Config,
14+
repo_root: &Path,
15+
) -> PatternRepositoryMap {
16+
let mut resolved = PatternRepositoryMap::new();
17+
if config.pattern_repositories.is_empty() {
18+
return resolved;
19+
}
20+
21+
for repo in &config.pattern_repositories {
22+
if resolved.contains_key(&repo.source) {
23+
continue;
24+
}
25+
26+
if let Some(path) = resolve_local_repository_path(&repo.source, repo_root) {
27+
resolved.insert(repo.source.clone(), path);
28+
continue;
29+
}
30+
31+
if is_git_source(&repo.source) {
32+
if let Some(path) = prepare_pattern_repository_checkout(&repo.source) {
33+
resolved.insert(repo.source.clone(), path);
34+
continue;
35+
}
36+
}
37+
38+
warn!(
39+
"Skipping pattern repository '{}' (not a readable local path or cloneable git source)",
40+
repo.source
41+
);
42+
}
43+
44+
resolved
45+
}

0 commit comments

Comments
 (0)