Skip to content
Merged
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
Binary file modified launchable/jar/exe_deploy.jar
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
Expand Down Expand Up @@ -65,6 +66,7 @@

import static com.google.common.collect.ImmutableList.*;
import static java.util.Arrays.*;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;

/**
* Compares what commits the local repository and the remote repository have, then send delta over.
Expand Down Expand Up @@ -451,25 +453,47 @@ private void collectFiles(RevCommit start, TreeWalk treeWalk, TreeReceiver treeR
OUTER:
while (treeWalk.next()) {
ObjectId head = treeWalk.getObjectId(0);

if (treeWalk.isSubtree()) {
// For directories, compare tree IDs to skip unchanged subtrees entirely
for (int i = 1; i < c; i++) {
if (head.equals(treeWalk.getObjectId(i))) {
continue OUTER;
}
}
treeWalk.enterSubtree();
continue;
}

String filePath = treeWalk.getPathString();
FileMode mode = treeWalk.getFileMode(0);
ObjectId blobId;

if (mode == FileMode.SYMLINK) {
blobId = resolveSymlinkTarget(start.getTree(), filePath, head);
if (blobId == null) {
continue;
}
} else if ((mode.getBits() & FileMode.TYPE_MASK) == FileMode.TYPE_FILE) {
blobId = head;
} else {
continue;
}

// Dedup check using the actual content blob ID.
// For symlinks, this uses the target file's blob ID so changes to the target
// are detected even when the symlink itself (its path string) is unchanged.
for (int i = 1; i < c; i++) {
if (head.equals(treeWalk.getObjectId(i))) {
if (blobId.equals(treeWalk.getObjectId(i))) {
// file at the head is identical to one of the uninteresting commits,
// meaning we have already seen this file/directory on the server.
// if it is a dir, there's no need to visit this whole subtree, so skip over
// meaning we have already seen this file on the server.
continue OUTER;
}
}

if (treeWalk.isSubtree()) {
treeWalk.enterSubtree();
} else {
if ((treeWalk.getFileMode(0).getBits() & FileMode.TYPE_MASK) == FileMode.TYPE_FILE) {
GitFile f = new GitFile(name, treeWalk.getPathString(), head, objectReader);
// to avoid excessive data transfer, skip files that are too big
if (f.size() < 1024 * 1024 && f.isText() && !f.path.equals(HEADER_FILE)) {
treeReceiver.accept(f);
}
}
GitFile f = new GitFile(name, filePath, blobId, objectReader);
if (f.size() < 1024 * 1024 && f.isText() && !f.path.equals(HEADER_FILE)) {
treeReceiver.accept(f);
}
}

Expand All @@ -488,6 +512,47 @@ private void collectFiles(RevCommit start, TreeWalk treeWalk, TreeReceiver treeR
}
}

private ObjectId resolveSymlinkTarget(AnyObjectId treeId, String symlinkPath, ObjectId symlinkBlobId) {
try {
byte[] raw = objectReader.open(symlinkBlobId, OBJ_BLOB).getCachedBytes(10_000);
String targetRelative = new String(raw, StandardCharsets.UTF_8).trim();

java.nio.file.Path symlinkDir = java.nio.file.Paths.get(symlinkPath).getParent();
java.nio.file.Path resolved;
if (symlinkDir != null) {
resolved = symlinkDir.resolve(targetRelative).normalize();
} else {
resolved = java.nio.file.Paths.get(targetRelative).normalize();
}

if (resolved.isAbsolute() || resolved.startsWith("..")) {
logger.debug("Skipping symlink {} -> {} (points outside repository)", symlinkPath, targetRelative);
return null;
}

String resolvedPath = resolved.toString().replace(java.io.File.separatorChar, '/');

try (TreeWalk tw = TreeWalk.forPath(git, resolvedPath, treeId)) {
if (tw == null) {
logger.debug("Skipping symlink {} -> {} (target not found in tree)", symlinkPath, resolvedPath);
return null;
}

FileMode targetMode = tw.getFileMode(0);
if ((targetMode.getBits() & FileMode.TYPE_MASK) == FileMode.TYPE_FILE) {
return tw.getObjectId(0);
}

logger.debug("Skipping symlink {} -> {} (target is not a regular file, mode={})",
symlinkPath, resolvedPath, targetMode);
return null;
}
} catch (IOException e) {
logger.warn("Failed to resolve symlink {}: {}", symlinkPath, e.getMessage());
return null;
}
}

/**
* Creates a per repository "header" file as a {@link VirtualFile}.
* Currently, this is just the list of files in the repository.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,98 @@ private void addCommitInSubRepo(Git mainrepo) throws Exception {
}
}

@Test
public void symlinkResolvedAsRegularFile() throws Exception {
try (Git repo = Git.init().setDirectory(ws).call()) {
Files.writeString(ws.toPath().resolve("real.txt"), "hello");
repo.add().addFilepattern("real.txt").call();
commit(repo).setMessage("add real file").call();

Files.createSymbolicLink(ws.toPath().resolve("link.txt"), java.nio.file.Path.of("real.txt"));
repo.add().addFilepattern("link.txt").call();
commit(repo).setMessage("add symlink").call();

assertThat(collectFilePaths(repo)).containsAtLeast("real.txt", "link.txt");
}
}

@Test
public void symlinkAcrossDirectories() throws Exception {
try (Git repo = Git.init().setDirectory(ws).call()) {
Files.createDirectory(ws.toPath().resolve("sub"));

// subdir -> parent: sub/link.txt -> ../root.txt
Files.writeString(ws.toPath().resolve("root.txt"), "root content");
Files.createSymbolicLink(ws.toPath().resolve("sub").resolve("link.txt"), java.nio.file.Path.of("../root.txt"));
repo.add().addFilepattern("root.txt").addFilepattern("sub/link.txt").call();

// parent -> subdir: link2.txt -> sub/deep.txt
Files.writeString(ws.toPath().resolve("sub").resolve("deep.txt"), "deep content");
Files.createSymbolicLink(ws.toPath().resolve("link2.txt"), java.nio.file.Path.of("sub/deep.txt"));
repo.add().addFilepattern("sub/deep.txt").addFilepattern("link2.txt").call();

commit(repo).setMessage("add cross-directory symlinks").call();

assertThat(collectFilePaths(repo)).containsAtLeast("root.txt", "sub/link.txt", "sub/deep.txt", "link2.txt");
}
}

@Test
public void brokenSymlinkSkipped() throws Exception {
try (Git repo = Git.init().setDirectory(ws).call()) {
Files.writeString(ws.toPath().resolve("target.txt"), "content");
Files.createSymbolicLink(ws.toPath().resolve("broken.txt"), java.nio.file.Path.of("target.txt"));
repo.add().addFilepattern("target.txt").addFilepattern("broken.txt").call();
commit(repo).setMessage("add files").call();

repo.rm().addFilepattern("target.txt").call();
commit(repo).setMessage("remove target").call();

assertThat(collectFilePaths(repo)).doesNotContain("broken.txt");
}
}

@Test
public void symlinkOutsideRepoSkipped() throws Exception {
try (Git repo = Git.init().setDirectory(ws).call()) {
Files.createSymbolicLink(ws.toPath().resolve("escape.txt"), java.nio.file.Path.of("../outside.txt"));
repo.add().addFilepattern("escape.txt").call();
commit(repo).setMessage("add symlink to outside").call();

assertThat(collectFilePaths(repo)).doesNotContain("escape.txt");
}
}

@Test
public void regularFilesUnaffectedBySymlinkChange() throws Exception {
try (Git repo = Git.init().setDirectory(ws).call()) {
Files.writeString(ws.toPath().resolve("a.txt"), "aaa");
Files.writeString(ws.toPath().resolve("b.txt"), "bbb");
repo.add().addFilepattern("a.txt").addFilepattern("b.txt").call();
commit(repo).setMessage("add regular files").call();

assertThat(collectFilePaths(repo)).containsExactly("a.txt", "b.txt");
}
}

private List<String> collectFilePaths(Git repo) throws IOException {
List<VirtualFile> files = new ArrayList<>();
CommitGraphCollector cgc = new CommitGraphCollector("test", repo.getRepository());
cgc.collectFiles(true);
cgc.new ByRepository(repo.getRepository(), "main")
.transfer(Collections.emptyList(), c -> {},
new PassThroughTreeReceiverImpl(),
FlushableConsumer.of(files::add));

List<String> paths = new ArrayList<>();
for (VirtualFile f : files) {
if (!f.path().equals(CommitGraphCollector.HEADER_FILE)) {
paths.add(f.path());
}
}
return paths;
}

private CommitCommand commit(Git r) {
return r.commit().setAll(true).setSign(false);
}
Expand Down
Loading