]> source.dussan.org Git - jgit.git/commitdiff
Introduce hook support into the FS implementations 04/40504/2
authorLaurent Goubet <laurent.goubet@obeo.fr>
Fri, 31 Oct 2014 13:58:07 +0000 (14:58 +0100)
committerChristian Halstrick <christian.halstrick@sap.com>
Mon, 2 Feb 2015 09:22:53 +0000 (10:22 +0100)
This introduces the background plumbing necessary to run git hooks from
JGit. This implementation will be OS-dependent as it aims to be
compatible with existing hooks, mostly written in Shell. It is
compatible with unix systems and windows as long as an Unix emulator
such as Cygwin is in its PATH.

Change-Id: I1f82a5205138fd8032614dd5b52aef14e02238ed
Signed-off-by: Laurent Goubet <laurent.goubet@obeo.fr>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
12 files changed:
org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_POSIX_Java7.java
org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_Win32_Java7Cygwin.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java
org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java
org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java
org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java
org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java
org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java
org.eclipse.jgit/src/org/eclipse/jgit/util/Hook.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/util/ProcessResult.java [new file with mode: 0644]

index 4a73a9bcf58a927402f2885267e21c21f1c11eea..300cf93bc87786eb1595fd53272e13b3c75a22b9 100644 (file)
@@ -53,6 +53,9 @@ import java.nio.file.Path;
 import java.nio.file.attribute.PosixFilePermission;
 import java.util.Set;
 
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+
 /**
  * FS implementation for Java7 on unix like systems
  */
@@ -344,4 +347,17 @@ public class FS_POSIX_Java7 extends FS_POSIX {
        public String normalize(String name) {
                return FileUtil.normalize(name);
        }
+
+       /**
+        * @since 3.7
+        */
+       @Override
+       public File findHook(Repository repository, Hook hook) {
+               final File gitdir = repository.getDirectory();
+               final Path hookPath = gitdir.toPath().resolve(Constants.HOOKS)
+                               .resolve(hook.getName());
+               if (Files.isExecutable(hookPath))
+                       return hookPath.toFile();
+               return null;
+       }
 }
index e40d7cf0b569989ca4752daa1098c5fd5ea263a4..b6e5d93885a17546fba31baec63dd1189f2c036d 100644 (file)
@@ -45,6 +45,11 @@ package org.eclipse.jgit.util;
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
 
 /**
  * FS for Java7 on Windows with Cygwin
@@ -135,4 +140,17 @@ public class FS_Win32_Java7Cygwin extends FS_Win32_Cygwin {
        public Attributes getAttributes(File path) {
                return FileUtil.getFileAttributesBasic(this, path);
        }
+
+       /**
+        * @since 3.7
+        */
+       @Override
+       public File findHook(Repository repository, Hook hook) {
+               final File gitdir = repository.getDirectory();
+               final Path hookPath = gitdir.toPath().resolve(Constants.HOOKS)
+                               .resolve(hook.getName());
+               if (Files.isExecutable(hookPath))
+                       return hookPath.toFile();
+               return null;
+       }
 }
index 7b1627854f11a73635a3e7e721f023c8cc70a8ba..d4be25c0cdef24d191c4fdcc608ff13600cdeddf 100644 (file)
@@ -50,6 +50,7 @@ import static org.junit.Assert.fail;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.regex.Matcher;
 
 import org.eclipse.jgit.junit.JGitTestUtil;
 import org.junit.After;
@@ -434,4 +435,82 @@ public class FileUtilTest {
                String target = fs.readSymLink(new File(trash, "x"));
                assertEquals("y", target);
        }
+
+       @Test
+       public void testRelativize_doc() {
+               // This is the javadoc example
+               String base = toOSPathString("c:\\Users\\jdoe\\eclipse\\git\\project");
+               String other = toOSPathString("c:\\Users\\jdoe\\eclipse\\git\\another_project\\pom.xml");
+               String expected = toOSPathString("..\\another_project\\pom.xml");
+
+               String actual = FileUtils.relativize(base, other);
+               assertEquals(expected, actual);
+       }
+
+       @Test
+       public void testRelativize_mixedCase() {
+               SystemReader systemReader = SystemReader.getInstance();
+               String oldOSName = null;
+               String base = toOSPathString("C:\\git\\jgit");
+               String other = toOSPathString("C:\\Git\\test\\d\\f.txt");
+               String expectedWindows = toOSPathString("..\\test\\d\\f.txt");
+               String expectedUnix = toOSPathString("..\\..\\Git\\test\\d\\f.txt");
+
+               if (!systemReader.isWindows()) {
+                       String actual = FileUtils.relativize(base, other);
+                       assertEquals(expectedUnix, actual);
+
+                       // FS_POSIX#isCaseSensitive will return "false" for mac OS X.
+                       // Use this to test both behaviors.
+                       oldOSName = System.getProperty("os.name");
+                       try {
+                               System.setProperty("os.name", "Mac OS X");
+
+                               actual = FileUtils.relativize(base, other);
+                               assertEquals(expectedWindows, actual);
+                       } finally {
+                               if (oldOSName != null)
+                                       System.setProperty("os.name", oldOSName);
+                       }
+               } else {
+                       String actual = FileUtils.relativize(base, other);
+                       assertEquals(expectedWindows, actual);
+               }
+       }
+
+       @Test
+       public void testRelativize_scheme() {
+               String base = toOSPathString("file:/home/eclipse/runtime-New_configuration/project_1/file.java");
+               String other = toOSPathString("file:/home/eclipse/runtime-New_configuration/project");
+               // 'file.java' is treated as a folder
+               String expected = toOSPathString("../../project");
+
+               String actual = FileUtils.relativize(base, other);
+               assertEquals(expected, actual);
+       }
+
+       @Test
+       public void testRelativize_equalPaths() {
+               String base = toOSPathString("file:/home/eclipse/runtime-New_configuration/project_1");
+               String other = toOSPathString("file:/home/eclipse/runtime-New_configuration/project_1");
+               String expected = "";
+
+               String actual = FileUtils.relativize(base, other);
+               assertEquals(expected, actual);
+       }
+
+       @Test
+       public void testRelativize_whitespaces() {
+               String base = toOSPathString("/home/eclipse 3.4/runtime New_configuration/project_1");
+               String other = toOSPathString("/home/eclipse 3.4/runtime New_configuration/project_1/file");
+               String expected = "file";
+
+               String actual = FileUtils.relativize(base, other);
+               assertEquals(expected, actual);
+       }
+
+       private String toOSPathString(String path) {
+               return path.replaceAll("/|\\\\",
+                               Matcher.quoteReplacement(File.separator));
+       }
 }
index 7e5f0b022c479fda9c06cb179f8b29f1b27b963e..71ec6b2bcd3f5a799d56c0c8f6470bc1078fbf54 100644 (file)
@@ -193,6 +193,7 @@ errorListing=Error listing {0}
 errorOccurredDuringUnpackingOnTheRemoteEnd=error occurred during unpacking on the remote end: {0}
 errorReadingInfoRefs=error reading info/refs
 errorSymlinksNotSupported=Symlinks are not supported with this OS/JRE
+exceptionCaughtDuringExecutionOfHook=Exception caught during execution of "{0}" hook.
 exceptionCaughtDuringExecutionOfAddCommand=Exception caught during execution of add command
 exceptionCaughtDuringExecutionOfArchiveCommand=Exception caught during execution of archive command
 exceptionCaughtDuringExecutionOfCherryPickCommand=Exception caught during execution of cherry-pick command. {0}
@@ -206,6 +207,7 @@ exceptionCaughtDuringExecutionOfResetCommand=Exception caught during execution o
 exceptionCaughtDuringExecutionOfRevertCommand=Exception caught during execution of revert command. {0}
 exceptionCaughtDuringExecutionOfRmCommand=Exception caught during execution of rm command
 exceptionCaughtDuringExecutionOfTagCommand=Exception caught during execution of tag command
+exceptionHookExecutionInterrupted=Execution of "{0}" hook interrupted.
 exceptionOccurredDuringAddingOfOptionToALogCommand=Exception occurred during adding of {0} as option to a Log command
 exceptionOccurredDuringReadingOfGIT_DIR=Exception occurred during reading of $GIT_DIR/{0}. {1}
 exceptionWhileReadingPack=ERROR: Exception caught while accessing pack file {0}, the pack file might be corrupt
index caf3e9072c99a956d9d48e9cb331a1524b33fd75..138f40f7ce7977848324e6aa865b89d3afbae4a8 100644 (file)
@@ -252,6 +252,7 @@ public class JGitText extends TranslationBundle {
        /***/ public String errorOccurredDuringUnpackingOnTheRemoteEnd;
        /***/ public String errorReadingInfoRefs;
        /***/ public String errorSymlinksNotSupported;
+       /***/ public String exceptionCaughtDuringExecutionOfHook;
        /***/ public String exceptionCaughtDuringExecutionOfAddCommand;
        /***/ public String exceptionCaughtDuringExecutionOfArchiveCommand;
        /***/ public String exceptionCaughtDuringExecutionOfCherryPickCommand;
@@ -265,6 +266,7 @@ public class JGitText extends TranslationBundle {
        /***/ public String exceptionCaughtDuringExecutionOfRevertCommand;
        /***/ public String exceptionCaughtDuringExecutionOfRmCommand;
        /***/ public String exceptionCaughtDuringExecutionOfTagCommand;
+       /***/ public String exceptionHookExecutionInterrupted;
        /***/ public String exceptionOccurredDuringAddingOfOptionToALogCommand;
        /***/ public String exceptionOccurredDuringReadingOfGIT_DIR;
        /***/ public String exceptionWhileReadingPack;
index 705d54cfa390630b490f4727a07dd0a795783613..ed0ed04d9479361ba5dba96bffd0143ea7204b42 100644 (file)
@@ -386,6 +386,13 @@ public final class Constants {
         */
        public static final String MODULES = "modules";
 
+       /**
+        * Name of the folder (inside gitDir) where the hooks are stored.
+        *
+        * @since 3.7
+        */
+       public static final String HOOKS = "hooks";
+
        /**
         * Create a new digest function for objects.
         *
index 0c63b190f2d7ee11c1c4cd641dbf8cdbe9978bce..081bf87c5472bdef2ac9fdd12808a3205d702f9c 100644 (file)
 package org.eclipse.jgit.util;
 
 import java.io.BufferedReader;
+import java.io.BufferedWriter;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintStream;
+import java.io.PrintWriter;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.text.MessageFormat;
 import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import org.eclipse.jgit.api.errors.JGitInternalException;
 import org.eclipse.jgit.errors.SymlinksNotSupportedException;
 import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.util.ProcessResult.Status;
 
 /** Abstraction to support various file system operations not in Java. */
 public abstract class FS {
@@ -613,6 +626,288 @@ public abstract class FS {
                                JGitText.get().errorSymlinksNotSupported);
        }
 
+       /**
+        * See {@link FileUtils#relativize(String, String)}.
+        *
+        * @param base
+        *            The path against which <code>other</code> should be
+        *            relativized.
+        * @param other
+        *            The path that will be made relative to <code>base</code>.
+        * @return A relative path that, when resolved against <code>base</code>,
+        *         will yield the original <code>other</code>.
+        * @see FileUtils#relativize(String, String)
+        * @since 3.7
+        */
+       public String relativize(String base, String other) {
+               return FileUtils.relativize(base, other);
+       }
+
+       /**
+        * Checks whether the given hook is defined for the given repository, then
+        * runs it with the given arguments.
+        * <p>
+        * The hook's standard output and error streams will be redirected to
+        * <code>System.out</code> and <code>System.err</code> respectively. The
+        * hook will have no stdin.
+        * </p>
+        *
+        * @param repository
+        *            The repository for which a hook should be run.
+        * @param hook
+        *            The hook to be executed.
+        * @param args
+        *            Arguments to pass to this hook. Cannot be <code>null</code>,
+        *            but can be an empty array.
+        * @return The ProcessResult describing this hook's execution.
+        * @throws JGitInternalException
+        *             if we fail to run the hook somehow. Causes may include an
+        *             interrupted process or I/O errors.
+        * @since 3.7
+        */
+       public ProcessResult runIfPresent(Repository repository, final Hook hook,
+                       String[] args) throws JGitInternalException {
+               return runIfPresent(repository, hook, args, System.out, System.err,
+                               null);
+       }
+
+       /**
+        * Checks whether the given hook is defined for the given repository, then
+        * runs it with the given arguments.
+        *
+        * @param repository
+        *            The repository for which a hook should be run.
+        * @param hook
+        *            The hook to be executed.
+        * @param args
+        *            Arguments to pass to this hook. Cannot be <code>null</code>,
+        *            but can be an empty array.
+        * @param outRedirect
+        *            A print stream on which to redirect the hook's stdout. Can be
+        *            <code>null</code>, in which case the hook's standard output
+        *            will be lost.
+        * @param errRedirect
+        *            A print stream on which to redirect the hook's stderr. Can be
+        *            <code>null</code>, in which case the hook's standard error
+        *            will be lost.
+        * @param stdinArgs
+        *            A string to pass on to the standard input of the hook. May be
+        *            <code>null</code>.
+        * @return The ProcessResult describing this hook's execution.
+        * @throws JGitInternalException
+        *             if we fail to run the hook somehow. Causes may include an
+        *             interrupted process or I/O errors.
+        * @since 3.7
+        */
+       public ProcessResult runIfPresent(Repository repository, final Hook hook,
+                       String[] args, PrintStream outRedirect, PrintStream errRedirect,
+                       String stdinArgs) throws JGitInternalException {
+               return new ProcessResult(Status.NOT_SUPPORTED);
+       }
+
+       /**
+        * See
+        * {@link #runIfPresent(Repository, Hook, String[], PrintStream, PrintStream, String)}
+        * . Should only be called by FS supporting shell scripts execution.
+        *
+        * @param repository
+        *            The repository for which a hook should be run.
+        * @param hook
+        *            The hook to be executed.
+        * @param args
+        *            Arguments to pass to this hook. Cannot be <code>null</code>,
+        *            but can be an empty array.
+        * @param outRedirect
+        *            A print stream on which to redirect the hook's stdout. Can be
+        *            <code>null</code>, in which case the hook's standard output
+        *            will be lost.
+        * @param errRedirect
+        *            A print stream on which to redirect the hook's stderr. Can be
+        *            <code>null</code>, in which case the hook's standard error
+        *            will be lost.
+        * @param stdinArgs
+        *            A string to pass on to the standard input of the hook. May be
+        *            <code>null</code>.
+        * @return The ProcessResult describing this hook's execution.
+        * @throws JGitInternalException
+        *             if we fail to run the hook somehow. Causes may include an
+        *             interrupted process or I/O errors.
+        * @since 3.7
+        */
+       protected ProcessResult internalRunIfPresent(Repository repository,
+                       final Hook hook, String[] args, PrintStream outRedirect,
+                       PrintStream errRedirect, String stdinArgs)
+                       throws JGitInternalException {
+               final File hookFile = findHook(repository, hook);
+               if (hookFile == null)
+                       return new ProcessResult(Status.NOT_PRESENT);
+
+               final String hookPath = hookFile.getAbsolutePath();
+               final File runDirectory;
+               if (repository.isBare())
+                       runDirectory = repository.getDirectory();
+               else
+                       runDirectory = repository.getWorkTree();
+               final String cmd = relativize(runDirectory.getAbsolutePath(),
+                               hookPath);
+               ProcessBuilder hookProcess = runInShell(cmd, args);
+               hookProcess.directory(runDirectory);
+               try {
+                       return new ProcessResult(runProcess(hookProcess, outRedirect,
+                                       errRedirect, stdinArgs), Status.OK);
+               } catch (IOException e) {
+                       throw new JGitInternalException(MessageFormat.format(
+                                       JGitText.get().exceptionCaughtDuringExecutionOfHook,
+                                       hook.getName()), e);
+               } catch (InterruptedException e) {
+                       throw new JGitInternalException(MessageFormat.format(
+                                       JGitText.get().exceptionHookExecutionInterrupted,
+                                       hook.getName()), e);
+               }
+       }
+
+
+       /**
+        * Tries to find a hook matching the given one in the given repository.
+        *
+        * @param repository
+        *            The repository within which to find a hook.
+        * @param hook
+        *            The hook we're trying to find.
+        * @return The {@link File} containing this particular hook if it exists in
+        *         the given repository, <code>null</code> otherwise.
+        * @since 3.7
+        */
+       public File findHook(Repository repository, final Hook hook) {
+               final File hookFile = new File(new File(repository.getDirectory(),
+                               Constants.HOOKS), hook.getName());
+               return hookFile.isFile() ? hookFile : null;
+       }
+
+       /**
+        * Runs the given process until termination, clearing its stdout and stderr
+        * streams on-the-fly.
+        *
+        * @param hookProcessBuilder
+        *            The process builder configured for this hook.
+        * @param outRedirect
+        *            A print stream on which to redirect the hook's stdout. Can be
+        *            <code>null</code>, in which case the hook's standard output
+        *            will be lost.
+        * @param errRedirect
+        *            A print stream on which to redirect the hook's stderr. Can be
+        *            <code>null</code>, in which case the hook's standard error
+        *            will be lost.
+        * @param stdinArgs
+        *            A string to pass on to the standard input of the hook. Can be
+        *            <code>null</code>.
+        * @return the exit value of this hook.
+        * @throws IOException
+        *             if an I/O error occurs while executing this hook.
+        * @throws InterruptedException
+        *             if the current thread is interrupted while waiting for the
+        *             process to end.
+        * @since 3.7
+        */
+       protected int runProcess(ProcessBuilder hookProcessBuilder,
+                       OutputStream outRedirect, OutputStream errRedirect, String stdinArgs)
+                       throws IOException, InterruptedException {
+               final ExecutorService executor = Executors.newFixedThreadPool(2);
+               Process process = null;
+               // We'll record the first I/O exception that occurs, but keep on trying
+               // to dispose of our open streams and file handles
+               IOException ioException = null;
+               try {
+                       process = hookProcessBuilder.start();
+                       final Callable<Void> errorGobbler = new StreamGobbler(
+                                       process.getErrorStream(), errRedirect);
+                       final Callable<Void> outputGobbler = new StreamGobbler(
+                                       process.getInputStream(), outRedirect);
+                       executor.submit(errorGobbler);
+                       executor.submit(outputGobbler);
+                       if (stdinArgs != null) {
+                               final PrintWriter stdinWriter = new PrintWriter(
+                                               process.getOutputStream());
+                               stdinWriter.print(stdinArgs);
+                               stdinWriter.flush();
+                               // We are done with this hook's input. Explicitly close its
+                               // stdin now to kick off any blocking read the hook might have.
+                               stdinWriter.close();
+                       }
+                       return process.waitFor();
+               } catch (IOException e) {
+                       ioException = e;
+               } finally {
+                       shutdownAndAwaitTermination(executor);
+                       if (process != null) {
+                               try {
+                                       process.waitFor();
+                               } catch (InterruptedException e) {
+                                       // Thrown by the outer try.
+                                       // Swallow this one to carry on our cleanup, and clear the
+                                       // interrupted flag (processes throw the exception without
+                                       // clearing the flag).
+                                       Thread.interrupted();
+                               }
+                               // A process doesn't clean its own resources even when destroyed
+                               // Explicitly try and close all three streams, preserving the
+                               // outer I/O exception if any.
+                               try {
+                                       process.getErrorStream().close();
+                               } catch (IOException e) {
+                                       ioException = ioException != null ? ioException : e;
+                               }
+                               try {
+                                       process.getInputStream().close();
+                               } catch (IOException e) {
+                                       ioException = ioException != null ? ioException : e;
+                               }
+                               try {
+                                       process.getOutputStream().close();
+                               } catch (IOException e) {
+                                       ioException = ioException != null ? ioException : e;
+                               }
+                               process.destroy();
+                       }
+               }
+               // We can only be here if the outer try threw an IOException.
+               throw ioException;
+       }
+
+       /**
+        * Shuts down an {@link ExecutorService} in two phases, first by calling
+        * {@link ExecutorService#shutdown() shutdown} to reject incoming tasks, and
+        * then calling {@link ExecutorService#shutdownNow() shutdownNow}, if
+        * necessary, to cancel any lingering tasks. Returns true if the pool has
+        * been properly shutdown, false otherwise.
+        * <p>
+        *
+        * @param pool
+        *            the pool to shutdown
+        * @return <code>true</code> if the pool has been properly shutdown,
+        *         <code>false</code> otherwise.
+        */
+       private static boolean shutdownAndAwaitTermination(ExecutorService pool) {
+               boolean hasShutdown = true;
+               pool.shutdown(); // Disable new tasks from being submitted
+               try {
+                       // Wait a while for existing tasks to terminate
+                       if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
+                               pool.shutdownNow(); // Cancel currently executing tasks
+                               // Wait a while for tasks to respond to being canceled
+                               if (!pool.awaitTermination(5, TimeUnit.SECONDS))
+                                       hasShutdown = false;
+                       }
+               } catch (InterruptedException ie) {
+                       // (Re-)Cancel if current thread also interrupted
+                       pool.shutdownNow();
+                       // Preserve interrupt status
+                       Thread.currentThread().interrupt();
+                       hasShutdown = false;
+               }
+               return hasShutdown;
+       }
+
        /**
         * Initialize a ProcesssBuilder to run a command using the system shell.
         *
@@ -802,4 +1097,50 @@ public abstract class FS {
        public String normalize(String name) {
                return name;
        }
+
+       /**
+        * This runnable will consume an input stream's content into an output
+        * stream as soon as it gets available.
+        * <p>
+        * Typically used to empty processes' standard output and error, preventing
+        * them to choke.
+        * </p>
+        * <p>
+        * <b>Note</b> that a {@link StreamGobbler} will never close either of its
+        * streams.
+        * </p>
+        */
+       private static class StreamGobbler implements Callable<Void> {
+               private final BufferedReader reader;
+
+               private final BufferedWriter writer;
+
+               public StreamGobbler(InputStream stream, OutputStream output) {
+                       this.reader = new BufferedReader(new InputStreamReader(stream));
+                       if (output == null)
+                               this.writer = null;
+                       else
+                               this.writer = new BufferedWriter(new OutputStreamWriter(output));
+               }
+
+               public Void call() throws IOException {
+                       boolean writeFailure = false;
+
+                       String line = null;
+                       while ((line = reader.readLine()) != null) {
+                               // Do not try to write again after a failure, but keep reading
+                               // as long as possible to prevent the input stream from choking.
+                               if (!writeFailure && writer != null) {
+                                       try {
+                                               writer.write(line);
+                                               writer.newLine();
+                                               writer.flush();
+                                       } catch (IOException e) {
+                                               writeFailure = true;
+                                       }
+                               }
+                       }
+                       return null;
+               }
+       }
 }
index b7de056ee44d3bbcf5ffa13f0b733f6beb955939..ee29584239382bf45e4debf1e6d8f9c04cb06ef9 100644 (file)
@@ -44,11 +44,14 @@ package org.eclipse.jgit.util;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.PrintStream;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.lib.Repository;
 
 /**
  * Base FS for POSIX based systems
@@ -121,4 +124,15 @@ public abstract class FS_POSIX extends FS {
                proc.command(argv);
                return proc;
        }
+
+       /**
+        * @since 3.7
+        */
+       @Override
+       public ProcessResult runIfPresent(Repository repository, Hook hook,
+                       String[] args, PrintStream outRedirect, PrintStream errRedirect,
+                       String stdinArgs) throws JGitInternalException {
+               return internalRunIfPresent(repository, hook, args, outRedirect,
+                               errRedirect, stdinArgs);
+       }
 }
index 81f2001125b2377d3da569d1510e390cf458e641..d0abd3327547a0e0a87b53f450f0f743b28ae1f6 100644 (file)
 package org.eclipse.jgit.util;
 
 import java.io.File;
+import java.io.PrintStream;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.lib.Repository;
 
 /**
  * FS implementation for Cygwin on Windows
@@ -135,4 +138,24 @@ public class FS_Win32_Cygwin extends FS_Win32 {
                proc.command(argv);
                return proc;
        }
+
+       /**
+        * @since 3.7
+        */
+       @Override
+       public String relativize(String base, String other) {
+               final String relativized = super.relativize(base, other);
+               return relativized.replace(File.separatorChar, '/');
+       }
+
+       /**
+        * @since 3.7
+        */
+       @Override
+       public ProcessResult runIfPresent(Repository repository, Hook hook,
+                       String[] args, PrintStream outRedirect, PrintStream errRedirect,
+                       String stdinArgs) throws JGitInternalException {
+               return internalRunIfPresent(repository, hook, args, outRedirect,
+                               errRedirect, stdinArgs);
+       }
 }
index 9e5b22180b3f92b6f52a828a4af2682e48c9e709..1e58245ea7548406d9de0610d47a96d9734716bf 100644 (file)
@@ -51,6 +51,7 @@ import java.nio.channels.FileLock;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.regex.Pattern;
 
 import org.eclipse.jgit.internal.JGitText;
 
@@ -387,4 +388,69 @@ public class FileUtils {
                }
                throw new IOException(JGitText.get().cannotCreateTempDir);
        }
+
+       /**
+        * This will try and make a given path relative to another.
+        * <p>
+        * For example, if this is called with the two following paths :
+        *
+        * <pre>
+        * <code>base = "c:\\Users\\jdoe\\eclipse\\git\\project"</code>
+        * <code>other = "c:\\Users\\jdoe\\eclipse\\git\\another_project\\pom.xml"</code>
+        * </pre>
+        *
+        * This will return "..\\another_project\\pom.xml".
+        * </p>
+        * <p>
+        * This method uses {@link File#separator} to split the paths into segments.
+        * </p>
+        * <p>
+        * <b>Note</b> that this will return the empty String if <code>base</code>
+        * and <code>other</code> are equal.
+        * </p>
+        *
+        * @param base
+        *            The path against which <code>other</code> should be
+        *            relativized. This will be assumed to denote the path to a
+        *            folder and not a file.
+        * @param other
+        *            The path that will be made relative to <code>base</code>.
+        * @return A relative path that, when resolved against <code>base</code>,
+        *         will yield the original <code>other</code>.
+        * @since 3.7
+        */
+       public static String relativize(String base, String other) {
+               if (base.equals(other))
+                       return ""; //$NON-NLS-1$
+
+               final boolean ignoreCase = !FS.DETECTED.isCaseSensitive();
+               final String[] baseSegments = base.split(Pattern.quote(File.separator));
+               final String[] otherSegments = other.split(Pattern
+                               .quote(File.separator));
+
+               int commonPrefix = 0;
+               while (commonPrefix < baseSegments.length
+                               && commonPrefix < otherSegments.length) {
+                       if (ignoreCase
+                                       && baseSegments[commonPrefix]
+                                                       .equalsIgnoreCase(otherSegments[commonPrefix]))
+                               commonPrefix++;
+                       else if (!ignoreCase
+                                       && baseSegments[commonPrefix]
+                                                       .equals(otherSegments[commonPrefix]))
+                               commonPrefix++;
+                       else
+                               break;
+               }
+
+               final StringBuilder builder = new StringBuilder();
+               for (int i = commonPrefix; i < baseSegments.length; i++)
+                       builder.append("..").append(File.separator); //$NON-NLS-1$
+               for (int i = commonPrefix; i < otherSegments.length; i++) {
+                       builder.append(otherSegments[i]);
+                       if (i < otherSegments.length - 1)
+                               builder.append(File.separator);
+               }
+               return builder.toString();
+       }
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/Hook.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/Hook.java
new file mode 100644 (file)
index 0000000..c24c9a3
--- /dev/null
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2014 Obeo.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.util;
+
+/**
+ * An enum describing the different hooks a user can implement to customize his
+ * repositories.
+ *
+ * @since 3.7
+ */
+public enum Hook {
+       /**
+        * Literal for the "pre-commit" git hook.
+        * <p>
+        * This hook is invoked by git commit, and can be bypassed with the
+        * "no-verify" option. It takes no parameter, and is invoked before
+        * obtaining the proposed commit log message and making a commit.
+        * </p>
+        * <p>
+        * A non-zero exit code from the called hook means that the commit should be
+        * aborted.
+        * </p>
+        */
+       PRE_COMMIT("pre-commit"), //$NON-NLS-1$
+
+       /**
+        * Literal for the "prepare-commit-msg" git hook.
+        * <p>
+        * This hook is invoked by git commit right after preparing the default
+        * message, and before any editing possibility is displayed to the user.
+        * </p>
+        * <p>
+        * A non-zero exit code from the called hook means that the commit should be
+        * aborted.
+        * </p>
+        */
+       PREPARE_COMMIT_MSG("prepare-commit-msg"), //$NON-NLS-1$
+
+       /**
+        * Literal for the "commit-msg" git hook.
+        * <p>
+        * This hook is invoked by git commit, and can be bypassed with the
+        * "no-verify" option. Its single parameter is the path to the file
+        * containing the prepared commit message (typically
+        * "&lt;gitdir>/COMMIT-EDITMSG").
+        * </p>
+        * <p>
+        * A non-zero exit code from the called hook means that the commit should be
+        * aborted.
+        * </p>
+        */
+       COMMIT_MSG("commit-msg"), //$NON-NLS-1$
+
+       /**
+        * Literal for the "post-commit" git hook.
+        * <p>
+        * This hook is invoked by git commit. It takes no parameter and is invoked
+        * after a commit has been made.
+        * </p>
+        * <p>
+        * The exit code of this hook has no significance.
+        * </p>
+        */
+       POST_COMMIT("post-commit"), //$NON-NLS-1$
+
+       /**
+        * Literal for the "post-rewrite" git hook.
+        * <p>
+        * This hook is invoked after commands that rewrite commits (currently, only
+        * "git rebase" and "git commit --amend"). It a single argument denoting the
+        * source of the call (one of <code>rebase</code> or <code>amend</code>). It
+        * then accepts a list of rewritten commits through stdin, in the form
+        * <code>&lt;old SHA-1> &lt;new SHA-1>LF</code>.
+        * </p>
+        * <p>
+        * The exit code of this hook has no significance.
+        * </p>
+        */
+       POST_REWRITE("post-rewrite"), //$NON-NLS-1$
+
+       /**
+        * Literal for the "pre-rebase" git hook.
+        * <p>
+        * </p>
+        * This hook is invoked right before the rebase operation runs. It accepts
+        * up to two parameters, the first being the upstream from which the branch
+        * to rebase has been forked. If the tip of the series of commits to rebase
+        * is HEAD, the other parameter is unset. Otherwise, that tip is passed as
+        * the second parameter of the script.
+        * <p>
+        * A non-zero exit code from the called hook means that the rebase should be
+        * aborted.
+        * </p>
+        */
+       PRE_REBASE("pre-rebase"); //$NON-NLS-1$
+
+       private final String name;
+
+       private Hook(String name) {
+               this.name = name;
+       }
+
+       /**
+        * @return The name of this hook.
+        */
+       public String getName() {
+               return name;
+       }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/ProcessResult.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/ProcessResult.java
new file mode 100644 (file)
index 0000000..f56bb15
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2014 Obeo.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.util;
+
+/**
+ * Describes the result of running an external process.
+ *
+ * @since 3.7
+ */
+public class ProcessResult {
+       /**
+        * Status of a process' execution.
+        */
+       public static enum Status {
+               /**
+                * The script was found and launched properly. It may still have exited
+                * with a non-zero {@link #exitCode}.
+                */
+               OK,
+
+               /** The script was not found on disk and thus could not be launched. */
+               NOT_PRESENT,
+
+               /**
+                * The script was found but could not be launched since it was not
+                * supported by the current {@link FS}.
+                */
+               NOT_SUPPORTED;
+       }
+
+       /** The exit code of the process. */
+       private final int exitCode;
+
+       /** Status of the process' execution. */
+       private final Status status;
+
+       /**
+        * Instantiates a process result with the given status and an exit code of
+        * <code>-1</code>.
+        *
+        * @param status
+        *            Status describing the execution of the external process.
+        */
+       public ProcessResult(Status status) {
+               this(-1, status);
+       }
+
+       /**
+        * @param exitCode
+        *            Exit code of the process.
+        * @param status
+        *            Status describing the execution of the external process.
+        */
+       public ProcessResult(int exitCode, Status status) {
+               this.exitCode = exitCode;
+               this.status = status;
+       }
+
+       /**
+        * @return The exit code of the process.
+        */
+       public int getExitCode() {
+               return exitCode;
+       }
+
+       /**
+        * @return The status of the process' execution.
+        */
+       public Status getStatus() {
+               return status;
+       }
+}