Add validator that checks for duplicate pathnames Project owner can now configure duplicate pathnames validator. Commits which contain duplicate pathnames will be rejected. If y and z are pathnames and y.equalsIgnoreCase(z), then y and z are duplicates. Change-Id: Iaac644a07fc88e855cc9a041f0237157bda9d010
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidator.java new file mode 100644 index 0000000..bf64f63 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidator.java
@@ -0,0 +1,250 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.uploadvalidator; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.gerrit.extensions.annotations.Exports; +import com.google.gerrit.extensions.annotations.PluginName; +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.server.config.PluginConfig; +import com.google.gerrit.server.config.PluginConfigFactory; +import com.google.gerrit.server.config.ProjectConfigEntry; +import com.google.gerrit.server.events.CommitReceivedEvent; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.validators.CommitValidationException; +import com.google.gerrit.server.git.validators.CommitValidationListener; +import com.google.gerrit.server.git.validators.CommitValidationMessage; +import com.google.gerrit.server.project.NoSuchProjectException; +import com.google.inject.AbstractModule; +import com.google.inject.Inject; + +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.treewalk.TreeWalk; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +public class DuplicatePathnameValidator implements CommitValidationListener { + + public static AbstractModule module() { + return new AbstractModule() { + private List<String> getAvailableLocales() { + return Lists.transform(Arrays.asList(Locale.getAvailableLocales()), + new Function<Locale, String>() { + @Override + public String apply(Locale input) { + return input.toString(); + } + }); + } + + @Override + protected void configure() { + DynamicSet.bind(binder(), CommitValidationListener.class) + .to(DuplicatePathnameValidator.class); + bind(ProjectConfigEntry.class) + .annotatedWith(Exports.named(KEY_REJECT_DUPLICATE_PATHNAMES)) + .toInstance(new ProjectConfigEntry("Reject Duplicate Pathnames", + null, ProjectConfigEntry.Type.BOOLEAN, null, false, + "Pushes of commits that contain duplicate pathnames, or that " + + "contain duplicates of existing pathnames will be " + + "rejected. Pathnames y and z are considered to be " + + "duplicates if they are equal, case-insensitive.")); + bind(ProjectConfigEntry.class) + .annotatedWith(Exports.named(KEY_REJECT_DUPLICATE_PATHNAMES_LOCALE)) + .toInstance(new ProjectConfigEntry("Reject Duplicate Pathnames Locale", + "en", ProjectConfigEntry.Type.STRING, getAvailableLocales(), false, + "To avoid problems caused by comparing pathnames with different " + + "locales it is possible to use a specific locale. The " + + "default is English (en).")); + } + }; + } + + public static String KEY_REJECT_DUPLICATE_PATHNAMES = + "rejectDuplicatePathnames"; + public static String KEY_REJECT_DUPLICATE_PATHNAMES_LOCALE = + "rejectDuplicatePathnamesLocale"; + + @VisibleForTesting + static boolean isActive(PluginConfig cfg) { + return cfg.getBoolean(KEY_REJECT_DUPLICATE_PATHNAMES, false); + } + + @VisibleForTesting + static Locale getLocale(PluginConfig cfg) { + return Locale.forLanguageTag( + cfg.getString(KEY_REJECT_DUPLICATE_PATHNAMES_LOCALE, "en")); + } + + @VisibleForTesting + Map<String, String> allPaths(Collection<String> leafs) { + Map<String, String> paths = new HashMap<>(); + for (String cp : leafs) { + int n = cp.indexOf('/'); + while (n > -1) { + String s = cp.substring(0, n); + paths.put(s.toLowerCase(locale), s); + n = cp.indexOf('/', n + 1); + } + paths.put(cp.toLowerCase(locale), cp); + } + return paths; + } + + Set<String> allParentFolders(Collection<String> paths) { + Set<String> folders = new HashSet<>(); + for (String cp : paths) { + int n = cp.indexOf('/'); + while (n > -1) { + String s = cp.substring(0, n); + folders.add(s); + n = cp.indexOf('/', n + 1); + } + } + return folders; + } + + @VisibleForTesting + static CommitValidationMessage conflict(String f1, String f2) { + return new CommitValidationMessage(f1 + ": pathname conflicts with " + f2, + true); + } + + private static boolean isDeleted(TreeWalk tw) { + return FileMode.MISSING.equals(tw.getRawMode(0)); + } + + private final String pluginName; + private final PluginConfigFactory cfgFactory; + private final GitRepositoryManager repoManager; + + private Locale locale; + + @VisibleForTesting + void setLocale(Locale locale) { + this.locale = locale; + } + + @Inject + DuplicatePathnameValidator(@PluginName String pluginName, + PluginConfigFactory cfgFactory, GitRepositoryManager repoManager) { + this.pluginName = pluginName; + this.cfgFactory = cfgFactory; + this.repoManager = repoManager; + } + + @Override + public List<CommitValidationMessage> onCommitReceived( + CommitReceivedEvent receiveEvent) throws CommitValidationException { + try { + PluginConfig cfg = cfgFactory + .getFromProjectConfig(receiveEvent.project.getNameKey(), pluginName); + if (!isActive(cfg)) { + return Collections.emptyList(); + } + locale = getLocale(cfg); + try (Repository repo = + repoManager.openRepository(receiveEvent.project.getNameKey())) { + List<CommitValidationMessage> messages = + performValidation(repo, receiveEvent.commit); + if (!messages.isEmpty()) { + throw new CommitValidationException("contains duplicate pathnames", + messages); + } + } + } catch (NoSuchProjectException | IOException e) { + throw new CommitValidationException( + "failed to check for duplicate pathnames", e); + } + return Collections.emptyList(); + } + + @VisibleForTesting + List<CommitValidationMessage> performValidation(Repository repo, RevCommit c) + throws IOException { + List<CommitValidationMessage> messages = new LinkedList<>(); + + Set<String> pathnames = CommitUtils.getChangedPaths(repo, c); + checkForDuplicatesInSet(pathnames, messages); + if (!messages.isEmpty() || c.getParentCount() == 0) { + return messages; + } + + try (TreeWalk tw = new TreeWalk(repo)) { + tw.setRecursive(false); + tw.addTree(c.getTree()); + checkForDuplicatesAgainstTheWholeTree(tw, pathnames, messages); + } + return messages; + } + + @VisibleForTesting + void checkForDuplicatesAgainstTheWholeTree(TreeWalk tw, + Set<String> changed, List<CommitValidationMessage> messages) + throws IOException { + Map<String, String> all = allPaths(changed); + + while (tw.next()) { + String currentPath = tw.getPathString(); + + if (isDeleted(tw)) { + continue; + } + + String potentialDuplicate = all.get(currentPath.toLowerCase(locale)); + if (potentialDuplicate == null) { + continue; + } else if (potentialDuplicate.equals(currentPath)) { + if (tw.isSubtree()) { + tw.enterSubtree(); + } + continue; + } else { + messages.add(conflict(potentialDuplicate, currentPath)); + } + } + } + + private void checkForDuplicatesInSet(Set<String> files, + List<CommitValidationMessage> messages) { + Set<String> filesAndFolders = Sets.newHashSet(files); + filesAndFolders.addAll(allParentFolders(files)); + Map<String, String> seen = new HashMap<>(); + for (String file : filesAndFolders) { + String lc = file.toLowerCase(locale); + String duplicate = seen.get(lc); + if (duplicate != null) { + messages.add(conflict(duplicate, file)); + } else { + seen.put(lc, file); + } + } + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java index 5d462b2..f63a707 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java +++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java
@@ -23,6 +23,7 @@ install(new PatternCacheModule()); install(ContentTypeUtil.module()); install(BlockedKeywordValidator.module()); + install(DuplicatePathnameValidator.module()); install(FileExtensionValidator.module()); install(FooterValidator.module()); install(InvalidFilenameValidator.module());
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md index dd2e4d4..5e4fdf3 100644 --- a/src/main/resources/Documentation/about.md +++ b/src/main/resources/Documentation/about.md
@@ -6,6 +6,7 @@ - invalid filenames - blocked keywords - blocked content types +- reject duplicate pathnames - reject Windows line endings - symbolic links - reject submodules
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md index 702a644..faa4498 100644 --- a/src/main/resources/Documentation/config.md +++ b/src/main/resources/Documentation/config.md
@@ -30,6 +30,8 @@ maxPathLength = 200 rejectSymlink = false rejectSubmodule = false + rejectDuplicatePathnames = false + rejectDuplicatePathnamesLocale = en ``` plugin.@PLUGIN@.blockedFileExtension @@ -177,3 +179,34 @@ interpreted as a blacklist. Defined patterns are *not* inherited by child projects. + +plugin.@PLUGIN@.rejectDuplicatePathnames +: Reject duplicate pathnames. + + This check looks for duplicate pathnames which only differ in case + in the tree of the commit as these can cause problems on case + insensitive filesystems commonly used e.g. on Windows or Mac. If the + check finds duplicate pathnames the push will be rejected. + + The default value is false. This means duplicate pathnames ignoring + case are allowed. + + This option is *not* inherited by child projects. + +plugin.@PLUGIN@.rejectDuplicatePathnamesLocale +: Reject duplicate pathnames locale. + + When the validator checks for duplicate pathnames it will convert + the pathnames to lower case. In some cases this leads to a [problem][5]. + + To avoid these kind of problems, this option is used to specify a + locale which is used when converting a pathname to lower case. + + Full list of supported locales can be found [here][6]. + + The default value is "en" (English). + + This option is *not* inherited by child projects. + +[5]: http://bugs.java.com/view_bug.do?bug_id=6208680 +[6]: http://www.oracle.com/technetwork/java/javase/javase7locales-334809.html
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidatorTest.java new file mode 100644 index 0000000..f3be61b --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/DuplicatePathnameValidatorTest.java
@@ -0,0 +1,197 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.uploadvalidator; + +import static com.google.common.truth.Truth.assertThat; +import static com.googlesource.gerrit.plugins.uploadvalidator.DuplicatePathnameValidator.conflict; +import static com.googlesource.gerrit.plugins.uploadvalidator.TestUtils.EMPTY_CONTENT; +import static com.googlesource.gerrit.plugins.uploadvalidator.TestUtils.EMPTY_PLUGIN_CONFIG; +import static com.googlesource.gerrit.plugins.uploadvalidator.TestUtils.createDirCacheEntry; +import static com.googlesource.gerrit.plugins.uploadvalidator.TestUtils.createEmptyDirCacheEntries; +import static com.googlesource.gerrit.plugins.uploadvalidator.TestUtils.makeCommit; +import static com.googlesource.gerrit.plugins.uploadvalidator.TestUtils.transformMessage; +import static com.googlesource.gerrit.plugins.uploadvalidator.TestUtils.transformMessages; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.google.gerrit.server.git.validators.CommitValidationMessage; + +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +public class DuplicatePathnameValidatorTest extends ValidatorTestCase { + private static final List<String> INITIAL_PATHNAMES = ImmutableList.of( + "a" , "ab", + "f1/a", "f1/ab", + "f2/a", "f2/ab", "f2/sF1/a", "f2/sF1/ab"); + + private TestRepository<Repository> testRepo; + private List<String> vistedPaths = Lists.newArrayList(); + private List<CommitValidationMessage> messages = Lists.newArrayList(); + private Set<String> changedPaths; + private DuplicatePathnameValidator validator; + + private void runCheck(List<String> existingTreePaths, Set<String> testPaths, + List<CommitValidationMessage> messages, List<String> visitedPaths) + throws Exception { + RevCommit c = makeCommit( + createEmptyDirCacheEntries(existingTreePaths, testRepo), testRepo); + try (TreeWalk tw = new TreeWalk(repo)) { + tw.setRecursive(false); + tw.addTree(c.getTree()); + tw.setFilter(new ListVisitedPathsFilter(visitedPaths)); + validator.checkForDuplicatesAgainstTheWholeTree(tw, testPaths, messages); + } + } + + @Override + @Before + public void init() throws IOException { + super.init(); + testRepo = new TestRepository<>(repo); + validator = new DuplicatePathnameValidator(null, null, null); + validator.setLocale(Locale.ENGLISH); + } + + @Test + public void testSkipSubTreesWithImproperPrefix() throws Exception { + changedPaths = Sets.newHashSet("f1/A"); + runCheck(INITIAL_PATHNAMES, changedPaths, messages, vistedPaths); + assertThat(transformMessages(messages)) + .containsExactly(transformMessage(conflict("f1/A", "f1/a"))); + assertThat(vistedPaths).containsExactlyElementsIn(ImmutableList.of("a", + "ab", "f1", "f1/a", "f1/ab", "f2")); + } + + @Test + public void testFindConflictingSubtree() throws Exception { + changedPaths = Sets.newHashSet("F1/a"); + runCheck(INITIAL_PATHNAMES, changedPaths, messages, vistedPaths); + assertThat(transformMessages(messages)) + .containsExactly(transformMessage(conflict("F1", "f1"))); + assertThat(vistedPaths).containsExactlyElementsIn( + ImmutableList.of("a", "ab", "f1", "f2")); + } + + @Test + public void testFindConflictingSubtree2() throws Exception { + changedPaths = Sets.newHashSet("f2/sf1", "F1/a"); + runCheck(INITIAL_PATHNAMES, changedPaths, messages, vistedPaths); + assertThat(transformMessages(messages)).containsExactly( + transformMessage(conflict("F1", "f1")), + transformMessage(conflict("f2/sf1", "f2/sF1"))); + assertThat(vistedPaths).containsExactlyElementsIn( + ImmutableList.of("a", "ab", "f1", "f2", "f2/a", "f2/ab", "f2/sF1")); + } + + @Test + public void testFindDuplicates() throws Exception { + changedPaths = Sets.newHashSet("AB", "f1/A", "f2/Ab"); + runCheck(INITIAL_PATHNAMES, changedPaths, messages, vistedPaths); + assertThat(transformMessages(messages)).containsExactly( + transformMessage(conflict("AB", "ab")), + transformMessage(conflict("f1/A", "f1/a")), + transformMessage(conflict("f2/Ab", "f2/ab"))); + assertThat(vistedPaths).containsExactlyElementsIn( + ImmutableList.of("a", "ab", "f1", "f1/a", "f1/ab", + "f2", "f2/a", "f2/ab", "f2/sF1")); + } + + @Test + public void testFindNoDuplicates() throws Exception { + changedPaths = Sets.newHashSet("a", "ab", "f1/ab"); + runCheck(INITIAL_PATHNAMES, changedPaths, messages, vistedPaths); + assertThat(messages).isEmpty(); + assertThat(vistedPaths).containsExactlyElementsIn(ImmutableList.of("a", + "ab", "f1", "f1/a", "f1/ab", "f2")); + } + + @Test + public void testCheckInsideOfCommit() throws Exception { + List<String> filenames = Lists.newArrayList(INITIAL_PATHNAMES); + // add files with conflicting pathnames + filenames.add("A"); + filenames.add("F1/ab"); + filenames.add("f2/sF1/aB"); + RevCommit c = + makeCommit(createEmptyDirCacheEntries(filenames, testRepo), testRepo); + List<CommitValidationMessage> m = validator.performValidation(repo, c); + assertThat(m).hasSize(4); + // During checking inside of the commit it's unknown which file is checked + // first, because of that, both capabilities must be checked. + assertThat(transformMessages(m)).containsAnyOf( + transformMessage(conflict("A", "a")), + transformMessage(conflict("a", "A"))); + + assertThat(transformMessages(m)).containsAnyOf( + transformMessage(conflict("F1", "f1")), + transformMessage(conflict("f1", "F1"))); + + assertThat(transformMessages(m)).containsAnyOf( + transformMessage(conflict("F1/ab", "f1/ab")), + transformMessage(conflict("f1/ab", "F1/ab"))); + + assertThat(transformMessages(m)).containsAnyOf( + transformMessage( + conflict("f2/sF1/aB", "f2/sF1/ab")), + transformMessage( + conflict("f2/sF1/ab", "f2/sF1/aB"))); + } + + @Test + public void testCheckRenaming() throws Exception { + RevCommit c = makeCommit( + createEmptyDirCacheEntries(INITIAL_PATHNAMES, testRepo), testRepo); + DirCacheEntry[] entries = new DirCacheEntry[INITIAL_PATHNAMES.size()]; + for (int x = 0; x < INITIAL_PATHNAMES.size(); x++) { + // Rename files + entries[x] = createDirCacheEntry(INITIAL_PATHNAMES.get(x).toUpperCase(), + EMPTY_CONTENT, testRepo); + } + RevCommit c1 = makeCommit(entries, testRepo, c); + List<CommitValidationMessage> m = validator.performValidation(repo, c1); + assertThat(m).isEmpty(); + } + + @Test + public void validatorInactiveWhenConfigEmpty() { + assertThat(DuplicatePathnameValidator.isActive(EMPTY_PLUGIN_CONFIG)) + .isFalse(); + } + + @Test + public void defaultLocale() { + assertThat(DuplicatePathnameValidator.getLocale(EMPTY_PLUGIN_CONFIG)) + .isEqualTo(Locale.ENGLISH); + } + + @Test + public void testGetParentFolder() { + assertThat(validator.allParentFolders(INITIAL_PATHNAMES)) + .containsExactlyElementsIn( + ImmutableList.of("f1", "f2", "f2/sF1")); + } +}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/ListVisitedPathsFilter.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/ListVisitedPathsFilter.java new file mode 100644 index 0000000..282590b --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/ListVisitedPathsFilter.java
@@ -0,0 +1,53 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.uploadvalidator; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.TreeFilter; + +import java.io.IOException; +import java.util.List; + +public class ListVisitedPathsFilter extends TreeFilter { + private List<String> visitedPaths = null; + + public ListVisitedPathsFilter(List<String> visitedPaths) { + super(); + this.visitedPaths = visitedPaths; + } + + @Override + public boolean include(TreeWalk walker) + throws MissingObjectException, IncorrectObjectTypeException, IOException { + visitedPaths.add(walker.getPathString()); + return true; + } + + @Override + public boolean shouldBeRecursive() { + return false; + } + + @Override + public TreeFilter clone() { + return this; + } + + public List<String> getVisitedPaths() { + return visitedPaths; + } +}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/TestUtils.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/TestUtils.java index 9590bdb..0f102fa 100644 --- a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/TestUtils.java +++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/TestUtils.java
@@ -14,6 +14,7 @@ package com.googlesource.gerrit.plugins.uploadvalidator; +import com.google.common.base.Charsets; import com.google.common.base.Function; import com.google.common.cache.CacheBuilder; import com.google.common.cache.LoadingCache; @@ -27,9 +28,12 @@ import org.eclipse.jgit.api.RmCommand; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.NoFilepatternException; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import java.io.File; @@ -44,6 +48,17 @@ public static final PluginConfig EMPTY_PLUGIN_CONFIG = new PluginConfig("", new Config()); + protected static final byte[] EMPTY_CONTENT = "".getBytes(Charsets.UTF_8); + + private static final Function<CommitValidationMessage, String> MESSAGE_TRANSFORMER = + new Function<CommitValidationMessage, String>() { + @Override + public String apply(CommitValidationMessage input) { + String pre = (input.isError()) ? "ERROR: " : "MSG: "; + return pre + input.getMessage(); + } + }; + public static final LoadingCache<String, Pattern> PATTERN_CACHE = CacheBuilder.newBuilder().build(new PatternCacheModule.Loader()); @@ -110,15 +125,44 @@ return new File(repo.getDirectory().getParent(), name); } + public static String transformMessage(CommitValidationMessage messages) { + return MESSAGE_TRANSFORMER.apply(messages); + } + public static List<String> transformMessages( List<CommitValidationMessage> messages) { - return Lists.transform(messages, - new Function<CommitValidationMessage, String>() { - @Override - public String apply(CommitValidationMessage input) { - String pre = (input.isError()) ? "ERROR: " : "MSG: "; - return pre + input.getMessage(); - } - }); + return Lists.transform(messages, MESSAGE_TRANSFORMER); + } + + public static DirCacheEntry[] createEmptyDirCacheEntries( + List<String> filenames, TestRepository<Repository> repo) + throws Exception { + DirCacheEntry[] entries = new DirCacheEntry[filenames.size()]; + for (int x = 0; x < filenames.size(); x++) { + entries[x] = createDirCacheEntry(filenames.get(x), EMPTY_CONTENT, repo); + } + return entries; + + } + + public static DirCacheEntry createDirCacheEntry(String pathname, + byte[] content, TestRepository<Repository> repo) + throws Exception { + return repo.file(pathname, repo.blob(content)); + } + + public static RevCommit makeCommit(DirCacheEntry[] entries, + TestRepository<Repository> repo) throws Exception { + return makeCommit(entries, repo, (RevCommit[]) null); + } + + public static RevCommit makeCommit(DirCacheEntry[] entries, + TestRepository<Repository> repo, RevCommit... parents) + throws Exception { + final RevTree ta = repo.tree(entries); + RevCommit c = + (parents == null) ? repo.commit(ta) : repo.commit(ta, parents); + repo.parseBody(c); + return c; } }