/**
* This file is part of git-as-svn. It is subject to the license terms
* in the LICENSE file found in the top-level directory of this distribution
* and at http://www.gnu.org/licenses/gpl-2.0.html. No part of git-as-svn,
* including this file, may be copied, modified, propagated, or distributed
* except according to the terms contained in the LICENSE file.
*/
package svnserver.repository.git.push;
import com.google.common.io.CharStreams;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tmatesoft.svn.core.SVNErrorCode;
import org.tmatesoft.svn.core.SVNErrorMessage;
import org.tmatesoft.svn.core.SVNException;
import svnserver.auth.User;
import svnserver.auth.UserDB;
import svnserver.config.ConfigHelper;
import svnserver.context.LocalContext;
import svnserver.context.SharedContext;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/**
* Git push mode.
*
* @author Artem V. Navrotskiy <bozaro@users.noreply.github.com>
*/
public class GitPushEmbedded implements GitPusher {
@NotNull
private static final Logger log = LoggerFactory.getLogger(GitPushEmbedded.class);
@NotNull
private final SharedContext context;
@Nullable
private final String preReceive;
@Nullable
private final String postReceive;
@Nullable
private final String update;
@FunctionalInterface
public interface HookRunner {
@NotNull
Process exec(@NotNull ProcessBuilder processBuilder) throws IOException;
}
public GitPushEmbedded(@NotNull LocalContext context, @Nullable String preReceive, @Nullable String postReceive, @Nullable String update) {
this.context = context.getShared();
this.preReceive = preReceive;
this.postReceive = postReceive;
this.update = update;
}
@Override
public boolean push(@NotNull Repository repository, @NotNull ObjectId ReceiveId, @NotNull String branch, @NotNull User userInfo) throws SVNException, IOException {
final RefUpdate refUpdate = repository.updateRef(branch);
refUpdate.getOldObjectId();
refUpdate.setNewObjectId(ReceiveId);
runReceiveHook(repository, refUpdate, preReceive, userInfo);
runUpdateHook(repository, refUpdate, update, userInfo);
final RefUpdate.Result result = refUpdate.update();
switch (result) {
case REJECTED:
return false;
case NEW:
case FAST_FORWARD:
runReceiveHook(repository, refUpdate, postReceive, userInfo);
return true;
default:
log.error("Unexpected push error: {}", result);
throw new SVNException(SVNErrorMessage.create(SVNErrorCode.IO_WRITE_ERROR, result.name()));
}
}
private void runReceiveHook(@NotNull Repository repository, @NotNull RefUpdate refUpdate, @NotNull String hook, @NotNull User userInfo) throws IOException, SVNException {
runHook(repository, hook, userInfo, processBuilder -> {
final Process process = processBuilder.start();
try (Writer stdin = new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8)) {
stdin.write(getObjectId(refUpdate.getOldObjectId()));
stdin.write(' ');
stdin.write(getObjectId(refUpdate.getNewObjectId()));
stdin.write(' ');
stdin.write(refUpdate.getName());
stdin.write('\n');
}
return process;
});
}
private void runUpdateHook(@NotNull Repository repository, @NotNull RefUpdate refUpdate, @Nullable String hook, @NotNull User userInfo) throws IOException, SVNException {
runHook(repository, hook, userInfo, processBuilder -> {
processBuilder.command().addAll(Arrays.asList(
refUpdate.getName(),
getObjectId(refUpdate.getOldObjectId()),
getObjectId(refUpdate.getNewObjectId())
));
return processBuilder.start();
});
}
private void runHook(@NotNull Repository repository, @Nullable String hook, @NotNull User userInfo, @NotNull HookRunner runner) throws IOException, SVNException {
if (hook == null || hook.isEmpty()) {
return;
}
final File script = ConfigHelper.joinPath(ConfigHelper.joinPath(repository.getDirectory(), "hooks"), hook);
if (script.isFile()) {
try {
final ProcessBuilder processBuilder = new ProcessBuilder(script.getAbsolutePath())
.directory(repository.getDirectory())
.redirectErrorStream(true);
processBuilder.environment().put("LANG", "en_US.utf8");
userInfo.updateEnvironment(processBuilder.environment());
context.sure(UserDB.class).updateEnvironment(processBuilder.environment(), userInfo);
final Process process = runner.exec(processBuilder);
final String hookMessage = CharStreams.toString(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new SVNException(SVNErrorMessage.create(SVNErrorCode.REPOS_HOOK_FAILURE, "Commit blocked by hook with output:\n" + hookMessage));
}
} catch (InterruptedException e) {
log.error("Hook interrupted: " + script.getAbsolutePath(), e);
throw new SVNException(SVNErrorMessage.create(SVNErrorCode.IO_WRITE_ERROR, e));
}
}
}
@NotNull
private static String getObjectId(@Nullable ObjectId objectId) {
return objectId == null ? ObjectId.zeroId().getName() : objectId.getName();
}
}