Add validator for max path length

On some operating systems there is a limit on the path length. Allow
project owners to configure a maximum path length, so that the project
stays cloneable on such operating systems.

Change-Id: I0d7d72c9a71f6064f2f68e57bde2707d65bfa9e1
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidator.java
index 6f350a2..ee90792 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/FileExtensionValidator.java
@@ -21,29 +21,19 @@
 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.Inject;
 
-import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.api.errors.GitAPIException;
-import org.eclipse.jgit.diff.DiffEntry;
-import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.AbstractTreeIterator;
-import org.eclipse.jgit.treewalk.CanonicalTreeParser;
-import org.eclipse.jgit.treewalk.TreeWalk;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 
-public class FileExtensionValidator implements CommitValidationListener {
+public class FileExtensionValidator extends PathValidator {
   public static String KEY_BLOCKED_FILE_EXTENSION = "blockedFileExtension";
 
   private final String pluginName;
@@ -95,41 +85,4 @@
 
     return Collections.emptyList();
   }
-
-  private List<String> getFiles(Repository repo, RevCommit c) throws IOException, GitAPIException {
-    List<String> files = new ArrayList<>();
-
-    if (c.getParentCount() > 0) {
-      Git git = new Git(repo);
-      List<DiffEntry> diffEntries =
-          git.diff().setOldTree(getTreeIterator(repo, c.getName() + "^"))
-              .setNewTree(getTreeIterator(repo, c.getName())).call();
-      for (DiffEntry e : diffEntries) {
-        if (e.getNewPath() != null) {
-          files.add(e.getNewPath());
-        }
-      }
-    } else {
-      TreeWalk tw = new TreeWalk(repo);
-      tw.addTree(c.getTree());
-      tw.setRecursive(true);
-      while (tw.next()) {
-        files.add(tw.getPathString());
-      }
-    }
-
-    return files;
-  }
-
-  private AbstractTreeIterator getTreeIterator(Repository repo, String name)
-      throws IOException {
-    CanonicalTreeParser p = new CanonicalTreeParser();
-    ObjectReader or = repo.newObjectReader();
-    try {
-      p.reset(or, new RevWalk(repo).parseTree(repo.resolve(name)));
-      return p;
-    } finally {
-      or.release();
-    }
-  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/MaxPathLengthValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/MaxPathLengthValidator.java
new file mode 100644
index 0000000..0f6c16a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/MaxPathLengthValidator.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2014 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.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+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.CommitValidationMessage;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+public class MaxPathLengthValidator extends PathValidator {
+  public static String KEY_MAX_PATH_LENGTH = "maxPathLength";
+
+  private final String pluginName;
+  private final PluginConfigFactory cfgFactory;
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  MaxPathLengthValidator(@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);
+      int maxPathLength = cfg.getInt(KEY_MAX_PATH_LENGTH, 0);
+      if (maxPathLength > 0) {
+        Repository repo = repoManager.openRepository(receiveEvent.project.getNameKey());
+        try {
+          List<CommitValidationMessage> messages = new LinkedList<>();
+          List<String> files = getFiles(repo, receiveEvent.commit);
+          for (String file : files) {
+            if (file.length() > maxPathLength) {
+              messages.add(new CommitValidationMessage("path too long: " + file, true));
+            }
+          }
+          if (!messages.isEmpty()) {
+            throw new CommitValidationException(
+                "contains files with too long paths (max path length: "
+                    + maxPathLength + ")", messages);
+          }
+        } finally {
+          repo.close();
+        }
+      }
+    } catch (NoSuchProjectException | IOException | GitAPIException e) {
+      throw new CommitValidationException("failed to check for max file path length", e);
+    }
+
+    return Collections.emptyList();
+  }
+}
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 44bf5c6..05b4366 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java
@@ -16,6 +16,7 @@
 
 import static com.googlesource.gerrit.plugins.uploadvalidator.FileExtensionValidator.KEY_BLOCKED_FILE_EXTENSION;
 import static com.googlesource.gerrit.plugins.uploadvalidator.FooterValidator.KEY_REQUIRED_FOOTER;
+import static com.googlesource.gerrit.plugins.uploadvalidator.MaxPathLengthValidator.KEY_MAX_PATH_LENGTH;
 
 import com.google.gerrit.extensions.annotations.Exports;
 import com.google.gerrit.extensions.registration.DynamicSet;
@@ -45,5 +46,15 @@
                 ProjectConfigEntry.Type.ARRAY, null, false,
                 "Required footers. Pushes of commits that miss any"
                     + " of the footers will be rejected."));
+
+    DynamicSet.bind(binder(), CommitValidationListener.class)
+        .to(MaxPathLengthValidator.class);
+    bind(ProjectConfigEntry.class)
+        .annotatedWith(Exports.named(KEY_MAX_PATH_LENGTH))
+        .toInstance(
+            new ProjectConfigEntry("Max Path Length", 0, false,
+                "Maximum path length. Pushes of commits that "
+                    + "contain files with longer paths will be rejected. "
+                    + "'0' means no limit."));
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/PathValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/PathValidator.java
new file mode 100644
index 0000000..04d9b69
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/PathValidator.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2014 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.gerrit.server.git.validators.CommitValidationListener;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.AbstractTreeIterator;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
+import org.eclipse.jgit.treewalk.TreeWalk;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class PathValidator implements CommitValidationListener {
+
+  protected List<String> getFiles(Repository repo, RevCommit c)
+      throws IOException, GitAPIException {
+    List<String> files = new ArrayList<>();
+
+    if (c.getParentCount() > 0) {
+      Git git = new Git(repo);
+      List<DiffEntry> diffEntries =
+          git.diff().setOldTree(getTreeIterator(repo, c.getName() + "^"))
+              .setNewTree(getTreeIterator(repo, c.getName())).call();
+      for (DiffEntry e : diffEntries) {
+        if (e.getNewPath() != null) {
+          files.add(e.getNewPath());
+        }
+      }
+    } else {
+      TreeWalk tw = new TreeWalk(repo);
+      tw.addTree(c.getTree());
+      tw.setRecursive(true);
+      while (tw.next()) {
+        files.add(tw.getPathString());
+      }
+    }
+
+    return files;
+  }
+
+  private AbstractTreeIterator getTreeIterator(Repository repo, String name)
+      throws IOException {
+    CanonicalTreeParser p = new CanonicalTreeParser();
+    ObjectReader or = repo.newObjectReader();
+    try {
+      p.reset(or, new RevWalk(repo).parseTree(repo.resolve(name)));
+      return p;
+    } finally {
+      or.release();
+    }
+  }
+}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index e9d1359..f855775 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -1,6 +1,5 @@
 This plugin allows to configure upload validations per project.
 
-Project owners can configure blocked file extensions and required
-footers. Pushes of commits that contain files with blocked extensions
-or that miss a required footer in the commit message are rejected by
-Gerrit.
+Project owners can configure blocked file extensions, required footers
+and a maximum allowed path length. Pushes of commits that violate these
+settings are rejected by Gerrit.
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 72386b3..3381675 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -14,6 +14,7 @@
     blockedFileExtension = war
     blockedFileExtension = exe
     requiredFooter = Bug
+    maxPathLength = 200
 ```
 
 plugin.@PLUGIN@.blockedFileExtension
@@ -25,3 +26,11 @@
 :	Footer that is required.
 
 	Required footers are *not* inherited by child projects.
+
+plugin.@PLUGIN@.maxPathLength
+:	Maximum allowed path length. '0' means no limit.
+
+	Defaults to '0'.
+
+	The maximum allowed path length is *not* inherited by child
+	projects.