Skip to content

Commit 2305c92

Browse files
committed
New path matching feature to handle exclude/ignore rules similar to .gitignore
1 parent fc202a8 commit 2305c92

6 files changed

Lines changed: 232 additions & 79 deletions

File tree

jsync-engine/src/main/java/com/fizzed/jsync/engine/DefaultJsyncEventHandler.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,13 @@ public void willExcludePath(VirtualPath sourcePath) {
3434
}
3535

3636
@Override
37-
public void willIgnorePath(VirtualPath sourcePath) {
38-
log.debug("Ignoring path {}", sourcePath);
37+
public void willIgnoreSourcePath(VirtualPath sourcePath) {
38+
log.debug("Ignoring source path {}", sourcePath);
39+
}
40+
41+
@Override
42+
public void willIgnoreTargetPath(VirtualPath targetPath) {
43+
log.debug("Ignoring target path {}", targetPath);
3944
}
4045

4146
@Override

jsync-engine/src/main/java/com/fizzed/jsync/engine/JsyncEngine.java

Lines changed: 21 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.fizzed.jsync.vfs.*;
44
import com.fizzed.jsync.vfs.util.Permissions;
5+
import com.fizzed.jsync.vfs.util.VirtualPathMatchers;
56
import org.slf4j.Logger;
67
import org.slf4j.LoggerFactory;
78

@@ -31,9 +32,10 @@ public class JsyncEngine {
3132
private List<String> ignores;
3233
// when running a sync
3334
private Checksum negotiatedChecksum;
34-
private List<VirtualPath> excludePaths;
35-
private List<VirtualPath> ignoreSourcePaths;
36-
private List<VirtualPath> ignoreTargetPaths;
35+
private VirtualPathMatchers excludeMatchers;
36+
private VirtualPathMatchers ignoreMatchers;
37+
private VirtualPath sourceRootPath;
38+
private VirtualPath targetRootPath;
3739

3840
public JsyncEngine() {
3941
this.eventHandler = new DefaultJsyncEventHandler();
@@ -225,6 +227,9 @@ public JsyncResult sync(VirtualFileSystem sourceVfs, String sourcePath, VirtualF
225227
final VirtualPath sourcePathAbsFinal = sourcePathAbs.normalize();
226228
final VirtualPath targetPathAbsFinal = targetPathAbs.normalize();
227229

230+
this.sourceRootPath = sourcePathAbsFinal;
231+
this.targetRootPath = targetPathAbsFinal;
232+
228233

229234
//
230235
// Negotiate checksum methods between source and target filesystems if necessary
@@ -236,29 +241,9 @@ public JsyncResult sync(VirtualFileSystem sourceVfs, String sourcePath, VirtualF
236241
log.debug("Source filesystem stat mode: {}", sourceVfs.getStatModel());
237242
log.debug("Target filesystem stat mode: {}", targetVfs.getStatModel());
238243

239-
// build exclude and ignore paths
240-
if (this.excludes != null) {
241-
this.excludePaths = this.excludes.stream()
242-
.map(VirtualPath::parse)
243-
.map(sourcePathAbsFinal::resolve)
244-
.collect(toList());
245-
} else {
246-
this.excludePaths = Collections.emptyList();
247-
}
248-
249-
if (this.ignores != null) {
250-
this.ignoreSourcePaths = this.ignores.stream()
251-
.map(VirtualPath::parse)
252-
.map(sourcePathAbsFinal::resolve)
253-
.collect(toList());
254-
this.ignoreTargetPaths = this.ignores.stream()
255-
.map(VirtualPath::parse)
256-
.map(targetPathAbsFinal::resolve)
257-
.collect(toList());
258-
} else {
259-
this.ignoreSourcePaths = Collections.emptyList();
260-
this.ignoreTargetPaths = Collections.emptyList();
261-
}
244+
// build exclude and ignore matchers
245+
this.excludeMatchers = VirtualPathMatchers.compile(this.excludes);
246+
this.ignoreMatchers = VirtualPathMatchers.compile(this.ignores);
262247

263248

264249
final long now = System.currentTimeMillis();
@@ -272,17 +257,6 @@ public JsyncResult sync(VirtualFileSystem sourceVfs, String sourcePath, VirtualF
272257
final List<VirtualPathPair> deferredFiles = new ArrayList<>();
273258

274259
if (sourcePathAbsFinal.isDirectory()) {
275-
// any excludes, let's resolve them against pwd of the source to make it easier to exclude them
276-
final List<VirtualPath> excludePaths;
277-
if (this.excludes != null) {
278-
excludePaths = this.excludes.stream()
279-
.map(VirtualPath::parse)
280-
.map(sourcePathAbsFinal::resolve)
281-
.collect(toList());
282-
} else {
283-
excludePaths = Collections.emptyList();
284-
}
285-
286260
// as we process files, only a subset may require more advanced methods of detecting whether they were modified
287261
// since that process could be "expensive", we keep a list of files on source/target that we will defer processing
288262
// until we have a chance to do some bulk processing of checksums, etc.
@@ -409,23 +383,19 @@ protected void syncDirectory(int level, JsyncResult result, List<VirtualPathPair
409383

410384

411385
// we need a list of files in both directories, so we can see what to add/delete
412-
List<VirtualPath> sourceChildPaths = sourceVfs.ls(sourcePath).stream()
386+
final List<VirtualPath> sourceChildPaths = sourceVfs.ls(sourcePath).stream()
413387
// apply filter to source files if they are on the exclude list
414388
.filter(v -> {
415-
for (VirtualPath p : this.excludePaths) {
416-
if (v.startsWith(p)) {
417-
this.eventHandler.willExcludePath(v);
418-
return false;
419-
}
389+
if (this.excludeMatchers.matches(this.sourceRootPath, v)) {
390+
this.eventHandler.willExcludePath(v);
391+
return false;
420392
}
421393
return true;
422394
})
423395
.filter(v -> {
424-
for (VirtualPath p : this.ignoreSourcePaths) {
425-
if (v.startsWith(p)) {
426-
this.eventHandler.willIgnorePath(v);
427-
return false;
428-
}
396+
if (this.ignoreMatchers.matches(this.sourceRootPath, v)) {
397+
this.eventHandler.willIgnoreSourcePath(v);
398+
return false;
429399
}
430400
return true;
431401
})
@@ -446,10 +416,9 @@ protected void syncDirectory(int level, JsyncResult result, List<VirtualPathPair
446416

447417
final List<VirtualPath> targetChildPaths = targetVfs.ls(targetPath).stream()
448418
.filter(v -> {
449-
for (VirtualPath p : this.ignoreTargetPaths) {
450-
if (v.startsWith(p)) {
451-
return false;
452-
}
419+
if (this.ignoreMatchers.matches(this.targetRootPath, v)) {
420+
this.eventHandler.willIgnoreTargetPath(v);
421+
return false;
453422
}
454423
return true;
455424
})

jsync-engine/src/main/java/com/fizzed/jsync/engine/JsyncEventHandler.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ public interface JsyncEventHandler {
1616

1717
void willEnd(VirtualFileSystem sourceVfs, VirtualPath sourcePath, VirtualFileSystem targetVfs, VirtualPath targetPath, JsyncResult result, long timeMillis);
1818

19-
void willExcludePath(VirtualPath targetPath);
19+
void willExcludePath(VirtualPath sourcePath);
2020

21-
void willIgnorePath(VirtualPath targetPath);
21+
void willIgnoreSourcePath(VirtualPath sourcePath);
22+
23+
void willIgnoreTargetPath(VirtualPath targetPath);
2224

2325
void willCreateDirectory(VirtualPath targetPath, boolean recursively);
2426

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.fizzed.jsync.vfs.util;
2+
3+
import com.fizzed.jsync.vfs.VirtualPath;
4+
5+
import java.nio.file.FileSystems;
6+
import java.nio.file.PathMatcher;
7+
import java.nio.file.Paths;
8+
9+
public class VirtualPathMatcher {
10+
11+
private final PathMatcher matcher;
12+
13+
public VirtualPathMatcher(PathMatcher matcher) {
14+
this.matcher = matcher;
15+
}
16+
17+
public boolean matches(VirtualPath rootPath, VirtualPath currentPath) {
18+
final String relativePath;
19+
20+
// resolve the current path against the root path, so we're left with the relative path we're matching against
21+
if (currentPath.isAbsolute()) {
22+
String rootFullPath = rootPath.toFullPath();
23+
String currentFullPath = currentPath.toFullPath();
24+
int pathStartPos = currentFullPath.indexOf(rootFullPath);
25+
if (pathStartPos >= 0) {
26+
// remove the leading path PLUS the file separator
27+
relativePath = currentFullPath.substring(pathStartPos + rootFullPath.length() + 1);
28+
} else {
29+
relativePath = currentFullPath;
30+
}
31+
} else {
32+
relativePath = currentPath.toString();
33+
}
34+
35+
return this.matcher.matches(Paths.get(relativePath));
36+
}
37+
38+
static public VirtualPathMatcher compile(String rule) {
39+
String glob = rule.trim();
40+
boolean isDirectory = false;
41+
boolean isRooted = false;
42+
43+
// 1. Check for Directory marker
44+
if (glob.endsWith("/")) {
45+
isDirectory = true;
46+
glob = glob.substring(0, glob.length() - 1); // Strip trailing slash
47+
}
48+
49+
// 2. Check for Root anchor
50+
if (glob.startsWith("/")) {
51+
isRooted = true;
52+
glob = glob.substring(1); // Strip leading slash
53+
}
54+
55+
// FIX: Handle "/**/" usually found in the middle of paths
56+
// Git: "docs/**/*.md" -> Zero or more dirs
57+
// Java: "docs/**/*.md" -> One or more dirs (fails on docs/file.md)
58+
// Solution: Replace "/**/" with "/{,**/}" which means "Empty OR /**/"
59+
// if (glob.contains("/**/")) {
60+
// glob = glob.replace("/**/", "/{,**/}");
61+
// }
62+
63+
// 3. Build the Glob
64+
// We need to construct a robust brace expansion {A,B,C...}
65+
StringBuilder finalGlob = new StringBuilder();
66+
finalGlob.append("glob:{");
67+
68+
if (isRooted) {
69+
// Rule: /target/ or /target
70+
finalGlob.append(glob); // Matches "target" at root
71+
if (isDirectory) {
72+
finalGlob.append(",").append(glob).append("/**"); // Matches "target/..." at root
73+
}
74+
} else {
75+
// Rule: target/ or target
76+
finalGlob.append(glob); // Matches "target" at root
77+
finalGlob.append(",**/").append(glob); // Matches "src/target" (nested)
78+
79+
if (isDirectory) {
80+
// If it's a directory, we must ALSO match the contents
81+
finalGlob.append(",").append(glob).append("/**"); // Matches "target/file.txt" (root)
82+
finalGlob.append(",**/").append(glob).append("/**"); // Matches "src/target/file.txt" (nested)
83+
}
84+
}
85+
86+
finalGlob.append("}");
87+
88+
PathMatcher matcher = FileSystems.getDefault().getPathMatcher(finalGlob.toString());
89+
90+
return new VirtualPathMatcher(matcher);
91+
}
92+
93+
}
Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,35 @@
11
package com.fizzed.jsync.vfs.util;
22

3-
import java.nio.file.FileSystems;
4-
import java.nio.file.PathMatcher;
3+
import com.fizzed.jsync.vfs.VirtualPath;
4+
5+
import java.util.ArrayList;
6+
import java.util.List;
57

68
public class VirtualPathMatchers {
79

8-
10+
private final List<VirtualPathMatcher> matchers = new ArrayList<>();
911

10-
static public PathMatcher compileRule(String rule) {
11-
String glob = rule.trim();
12+
public VirtualPathMatchers(List<VirtualPathMatcher> matchers) {
13+
this.matchers.addAll(matchers);
14+
}
1215

13-
// 1. Handle directory-only rules (e.g., "build/")
14-
// Git implies "everything inside this directory"
15-
if (glob.endsWith("/")) {
16-
glob = glob + "**";
16+
public boolean matches(VirtualPath rootPath, VirtualPath path) {
17+
for (VirtualPathMatcher matcher : matchers) {
18+
if (matcher.matches(rootPath, path)) {
19+
return true;
20+
}
1721
}
22+
return false;
23+
}
1824

19-
// 2. Handle "rooted" vs "anywhere" rules
20-
// If it starts with '/', it matches from the root only.
21-
// If NOT, it matches anywhere (e.g., "*.log" -> "** /*.log")
22-
if (glob.startsWith("/")) {
23-
// Remove leading slash for Java PathMatcher consistency on relative paths
24-
glob = glob.substring(1);
25-
} else {
26-
// If it's not rooted, allow it to match deep in the tree
27-
// Example: "tmp" becomes "**/tmp"
28-
if (!glob.startsWith("**/") && !glob.equals("*")) {
29-
glob = "**/" + glob;
25+
static public VirtualPathMatchers compile(List<String> rules) {
26+
List<VirtualPathMatcher> matchers = new ArrayList<>();
27+
if (rules != null) {
28+
for (String rule : rules) {
29+
matchers.add(VirtualPathMatcher.compile(rule));
3030
}
3131
}
32-
33-
// Create the matcher using "glob" syntax
34-
return FileSystems.getDefault().getPathMatcher("glob:" + glob);
32+
return new VirtualPathMatchers(matchers);
3533
}
3634

3735
}

0 commit comments

Comments
 (0)