001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.scm.provider.git.gitexe.command;
020
021import java.io.File;
022import java.io.IOException;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026
027import org.apache.commons.io.FilenameUtils;
028import org.apache.commons.lang3.StringUtils;
029import org.apache.maven.scm.ScmException;
030import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository;
031import org.apache.maven.scm.provider.git.util.GitUtil;
032import org.apache.maven.scm.providers.gitlib.settings.Settings;
033import org.codehaus.plexus.util.cli.CommandLineException;
034import org.codehaus.plexus.util.cli.CommandLineUtils;
035import org.codehaus.plexus.util.cli.Commandline;
036import org.codehaus.plexus.util.cli.StreamConsumer;
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040/**
041 * Command line construction utility.
042 *
043 * @author Brett Porter
044 * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
045 */
046public final class GitCommandLineUtils {
047    private static final Logger LOGGER = LoggerFactory.getLogger(GitCommandLineUtils.class);
048
049    // https://git-scm.com/docs/git#Documentation/git.txt-codeGITSSHCOMMANDcode, requires git 2.3.0 or newer
050    public static final String VARIABLE_GIT_SSH_COMMAND = "GIT_SSH_COMMAND";
051
052    private GitCommandLineUtils() {}
053
054    public static void addTarget(Commandline commandLine, List<File> files) {
055        if (files == null || files.isEmpty()) {
056            return;
057        }
058        final File workingDirectory = commandLine.getWorkingDirectory();
059        try {
060            final String canonicalWorkingDirectory = workingDirectory.getCanonicalPath();
061            for (File file : files) {
062                String relativeFile = file.getPath();
063
064                final String canonicalFile = file.getCanonicalPath();
065                if (canonicalFile.startsWith(canonicalWorkingDirectory)) {
066                    // so we can omit the starting characters
067                    relativeFile = canonicalFile.substring(canonicalWorkingDirectory.length());
068
069                    if (relativeFile.startsWith(File.separator)) {
070                        relativeFile = relativeFile.substring(File.separator.length());
071                    }
072                }
073
074                // no setFile() since this screws up the working directory!
075                commandLine.createArg().setValue(FilenameUtils.separatorsToUnix(relativeFile));
076            }
077        } catch (IOException ex) {
078            throw new IllegalArgumentException(
079                    "Could not get canonical paths for workingDirectory = " + workingDirectory + " or files=" + files,
080                    ex);
081        }
082    }
083
084    /**
085     * Use this only for commands not requiring environment variables (i.e. local commands).
086     */
087    public static Commandline getBaseGitCommandLine(File workingDirectory, String command) {
088        return getBaseGitCommandLine(workingDirectory, command, null, null);
089    }
090
091    /**
092     * Use this for commands requiring environment variables (i.e. remote commands).
093     */
094    public static Commandline getBaseGitCommandLine(
095            File workingDirectory,
096            String command,
097            GitScmProviderRepository repository,
098            Map<String, String> environment) {
099        Commandline commandLine = getAnonymousBaseGitCommandLine(workingDirectory, command);
100        if (repository != null) {
101            prepareEnvVariablesForRepository(repository, environment).forEach(commandLine::addEnvironment);
102        } else if (environment != null) {
103            environment.forEach(commandLine::addEnvironment);
104        }
105        return commandLine;
106    }
107
108    /**
109     * Creates a {@link Commandline} for which toString() does not display
110     * the password.
111     *
112     * @param workingDirectory
113     * @param command
114     * @return CommandLine with anonymous output
115     */
116    private static Commandline getAnonymousBaseGitCommandLine(File workingDirectory, String command) {
117        if (command == null || command.isEmpty()) {
118            return null;
119        }
120
121        Commandline commandLine = new AnonymousCommandLine();
122
123        composeCommand(workingDirectory, command, commandLine);
124
125        return commandLine;
126    }
127
128    private static void composeCommand(File workingDirectory, String command, Commandline commandLine) {
129        Settings settings = GitUtil.getSettings();
130
131        commandLine.setExecutable(settings.getGitCommand());
132
133        commandLine.createArg().setValue(command);
134
135        if (workingDirectory != null) {
136            commandLine.setWorkingDirectory(workingDirectory.getAbsolutePath());
137        }
138    }
139
140    public static int execute(
141            Commandline commandline, StreamConsumer consumer, CommandLineUtils.StringStreamConsumer stderr)
142            throws ScmException {
143        if (LOGGER.isDebugEnabled()) {
144            LOGGER.debug("Executing: " + commandline);
145            LOGGER.debug(
146                    "Working directory: " + commandline.getWorkingDirectory().getAbsolutePath());
147        }
148
149        int exitCode;
150        try {
151            exitCode = CommandLineUtils.executeCommandLine(commandline, consumer, stderr);
152        } catch (CommandLineException ex) {
153            throw new ScmException("Error while executing command.", ex);
154        }
155
156        return exitCode;
157    }
158
159    public static int execute(
160            Commandline commandLine,
161            CommandLineUtils.StringStreamConsumer stdout,
162            CommandLineUtils.StringStreamConsumer stderr)
163            throws ScmException {
164        if (LOGGER.isDebugEnabled()) {
165            LOGGER.debug("Executing: " + commandLine);
166            LOGGER.debug(
167                    "Working directory: " + commandLine.getWorkingDirectory().getAbsolutePath());
168        }
169
170        int exitCode;
171        try {
172            exitCode = CommandLineUtils.executeCommandLine(commandLine, stdout, stderr);
173        } catch (CommandLineException ex) {
174            throw new ScmException("Error while executing command.", ex);
175        }
176
177        return exitCode;
178    }
179
180    static Map<String, String> prepareEnvVariablesForRepository(
181            GitScmProviderRepository repository, Map<String, String> environmentVariables) {
182        Map<String, String> effectiveEnvironmentVariables = new HashMap<>();
183        if (environmentVariables != null) {
184            effectiveEnvironmentVariables.putAll(environmentVariables);
185        }
186        if (StringUtils.isNotBlank(repository.getPrivateKey())) {
187            if (effectiveEnvironmentVariables.putIfAbsent(
188                            VARIABLE_GIT_SSH_COMMAND,
189                            "ssh -o IdentitiesOnly=yes -i "
190                                    + FilenameUtils.separatorsToUnix(repository.getPrivateKey()))
191                    != null) {
192                LOGGER.warn(
193                        "Ignore GitScmProviderRepository.privateKey as environment variable {} is already set",
194                        VARIABLE_GIT_SSH_COMMAND);
195            }
196        }
197        if (StringUtils.isNotBlank(repository.getPassphrase())) {
198            LOGGER.warn("GitScmProviderRepository.passphrase currently not supported by provider 'git'");
199        }
200        return effectiveEnvironmentVariables;
201    }
202}