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.checkin;
020
021import java.io.File;
022import java.io.IOException;
023import java.net.URI;
024import java.util.ArrayList;
025import java.util.Collections;
026import java.util.List;
027import java.util.Map;
028
029import org.apache.commons.io.FilenameUtils;
030import org.apache.maven.scm.CommandParameters.SignOption;
031import org.apache.maven.scm.ScmException;
032import org.apache.maven.scm.ScmFile;
033import org.apache.maven.scm.ScmFileSet;
034import org.apache.maven.scm.ScmFileStatus;
035import org.apache.maven.scm.ScmVersion;
036import org.apache.maven.scm.command.checkin.AbstractCheckInCommand;
037import org.apache.maven.scm.command.checkin.CheckInScmResult;
038import org.apache.maven.scm.provider.ScmProviderRepository;
039import org.apache.maven.scm.provider.git.command.GitCommand;
040import org.apache.maven.scm.provider.git.gitexe.command.GitCommandLineUtils;
041import org.apache.maven.scm.provider.git.gitexe.command.add.GitAddCommand;
042import org.apache.maven.scm.provider.git.gitexe.command.branch.GitBranchCommand;
043import org.apache.maven.scm.provider.git.gitexe.command.status.GitStatusCommand;
044import org.apache.maven.scm.provider.git.gitexe.command.status.GitStatusConsumer;
045import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository;
046import org.apache.maven.scm.provider.git.util.GitUtil;
047import org.codehaus.plexus.util.FileUtils;
048import org.codehaus.plexus.util.Os;
049import org.codehaus.plexus.util.cli.CommandLineUtils;
050import org.codehaus.plexus.util.cli.Commandline;
051
052/**
053 * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
054 * @author Olivier Lamy
055 */
056public class GitCheckInCommand extends AbstractCheckInCommand implements GitCommand {
057    private final Map<String, String> environmentVariables;
058
059    public GitCheckInCommand(Map<String, String> environmentVariables) {
060        super();
061        this.environmentVariables = environmentVariables;
062    }
063
064    @Override
065    protected CheckInScmResult executeCheckInCommand(
066            ScmProviderRepository repository, ScmFileSet fileSet, String message, ScmVersion scmVersion)
067            throws ScmException {
068        return executeCheckInCommand(repository, fileSet, message, scmVersion, SignOption.DEFAULT);
069    }
070
071    /**
072     * {@inheritDoc}
073     */
074    protected CheckInScmResult executeCheckInCommand(
075            ScmProviderRepository repo, ScmFileSet fileSet, String message, ScmVersion version, SignOption signOption)
076            throws ScmException {
077        GitScmProviderRepository repository = (GitScmProviderRepository) repo;
078
079        CommandLineUtils.StringStreamConsumer stderr = new CommandLineUtils.StringStreamConsumer();
080        CommandLineUtils.StringStreamConsumer stdout = new CommandLineUtils.StringStreamConsumer();
081
082        int exitCode = -1;
083
084        File messageFile = FileUtils.createTempFile("maven-scm-", ".commit", null);
085        try {
086            FileUtils.fileWrite(messageFile.getAbsolutePath(), "UTF-8", message);
087        } catch (IOException ex) {
088            return new CheckInScmResult(
089                    null,
090                    "Error while making a temporary file for the commit message: " + ex.getMessage(),
091                    null,
092                    false);
093        }
094
095        try {
096            if (!fileSet.getFileList().isEmpty()) {
097                // if specific fileSet is given, we have to git-add them first
098                // otherwise we will use 'git-commit -a' later
099
100                Commandline clAdd = null;
101
102                // SCM-714: Workaround for the Windows terminal command limit
103                if (Os.isFamily(Os.FAMILY_WINDOWS)) {
104                    for (File file : fileSet.getFileList()) {
105                        clAdd = GitAddCommand.createCommandLine(fileSet.getBasedir(), Collections.singletonList(file));
106                        exitCode = GitCommandLineUtils.execute(clAdd, stdout, stderr);
107
108                        if (exitCode != 0) {
109                            break;
110                        }
111                    }
112                } else {
113                    clAdd = GitAddCommand.createCommandLine(fileSet.getBasedir(), fileSet.getFileList());
114                    exitCode = GitCommandLineUtils.execute(clAdd, stdout, stderr);
115                }
116
117                if (exitCode != 0) {
118                    return new CheckInScmResult(
119                            clAdd.toString(), "The git-add command failed.", stderr.getOutput(), false);
120                }
121            }
122
123            // SCM-709: statusCommand uses repositoryRoot instead of workingDirectory, adjust it with
124            // relativeRepositoryPath
125            URI relativeRepositoryPath = GitStatusCommand.getRelativeCWD(logger, fileSet);
126
127            // git-commit doesn't show single files, but only summary :/
128            // so we must run git-status and consume the output
129            // borrow a few things from the git-status command
130            Commandline clStatus = GitStatusCommand.createCommandLine(repository, fileSet);
131
132            GitStatusConsumer statusConsumer =
133                    new GitStatusConsumer(fileSet.getBasedir(), relativeRepositoryPath, fileSet);
134            exitCode = GitCommandLineUtils.execute(clStatus, statusConsumer, stderr);
135            if (exitCode != 0) {
136                // git-status returns non-zero if nothing to do
137                if (logger.isInfoEnabled()) {
138                    logger.info("nothing added to commit but untracked files present (use \"git add\" to " + "track)");
139                }
140            }
141
142            if (statusConsumer.getChangedFiles().isEmpty()) {
143                return new CheckInScmResult(null, statusConsumer.getChangedFiles());
144            }
145
146            Commandline clCommit =
147                    createCommitCommandLine(repository, fileSet, messageFile, environmentVariables, signOption);
148
149            exitCode = GitCommandLineUtils.execute(clCommit, stdout, stderr);
150            if (exitCode != 0) {
151                return new CheckInScmResult(
152                        clCommit.toString(), "The git-commit command failed.", stderr.getOutput(), false);
153            }
154
155            if (repo.isPushChanges()) {
156                Commandline cl = createPushCommandLine(repository, fileSet, version);
157
158                exitCode = GitCommandLineUtils.execute(cl, stdout, stderr);
159                if (exitCode != 0) {
160                    return new CheckInScmResult(
161                            cl.toString(), "The git-push command failed.", stderr.getOutput(), false);
162                }
163            }
164
165            List<ScmFile> checkedInFiles =
166                    new ArrayList<>(statusConsumer.getChangedFiles().size());
167
168            // rewrite all detected files to now have status 'checked_in'
169            for (ScmFile changedFile : statusConsumer.getChangedFiles()) {
170                ScmFile scmfile = new ScmFile(changedFile.getPath(), ScmFileStatus.CHECKED_IN);
171
172                if (fileSet.getFileList().isEmpty()) {
173                    checkedInFiles.add(scmfile);
174                } else {
175                    // if a specific fileSet is given, we have to check if the file is really tracked
176                    for (File f : fileSet.getFileList()) {
177                        if (FilenameUtils.separatorsToUnix(f.getPath()).equals(scmfile.getPath())) {
178                            checkedInFiles.add(scmfile);
179                        }
180                    }
181                }
182            }
183
184            return new CheckInScmResult(clCommit.toString(), checkedInFiles);
185        } finally {
186            try {
187                FileUtils.forceDelete(messageFile);
188            } catch (IOException ex) {
189                // ignore
190            }
191        }
192    }
193
194    // ----------------------------------------------------------------------
195    //
196    // ----------------------------------------------------------------------
197
198    public Commandline createPushCommandLine(
199            GitScmProviderRepository repository, ScmFileSet fileSet, ScmVersion version) throws ScmException {
200        Commandline cl = GitCommandLineUtils.getBaseGitCommandLine(
201                fileSet.getBasedir(), "push", repository, environmentVariables);
202
203        String branch = GitBranchCommand.getCurrentBranch(repository, fileSet);
204
205        if (branch == null || branch.length() == 0) {
206            throw new ScmException("Could not detect the current branch. Don't know where I should push to!");
207        }
208
209        cl.createArg().setValue(repository.getPushUrl());
210
211        cl.createArg().setValue("refs/heads/" + branch + ":" + "refs/heads/" + branch);
212
213        return cl;
214    }
215
216    @Deprecated
217    public static Commandline createCommitCommandLine(
218            GitScmProviderRepository repository, ScmFileSet fileSet, File messageFile) throws ScmException {
219        return createCommitCommandLine(repository, fileSet, messageFile, Collections.emptyMap());
220    }
221
222    @Deprecated
223    public static Commandline createCommitCommandLine(
224            GitScmProviderRepository repository,
225            ScmFileSet fileSet,
226            File messageFile,
227            Map<String, String> environmentVariables)
228            throws ScmException {
229        return createCommitCommandLine(repository, fileSet, messageFile, environmentVariables, SignOption.DEFAULT);
230    }
231
232    public static Commandline createCommitCommandLine(
233            GitScmProviderRepository repository,
234            ScmFileSet fileSet,
235            File messageFile,
236            Map<String, String> environmentVariables,
237            SignOption signOption)
238            throws ScmException {
239        Commandline cl = GitCommandLineUtils.getBaseGitCommandLine(fileSet.getBasedir(), "commit");
240
241        cl.createArg().setValue("--verbose");
242
243        cl.createArg().setValue("-F");
244
245        cl.createArg().setValue(messageFile.getAbsolutePath());
246
247        if (fileSet.getFileList().isEmpty()) {
248            // commit all tracked files
249            cl.createArg().setValue("-a");
250        }
251
252        if (GitUtil.getSettings().isCommitNoVerify()) {
253            cl.createArg().setValue("--no-verify");
254        }
255
256        if (signOption != null) {
257            switch (signOption) {
258                case FORCE_SIGN:
259                    cl.createArg().setValue("--gpg-sign");
260                    break;
261                case FORCE_NO_SIGN:
262                    cl.createArg().setValue("--no-gpg-sign");
263                    break;
264                default:
265                    // do nothing, this is the default
266                    break;
267            }
268        }
269        return cl;
270    }
271}