/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.plugin.svn.server; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.io.Files; import com.google.common.net.MediaType; import com.google.inject.Singleton; import org.eclipse.che.api.core.ApiException; import org.eclipse.che.api.core.ErrorCodes; import org.eclipse.che.api.core.ServerException; import org.eclipse.che.api.core.UnauthorizedException; import org.eclipse.che.api.core.util.LineConsumerFactory; import org.eclipse.che.api.vfs.util.DeleteOnCloseFileInputStream; import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.commons.lang.IoUtil; import org.eclipse.che.commons.lang.ZipUtils; import org.eclipse.che.dto.server.DtoFactory; import org.eclipse.che.plugin.ssh.key.script.SshScriptProvider; import org.eclipse.che.plugin.svn.server.repository.RepositoryUrlProvider; import org.eclipse.che.plugin.svn.server.upstream.CommandLineResult; import org.eclipse.che.plugin.svn.server.upstream.UpstreamUtils; import org.eclipse.che.plugin.svn.server.utils.InfoUtils; import org.eclipse.che.plugin.svn.server.utils.SshEnvironment; import org.eclipse.che.plugin.svn.server.utils.SubversionUtils; import org.eclipse.che.plugin.svn.shared.AddRequest; import org.eclipse.che.plugin.svn.shared.CLIOutputResponse; import org.eclipse.che.plugin.svn.shared.CLIOutputResponseList; import org.eclipse.che.plugin.svn.shared.CLIOutputWithRevisionResponse; import org.eclipse.che.plugin.svn.shared.CheckoutRequest; import org.eclipse.che.plugin.svn.shared.CleanupRequest; import org.eclipse.che.plugin.svn.shared.CommitRequest; import org.eclipse.che.plugin.svn.shared.CopyRequest; import org.eclipse.che.plugin.svn.shared.GetRevisionsRequest; import org.eclipse.che.plugin.svn.shared.GetRevisionsResponse; import org.eclipse.che.plugin.svn.shared.InfoRequest; import org.eclipse.che.plugin.svn.shared.InfoResponse; import org.eclipse.che.plugin.svn.shared.ListRequest; import org.eclipse.che.plugin.svn.shared.ListResponse; import org.eclipse.che.plugin.svn.shared.LockRequest; import org.eclipse.che.plugin.svn.shared.MergeRequest; import org.eclipse.che.plugin.svn.shared.MoveRequest; import org.eclipse.che.plugin.svn.shared.PropertyDeleteRequest; import org.eclipse.che.plugin.svn.shared.PropertyGetRequest; import org.eclipse.che.plugin.svn.shared.PropertyListRequest; import org.eclipse.che.plugin.svn.shared.PropertySetRequest; import org.eclipse.che.plugin.svn.shared.RemoveRequest; import org.eclipse.che.plugin.svn.shared.ResolveRequest; import org.eclipse.che.plugin.svn.shared.RevertRequest; import org.eclipse.che.plugin.svn.shared.ShowDiffRequest; import org.eclipse.che.plugin.svn.shared.ShowLogRequest; import org.eclipse.che.plugin.svn.shared.StatusRequest; import org.eclipse.che.plugin.svn.shared.SubversionItem; import org.eclipse.che.plugin.svn.shared.SwitchRequest; import org.eclipse.che.plugin.svn.shared.UpdateRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.validation.constraints.NotNull; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import static com.google.common.base.Strings.isNullOrEmpty; import static java.util.Collections.singletonList; import static org.eclipse.che.dto.server.DtoFactory.newDto; import static org.eclipse.che.plugin.svn.server.utils.InfoUtils.getRelativeUrl; import static org.eclipse.che.plugin.svn.server.utils.InfoUtils.getRepositoryRoot; import static org.eclipse.che.plugin.svn.server.utils.SubversionUtils.recognizeProjectUri; /** * Provides Subversion APIs. */ @Singleton public class SubversionApi { private static Logger LOG = LoggerFactory.getLogger(SubversionApi.class); private final RepositoryUrlProvider repositoryUrlProvider; private final SshScriptProvider sshScriptProvider; protected LineConsumerFactory svnOutputPublisherFactory; @Inject public SubversionApi(RepositoryUrlProvider repositoryUrlProvider, SshScriptProvider sshScriptProvider) { this.repositoryUrlProvider = repositoryUrlProvider; this.sshScriptProvider = sshScriptProvider; } /** * Set up std output consumer. * * @param svnOutputPublisherFactory * std output line consumer factory. */ public void setOutputLineConsumerFactory(LineConsumerFactory svnOutputPublisherFactory) { this.svnOutputPublisherFactory = svnOutputPublisherFactory; } /** * Perform an "svn add" based on the request. * * @param request * the request * @return the response * @throws IOException * if there is a problem executing the command * @throws SubversionException * if there is a Subversion issue */ public CLIOutputResponse add(final AddRequest request) throws IOException, SubversionException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); final List<String> args = defaultArgs(); // Flags addFlag(args, "--no-ignore", request.isAddIgnored()); addFlag(args, "--parents", request.isAddParents()); if (request.isAutoProps()) { args.add("--auto-props"); } if (request.isNotAutoProps()) { args.add("--no-auto-props"); } // Options addOption(args, "--depth", request.getDepth()); // Command Name args.add("add"); // Command Arguments final CommandLineResult result = runCommand(null, args, projectPath, request.getPaths()); return DtoFactory.getInstance() .createDto(CLIOutputResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()); } /** * Perform an "svn revert" based on the request. * * @param request * the request * @return the response * @throws IOException * if there is a problem executing the command * @throws SubversionException * if there is a Subversion issue */ public CLIOutputResponse revert(RevertRequest request) throws IOException, SubversionException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); final List<String> cliArgs = defaultArgs(); addOption(cliArgs, "--depth", request.getDepth()); cliArgs.add("revert"); final CommandLineResult result = runCommand(null, cliArgs, projectPath, addWorkingCopyPathIfNecessary(request.getPaths())); return DtoFactory.getInstance() .createDto(CLIOutputResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()); } /** * Perform an "svn copy" based on the request. * * @param request * the request * @return the response * @throws IOException * if there is a problem executing the command * @throws SubversionException * if there is a Subversion issue */ public CLIOutputResponse copy(final CopyRequest request) throws IOException, SubversionException, UnauthorizedException { //for security reason we should forbid file protocol if (request.getSource().startsWith("file://") || request.getDestination().startsWith("file://")) { throw new SubversionException("Url is not acceptable"); } final File projectPath = new File(request.getProjectPath()); final List<String> cliArgs = defaultArgs(); if (!isNullOrEmpty(request.getComment())) { addOption(cliArgs, "--message", "\"" + request.getComment() + "\""); } // Command Name cliArgs.add("copy"); final CommandLineResult result = runCommand(null, cliArgs, projectPath, Arrays.asList(request.getSource(), request.getDestination()), request.getUsername(), request.getPassword()); return DtoFactory.getInstance() .createDto(CLIOutputResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()); } /** * Perform a "svn switch" based on the request. * * @param request * the switch request * @return the response * @throws ApiException * if there is a Subversion issue */ public CLIOutputWithRevisionResponse doSwitch(final SwitchRequest request) throws ApiException { final File projectPath = new File(request.getProjectPath()); final List<String> cliArgs = defaultArgs(); // Flags addFlag(cliArgs, "--ignore-externals", request.isIgnoreExternals()); addFlag(cliArgs, "--ignore-ancestry", request.isIgnoreAncestry()); addFlag(cliArgs, "--relocate", request.isRelocate()); addFlag(cliArgs, "--force", request.isForce()); // Options addOption(cliArgs, "--depth", request.getDepth()); addOption(cliArgs, "--set-depth", request.getSetDepth()); addOption(cliArgs, "--revision", request.getRevision()); addOption(cliArgs, "--accept", request.getAccept()); // Command Name cliArgs.add("switch"); CommandLineResult result = runCommand(null, cliArgs, projectPath, singletonList(request.getLocation()), request.getUsername(), request.getPassword()); return newDto(CLIOutputWithRevisionResponse.class).withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()) .withRevision(SubversionUtils.getUpdateRevision(result.getStdout())); } /** * Perform an "svn checkout" based on the request. * * @param request * the request * @return the response * @throws IOException * if there is a problem executing the command * @throws SubversionException * if there is a Subversion issue */ public CLIOutputWithRevisionResponse checkout(final CheckoutRequest request) throws IOException, SubversionException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); final List<String> cliArgs = defaultArgs(); // Flags addFlag(cliArgs, "--ignore-externals", request.isIgnoreExternals()); // Options addOption(cliArgs, "--depth", request.getDepth()); addOption(cliArgs, "--revision", request.getRevision()); // Command Name cliArgs.add("checkout"); // Command Arguments cliArgs.add(request.getUrl()); cliArgs.add(projectPath.getAbsolutePath()); CommandLineResult result = runCommand(null, cliArgs, projectPath, request.getPaths(), request.getUsername(), request.getPassword(), request.getUrl()); return DtoFactory.getInstance().createDto(CLIOutputWithRevisionResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()) .withRevision(SubversionUtils.getCheckoutRevision(result.getStdout())); } /** * Perform an "svn commit" based on the request. * * @param request * the request * @return the response * @throws IOException * if there is a problem executing the command * @throws SubversionException * if there is a Subversion issue */ public CLIOutputWithRevisionResponse commit(final CommitRequest request) throws IOException, SubversionException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); final List<String> cliArgs = defaultArgs(); // Flags addFlag(cliArgs, "--keep-changelists", request.isKeepChangeLists()); addFlag(cliArgs, "--no-unlock", request.isKeepLocks()); // Command Name cliArgs.add("commit"); // Command Arguments cliArgs.add("-m"); cliArgs.add(request.getMessage()); final CommandLineResult result = runCommand(null, cliArgs, projectPath, addWorkingCopyPathIfNecessary(request.getPaths())); return DtoFactory.getInstance().createDto(CLIOutputWithRevisionResponse.class) .withCommand(result.getCommandLine().toString()) .withRevision(SubversionUtils.getCommitRevision(result.getStdout())) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()); } /** * Perform an "svn remove" based on the request. * * @param request * the request * @return the response * @throws IOException * if there is a problem executing the command * @throws SubversionException * if there is a Subversion issue */ public CLIOutputResponse remove(final RemoveRequest request) throws IOException, SubversionException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); final List<String> cliArgs = defaultArgs(); // Command Name cliArgs.add("remove"); final CommandLineResult result = runCommand(null, cliArgs, projectPath, request.getPaths()); return DtoFactory.getInstance().createDto(CLIOutputResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()); } /** * Perform an "svn status" based on the request. * * @param request * the request * @return the response * @throws IOException * if there is a problem executing the command * @throws SubversionException * if there is a Subversion issue */ public CLIOutputResponse status(final StatusRequest request) throws IOException, SubversionException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); final List<String> cliArgs = defaultArgs(); // Flags addFlag(cliArgs, "--ignore-externals", request.isIgnoreExternals()); addFlag(cliArgs, "--no-ignore", request.isShowIgnored()); addFlag(cliArgs, "--quiet", !request.isShowUnversioned()); addFlag(cliArgs, "--show-updates", request.isShowUpdates()); addFlag(cliArgs, "--verbose", request.isVerbose()); // Options addOptionList(cliArgs, "--changelist", request.getChangeLists()); addOption(cliArgs, "--depth", request.getDepth()); // Command Name cliArgs.add("status"); final CommandLineResult result = runCommand(null, cliArgs, projectPath, addWorkingCopyPathIfNecessary(request.getPaths())); return DtoFactory.getInstance().createDto(CLIOutputResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()); } /** * Perform an "svn checkout" based on the request. * * @param request * the request * @return the response * @throws IOException * if there is a problem executing the command * @throws SubversionException * if there is a Subversion issue */ public CLIOutputWithRevisionResponse update(final UpdateRequest request) throws IOException, SubversionException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); final List<String> uArgs = defaultArgs(); // Flags addFlag(uArgs, "--ignore-externals", request.isIgnoreExternals()); // Options addOption(uArgs, "--depth", request.getDepth()); addOption(uArgs, "--revision", request.getRevision()); // Command Name uArgs.add("update"); final CommandLineResult result = runCommand(null, uArgs, projectPath, addWorkingCopyPathIfNecessary(request.getPaths()), request.getUsername(), request.getPassword()); return DtoFactory.getInstance().createDto(CLIOutputWithRevisionResponse.class) .withCommand(result.getCommandLine().toString()) .withRevision(SubversionUtils.getUpdateRevision(result.getStdout())) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()); } /** * Perform an "svn log" based on the request. * * @param request * the request * @return the response * @throws IOException * if there is a problem executing the command * @throws SubversionException * if there is a Subversion issue */ public CLIOutputResponse showLog(final ShowLogRequest request) throws IOException, SubversionException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); final List<String> uArgs = defaultArgs(); addOption(uArgs, "--revision", request.getRevision()); uArgs.add("log"); final CommandLineResult result = runCommand(null, uArgs, projectPath, request.getPaths()); return DtoFactory.getInstance().createDto(CLIOutputResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()); } public CLIOutputResponse lockUnlock(final LockRequest request, final boolean lock) throws IOException, SubversionException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); final List<String> args = defaultArgs(); addFlag(args, "--force", request.isForce()); // command if (lock) { args.add("lock"); } else { args.add("unlock"); } final CommandLineResult result = runCommand(null, args, projectPath, request.getTargets(), request.getUsername(), request.getPassword()); return DtoFactory.getInstance() .createDto(CLIOutputResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()); } /** * Perform an "svn diff" based on the request. * * @param request * the request * @return the response * @throws IOException * if there is a problem executing the command * @throws SubversionException * if there is a Subversion issue */ public CLIOutputResponse showDiff(final ShowDiffRequest request) throws IOException, SubversionException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); final List<String> uArgs = defaultArgs(); addOption(uArgs, "--revision", request.getRevision()); uArgs.add("diff"); final CommandLineResult result = runCommand(null, uArgs, projectPath, request.getPaths(), request.getUsername(), request.getPassword()); return DtoFactory.getInstance().createDto(CLIOutputResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()); } /** * Returns list of the branches of the project. * * @param request * the request * * @see #list(ListRequest) * @see #info(InfoRequest) */ public ListResponse listBranches(final ListRequest request) throws ApiException { InfoResponse info = info(newDto(InfoRequest.class).withProjectPath(request.getProjectPath()) .withTarget(".") .withPassword(request.getPassword()) .withUsername(request.getUsername())); final List<String> args = defaultArgs(); args.add("list"); String repositoryRoot = getRepositoryRoot(info.getOutput()); String projectRelativeUrl = getRelativeUrl(info.getOutput()); String projectUri = recognizeProjectUri(repositoryRoot, projectRelativeUrl); String path = projectUri == null ? "^/branches" : (projectUri + "/branches"); final CommandLineResult result = runCommand(null, args, new File(request.getProjectPath()), singletonList(path), request.getUsername(), request.getPassword()); return newDto(ListResponse.class).withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout() .stream() .filter(s -> s.endsWith("/")) .map(s -> s.substring(0, s.length() - 1)) .collect(Collectors.toList())) .withErrorOutput(result.getStderr()); } /** * Returns list of the tags of the project. * * @param request * the request * * @see #list(ListRequest) * @see #info(InfoRequest) */ public ListResponse listTags(final ListRequest request) throws ApiException { InfoResponse info = info(newDto(InfoRequest.class).withProjectPath(request.getProjectPath()) .withTarget(".") .withPassword(request.getPassword()) .withUsername(request.getUsername())); final List<String> args = defaultArgs(); args.add("list"); String repositoryRoot = getRepositoryRoot(info.getOutput()); String projectRelativeUrl = getRelativeUrl(info.getOutput()); String projectUri = recognizeProjectUri(repositoryRoot, projectRelativeUrl); String branchesPath = projectUri == null ? "^/tags" : (projectUri + "/tags"); final CommandLineResult result = runCommand(null, args, new File(request.getProjectPath()), singletonList(branchesPath), request.getUsername(), request.getPassword()); return newDto(ListResponse.class).withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout() .stream() .filter(s -> s.endsWith("/")) .map(s -> s.substring(0, s.length() - 1)) .collect(Collectors.toList())) .withErrorOutput(result.getStderr()); } /** * List remote subversion directory. * * @param request * the request * * @return the response containing target children */ public ListResponse list(final ListRequest request) throws ApiException { final List<String> args = defaultArgs(); args.add("list"); final CommandLineResult result = runCommand(null, args, new File(request.getProjectPath()), singletonList(request.getTargetPath()), request.getUsername(), request.getPassword()); return newDto(ListResponse.class).withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrorOutput(result.getStderr()); } /** * Perform an "svn resolve" based on the request. * * @param request * the request * @return the response * @throws IOException * if there is a problem executing the command * @throws SubversionException * if there is a Subversion issue */ public CLIOutputResponseList resolve(final ResolveRequest request) throws IOException, SubversionException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); Map<String, String> resolutions = request.getConflictResolutions(); List<CLIOutputResponse> results = new ArrayList<>(); for (String path : resolutions.keySet()) { final List<String> uArgs = defaultArgs(); addDepth(uArgs, request.getDepth()); addOption(uArgs, "--accept", resolutions.get(path)); uArgs.add("resolve"); final CommandLineResult result = runCommand(null, uArgs, projectPath, Arrays.asList(path)); CLIOutputResponse outputResponse = DtoFactory.getInstance().createDto(CLIOutputResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()); results.add(outputResponse); } return DtoFactory.getInstance().createDto(CLIOutputResponseList.class) .withCLIOutputResponses(results); } /** * Perform an "svn export" based on the request. * * @param projectPath * project path * @param path * exported path * @param revision * specified revision to export * @return Response which contains hyperlink with download url * @throws IOException * if there is a problem executing the command * @throws ServerException * if there is an exporting issue */ public Response exportPath(@NotNull final String projectPath, @NotNull final String path, String revision) throws IOException, ServerException, UnauthorizedException { final File project = new File(projectPath); final List<String> uArgs = defaultArgs(); if (!isNullOrEmpty(revision)) { addOption(uArgs, "--revision", revision); } uArgs.add("--force"); uArgs.add("export"); File tempDir = null; File zip = null; try { tempDir = Files.createTempDir(); final CommandLineResult result = runCommand(null, uArgs, project, Arrays.asList(path, tempDir.getAbsolutePath())); if (result.getExitCode() != 0) { LOG.warn("Svn export process finished with exit status {}", result.getExitCode()); throw new ServerException("Export failed"); } zip = new File(Files.createTempDir(), "export.zip"); ZipUtils.zipDir(tempDir.getPath(), tempDir, zip, IoUtil.ANY_FILTER); } finally { if (tempDir != null) { IoUtil.deleteRecursive(tempDir); } } final Response.ResponseBuilder responseBuilder = Response .ok(new DeleteOnCloseFileInputStream(zip), MediaType.ZIP.toString()) .lastModified(new Date(zip.lastModified())) .header(HttpHeaders.CONTENT_LENGTH, Long.toString(zip.length())) .header("Content-Disposition", "attachment; filename=\"export.zip\""); return responseBuilder.build(); } /** * Perform an "svn move" based on the request. * * @param request * the request * @return the response * @throws IOException * if there is a problem executing the command * @throws SubversionException * if there is a Subversion issue */ public CLIOutputResponse move(final MoveRequest request) throws IOException, SubversionException, UnauthorizedException { Predicate<String> sourcePredicate = new Predicate<String>() { @Override public boolean apply(String input) { return input.startsWith("file://"); } }; //for security reason we should forbid file protocol if (Iterables.any(request.getSource(), sourcePredicate) || request.getDestination().startsWith("file://")) { throw new SubversionException("Url is not acceptable"); } final File projectPath = new File(request.getProjectPath()); final List<String> cliArgs = defaultArgs(); if (!isNullOrEmpty(request.getComment())) { addOption(cliArgs, "--message", "\"" + request.getComment() + "\""); } // Command Name cliArgs.add("move"); final List<String> paths = new ArrayList<>(); paths.addAll(request.getSource()); paths.add(request.getDestination()); final CommandLineResult result = runCommand(null, cliArgs, projectPath, paths, request.getUsername(), request.getPassword()); return DtoFactory.getInstance().createDto(CLIOutputResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()); } /** * Perform an "svn propset" based on the request. * * @param request * the request * @return the response * @throws IOException * if there is a problem executing the command * @throws ServerException * if there is a Subversion issue */ public CLIOutputResponse propset(final PropertySetRequest request) throws IOException, ServerException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); final List<String> uArgs = defaultArgs(); if (request.isForce()) { uArgs.add("--force"); } addDepth(uArgs, request.getDepth().getValue()); uArgs.add("propset"); uArgs.add(request.getName()); String value = request.getValue(); Path valueFile = null; if (value.contains("\n")) { try { valueFile = java.nio.file.Files.createTempFile("svn-propset-value-", null); java.nio.file.Files.write(valueFile, value.getBytes()); uArgs.add("-F"); uArgs.add(valueFile.toString()); } catch (IOException e) { uArgs.add(value); } } else { uArgs.add(value); } final CommandLineResult result = runCommand(null, uArgs, projectPath, Arrays.asList(request.getPath())); return DtoFactory.getInstance().createDto(CLIOutputResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()); } /** * Perform an "svn propdel" based on the request. * * @param request * the request * @return the response * @throws IOException * if there is a problem executing the command * @throws ServerException * if there is a Subversion issue */ public CLIOutputResponse propdel(final PropertyDeleteRequest request) throws IOException, ServerException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); final List<String> uArgs = defaultArgs(); addDepth(uArgs, request.getDepth().getValue()); uArgs.add("propdel"); uArgs.add(request.getName()); final CommandLineResult result = runCommand(null, uArgs, projectPath, Arrays.asList(request.getPath())); return DtoFactory.getInstance().createDto(CLIOutputResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()); } /** * Perform an "svn propget" based on the request. * * @param request * the request * @return the response * @throws IOException * if there is a problem executing the command * @throws ServerException * if there is a Subversion issue */ public CLIOutputResponse propget(final PropertyGetRequest request) throws IOException, ServerException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); final List<String> uArgs = defaultArgs(); uArgs.add("propget"); uArgs.add(request.getName()); final CommandLineResult result = runCommand(null, uArgs, projectPath, Arrays.asList(request.getPath())); return DtoFactory.getInstance() .createDto(CLIOutputResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()); } /** * Perform an "svn proplist" based on the request. * * @param request * the request * @return the response * @throws IOException * if there is a problem executing the command * @throws ServerException * if there is a Subversion issue */ public CLIOutputResponse proplist(final PropertyListRequest request) throws IOException, ServerException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); final List<String> uArgs = defaultArgs(); uArgs.add("proplist"); final CommandLineResult result = runCommand(null, uArgs, projectPath, Arrays.asList(request.getPath())); List<String> output; if (result.getStdout() != null && result.getStdout().size() > 0) { output = result.getStdout().subList(1, result.getStdout().size()); } else { output = result.getStdout(); } return DtoFactory.getInstance() .createDto(CLIOutputResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(output); } private static void addDepth(final List<String> args, final String depth) { if (depth != null && !depth.isEmpty()) { args.add("--depth"); args.add(depth); } } /** Adds flag to arguments when value is true. */ private void addFlag(final List<String> args, final String argName, final boolean value) { if (value) { args.add(argName); } } /** Adds an option to arguments. */ private void addOption(final List<String> args, final String optName, final String value) { if (value != null && !value.isEmpty()) { args.add(optName); args.add(value); } } /** Adds multivalued option to arguments. */ private void addOptionList(final List<String> args, final String optName, final List<String> values) { for (final String value : values) { if (value != null && !value.isEmpty()) { args.add(optName); args.add(value); } } } /** * Creates a list of arguments containing default values. * * @return list of arguments */ private List<String> defaultArgs() { List<String> args = new ArrayList<>(); args.add("--non-interactive"); args.add("--trust-server-cert"); return args; } private List<String> addWorkingCopyPathIfNecessary(List<String> paths) { if (paths == null) { paths = new ArrayList<>(); } // If there are no paths, add the working copy root to the list of paths if (paths.isEmpty()) { paths.add("."); } return paths; } private CommandLineResult runCommand(@Nullable Map<String, String> env, List<String> args, File projectPath, List<String> paths) throws SubversionException, UnauthorizedException { String repoUrl = getRepositoryUrl(projectPath.getAbsolutePath()); return runCommand(env, args, projectPath, paths, null, null, repoUrl); } private CommandLineResult runCommand(@Nullable Map<String, String> env, List<String> args, File projectPath, List<String> paths, @Nullable String username, @Nullable String password) throws SubversionException, UnauthorizedException { String repoUrl = getRepositoryUrl(projectPath.getAbsolutePath()); return runCommand(env, args, projectPath, paths, username, password, repoUrl); } private CommandLineResult runCommand(@Nullable Map<String, String> env, List<String> args, File projectPath, List<String> paths, @Nullable String username, @Nullable String password, String repoUrl) throws SubversionException, UnauthorizedException { final List<String> lines = new ArrayList<>(); final CommandLineResult result; final StringBuffer buffer; boolean isWarning = false; // Add paths to the end of the list of arguments for (final String path : paths) { args.add(path); } String[] credentialsArgs; if (!isNullOrEmpty(username) && !isNullOrEmpty(password)) { credentialsArgs = new String[] {"--username", username, "--password", password}; } else { credentialsArgs = null; } SshEnvironment sshEnvironment = null; if (SshEnvironment.isSSH(repoUrl)) { sshEnvironment = new SshEnvironment(sshScriptProvider, repoUrl); if (env == null) { env = new HashMap<>(); } env.putAll(sshEnvironment.get()); } try { result = UpstreamUtils.executeCommandLine(env, "svn", args.toArray(new String[args.size()]), credentialsArgs, -1, projectPath, svnOutputPublisherFactory); } catch (IOException e) { throw new SubversionException(e); } finally { if (sshEnvironment != null) { sshEnvironment.cleanUp(); } } if (result.getExitCode() != 0) { buffer = new StringBuffer(); lines.addAll(result.getStdout()); lines.addAll(result.getStderr()); for (final String line : lines) { // Subversion returns an error code of 1 even when the "error" is just a warning if (line.startsWith("svn: warning: ")) { isWarning = true; } buffer.append(line); buffer.append("\n"); } if (!isWarning) { String errorMessage = buffer.toString(); if (errorMessage.endsWith("Authentication failed\n")) { throw new UnauthorizedException("Authentication failed", ErrorCodes.UNAUTHORIZED_SVN_OPERATION); } else { throw new SubversionException(errorMessage); } } } return result; } public String getRepositoryUrl(final String projectPath) throws SubversionException { return repositoryUrlProvider.getRepositoryUrl(projectPath); } /** * Returns information about specified target. * * @param request * request * @return response containing list of subversion items * @throws SubversionException */ public InfoResponse info(final InfoRequest request) throws SubversionException, UnauthorizedException { final List<String> args = defaultArgs(); if (request.getRevision() != null && !request.getRevision().trim().isEmpty()) { addOption(args, "--revision", request.getRevision()); } if (request.getChildren()) { addOption(args, "--depth", "immediates"); } args.add("info"); List<String> paths = new ArrayList<>(); paths.add(request.getTarget()); final CommandLineResult result = runCommand(null, args, new File(request.getProjectPath()), addWorkingCopyPathIfNecessary(paths), request.getUsername(), request.getPassword()); final InfoResponse response = DtoFactory.getInstance().createDto(InfoResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrorOutput(result.getStderr()); if (result.getExitCode() == 0) { List<SubversionItem> items = new ArrayList<>(); response.withItems(items); Iterator<String> iterator = result.getStdout().iterator(); List<String> itemProperties = new ArrayList<>(); while (iterator.hasNext()) { String propertyLine = iterator.next(); if (propertyLine.isEmpty()) { // create Subversion item filling properties from the list String repositoryRoot = getRepositoryRoot(itemProperties); String relativeUrl = getRelativeUrl(itemProperties); final SubversionItem item = DtoFactory.getInstance().createDto(SubversionItem.class) .withPath(InfoUtils.getPath(itemProperties)) .withName(InfoUtils.getName(itemProperties)) .withURL(InfoUtils.getUrl(itemProperties)) .withRelativeURL(relativeUrl) .withRepositoryRoot(repositoryRoot) .withRepositoryUUID(InfoUtils.getRepositoryUUID(itemProperties)) .withRevision(InfoUtils.getRevision(itemProperties)) .withNodeKind(InfoUtils.getNodeKind(itemProperties)) .withSchedule(InfoUtils.getSchedule(itemProperties)) .withLastChangedRev(InfoUtils.getLastChangedRev(itemProperties)) .withLastChangedDate(InfoUtils.getLastChangedDate(itemProperties)) .withProjectUri(recognizeProjectUri(repositoryRoot, relativeUrl)); items.add(item); // clear item properties itemProperties.clear(); } else { // add property line to property list itemProperties.add(propertyLine); } } } else { response.withErrorOutput(result.getStderr()); } return response; } /** * Merges target with specified URL. * * @param request * merge request * @return merge response * @throws IOException * @throws SubversionException */ public CLIOutputResponse merge(final MergeRequest request) throws IOException, SubversionException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); final List<String> cliArgs = defaultArgs(); // Command Name cliArgs.add("merge"); cliArgs.add(request.getSourceURL()); List<String> paths = new ArrayList<String>(); paths.add(request.getTarget()); final CommandLineResult result = runCommand(null, cliArgs, projectPath, paths); return DtoFactory.getInstance().createDto(CLIOutputResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()); } public CLIOutputResponse cleanup(final CleanupRequest request) throws SubversionException, IOException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); final List<String> cliArgs = defaultArgs(); // Command Name cliArgs.add("cleanup"); final CommandLineResult result = runCommand(null, cliArgs, projectPath, addWorkingCopyPathIfNecessary(request.getPaths())); return DtoFactory.getInstance() .createDto(CLIOutputResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()); } public GetRevisionsResponse getRevisions(GetRevisionsRequest request) throws IOException, SubversionException, UnauthorizedException { final File projectPath = new File(request.getProjectPath()); final List<String> uArgs = defaultArgs(); addOption(uArgs, "--revision", request.getRevisionRange()); uArgs.add("log"); final CommandLineResult result = runCommand(null, uArgs, projectPath, Arrays.asList(request.getPath())); final GetRevisionsResponse response = DtoFactory.getInstance().createDto(GetRevisionsResponse.class) .withCommand(result.getCommandLine().toString()) .withOutput(result.getStdout()) .withErrOutput(result.getStderr()); if (result.getExitCode() == 0) { List<String> revisions = result.getStdout().parallelStream() .filter(line -> line.split("\\|").length == 4) .map(line -> line.split("\\|")[0].trim()) .collect(Collectors.toList()); response.withRevisions(revisions); } return response; } }