From 6aed51e3cea6f7114b42bcbc4f66abb48e6ae490 Mon Sep 17 00:00:00 2001 From: Laurent Goubet Date: Fri, 31 Oct 2014 14:58:07 +0100 Subject: [PATCH] Introduce hook support into the FS implementations 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 Signed-off-by: Matthias Sohn --- .../org/eclipse/jgit/util/FS_POSIX_Java7.java | 16 + .../jgit/util/FS_Win32_Java7Cygwin.java | 18 + .../org/eclipse/jgit/util/FileUtilTest.java | 79 ++++ .../eclipse/jgit/internal/JGitText.properties | 2 + .../org/eclipse/jgit/internal/JGitText.java | 2 + .../src/org/eclipse/jgit/lib/Constants.java | 7 + .../src/org/eclipse/jgit/util/FS.java | 341 ++++++++++++++++++ .../src/org/eclipse/jgit/util/FS_POSIX.java | 14 + .../eclipse/jgit/util/FS_Win32_Cygwin.java | 23 ++ .../src/org/eclipse/jgit/util/FileUtils.java | 66 ++++ .../src/org/eclipse/jgit/util/Hook.java | 149 ++++++++ .../org/eclipse/jgit/util/ProcessResult.java | 112 ++++++ 12 files changed, 829 insertions(+) create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/util/Hook.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/util/ProcessResult.java diff --git a/org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_POSIX_Java7.java b/org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_POSIX_Java7.java index 4a73a9bcf..300cf93bc 100644 --- a/org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_POSIX_Java7.java +++ b/org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_POSIX_Java7.java @@ -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; + } } diff --git a/org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_Win32_Java7Cygwin.java b/org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_Win32_Java7Cygwin.java index e40d7cf0b..b6e5d9388 100644 --- a/org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_Win32_Java7Cygwin.java +++ b/org.eclipse.jgit.java7/src/org/eclipse/jgit/util/FS_Win32_Java7Cygwin.java @@ -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; + } } diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java index 7b1627854..d4be25c0c 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/FileUtilTest.java @@ -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)); + } } diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 7e5f0b022..71ec6b2bc 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -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 diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index caf3e9072..138f40f7c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -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; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java index 705d54cfa..ed0ed04d9 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Constants.java @@ -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. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java index 0c63b190f..081bf87c5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS.java @@ -44,18 +44,31 @@ 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 other should be + * relativized. + * @param other + * The path that will be made relative to base. + * @return A relative path that, when resolved against base, + * will yield the original other. + * @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. + *

+ * The hook's standard output and error streams will be redirected to + * System.out and System.err respectively. The + * hook will have no stdin. + *

+ * + * @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 null, + * 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 null, + * but can be an empty array. + * @param outRedirect + * A print stream on which to redirect the hook's stdout. Can be + * null, 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 + * null, 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 + * null. + * @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 null, + * but can be an empty array. + * @param outRedirect + * A print stream on which to redirect the hook's stdout. Can be + * null, 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 + * null, 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 + * null. + * @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, null 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 + * null, 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 + * null, 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 + * null. + * @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 errorGobbler = new StreamGobbler( + process.getErrorStream(), errRedirect); + final Callable 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. + *

+ * + * @param pool + * the pool to shutdown + * @return true if the pool has been properly shutdown, + * false 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. + *

+ * Typically used to empty processes' standard output and error, preventing + * them to choke. + *

+ *

+ * Note that a {@link StreamGobbler} will never close either of its + * streams. + *

+ */ + private static class StreamGobbler implements Callable { + 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; + } + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java index b7de056ee..ee2958423 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_POSIX.java @@ -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); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java index 81f200112..d0abd3327 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FS_Win32_Cygwin.java @@ -44,12 +44,15 @@ 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); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java index 9e5b22180..1e58245ea 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/FileUtils.java @@ -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. + *

+ * For example, if this is called with the two following paths : + * + *

+	 * base = "c:\\Users\\jdoe\\eclipse\\git\\project"
+	 * other = "c:\\Users\\jdoe\\eclipse\\git\\another_project\\pom.xml"
+	 * 
+ * + * This will return "..\\another_project\\pom.xml". + *

+ *

+ * This method uses {@link File#separator} to split the paths into segments. + *

+ *

+ * Note that this will return the empty String if base + * and other are equal. + *

+ * + * @param base + * The path against which other 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 base. + * @return A relative path that, when resolved against base, + * will yield the original other. + * @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 index 000000000..c24c9a3d0 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/Hook.java @@ -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. + *

+ * 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. + *

+ *

+ * A non-zero exit code from the called hook means that the commit should be + * aborted. + *

+ */ + PRE_COMMIT("pre-commit"), //$NON-NLS-1$ + + /** + * Literal for the "prepare-commit-msg" git hook. + *

+ * This hook is invoked by git commit right after preparing the default + * message, and before any editing possibility is displayed to the user. + *

+ *

+ * A non-zero exit code from the called hook means that the commit should be + * aborted. + *

+ */ + PREPARE_COMMIT_MSG("prepare-commit-msg"), //$NON-NLS-1$ + + /** + * Literal for the "commit-msg" git hook. + *

+ * 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 + * "<gitdir>/COMMIT-EDITMSG"). + *

+ *

+ * A non-zero exit code from the called hook means that the commit should be + * aborted. + *

+ */ + COMMIT_MSG("commit-msg"), //$NON-NLS-1$ + + /** + * Literal for the "post-commit" git hook. + *

+ * This hook is invoked by git commit. It takes no parameter and is invoked + * after a commit has been made. + *

+ *

+ * The exit code of this hook has no significance. + *

+ */ + POST_COMMIT("post-commit"), //$NON-NLS-1$ + + /** + * Literal for the "post-rewrite" git hook. + *

+ * 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 rebase or amend). It + * then accepts a list of rewritten commits through stdin, in the form + * <old SHA-1> <new SHA-1>LF. + *

+ *

+ * The exit code of this hook has no significance. + *

+ */ + POST_REWRITE("post-rewrite"), //$NON-NLS-1$ + + /** + * Literal for the "pre-rebase" git hook. + *

+ *

+ * 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. + *

+ * A non-zero exit code from the called hook means that the rebase should be + * aborted. + *

+ */ + 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 index 000000000..f56bb1577 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/ProcessResult.java @@ -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 + * -1. + * + * @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; + } +}