Skip to content

Commit 1a9c0d0

Browse files
committed
file history rename detection
find more renames that git-log --follow would simplify
1 parent aa7aa7a commit 1a9c0d0

File tree

6 files changed

+212
-22
lines changed

6 files changed

+212
-22
lines changed

asyncgit/src/sync/commit_files.rs

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
use super::{diff::DiffOptions, CommitId, RepoPath};
44
use crate::{
55
error::Result,
6-
sync::{get_stashes, repository::repo},
7-
StatusItem, StatusItemType,
6+
sync::{get_stashes, repository::repo, utils::bytes2string},
7+
Error, StatusItem, StatusItemType,
88
};
9-
use git2::{Diff, Repository};
9+
use git2::{Diff, DiffFindOptions, Repository};
1010
use scopetime::scope_time;
1111
use std::{cmp::Ordering, collections::HashSet};
1212

@@ -153,14 +153,94 @@ pub(crate) fn get_commit_diff<'a>(
153153
Ok(diff)
154154
}
155155

156+
///
157+
pub(crate) fn commit_contains_file(
158+
repo: &Repository,
159+
id: CommitId,
160+
pathspec: &str,
161+
) -> Result<Option<git2::Delta>> {
162+
let commit = repo.find_commit(id.into())?;
163+
let commit_tree = commit.tree()?;
164+
165+
let parent = if commit.parent_count() > 0 {
166+
repo.find_commit(commit.parent_id(0)?)
167+
.ok()
168+
.and_then(|c| c.tree().ok())
169+
} else {
170+
None
171+
};
172+
173+
let mut opts = git2::DiffOptions::new();
174+
opts.pathspec(pathspec.to_string())
175+
.skip_binary_check(true)
176+
.context_lines(0);
177+
178+
let diff = repo.diff_tree_to_tree(
179+
parent.as_ref(),
180+
Some(&commit_tree),
181+
Some(&mut opts),
182+
)?;
183+
184+
if diff.stats()?.files_changed() == 0 {
185+
return Ok(None);
186+
}
187+
188+
Ok(diff.deltas().map(|delta| delta.status()).next())
189+
}
190+
191+
///
192+
pub(crate) fn commit_detect_file_rename(
193+
repo: &Repository,
194+
id: CommitId,
195+
pathspec: &str,
196+
) -> Result<Option<String>> {
197+
scope_time!("commit_detect_file_rename");
198+
199+
let mut diff = get_commit_diff(repo, id, None, None, None)?;
200+
201+
diff.find_similar(Some(
202+
DiffFindOptions::new()
203+
.renames(true)
204+
.renames_from_rewrites(true)
205+
.rename_from_rewrite_threshold(100),
206+
))?;
207+
208+
let current_path = std::path::Path::new(pathspec);
209+
210+
for delta in diff.deltas() {
211+
let new_file_matches = delta
212+
.new_file()
213+
.path()
214+
.map(|path| path == current_path)
215+
.unwrap_or_default();
216+
217+
if new_file_matches
218+
&& matches!(delta.status(), git2::Delta::Renamed)
219+
{
220+
return Ok(Some(bytes2string(
221+
delta.old_file().path_bytes().ok_or_else(|| {
222+
Error::Generic(String::from("old_file error"))
223+
})?,
224+
)?));
225+
}
226+
}
227+
228+
Ok(None)
229+
}
230+
156231
#[cfg(test)]
157232
mod tests {
158233
use super::get_commit_files;
159234
use crate::{
160235
error::Result,
161236
sync::{
162-
commit, stage_add_file, stash_save,
163-
tests::{get_statuses, repo_init},
237+
commit,
238+
commit_files::commit_detect_file_rename,
239+
stage_add_all, stage_add_file, stash_save,
240+
tests::{
241+
get_statuses, rename_file, repo_init,
242+
repo_init_empty, write_commit_file,
243+
},
164244
RepoPath,
165245
},
166246
StatusItemType,
@@ -240,4 +320,28 @@ mod tests {
240320

241321
Ok(())
242322
}
323+
324+
#[test]
325+
fn test_rename_detection() {
326+
let (td, repo) = repo_init_empty().unwrap();
327+
let repo_path: RepoPath = td.path().into();
328+
329+
write_commit_file(&repo, "foo.txt", "foobar", "c1");
330+
rename_file(&repo, "foo.txt", "bar.txt");
331+
stage_add_all(
332+
&repo_path,
333+
"*",
334+
Some(crate::sync::ShowUntrackedFilesConfig::All),
335+
)
336+
.unwrap();
337+
let rename_commit = commit(&repo_path, "c2").unwrap();
338+
339+
let rename = commit_detect_file_rename(
340+
&repo,
341+
rename_commit,
342+
"bar.txt",
343+
)
344+
.unwrap();
345+
assert_eq!(rename, Some(String::from("foo.txt")));
346+
}
243347
}

asyncgit/src/sync/commit_filter.rs

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,67 @@
1-
use super::{commit_files::get_commit_diff, CommitId};
2-
use crate::error::Result;
1+
use super::{
2+
commit_files::{commit_contains_file, get_commit_diff},
3+
CommitId,
4+
};
5+
use crate::{
6+
error::Result, sync::commit_files::commit_detect_file_rename,
7+
};
38
use bitflags::bitflags;
49
use fuzzy_matcher::FuzzyMatcher;
510
use git2::{Diff, Repository};
6-
use std::sync::Arc;
11+
use std::sync::{Arc, RwLock};
712

813
///
914
pub type SharedCommitFilterFn = Arc<
1015
Box<dyn Fn(&Repository, &CommitId) -> Result<bool> + Send + Sync>,
1116
>;
1217

1318
///
14-
pub fn diff_contains_file(file_path: String) -> SharedCommitFilterFn {
19+
pub fn diff_contains_file(
20+
file_path: Arc<RwLock<String>>,
21+
) -> SharedCommitFilterFn {
1522
Arc::new(Box::new(
1623
move |repo: &Repository,
1724
commit_id: &CommitId|
1825
-> Result<bool> {
19-
let diff = get_commit_diff(
26+
let current_file_path = file_path.read()?.to_string();
27+
28+
if let Some(delta) = commit_contains_file(
2029
repo,
2130
*commit_id,
22-
Some(file_path.clone()),
23-
None,
24-
None,
25-
)?;
31+
current_file_path.as_str(),
32+
)? {
33+
//note: only do rename test in case file looks like being added in this commit
34+
35+
// log::info!(
36+
// "edit: [{}] ({:?}) - {}",
37+
// commit_id.get_short_string(),
38+
// delta,
39+
// &current_file_path
40+
// );
41+
42+
if matches!(delta, git2::Delta::Added) {
43+
let rename = commit_detect_file_rename(
44+
repo,
45+
*commit_id,
46+
current_file_path.as_str(),
47+
)?;
2648

27-
let contains_file = diff.deltas().len() > 0;
49+
if let Some(old_name) = rename {
50+
// log::info!(
51+
// "rename: [{}] {:?} <- {:?}",
52+
// commit_id.get_short_string(),
53+
// current_file_path,
54+
// old_name,
55+
// );
56+
57+
(*file_path.write()?) = old_name;
58+
}
59+
}
60+
61+
return Ok(true);
62+
}
2863

29-
Ok(contains_file)
64+
Ok(false)
3065
},
3166
))
3267
}

asyncgit/src/sync/logwalker.rs

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,16 +113,17 @@ mod tests {
113113
use super::*;
114114
use crate::error::Result;
115115
use crate::sync::commit_filter::{SearchFields, SearchOptions};
116-
use crate::sync::tests::write_commit_file;
116+
use crate::sync::tests::{rename_file, write_commit_file};
117117
use crate::sync::{
118118
commit, get_commits_info, stage_add_file,
119119
tests::repo_init_empty,
120120
};
121121
use crate::sync::{
122-
diff_contains_file, filter_commit_by_search, LogFilterSearch,
123-
LogFilterSearchOptions, RepoPath,
122+
diff_contains_file, filter_commit_by_search, stage_add_all,
123+
LogFilterSearch, LogFilterSearchOptions, RepoPath,
124124
};
125125
use pretty_assertions::assert_eq;
126+
use std::sync::{Arc, RwLock};
126127
use std::{fs::File, io::Write, path::Path};
127128

128129
#[test]
@@ -207,7 +208,8 @@ mod tests {
207208

208209
let _third_commit_id = commit(&repo_path, "commit3").unwrap();
209210

210-
let diff_contains_baz = diff_contains_file("baz".into());
211+
let file_path = Arc::new(RwLock::new(String::from("baz")));
212+
let diff_contains_baz = diff_contains_file(file_path);
211213

212214
let mut items = Vec::new();
213215
let mut walker = LogWalker::new(&repo, 100)?
@@ -222,7 +224,8 @@ mod tests {
222224

223225
assert_eq!(items.len(), 0);
224226

225-
let diff_contains_bar = diff_contains_file("bar".into());
227+
let file_path = Arc::new(RwLock::new(String::from("bar")));
228+
let diff_contains_bar = diff_contains_file(file_path);
226229

227230
let mut items = Vec::new();
228231
let mut walker = LogWalker::new(&repo, 100)?
@@ -280,4 +283,37 @@ mod tests {
280283

281284
assert_eq!(items.len(), 2);
282285
}
286+
287+
#[test]
288+
fn test_logwalker_with_filter_rename() {
289+
let (td, repo) = repo_init_empty().unwrap();
290+
let repo_path: RepoPath = td.path().into();
291+
292+
write_commit_file(&repo, "foo.txt", "foobar", "c1");
293+
rename_file(&repo, "foo.txt", "bar.txt");
294+
stage_add_all(
295+
&repo_path,
296+
"*",
297+
Some(crate::sync::ShowUntrackedFilesConfig::All),
298+
)
299+
.unwrap();
300+
let rename_commit = commit(&repo_path, "c2").unwrap();
301+
302+
write_commit_file(&repo, "bar.txt", "new content", "c3");
303+
304+
let file_path =
305+
Arc::new(RwLock::new(String::from("bar.txt")));
306+
let log_filter = diff_contains_file(file_path.clone());
307+
308+
let mut items = Vec::new();
309+
let mut walker = LogWalker::new(&repo, 100)
310+
.unwrap()
311+
.filter(Some(log_filter));
312+
walker.read(&mut items).unwrap();
313+
314+
assert_eq!(items.len(), 3);
315+
assert_eq!(items[1], rename_commit);
316+
317+
assert_eq!(file_path.read().unwrap().as_str(), "foo.txt");
318+
}
283319
}

asyncgit/src/sync/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@ mod tests {
143143
});
144144
}
145145

146+
pub fn rename_file(repo: &Repository, old: &str, new: &str) {
147+
let dir = repo.workdir().unwrap();
148+
let old = dir.join(old);
149+
let new = dir.join(new);
150+
std::fs::rename(old, new).unwrap();
151+
}
152+
146153
/// write, stage and commit a file
147154
pub fn write_commit_file(
148155
repo: &Repository,

asyncgit/src/sync/repository.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ impl From<&str> for RepoPath {
4747
Self::Path(PathBuf::from(p))
4848
}
4949
}
50+
impl From<&Path> for RepoPath {
51+
fn from(p: &Path) -> Self {
52+
Self::Path(PathBuf::from(p))
53+
}
54+
}
5055

5156
pub fn repo(repo_path: &RepoPath) -> Result<Repository> {
5257
let repo = Repository::open_ext(

src/components/file_revlog.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::sync::{Arc, RwLock};
2+
13
use super::utils::logitems::ItemBatch;
24
use super::{visibility_blocking, BlameFileOpen, InspectCommitOpen};
35
use crate::keys::key_match;
@@ -116,7 +118,8 @@ impl FileRevlogComponent {
116118
pub fn open(&mut self, open_request: FileRevOpen) -> Result<()> {
117119
self.open_request = Some(open_request.clone());
118120

119-
let filter = diff_contains_file(open_request.file_path);
121+
let file_name = Arc::new(RwLock::new(open_request.file_path));
122+
let filter = diff_contains_file(file_name);
120123
self.git_log = Some(AsyncLog::new(
121124
self.repo_path.borrow().clone(),
122125
&self.sender,

0 commit comments

Comments
 (0)