View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.scm.provider.git.jgit.command.checkin;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.net.InetAddress;
24  import java.net.UnknownHostException;
25  import java.util.Collections;
26  import java.util.EnumSet;
27  import java.util.List;
28  import java.util.Optional;
29  import java.util.Set;
30  import java.util.function.BiFunction;
31  
32  import org.apache.commons.lang3.StringUtils;
33  import org.apache.maven.scm.CommandParameter;
34  import org.apache.maven.scm.CommandParameters;
35  import org.apache.maven.scm.ScmException;
36  import org.apache.maven.scm.ScmFile;
37  import org.apache.maven.scm.ScmFileSet;
38  import org.apache.maven.scm.ScmVersion;
39  import org.apache.maven.scm.command.checkin.AbstractCheckInCommand;
40  import org.apache.maven.scm.command.checkin.CheckInScmResult;
41  import org.apache.maven.scm.provider.ScmProviderRepository;
42  import org.apache.maven.scm.provider.git.command.GitCommand;
43  import org.apache.maven.scm.provider.git.jgit.command.CustomizableSshSessionFactoryCommand;
44  import org.apache.maven.scm.provider.git.jgit.command.JGitTransportConfigCallback;
45  import org.apache.maven.scm.provider.git.jgit.command.JGitUtils;
46  import org.apache.maven.scm.provider.git.jgit.command.PushException;
47  import org.apache.maven.scm.provider.git.jgit.command.ScmProviderAwareSshdSessionFactory;
48  import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository;
49  import org.apache.maven.scm.provider.git.util.GitUtil;
50  import org.eclipse.jgit.api.AddCommand;
51  import org.eclipse.jgit.api.CommitCommand;
52  import org.eclipse.jgit.api.Git;
53  import org.eclipse.jgit.api.Status;
54  import org.eclipse.jgit.api.TransportConfigCallback;
55  import org.eclipse.jgit.api.errors.GitAPIException;
56  import org.eclipse.jgit.lib.Constants;
57  import org.eclipse.jgit.lib.UserConfig;
58  import org.eclipse.jgit.revwalk.RevCommit;
59  import org.eclipse.jgit.transport.RefSpec;
60  import org.eclipse.jgit.transport.RemoteRefUpdate;
61  import org.slf4j.Logger;
62  
63  /**
64   * This provider uses the following strategy to discover the committer and author name/mail for a commit:
65   * <ol>
66   * <li>"user" section in .gitconfig</li>
67   * <li>"username" passed to maven execution</li>
68   * <li>default git config (system user and hostname for email)</li>
69   * </ol>
70   * the "maven-scm" config can be configured like this: <br>
71   * the default email domain to be used (will be used to create an email from the username passed to maven):<br>
72   * <code>git config --global maven-scm.maildomain mycomp.com</code> <br>
73   * you can also enforce the usage of the username for the author and committer:<br>
74   * <code>git config --global maven-scm.forceUsername true</code> <br>
75   *
76   * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
77   * @author Dominik Bartholdi (imod)
78   * @since 1.9
79   */
80  public class JGitCheckInCommand extends AbstractCheckInCommand
81          implements GitCommand, CustomizableSshSessionFactoryCommand {
82  
83      protected static final String GIT_MAVEN_SECTION = "maven-scm";
84  
85      protected static final String GIT_MAILDOMAIN = "maildomain";
86  
87      protected static final String GIT_FORCE = "forceUsername";
88  
89      private BiFunction<GitScmProviderRepository, Logger, ScmProviderAwareSshdSessionFactory> sshSessionFactorySupplier;
90  
91      public JGitCheckInCommand() {
92          sshSessionFactorySupplier = ScmProviderAwareSshdSessionFactory::new;
93      }
94  
95      @Override
96      public void setSshSessionFactorySupplier(
97              BiFunction<GitScmProviderRepository, Logger, ScmProviderAwareSshdSessionFactory>
98                      sshSessionFactorySupplier) {
99          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 }