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.jgit.command.checkin;
020
021import java.io.File;
022import java.io.IOException;
023import java.net.InetAddress;
024import java.net.UnknownHostException;
025import java.util.Collections;
026import java.util.EnumSet;
027import java.util.List;
028import java.util.Optional;
029import java.util.Set;
030import java.util.function.BiFunction;
031
032import org.apache.commons.lang3.StringUtils;
033import org.apache.maven.scm.CommandParameter;
034import org.apache.maven.scm.CommandParameters;
035import org.apache.maven.scm.ScmException;
036import org.apache.maven.scm.ScmFile;
037import org.apache.maven.scm.ScmFileSet;
038import org.apache.maven.scm.ScmVersion;
039import org.apache.maven.scm.command.checkin.AbstractCheckInCommand;
040import org.apache.maven.scm.command.checkin.CheckInScmResult;
041import org.apache.maven.scm.provider.ScmProviderRepository;
042import org.apache.maven.scm.provider.git.command.GitCommand;
043import org.apache.maven.scm.provider.git.jgit.command.CustomizableSshSessionFactoryCommand;
044import org.apache.maven.scm.provider.git.jgit.command.JGitTransportConfigCallback;
045import org.apache.maven.scm.provider.git.jgit.command.JGitUtils;
046import org.apache.maven.scm.provider.git.jgit.command.PushException;
047import org.apache.maven.scm.provider.git.jgit.command.ScmProviderAwareSshdSessionFactory;
048import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository;
049import org.apache.maven.scm.provider.git.util.GitUtil;
050import org.eclipse.jgit.api.AddCommand;
051import org.eclipse.jgit.api.CommitCommand;
052import org.eclipse.jgit.api.Git;
053import org.eclipse.jgit.api.Status;
054import org.eclipse.jgit.api.TransportConfigCallback;
055import org.eclipse.jgit.api.errors.GitAPIException;
056import org.eclipse.jgit.lib.Constants;
057import org.eclipse.jgit.lib.UserConfig;
058import org.eclipse.jgit.revwalk.RevCommit;
059import org.eclipse.jgit.transport.RefSpec;
060import org.eclipse.jgit.transport.RemoteRefUpdate;
061import org.slf4j.Logger;
062
063/**
064 * This provider uses the following strategy to discover the committer and author name/mail for a commit:
065 * <ol>
066 * <li>"user" section in .gitconfig</li>
067 * <li>"username" passed to maven execution</li>
068 * <li>default git config (system user and hostname for email)</li>
069 * </ol>
070 * the "maven-scm" config can be configured like this: <br>
071 * the default email domain to be used (will be used to create an email from the username passed to maven):<br>
072 * <code>git config --global maven-scm.maildomain mycomp.com</code> <br>
073 * you can also enforce the usage of the username for the author and committer:<br>
074 * <code>git config --global maven-scm.forceUsername true</code> <br>
075 *
076 * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
077 * @author Dominik Bartholdi (imod)
078 * @since 1.9
079 */
080public class JGitCheckInCommand extends AbstractCheckInCommand
081        implements GitCommand, CustomizableSshSessionFactoryCommand {
082
083    protected static final String GIT_MAVEN_SECTION = "maven-scm";
084
085    protected static final String GIT_MAILDOMAIN = "maildomain";
086
087    protected static final String GIT_FORCE = "forceUsername";
088
089    private BiFunction<GitScmProviderRepository, Logger, ScmProviderAwareSshdSessionFactory> sshSessionFactorySupplier;
090
091    public JGitCheckInCommand() {
092        sshSessionFactorySupplier = ScmProviderAwareSshdSessionFactory::new;
093    }
094
095    @Override
096    public void setSshSessionFactorySupplier(
097            BiFunction<GitScmProviderRepository, Logger, ScmProviderAwareSshdSessionFactory>
098                    sshSessionFactorySupplier) {
099        this.sshSessionFactorySupplier = sshSessionFactorySupplier;
100    }
101
102    @Override
103    public CheckInScmResult executeCommand(ScmProviderRepository repo, ScmFileSet fileSet, CommandParameters parameters)
104            throws ScmException {
105        String message = parameters.getString(CommandParameter.MESSAGE);
106
107        ScmVersion version = parameters.getScmVersion(CommandParameter.SCM_VERSION, null);
108
109        Git git = null;
110        try {
111            File basedir = fileSet.getBasedir();
112            git = JGitUtils.openRepo(basedir);
113
114            boolean doCommit = false;
115
116            if (!fileSet.getFileList().isEmpty()) {
117                // add files first
118                doCommit = JGitUtils.addAllFiles(git, fileSet).size() > 0;
119                if (!doCommit) {
120                    Status status = git.status().call();
121                    doCommit = status.getAdded().size() > 0
122                            || status.getChanged().size() > 0
123                            || status.getRemoved().size() > 0;
124                }
125            } else {
126                // add all tracked files which are modified manually
127                Status status = git.status().call();
128                Set<String> changeds = git.status().call().getModified();
129                if (changeds.isEmpty()) {
130                    if (!status.hasUncommittedChanges()) {
131                        // warn there is nothing to add
132                        logger.warn("There are neither files to be added nor any uncommitted changes");
133                        doCommit = false;
134                    } else {
135                        logger.debug("There are uncommitted changes in the git index");
136                        doCommit = true;
137                    }
138                } else {
139                    // TODO: gitexe only adds if fileSet is not empty
140                    AddCommand add = git.add();
141                    for (String changed : changeds) {
142                        logger.debug("Add manually: {}", changed);
143                        add.addFilepattern(changed);
144                        doCommit = true;
145                    }
146                    add.call();
147                }
148            }
149
150            List<ScmFile> checkedInFiles = Collections.emptyList();
151            if (doCommit) {
152                UserInfo author = getAuthor(repo, git);
153                UserInfo committer = getCommitter(repo, git);
154
155                CommitCommand command = git.commit()
156                        .setMessage(message)
157                        .setAuthor(author.name, author.email)
158                        .setCommitter(committer.name, committer.email);
159                if (GitUtil.getSettings().isCommitNoVerify()) {
160                    command.setNoVerify(true);
161                }
162                RevCommit commitRev = command.call();
163
164                logger.debug("commit done: " + commitRev.getShortMessage());
165                checkedInFiles = JGitUtils.getFilesInCommit(git.getRepository(), commitRev, fileSet.getBasedir());
166                if (logger.isDebugEnabled()) {
167                    for (ScmFile scmFile : checkedInFiles) {
168                        logger.debug("in commit: " + scmFile);
169                    }
170                }
171            } else {
172                logger.info("nothing to commit");
173            }
174
175            if (repo.isPushChanges()) {
176                String branch = version != null ? version.getName() : null;
177                if (StringUtils.isBlank(branch)) {
178                    branch = git.getRepository().getBranch();
179                }
180                RefSpec refSpec = new RefSpec(Constants.R_HEADS + branch + ":" + Constants.R_HEADS + branch);
181                logger.info("push changes to remote... " + refSpec);
182                TransportConfigCallback transportConfigCallback = new JGitTransportConfigCallback(
183                        sshSessionFactorySupplier.apply((GitScmProviderRepository) repo, logger));
184
185                JGitUtils.push(
186                        git,
187                        (GitScmProviderRepository) repo,
188                        refSpec,
189                        EnumSet.of(RemoteRefUpdate.Status.OK, RemoteRefUpdate.Status.UP_TO_DATE),
190                        Optional.of(transportConfigCallback));
191            }
192
193            return new CheckInScmResult("JGit checkin", checkedInFiles);
194        } catch (PushException e) {
195            logger.debug("Failed to push commits", e);
196            return new CheckInScmResult("JGit checkin", "Failed to push changes: " + e.getMessage(), "", false);
197        } catch (IOException | GitAPIException e) {
198            throw new ScmException("JGit checkin failure!", e);
199        } finally {
200            JGitUtils.closeRepo(git);
201        }
202    }
203
204    /**
205     * {@inheritDoc}
206     */
207    protected CheckInScmResult executeCheckInCommand(
208            ScmProviderRepository repo, ScmFileSet fileSet, String message, ScmVersion version) throws ScmException {
209
210        CommandParameters parameters = new CommandParameters();
211        parameters.setString(CommandParameter.MESSAGE, message);
212        parameters.setScmVersion(CommandParameter.SCM_VERSION, version);
213        return executeCommand(repo, fileSet, parameters);
214    }
215
216    private static final class UserInfo {
217
218        final String name;
219
220        final String email;
221
222        UserInfo(String name, String email) {
223            this.name = name;
224            this.email = email;
225        }
226    }
227
228    private UserInfo getCommitter(ScmProviderRepository repo, Git git) {
229        boolean forceMvnUser = git.getRepository().getConfig().getBoolean(GIT_MAVEN_SECTION, GIT_FORCE, false);
230
231        // git config
232        UserConfig user = git.getRepository().getConfig().get(UserConfig.KEY);
233        String committerName = null;
234        if (!forceMvnUser && !user.isCommitterNameImplicit()) {
235            committerName = user.getCommitterName();
236        }
237
238        // mvn parameter
239        if (StringUtils.isBlank(committerName)) {
240            committerName = repo.getUser();
241        }
242
243        // git default
244        if (StringUtils.isBlank(committerName)) {
245            committerName = user.getCommitterName();
246        }
247
248        // git config
249        String committerMail = null;
250        if (!user.isCommitterEmailImplicit()) {
251            committerMail = user.getCommitterEmail();
252        }
253
254        if (StringUtils.isBlank(committerMail)) {
255            String defaultDomain = git.getRepository().getConfig().getString(GIT_MAVEN_SECTION, null, GIT_MAILDOMAIN);
256            defaultDomain = StringUtils.isNotBlank(defaultDomain) ? defaultDomain : getHostname();
257
258            // mvn parameter (constructed with username) or git default
259            committerMail = StringUtils.isNotBlank(repo.getUser())
260                    ? repo.getUser() + "@" + defaultDomain
261                    : user.getCommitterEmail();
262        }
263
264        return new UserInfo(committerName, committerMail);
265    }
266
267    private UserInfo getAuthor(ScmProviderRepository repo, Git git) {
268        boolean forceMvnUser = git.getRepository().getConfig().getBoolean(GIT_MAVEN_SECTION, GIT_FORCE, false);
269
270        // git config
271        UserConfig user = git.getRepository().getConfig().get(UserConfig.KEY);
272        String authorName = null;
273        if (!forceMvnUser && !user.isAuthorNameImplicit()) {
274            authorName = user.getAuthorName();
275        }
276
277        // mvn parameter
278        if (StringUtils.isBlank(authorName)) {
279            authorName = repo.getUser();
280        }
281
282        // git default
283        if (StringUtils.isBlank(authorName)) {
284            authorName = user.getAuthorName();
285        }
286
287        // git config
288        String authorMail = null;
289        if (!user.isAuthorEmailImplicit()) {
290            authorMail = user.getAuthorEmail();
291        }
292
293        if (StringUtils.isBlank(authorMail)) {
294            String defaultDomain = git.getRepository().getConfig().getString(GIT_MAVEN_SECTION, null, GIT_MAILDOMAIN);
295            defaultDomain = StringUtils.isNotBlank(defaultDomain) ? defaultDomain : getHostname();
296
297            // mvn parameter (constructed with username) or git default
298            authorMail = StringUtils.isNotBlank(repo.getUser())
299                    ? repo.getUser() + "@" + defaultDomain
300                    : user.getAuthorEmail();
301        }
302
303        return new UserInfo(authorName, authorMail);
304    }
305
306    private String getHostname() {
307        String hostname;
308        try {
309            InetAddress localhost = java.net.InetAddress.getLocalHost();
310            hostname = localhost.getHostName();
311        } catch (UnknownHostException e) {
312            logger.warn(
313                    "failed to resolve hostname to create mail address, " + "defaulting to 'maven-scm-provider-jgit'");
314            hostname = "maven-scm-provider-jgit";
315        }
316        return hostname;
317    }
318}