/** * This file is part of git-as-svn. It is subject to the license terms * in the LICENSE file found in the top-level directory of this distribution * and at http://www.gnu.org/licenses/gpl-2.0.html. No part of git-as-svn, * including this file, may be copied, modified, propagated, or distributed * except according to the terms contained in the LICENSE file. */ package svnserver.server; import com.google.common.collect.ImmutableMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.testng.annotations.Test; import org.testng.internal.junit.ArrayAsserts; import org.tmatesoft.svn.core.*; import org.tmatesoft.svn.core.io.ISVNEditor; import org.tmatesoft.svn.core.io.SVNRepository; import svnserver.SvnTestServer; import java.util.*; import static svnserver.SvnTestHelper.*; /** * Check file properties. * * @author Artem V. Navrotskiy <bozaro@users.noreply.github.com> */ public class SvnLogTest { private static class LogEntry { private final long revision; @Nullable private final String message; @NotNull private final Set<String> paths; private LogEntry(@NotNull SVNLogEntry logEntry) { this(logEntry.getRevision(), logEntry.getMessage(), convert(logEntry.getChangedPaths().values())); } private static Collection<String> convert(@NotNull Collection<SVNLogEntryPath> changedPaths) { List<String> result = new ArrayList<>(); for (SVNLogEntryPath logPath : changedPaths) { result.add(logPath.getType() + " " + logPath.getPath()); } return result; } private LogEntry(long revision, @Nullable String message, @NotNull String... paths) { this(revision, message, Arrays.asList(paths)); } private LogEntry(long revision, @Nullable String message, @NotNull Collection<String> paths) { this.revision = revision; this.message = message; this.paths = new TreeSet<>(paths); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; final LogEntry logEntry = (LogEntry) o; return revision == logEntry.revision && Objects.equals(message, logEntry.message) && paths.equals(logEntry.paths); } @Override public int hashCode() { int result = (int) (revision ^ (revision >>> 32)); if (message != null) result = 31 * result + message.hashCode(); result = 31 * result + paths.hashCode(); return result; } @Override public String toString() { return "LogEntry{" + "revision=" + revision + ", message='" + message + '\'' + ", paths=" + paths + '}'; } } @NotNull private final static Map<String, String> propsEolNative = ImmutableMap.<String, String>builder() .put(SVNProperty.EOL_STYLE, SVNProperty.EOL_STYLE_NATIVE) .build(); /** * Check simple svn log behaviour. * * @throws Exception */ @Test public void simple() throws Exception { try (SvnTestServer server = SvnTestServer.createEmpty()) { final SVNRepository repo = server.openSvnRepository(); // r1 - add single file. createFile(repo, "/foo.txt", "", propsEolNative); // r2 - add file in directory. { final long latestRevision = repo.getLatestRevision(); final ISVNEditor editor = repo.getCommitEditor("Create directory: /foo", null, false, null); editor.openRoot(latestRevision); editor.addDir("/foo", null, -1); editor.addFile("/foo/bar.txt", null, -1); editor.changeFileProperty("/foo/bar.txt", SVNProperty.EOL_STYLE, SVNPropertyValue.create(SVNProperty.EOL_STYLE_NATIVE)); sendDeltaAndClose(editor, "/foo/bar.txt", null, "File body"); editor.closeDir(); editor.closeDir(); editor.closeEdit(); } // r3 - change file in directory. modifyFile(repo, "/foo/bar.txt", "New body", repo.getLatestRevision()); // r4 - change file in directory. createFile(repo, "/foo/foo.txt", "New body", propsEolNative); // svn log from root final long last = repo.getLatestRevision(); checkLog(repo, last, 0, "/", new LogEntry(4, "Create file: /foo/foo.txt", "A /foo/foo.txt"), new LogEntry(3, "Modify file: /foo/bar.txt", "M /foo/bar.txt"), new LogEntry(2, "Create directory: /foo", "A /foo", "A /foo/bar.txt"), new LogEntry(1, "Create file: /foo.txt", "A /foo.txt"), new LogEntry(0, null) ); // svn log from root checkLog(repo, last, 0, "/foo", new LogEntry(4, "Create file: /foo/foo.txt", "A /foo/foo.txt"), new LogEntry(3, "Modify file: /foo/bar.txt", "M /foo/bar.txt"), new LogEntry(2, "Create directory: /foo", "A /foo", "A /foo/bar.txt") ); // svn log from root checkLog(repo, last, 0, "/foo/bar.txt", new LogEntry(3, "Modify file: /foo/bar.txt", "M /foo/bar.txt"), new LogEntry(2, "Create directory: /foo", "A /foo", "A /foo/bar.txt") ); // svn empty log checkLog(repo, 0, 0, "/", new LogEntry(0, null) ); } } /** * Check file recreate log test. */ @Test public void recreateFile() throws Exception { try (SvnTestServer server = SvnTestServer.createEmpty()) { final SVNRepository repo = server.openSvnRepository(); // r1 - add single file. createFile(repo, "/foo.txt", "", propsEolNative); // r2 - modify file. modifyFile(repo, "/foo.txt", "New content", repo.getLatestRevision()); // r3 - remove file. deleteFile(repo, "/foo.txt"); final long delete = repo.getLatestRevision(); // r4 - recreate file. createFile(repo, "/foo.txt", "", propsEolNative); // svn log from root final long last = repo.getLatestRevision(); checkLog(repo, last, 0, "/foo.txt", new LogEntry(4, "Create file: /foo.txt", "A /foo.txt") ); // svn log from root checkLog(repo, delete - 1, 0, "/foo.txt", new LogEntry(2, "Modify file: /foo.txt", "M /foo.txt"), new LogEntry(1, "Create file: /foo.txt", "A /foo.txt") ); } } /** * Check file recreate log test. */ @Test public void recreateDirectory() throws Exception { try (SvnTestServer server = SvnTestServer.createEmpty()) { final SVNRepository repo = server.openSvnRepository(); // r1 - add single file. { final ISVNEditor editor = repo.getCommitEditor("Create directory: /foo", null, false, null); editor.openRoot(-1); editor.addDir("/foo", null, -1); // Empty file. final String file = "/foo/bar.txt"; editor.addFile(file, null, -1); editor.changeFileProperty(file, SVNProperty.EOL_STYLE, SVNPropertyValue.create(SVNProperty.EOL_STYLE_NATIVE)); sendDeltaAndClose(editor, file, null, ""); // Close dir editor.closeDir(); editor.closeDir(); editor.closeEdit(); } // r2 - modify file. modifyFile(repo, "/foo/bar.txt", "New content", repo.getLatestRevision()); // r3 - remove directory. deleteFile(repo, "/foo"); final long delete = repo.getLatestRevision(); // r4 - recreate file. { final ISVNEditor editor = repo.getCommitEditor("Create directory: /foo", null, false, null); editor.openRoot(-1); editor.addDir("/foo", null, -1); // Empty file. final String file = "/foo/bar.txt"; editor.addFile(file, null, -1); editor.changeFileProperty(file, SVNProperty.EOL_STYLE, SVNPropertyValue.create(SVNProperty.EOL_STYLE_NATIVE)); sendDeltaAndClose(editor, file, null, ""); // Close dir editor.closeDir(); editor.closeDir(); editor.closeEdit(); } // svn log from latest revision final long last = repo.getLatestRevision(); checkLog(repo, last, 0, "/foo/bar.txt", new LogEntry(4, "Create directory: /foo", "A /foo", "A /foo/bar.txt") ); // svn log from revision before delete checkLog(repo, delete - 1, 0, "/foo/bar.txt", new LogEntry(2, "Modify file: /foo/bar.txt", "M /foo/bar.txt"), new LogEntry(1, "Create directory: /foo", "A /foo", "A /foo/bar.txt") ); } } /** * Check file move log test. */ @Test public void moveFile() throws Exception { try (SvnTestServer server = SvnTestServer.createEmpty()) { final SVNRepository repo = server.openSvnRepository(); // r1 - add single file. createFile(repo, "/foo.txt", "Foo content", propsEolNative); // r2 - rename file { final long revision = repo.getLatestRevision(); final ISVNEditor editor = repo.getCommitEditor("Rename: /foo.txt to /bar.txt", null, false, null); editor.openRoot(-1); // Empty file. editor.addFile("/bar.txt", "/foo.txt", revision); editor.changeFileProperty("/bar.txt", SVNProperty.EOL_STYLE, SVNPropertyValue.create(SVNProperty.EOL_STYLE_NATIVE)); editor.closeFile("/bar.txt", null); editor.deleteEntry("/foo.txt", revision); // Close dir editor.closeDir(); editor.closeEdit(); } // r3 - modify file. modifyFile(repo, "/bar.txt", "Bar content", repo.getLatestRevision()); // r4 - rename file { final long revision = repo.getLatestRevision(); final ISVNEditor editor = repo.getCommitEditor("Rename: /bar.txt to /baz.txt", null, false, null); editor.openRoot(-1); // Empty file. editor.addFile("/baz.txt", "/bar.txt", revision); editor.changeFileProperty("/baz.txt", SVNProperty.EOL_STYLE, SVNPropertyValue.create(SVNProperty.EOL_STYLE_NATIVE)); editor.closeFile("/baz.txt", null); editor.deleteEntry("/bar.txt", revision); // Close dir editor.closeDir(); editor.closeEdit(); } // r5 - modify file. modifyFile(repo, "/baz.txt", "Baz content", repo.getLatestRevision()); final long last = repo.getLatestRevision(); // r6 - remove file. deleteFile(repo, "/baz.txt"); // svn log from last file exists revision checkLog(repo, last, 0, "/baz.txt", new LogEntry(5, "Modify file: /baz.txt", "M /baz.txt"), new LogEntry(4, "Rename: /bar.txt to /baz.txt", "D /bar.txt", "A /baz.txt"), new LogEntry(3, "Modify file: /bar.txt", "M /bar.txt"), new LogEntry(2, "Rename: /foo.txt to /bar.txt", "D /foo.txt", "A /bar.txt"), new LogEntry(1, "Create file: /foo.txt", "A /foo.txt") ); // svn log from last file exists revision checkLog(repo, 0, last, "/baz.txt", new LogEntry(1, "Create file: /foo.txt", "A /foo.txt"), new LogEntry(2, "Rename: /foo.txt to /bar.txt", "D /foo.txt", "A /bar.txt"), new LogEntry(3, "Modify file: /bar.txt", "M /bar.txt"), new LogEntry(4, "Rename: /bar.txt to /baz.txt", "D /bar.txt", "A /baz.txt"), new LogEntry(5, "Modify file: /baz.txt", "M /baz.txt") ); // svn log from last file exists revision checkLogLimit(repo, last, 0, 3, "/baz.txt", new LogEntry(5, "Modify file: /baz.txt", "M /baz.txt"), new LogEntry(4, "Rename: /bar.txt to /baz.txt", "D /bar.txt", "A /baz.txt"), new LogEntry(3, "Modify file: /bar.txt", "M /bar.txt") ); // svn log from last file exists revision checkLogLimit(repo, 0, last, 3, "/baz.txt", new LogEntry(1, "Create file: /foo.txt", "A /foo.txt"), new LogEntry(2, "Rename: /foo.txt to /bar.txt", "D /foo.txt", "A /bar.txt"), new LogEntry(3, "Modify file: /bar.txt", "M /bar.txt") ); // svn log from last file exists revision checkLog(repo, 3, 0, "/bar.txt", new LogEntry(3, "Modify file: /bar.txt", "M /bar.txt"), new LogEntry(2, "Rename: /foo.txt to /bar.txt", "D /foo.txt", "A /bar.txt"), new LogEntry(1, "Create file: /foo.txt", "A /foo.txt") ); } } /** * Check file move log test. */ @Test public void moveDirectory() throws Exception { try (SvnTestServer server = SvnTestServer.createEmpty()) { final SVNRepository repo = server.openSvnRepository(); // r1 - add single file. { final ISVNEditor editor = repo.getCommitEditor("Create directory: /foo", null, false, null); editor.openRoot(-1); editor.addDir("/foo", null, -1); // Some file. editor.addFile("/foo/test.txt", null, -1); editor.changeFileProperty("/foo/test.txt", SVNProperty.EOL_STYLE, SVNPropertyValue.create(SVNProperty.EOL_STYLE_NATIVE)); sendDeltaAndClose(editor, "/foo/test.txt", null, "Foo content"); // Close dir editor.closeDir(); editor.closeDir(); editor.closeEdit(); } // r2 - rename dir { final long revision = repo.getLatestRevision(); final ISVNEditor editor = repo.getCommitEditor("Rename: /foo to /bar", null, false, null); editor.openRoot(-1); // Move dir. editor.addDir("/bar", "/foo", revision); editor.closeDir(); editor.deleteEntry("/foo", revision); // Close dir editor.closeDir(); editor.closeEdit(); } // r3 - modify file. modifyFile(repo, "/bar/test.txt", "Bar content", repo.getLatestRevision()); // r4 - rename dir { final long revision = repo.getLatestRevision(); final ISVNEditor editor = repo.getCommitEditor("Rename: /bar to /baz", null, false, null); editor.openRoot(-1); // Move dir. editor.addDir("/baz", "/bar", revision); editor.closeDir(); editor.deleteEntry("/bar", revision); // Close dir editor.closeDir(); editor.closeEdit(); } // r5 - modify file. modifyFile(repo, "/baz/test.txt", "Baz content", repo.getLatestRevision()); final long last = repo.getLatestRevision(); // svn log from last file exists revision checkLog(repo, last, 0, "/baz/test.txt", new LogEntry(5, "Modify file: /baz/test.txt", "M /baz/test.txt"), new LogEntry(4, "Rename: /bar to /baz", "D /bar", "A /baz", "A /baz/test.txt"), new LogEntry(3, "Modify file: /bar/test.txt", "M /bar/test.txt"), new LogEntry(2, "Rename: /foo to /bar", "D /foo", "A /bar", "A /bar/test.txt"), new LogEntry(1, "Create directory: /foo", "A /foo", "A /foo/test.txt") ); // svn log from last file exists revision checkLog(repo, 0, last, "/baz/test.txt", new LogEntry(1, "Create directory: /foo", "A /foo", "A /foo/test.txt"), new LogEntry(2, "Rename: /foo to /bar", "D /foo", "A /bar", "A /bar/test.txt"), new LogEntry(3, "Modify file: /bar/test.txt", "M /bar/test.txt"), new LogEntry(4, "Rename: /bar to /baz", "D /bar", "A /baz", "A /baz/test.txt"), new LogEntry(5, "Modify file: /baz/test.txt", "M /baz/test.txt") ); // svn log from last file exists revision checkLogLimit(repo, last, 0, 3, "/baz/test.txt", new LogEntry(5, "Modify file: /baz/test.txt", "M /baz/test.txt"), new LogEntry(4, "Rename: /bar to /baz", "D /bar", "A /baz", "A /baz/test.txt"), new LogEntry(3, "Modify file: /bar/test.txt", "M /bar/test.txt") ); // svn log from last file exists revision checkLogLimit(repo, 0, last, 3, "/baz/test.txt", new LogEntry(1, "Create directory: /foo", "A /foo", "A /foo/test.txt"), new LogEntry(2, "Rename: /foo to /bar", "D /foo", "A /bar", "A /bar/test.txt"), new LogEntry(3, "Modify file: /bar/test.txt", "M /bar/test.txt") ); } } private void checkLog(@NotNull SVNRepository repo, long r1, long r2, @NotNull String path, @NotNull LogEntry... expecteds) throws SVNException { checkLogLimit(repo, r1, r2, 0, path, expecteds); } private void checkLogLimit(@NotNull SVNRepository repo, long r1, long r2, int limit, @NotNull String path, @NotNull LogEntry... expecteds) throws SVNException { final List<LogEntry> actual = new ArrayList<>(); repo.log(new String[]{path}, r1, r2, true, false, limit, logEntry -> actual.add(new LogEntry(logEntry))); ArrayAsserts.assertArrayEquals(expecteds, actual.toArray(new LogEntry[actual.size()])); } }