/*
* ====================================================================
* Copyright (c) 2004-2012 TMate Software Ltd. All rights reserved.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at http://svnkit.com/license.html
* If newer versions of this license are posted there, you may use a
* newer version instead, at your option.
* ====================================================================
*/
package org.tmatesoft.svn.core.internal.io.fs;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.List;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.internal.util.SVNStreamGobbler;
import org.tmatesoft.svn.core.internal.wc.SVNErrorManager;
import org.tmatesoft.svn.core.internal.wc.SVNFileType;
import org.tmatesoft.svn.core.internal.wc.SVNFileUtil;
import org.tmatesoft.svn.util.SVNLogType;
/**
* @version 1.3
* @author TMate Software Ltd.
*/
public class FSHooks {
public static final String SVN_REPOS_HOOK_START_COMMIT = "start-commit";
public static final String SVN_REPOS_HOOK_PRE_COMMIT = "pre-commit";
public static final String SVN_REPOS_HOOK_POST_COMMIT = "post-commit";
public static final String SVN_REPOS_HOOK_PRE_REVPROP_CHANGE = "pre-revprop-change";
public static final String SVN_REPOS_HOOK_POST_REVPROP_CHANGE = "post-revprop-change";
public static final String SVN_REPOS_HOOK_PRE_LOCK = "pre-lock";
public static final String SVN_REPOS_HOOK_POST_LOCK = "post-lock";
public static final String SVN_REPOS_HOOK_PRE_UNLOCK = "pre-unlock";
public static final String SVN_REPOS_HOOK_POST_UNLOCK = "post-unlock";
public static final String SVN_REPOS_HOOK_READ_SENTINEL = "read-sentinels";
public static final String SVN_REPOS_HOOK_WRITE_SENTINEL = "write-sentinels";
public static final String SVN_REPOS_HOOK_DESC_EXT = ".tmpl";
public static final String SVN_REPOS_HOOKS_DIR = "hooks";
public static final String REVPROP_DELETE = "D";
public static final String REVPROP_ADD = "A";
public static final String REVPROP_MODIFY = "M";
private static final String[] winExtensions = {
".exe", ".bat", ".cmd"
};
private static Boolean ourIsHooksEnabled;
public static void setHooksEnabled(boolean enabled) {
ourIsHooksEnabled = enabled ? Boolean.TRUE : Boolean.FALSE;
}
public static boolean isHooksEnabled() {
if (ourIsHooksEnabled == null) {
ourIsHooksEnabled = Boolean.valueOf(System.getProperty("svnkit.hooksEnabled", System.getProperty("javasvn.hooksEnabled", "true")));
}
return ourIsHooksEnabled.booleanValue();
}
public static String runPreLockHook(File reposRootDir, String path, String username, String comment, boolean stealLock) throws SVNException {
username = username == null ? "" : username;
path = path == null ? "" : path;
return runHook(reposRootDir, SVN_REPOS_HOOK_PRE_LOCK, new String[] {path, username, comment != null ? comment : "", stealLock ? "1" : "0"}, null);
}
public static void runPostLockHook(File reposRootDir, String[] paths, String username) throws SVNException {
StringBuffer pathsStr = new StringBuffer();
for (int i = 0; i < paths.length; i++) {
pathsStr.append(paths[i]);
pathsStr.append("\n");
}
runLockHook(reposRootDir, SVN_REPOS_HOOK_POST_LOCK, null, username, pathsStr.toString());
}
public static void runPreUnlockHook(File reposRootDir, String path, String username) throws SVNException {
runLockHook(reposRootDir, SVN_REPOS_HOOK_PRE_UNLOCK, path, username, null);
}
public static void runPostUnlockHook(File reposRootDir, String[] paths, String username) throws SVNException {
StringBuffer pathsStr = new StringBuffer();
for (int i = 0; i < paths.length; i++) {
pathsStr.append(paths[i]);
pathsStr.append("\n");
}
runLockHook(reposRootDir, SVN_REPOS_HOOK_POST_UNLOCK, null, username, pathsStr.toString());
}
private static void runLockHook(File reposRootDir, String hookName, String path, String username, String paths) throws SVNException {
username = username == null ? "" : username;
path = path == null ? "" : path;
byte[] bytes = null;
try {
bytes = paths == null ? null : paths.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
bytes = paths.getBytes();
}
runHook(reposRootDir, hookName, new String[] {path, username}, bytes);
}
public static void runPreRevPropChangeHook(File reposRootDir, String propName, byte[] propNewValue, String author, long revision, String action) throws SVNException {
runChangeRevPropHook(reposRootDir, SVN_REPOS_HOOK_PRE_REVPROP_CHANGE, propName, propNewValue, author, revision, action, true);
}
public static void runPostRevPropChangeHook(File reposRootDir, String propName, byte[] propOldValue, String author, long revision, String action) throws SVNException {
runChangeRevPropHook(reposRootDir, SVN_REPOS_HOOK_POST_REVPROP_CHANGE, propName, propOldValue, author, revision, action, false);
}
private static void runChangeRevPropHook(File reposRootDir, String hookName, String propName, byte[] propValue, String author, long revision, String action, boolean isPre) throws SVNException {
File hookFile = getHookFile(reposRootDir, hookName);
if (hookFile == null) {
if (isPre) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_DISABLED_FEATURE,
"Repository has not been enabled to accept revision propchanges;\nask the administrator to create a pre-revprop-change hook");
SVNErrorManager.error(err, SVNLogType.FSFS);
}
return;
}
author = author == null ? "" : author;
runHook(reposRootDir, hookName, new String[] {String.valueOf(revision), author, propName, action}, propValue);
}
public static void runStartCommitHook(File reposRootDir, String author, List<?> capabilities) throws SVNException {
author = author == null ? "" : author;
String capsString = getCapabilitiesAsString(capabilities);
String[] args = capsString == null ? new String[] { author } : new String[] { author, capsString };
runHook(reposRootDir, SVN_REPOS_HOOK_START_COMMIT, args, null);
}
public static void runPreCommitHook(File reposRootDir, String txnName) throws SVNException {
runHook(reposRootDir, SVN_REPOS_HOOK_PRE_COMMIT, new String[] {txnName}, null);
}
public static void runPostCommitHook(File reposRootDir, long committedRevision) throws SVNException {
runHook(reposRootDir, SVN_REPOS_HOOK_POST_COMMIT, new String[] {String.valueOf(committedRevision)}, null);
}
private static String runHook(File reposRootDir, String hookName, String[] args, byte[] input) throws SVNException {
File hookFile = getHookFile(reposRootDir, hookName);
if (hookFile == null) {
return null;
}
if (args == null) {
args = new String[0];
}
Process hookProc = null;
String reposPath = reposRootDir.getAbsolutePath().replace(File.separatorChar, '/');
String executableName = hookFile.getName().toLowerCase();
boolean useCmd = (executableName.endsWith(".bat") || executableName.endsWith(".cmd")) && SVNFileUtil.isWindows;
String[] cmd = useCmd ? new String[4 + args.length] : new String[2 + args.length];
if (useCmd) {
cmd = new String[] {"cmd", "/C", ""};
cmd[2] = "\"" + "\"" + hookFile.getAbsolutePath() + "\" \"" + reposPath + "\"";
for (int i = 0; i < args.length; i++) {
cmd[2] += " \"" + args[i] + "\"";
}
cmd[2] += "\"";
} else {
int i = 0;
if (useCmd) {
cmd[0] = "cmd";
cmd[1] = "/C";
i = 2;
}
cmd[i] = hookFile.getAbsolutePath();
i++;
cmd[i] = reposPath;
i++;
for(int j = 0; j < args.length; j++) {
cmd[i + j] = args[j];
}
}
try {
hookProc = Runtime.getRuntime().exec(cmd);
} catch (IOException ioe) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_HOOK_FAILURE, "Failed to start ''{0}'' hook: {1}", new Object[] {
hookFile, ioe.getLocalizedMessage()
});
SVNErrorManager.error(err, ioe, SVNLogType.FSFS);
}
return feedHook(hookFile, hookName, hookProc, input);
}
private static String feedHook(File hook, String hookName, Process hookProcess, byte[] stdInValue) throws SVNException {
if (hookProcess == null) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_HOOK_FAILURE, "Failed to start ''{0}'' hook", hook);
SVNErrorManager.error(err, SVNLogType.FSFS);
}
SVNStreamGobbler inputGobbler = new SVNStreamGobbler(hookProcess.getInputStream());
SVNStreamGobbler errorGobbler = new SVNStreamGobbler(hookProcess.getErrorStream());
inputGobbler.start();
errorGobbler.start();
if (stdInValue != null) {
OutputStream osToStdIn = hookProcess.getOutputStream();
try {
for (int i = 0; i < stdInValue.length; i += 1024) {
osToStdIn.write(stdInValue, i, Math.min(1024, stdInValue.length - i));
osToStdIn.flush();
}
} catch (IOException ioe) {
//
} finally {
SVNFileUtil.closeFile(osToStdIn);
}
}
int rc = -1;
try {
inputGobbler.waitFor();
errorGobbler.waitFor();
rc = hookProcess.waitFor();
} catch (InterruptedException ie) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_HOOK_FAILURE, "Failed to start ''{0}'' hook: {1}", new Object[] {
hook, ie.getLocalizedMessage()
});
SVNErrorManager.error(err, ie, SVNLogType.FSFS);
} finally {
errorGobbler.close();
inputGobbler.close();
hookProcess.destroy();
}
if (rc == 0 ) {
if (errorGobbler.getError() != null) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_HOOK_FAILURE, "''{0}'' hook succeeded, but error output could not be read", hookName);
SVNErrorManager.error(err, errorGobbler.getError(), SVNLogType.FSFS);
}
return inputGobbler.getResult();
} else {
String actionName = null;
if (SVN_REPOS_HOOK_START_COMMIT.equals(hookName) || SVN_REPOS_HOOK_PRE_COMMIT.equals(hookName)) {
actionName = "Commit";
} else if (SVN_REPOS_HOOK_PRE_REVPROP_CHANGE.equals(hookName)) {
actionName = "Revprop change";
} else if (SVN_REPOS_HOOK_PRE_LOCK.equals(hookName)) {
actionName = "Lock";
} else if (SVN_REPOS_HOOK_PRE_UNLOCK.equals(hookName)) {
actionName = "Unlock";
}
String stdErrMessage = errorGobbler.getError() != null ? "[Error output could not be read.]" : errorGobbler.getResult();
String errorMessage = actionName != null ?
actionName + " blocked by {0} hook (exit code {1})" : "{0} hook failed (exit code {1})";
if (stdErrMessage != null && stdErrMessage.length() > 0) {
errorMessage += " with output:\n{2}";
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_HOOK_FAILURE, errorMessage, new Object[] {hookName, new Integer(rc), stdErrMessage});
SVNErrorManager.error(err, SVNLogType.FSFS);
} else {
errorMessage += " with no output.";
}
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_HOOK_FAILURE, errorMessage, new Object[] {hookName, new Integer(rc)});
SVNErrorManager.error(err, SVNLogType.FSFS);
}
return null;
}
private static File getHookFile(File reposRootDir, String hookName) throws SVNException {
if (!isHooksEnabled()) {
return null;
}
File hookFile = null;
if (SVNFileUtil.isWindows) {
for (int i = 0; i < winExtensions.length; i++) {
hookFile = new File(getHooksDir(reposRootDir), hookName + winExtensions[i]);
SVNFileType type = SVNFileType.getType(hookFile);
if (type == SVNFileType.FILE) {
return hookFile;
}
}
} else {
hookFile = new File(getHooksDir(reposRootDir), hookName);
SVNFileType type = SVNFileType.getType(hookFile);
if (type == SVNFileType.FILE) {
return hookFile;
} else if (type == SVNFileType.SYMLINK) {
// should first resolve the symlink and then decide if it's
// broken and
// throw an exception
File realFile = SVNFileUtil.resolveSymlinkToFile(hookFile);
if (realFile == null) {
SVNErrorMessage err = SVNErrorMessage.create(SVNErrorCode.REPOS_HOOK_FAILURE, "Failed to run ''{0}'' hook; broken symlink", hookFile);
SVNErrorManager.error(err, SVNLogType.FSFS);
}
return hookFile;
}
}
return null;
}
private static File getHooksDir(File reposRootDir) {
return new File(reposRootDir, SVN_REPOS_HOOKS_DIR);
}
private static String getCapabilitiesAsString(List<?> capabilities) {
if (capabilities == null || capabilities.isEmpty()) {
return "";
}
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < capabilities.size(); i++) {
Object cap = capabilities.get(i);
buffer.append(cap);
if (i < capabilities.size() - 1) {
buffer.append(":");
}
}
return buffer.toString();
}
}