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 *
046 */
047public final class GitCommandLineUtils {
048    private static final Logger LOGGER = LoggerFactory.getLogger(GitCommandLineUtils.class);
049
050    // https://git-scm.com/docs/git#Documentation/git.txt-codeGITSSHCOMMANDcode, requires git 2.3.0 or newer
051    public static final String VARIABLE_GIT_SSH_COMMAND = "GIT_SSH_COMMAND";
052
053    private GitCommandLineUtils() {}
054
055    public static void addTarget(Commandline commandLine, List<File> files) {
056        if (files == null || files.isEmpty()) {
057            return;
058        }
059        final File workingDirectory = commandLine.getWorkingDirectory();
060        try {
061            final String canonicalWorkingDirectory = workingDirectory.getCanonicalPath();
062            for (File file : files) {
063                String relativeFile = file.getPath();
064
065                final String canonicalFile = file.getCanonicalPath();
066                if (canonicalFile.startsWith(canonicalWorkingDirectory)) {
067                    // so we can omit the starting characters
068                    relativeFile = canonicalFile.substring(canonicalWorkingDirectory.length());
069
070                    if (relativeFile.startsWith(File.separator)) {
071                        relativeFile = relativeFile.substring(File.separator.length());
072                    }
073                }
074
075                // no setFile() since this screws up the working directory!
076                commandLine.createArg().setValue(FilenameUtils.separatorsToUnix(relativeFile));
077            }
078        } catch (IOException ex) {
079            throw new IllegalArgumentException(
080                    "Could not get canonical paths for workingDirectory = " + workingDirectory + " or files=" + files,
081                    ex);
082        }
083    }
084
085    /**
086     * Use this only for commands not requiring environment variables (i.e. local commands).
087     */
088    public static Commandline getBaseGitCommandLine(File workingDirectory, String command) {
089        return getBaseGitCommandLine(workingDirectory, command, null, null);
090    }
091
092    /**
093     * Use this for commands requiring environment variables (i.e. remote commands).
094     */
095    public static Commandline getBaseGitCommandLine(
096            File workingDirectory,
097            String command,
098            GitScmProviderRepository repository,
099            Map<String, String> environment) {
100        Commandline commandLine = getAnonymousBaseGitCommandLine(workingDirectory, command);
101        if (repository != null) {
102            prepareEnvVariablesForRepository(repository, environment).forEach(commandLine::addEnvironment);
103        } else if (environment != null) {
104            environment.forEach(commandLine::addEnvironment);
105        }
106        return commandLine;
107    }
108
109    /**
110     * Creates a {@link Commandline} for which toString() does not display
111     * the password.
112     *
113     * @param workingDirectory
114     * @param command
115     * @return CommandLine with anonymous output
116     */
117    private static Commandline getAnonymousBaseGitCommandLine(File workingDirectory, String command) {
118        if (command == null || command.isEmpty()) {
119            return null;
120        }
121
122        Commandline commandLine = new AnonymousCommandLine();
123
124        composeCommand(workingDirectory, command, commandLine);
125
126        return commandLine;
127    }
128
129    private static void composeCommand(File workingDirectory, String command, Commandline commandLine) {
130        Settings settings = GitUtil.getSettings();
131
132        commandLine.setExecutable(settings.getGitCommand());
133
134        commandLine.createArg().setValue(command);
135
136        if (workingDirectory != null) {
137            commandLine.setWorkingDirectory(workingDirectory.getAbsolutePath());
138        }
139    }
140
141    public static int execute(
142            Commandline commandline, StreamConsumer consumer, CommandLineUtils.StringStreamConsumer stderr)
143            throws ScmException {
144        if (LOGGER.isInfoEnabled()) {
145            LOGGER.info("Executing: " + commandline);
146            LOGGER.info(
147                    "Working directory: " + commandline.getWorkingDirectory().getAbsolutePath());
148        }
149
150        int exitCode;
151        try {
152            exitCode = CommandLineUtils.executeCommandLine(commandline, consumer, stderr);
153        } catch (CommandLineException ex) {
154            throw new ScmException("Error while executing command.", ex);
155        }
156
157        return exitCode;
158    }
159
160    public static int execute(
161            Commandline commandLine,
162            CommandLineUtils.StringStreamConsumer stdout,
163            CommandLineUtils.StringStreamConsumer stderr)
164            throws ScmException {
165        if (LOGGER.isInfoEnabled()) {
166            LOGGER.info("Executing: " + commandLine);
167            LOGGER.info(
168                    "Working directory: " + commandLine.getWorkingDirectory().getAbsolutePath());
169        }
170
171        int exitCode;
172        try {
173            exitCode = CommandLineUtils.executeCommandLine(commandLine, stdout, stderr);
174        } catch (CommandLineException ex) {
175            throw new ScmException("Error while executing command.", ex);
176        }
177
178        return exitCode;
179    }
180
181    static Map<String, String> prepareEnvVariablesForRepository(
182            GitScmProviderRepository repository, Map<String, String> environmentVariables) {
183        Map<String, String> effectiveEnvironmentVariables = new HashMap<>();
184        if (environmentVariables != null) {
185            effectiveEnvironmentVariables.putAll(environmentVariables);
186        }
187        if (StringUtils.isNotBlank(repository.getPrivateKey())) {
188            if (effectiveEnvironmentVariables.putIfAbsent(
189                            VARIABLE_GIT_SSH_COMMAND,
190                            "ssh -o IdentitiesOnly=yes -i "
191                                    + FilenameUtils.separatorsToUnix(repository.getPrivateKey()))
192                    != null) {
193                LOGGER.warn(
194                        "Ignore GitScmProviderRepository.privateKey as environment variable {} is already set",
195                        VARIABLE_GIT_SSH_COMMAND);
196            }
197        }
198        if (StringUtils.isNotBlank(repository.getPassphrase())) {
199            LOGGER.warn("GitScmProviderRepository.passphrase currently not supported by provider 'git'");
200        }
201        return effectiveEnvironmentVariables;
202    }
203}