Skip to content

Commit 65e78be

Browse files
authored
Refactor git branch (#8)
* [build] bump crate version to 0.7.1 * [fix] scan all source files before filtering git-branch duplicates
1 parent 904b59d commit 65e78be

5 files changed

Lines changed: 139 additions & 21 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "codem8"
3-
version = "0.7.0"
3+
version = "0.7.1"
44
edition = "2021"
55
rust-version = "1.85"
66
license = "MIT"

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,10 @@ trailing Unicode whitespace are removed before hashing and comparison. Empty
8484
trimmed lines are ignored. CodeM8 currently expects UTF-8 source files; invalid
8585
UTF-8 produces a clear error rather than lossy output.
8686

87-
Use `-git-branch` to analyze only files changed on the current local branch
88-
compared to the origin base branch. CodeM8 resolves that base from `origin/HEAD`
89-
with `origin/main` and `origin/master` fallbacks. This includes committed,
90-
staged, unstaged, and untracked files that still exist in the worktree. The
91-
option requires a Git repository and cannot be combined with `-files`.
87+
Use `-git-branch` to search duplicate code only in files changed on the current
88+
local branch. CodeM8 resolves that branch set from `origin/HEAD` with
89+
`origin/main` and `origin/master` fallbacks. The option requires a Git
90+
repository and cannot be combined with `-files`.
9291

9392
Duplicate block weight is calculated as:
9493

src/cli/help.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,8 @@ OPTIONS:
2929
Example: -files=src/a.ts,src/b.js
3030
3131
-git-branch
32-
Analyze files changed on the current local Git branch compared to the
33-
origin base branch, including committed, staged, unstaged, and untracked
34-
files. Cannot be combined with -files.
32+
Search duplicate code only in files changed on the current local Git
33+
branch. Cannot be combined with -files.
3534
3635
-verbose
3736
Include duplicate block metrics in report output.

src/lib.rs

Lines changed: 131 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ pub mod model;
77
pub mod paths;
88
pub mod report;
99

10+
use std::collections::HashSet;
1011
use std::io::Write;
1112
use std::path::Path;
1213
use std::time::{Duration, Instant};
1314

1415
use crate::error::{CodeM8Error, Result};
16+
use crate::model::ProcessedFile;
17+
use crate::paths::format_path;
1518

1619
/// Runs the CLI workflow and writes the selected report to the provided writer.
1720
///
@@ -31,23 +34,31 @@ where
3134
.map_err(|error| CodeM8Error::new(format!("could not write help output: {error}")))?,
3235
cli::CliCommand::ReportDuplicate(config) => {
3336
let should_report_scanned_files = config.git_branch || config.files.is_some();
37+
let git_branch_files = if config.git_branch {
38+
Some(discovery::changed_files_against_origin(current_dir)?)
39+
} else {
40+
None
41+
};
3442
let (source_files, discovery_duration) = time_result(config.verbose, || {
35-
let git_branch_files = if config.git_branch {
36-
Some(discovery::changed_files_against_origin(current_dir)?)
37-
} else {
38-
None
39-
};
4043
discovery::discover_source_files(
4144
current_dir,
4245
&config.file_extensions,
43-
git_branch_files.as_deref().or(config.files.as_deref()),
46+
if config.git_branch {
47+
None
48+
} else {
49+
config.files.as_deref()
50+
},
4451
)
4552
})?;
4653
let (processed_files, file_processing_duration) =
4754
time_result(config.verbose, || line::process_source_files(&source_files))?;
55+
let duplicate_source_files = git_branch_files.as_deref().map_or_else(
56+
|| processed_files.clone(),
57+
|git_branch_files| filtered_processed_files(&processed_files, git_branch_files),
58+
);
4859
let (duplicate_blocks, duplicate_detection_duration) =
4960
time_value(config.verbose, || {
50-
report::detect_duplicate_blocks(&processed_files)
61+
report::detect_duplicate_blocks(&duplicate_source_files)
5162
});
5263
let report = report::DuplicateReport {
5364
analyzed_files: source_files.len(),
@@ -99,10 +110,28 @@ fn time_value<T>(enabled: bool, operation: impl FnOnce() -> T) -> (T, Option<Dur
99110
(value, started_at.map(|instant| instant.elapsed()))
100111
}
101112

113+
fn filtered_processed_files(
114+
processed_files: &[ProcessedFile],
115+
selected_files: &[std::path::PathBuf],
116+
) -> Vec<ProcessedFile> {
117+
let selected_files = selected_files
118+
.iter()
119+
.map(|path| format_path(path))
120+
.collect::<HashSet<_>>();
121+
processed_files
122+
.iter()
123+
.filter(|processed_file| {
124+
selected_files.contains(&format_path(&processed_file.source.display_path))
125+
})
126+
.cloned()
127+
.collect()
128+
}
129+
102130
#[cfg(test)]
103131
mod tests {
104132
use std::fs;
105133
use std::path::{Path, PathBuf};
134+
use std::process::Command;
106135
use std::sync::atomic::{AtomicUsize, Ordering};
107136

108137
use super::*;
@@ -132,24 +161,96 @@ mod tests {
132161
}
133162
fs::write(path, contents).expect("write test file");
134163
}
164+
}
135165

136-
fn path(&self) -> &Path {
166+
impl Drop for TempProject {
167+
fn drop(&mut self) {
168+
let _ = fs::remove_dir_all(&self.path);
169+
}
170+
}
171+
172+
impl AsRef<Path> for TempProject {
173+
fn as_ref(&self) -> &Path {
137174
&self.path
138175
}
139176
}
140177

141-
impl Drop for TempProject {
178+
struct TempGitRepo {
179+
path: PathBuf,
180+
}
181+
182+
impl TempGitRepo {
183+
fn new(name: &str) -> Self {
184+
let id = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
185+
let path =
186+
std::env::temp_dir().join(format!("codem8-git-{name}-{}-{id}", std::process::id()));
187+
if path.exists() {
188+
fs::remove_dir_all(&path).expect("remove stale test directory");
189+
}
190+
fs::create_dir_all(&path).expect("create test directory");
191+
Self { path }
192+
}
193+
194+
fn write(&self, relative_path: &str, contents: &str) {
195+
let path = self.path.join(relative_path);
196+
if let Some(parent) = path.parent() {
197+
fs::create_dir_all(parent).expect("create test parent directory");
198+
}
199+
fs::write(path, contents).expect("write test file");
200+
}
201+
202+
fn git(&self, args: &[&str]) {
203+
let status = Command::new("git")
204+
.arg("-C")
205+
.arg(&self.path)
206+
.args(args)
207+
.status()
208+
.expect("run git");
209+
assert!(status.success(), "git command failed: {args:?}");
210+
}
211+
212+
fn commit(&self, message: &str) {
213+
self.git(&["add", "."]);
214+
self.git(&[
215+
"-c",
216+
"user.name=CodeM8 Test",
217+
"-c",
218+
"user.email=codem8@example.invalid",
219+
"commit",
220+
"-m",
221+
message,
222+
]);
223+
}
224+
}
225+
226+
impl Drop for TempGitRepo {
142227
fn drop(&mut self) {
143228
let _ = fs::remove_dir_all(&self.path);
144229
}
145230
}
146231

147-
fn run_in(project: &TempProject, args: &[&str]) -> std::result::Result<String, CodeM8Error> {
232+
impl AsRef<Path> for TempGitRepo {
233+
fn as_ref(&self) -> &Path {
234+
&self.path
235+
}
236+
}
237+
238+
fn run_in<P: AsRef<Path>>(
239+
project: P,
240+
args: &[&str],
241+
) -> std::result::Result<String, CodeM8Error> {
148242
let mut output = Vec::new();
149-
run(args.iter().copied(), project.path(), &mut output)?;
243+
run(args.iter().copied(), project.as_ref(), &mut output)?;
150244
Ok(String::from_utf8(output).expect("report is UTF-8"))
151245
}
152246

247+
fn git_is_available() -> bool {
248+
Command::new("git")
249+
.arg("--version")
250+
.status()
251+
.is_ok_and(|status| status.success())
252+
}
253+
153254
#[test]
154255
fn duplicate_report_snapshot_is_stable() {
155256
let project = TempProject::new("snapshot");
@@ -258,6 +359,25 @@ mod tests {
258359
assert!(js_output.contains("Duplicate blocks found: 1"));
259360
}
260361

362+
#[test]
363+
fn git_branch_mode_limits_duplicate_search_to_changed_files() {
364+
if !git_is_available() {
365+
return;
366+
}
367+
let project = TempGitRepo::new("git-branch-scope");
368+
project.git(&["init"]);
369+
project.write("src/a.ts", "const original = 1;\n");
370+
project.write("src/b.ts", "const shared = 1;\n");
371+
project.commit("initial");
372+
project.git(&["update-ref", "refs/remotes/origin/main", "HEAD"]);
373+
project.git(&["branch", "-M", "feature"]);
374+
project.write("src/a.ts", "const shared = 1;\n");
375+
let output =
376+
run_in(&project, &["--report-duplicate", "-git-branch"]).expect("report succeeds");
377+
assert!(output.contains("Number of files scanned: 2"));
378+
assert!(output.contains("Duplicate blocks found: 0"));
379+
}
380+
261381
#[test]
262382
fn invalid_explicit_file_returns_a_clear_error() {
263383
let project = TempProject::new("invalid-file");

0 commit comments

Comments
 (0)