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.net.InetAddress;
23  import java.net.UnknownHostException;
24  import java.util.Collections;
25  import java.util.EnumSet;
26  import java.util.List;
27  import java.util.Optional;
28  import java.util.Set;
29  import java.util.function.BiFunction;
30  
31  import org.apache.commons.lang3.StringUtils;
32  import org.apache.maven.scm.CommandParameter;
33  import org.apache.maven.scm.CommandParameters;
34  import org.apache.maven.scm.ScmException;
35  import org.apache.maven.scm.ScmFile;
36  import org.apache.maven.scm.ScmFileSet;
37  import org.apache.maven.scm.ScmVersion;
38  import org.apache.maven.scm.command.checkin.AbstractCheckInCommand;
39  import org.apache.maven.scm.command.checkin.CheckInScmResult;
40  import org.apache.maven.scm.provider.ScmProviderRepository;
41  import org.apache.maven.scm.provider.git.command.GitCommand;
42  import org.apache.maven.scm.provider.git.jgit.command.CustomizableSshSessionFactoryCommand;
43  import org.apache.maven.scm.provider.git.jgit.command.JGitTransportConfigCallback;
44  import org.apache.maven.scm.provider.git.jgit.command.JGitUtils;
45  import org.apache.maven.scm.provider.git.jgit.command.PushException;
46  import org.apache.maven.scm.provider.git.jgit.command.ScmProviderAwareSshdSessionFactory;
47  import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository;
48  import org.eclipse.jgit.api.AddCommand;
49  import org.eclipse.jgit.api.CommitCommand;
50  import org.eclipse.jgit.api.Git;
51  import org.eclipse.jgit.api.Status;
52  import org.eclipse.jgit.api.TransportConfigCallback;
53  import org.eclipse.jgit.lib.Constants;
54  import org.eclipse.jgit.lib.UserConfig;
55  import org.eclipse.jgit.revwalk.RevCommit;
56  import org.eclipse.jgit.transport.RefSpec;
57  import org.eclipse.jgit.transport.RemoteRefUpdate;
58  import org.slf4j.Logger;
59  
60  /**
61   * This provider uses the following strategy to discover the committer and author name/mail for a commit:
62   * <ol>
63   * <li>"user" section in .gitconfig</li>
64   * <li>"username" passed to maven execution</li>
65   * <li>default git config (system user and hostname for email)</li>
66   * </ol>
67   * the "maven-scm" config can be configured like this: <br>
68   * the default email domain to be used (will be used to create an email from the username passed to maven):<br>
69   * <code>git config --global maven-scm.maildomain mycomp.com</code> <br>
70   * you can also enforce the usage of the username for the author and committer:<br>
71   * <code>git config --global maven-scm.forceUsername true</code> <br>
72   *
73   * @author <a href="mailto:struberg@yahoo.de">Mark Struberg</a>
74   * @author Dominik Bartholdi (imod)
75   * @since 1.9
76   */
77  public class JGitCheckInCommand extends AbstractCheckInCommand
78          implements GitCommand, CustomizableSshSessionFactoryCommand {
79  
80      protected static final String GIT_MAVEN_SECTION = "maven-scm";
81  
82      protected static final String GIT_MAILDOMAIN = "maildomain";
83  
84      protected static final String GIT_FORCE = "forceUsername";
85  
86      private BiFunction<GitScmProviderRepository, Logger, ScmProviderAwareSshdSessionFactory> sshSessionFactorySupplier;
87  
88      public JGitCheckInCommand() {
89          sshSessionFactorySupplier = ScmProviderAwareSshdSessionFactory::new;
90      }
91  
92      @Override
93      public void setSshSessionFactorySupplier(
94              BiFunction<GitScmProviderRepository, Logger, ScmProviderAwareSshdSessionFactory>
95                      sshSessionFactorySupplier) {
96          this.sshSessionFactorySupplier = sshSessionFactorySupplier;
97      }
98  
99      @Override
100     public CheckInScmResult executeCommand(ScmProviderRepository repo, ScmFileSet fileSet, CommandParameters parameters)
101             throws ScmException {
102         String message = parameters.getString(CommandParameter.MESSAGE);
103 
104         ScmVersion version = parameters.getScmVersion(CommandParameter.SCM_VERSION, null);
105 
106         Git git = null;
107         try {
108             File basedir = fileSet.getBasedir();
109             git = JGitUtils.openRepo(basedir);
110 
111             boolean doCommit = false;
112 
113             if (!fileSet.getFileList().isEmpty()) {
114                 // add files first
115                 doCommit = JGitUtils.addAllFiles(git, fileSet).size() > 0;
116                 if (!doCommit) {
117                     Status status = git.status().call();
118                     doCommit = status.getAdded().size() > 0
119                             || status.getChanged().size() > 0
120                             || status.getRemoved().size() > 0;
121                 }
122             } else {
123                 // add all tracked files which are modified manually
124                 Status status = git.status().call();
125                 Set<String> changeds = git.status().call().getModified();
126                 if (changeds.isEmpty()) {
127                     if (!status.hasUncommittedChanges()) {
128                         // warn there is nothing to add
129                         logger.warn("There are neither files to be added nor any uncommitted changes");
130                         doCommit = false;
131                     } else {
132                         logger.debug("There are uncommitted changes in the git index");
133                         doCommit = true;
134                     }
135                 } else {
136                     // TODO: gitexe only adds if fileSet is not empty
137                     AddCommand add = git.add();
138                     for (String changed : changeds) {
139                         logger.debug("Add manually: {}", changed);
140                         add.addFilepattern(changed);
141                         doCommit = true;
142                     }
143                     add.call();
144                 }
145             }
146 
147             List<ScmFile> checkedInFiles = Collections.emptyList();
148             if (doCommit) {
149                 UserInfo author = getAuthor(repo, git);
150                 UserInfo committer = getCommitter(repo, git);
151 
152                 CommitCommand command = git.commit()
153                         .setMessage(message)
154                         .setAuthor(author.name, author.email)
155                         .setCommitter(committer.name, committer.email)
156                         .setSign(parameters.getBoolean(CommandParameter.SCM_COMMIT_SIGN, true));
157                 RevCommit commitRev = command.call();
158 
159                 logger.info("commit done: " + commitRev.getShortMessage());
160                 checkedInFiles = JGitUtils.getFilesInCommit(git.getRepository(), commitRev, fileSet.getBasedir());
161                 if (logger.isDebugEnabled()) {
162                     for (ScmFile scmFile : checkedInFiles) {
163                         logger.debug("in commit: " + scmFile);
164                     }
165                 }
166             } else {
167                 logger.info("nothing to commit");
168             }
169 
170             if (repo.isPushChanges()) {
171                 String branch = version != null ? version.getName() : null;
172                 if (StringUtils.isBlank(branch)) {
173                     branch = git.getRepository().getBranch();
174                 }
175                 RefSpec refSpec = new RefSpec(Constants.R_HEADS + branch + ":" + Constants.R_HEADS + branch);
176                 logger.info("push changes to remote... " + refSpec);
177                 TransportConfigCallback transportConfigCallback = new JGitTransportConfigCallback(
178                         sshSessionFactorySupplier.apply((GitScmProviderRepository) repo, logger));
179 
180                 JGitUtils.push(
181                         git,
182                         (GitScmProviderRepository) repo,
183                         refSpec,
184                         EnumSet.of(RemoteRefUpdate.Status.OK, RemoteRefUpdate.Status.UP_TO_DATE),
185                         Optional.of(transportConfigCallback));
186             }
187 
188             return new CheckInScmResult("JGit checkin", checkedInFiles);
189         } catch (PushException e) {
190             logger.debug("Failed to push commits", e);
191             return new CheckInScmResult("JGit checkin", "Failed to push changes: " + e.getMessage(), "", false);
192         } catch (Exception e) {
193             throw new ScmException("JGit checkin failure!", e);
194         } finally {
195             JGitUtils.closeRepo(git);
196         }
197     }
198 
199     /**
200      * {@inheritDoc}
201      */
202     protected CheckInScmResult executeCheckInCommand(
203             ScmProviderRepository repo, ScmFileSet fileSet, String message, ScmVersion version) throws ScmException {
204 
205         CommandParameters parameters = new CommandParameters();
206         parameters.setString(CommandParameter.MESSAGE, message);
207         parameters.setScmVersion(CommandParameter.SCM_VERSION, version);
208         return executeCommand(repo, fileSet, parameters);
209     }
210 
211     private static final class UserInfo {
212 
213         final String name;
214 
215         final String email;
216 
217         UserInfo(String name, String email) {
218             this.name = name;
219             this.email = email;
220         }
221     }
222 
223     private UserInfo getCommitter(ScmProviderRepository repo, Git git) {
224         boolean forceMvnUser = git.getRepository().getConfig().getBoolean(GIT_MAVEN_SECTION, GIT_FORCE, false);
225 
226         // git config
227         UserConfig user = git.getRepository().getConfig().get(UserConfig.KEY);
228         String committerName = null;
229         if (!forceMvnUser && !user.isCommitterNameImplicit()) {
230             committerName = user.getCommitterName();
231         }
232 
233         // mvn parameter
234         if (StringUtils.isBlank(committerName)) {
235             committerName = repo.getUser();
236         }
237 
238         // git default
239         if (StringUtils.isBlank(committerName)) {
240             committerName = user.getCommitterName();
241         }
242 
243         // git config
244         String committerMail = null;
245         if (!user.isCommitterEmailImplicit()) {
246             committerMail = user.getCommitterEmail();
247         }
248 
249         if (StringUtils.isBlank(committerMail)) {
250             String defaultDomain = git.getRepository().getConfig().getString(GIT_MAVEN_SECTION, null, GIT_MAILDOMAIN);
251             defaultDomain = StringUtils.isNotBlank(defaultDomain) ? defaultDomain : getHostname();
252 
253             // mvn parameter (constructed with username) or git default
254             committerMail = StringUtils.isNotBlank(repo.getUser())
255                     ? repo.getUser() + "@" + defaultDomain
256                     : user.getCommitterEmail();
257         }
258 
259         return new UserInfo(committerName, committerMail);
260     }
261 
262     private UserInfo getAuthor(ScmProviderRepository repo, Git git) {
263         boolean forceMvnUser = git.getRepository().getConfig().getBoolean(GIT_MAVEN_SECTION, GIT_FORCE, false);
264 
265         // git config
266         UserConfig user = git.getRepository().getConfig().get(UserConfig.KEY);
267         String authorName = null;
268         if (!forceMvnUser && !user.isAuthorNameImplicit()) {
269             authorName = user.getAuthorName();
270         }
271 
272         // mvn parameter
273         if (StringUtils.isBlank(authorName)) {
274             authorName = repo.getUser();
275         }
276 
277         // git default
278         if (StringUtils.isBlank(authorName)) {
279             authorName = user.getAuthorName();
280         }
281 
282         // git config
283         String authorMail = null;
284         if (!user.isAuthorEmailImplicit()) {
285             authorMail = user.getAuthorEmail();
286         }
287 
288         if (StringUtils.isBlank(authorMail)) {
289             String defaultDomain = git.getRepository().getConfig().getString(GIT_MAVEN_SECTION, null, GIT_MAILDOMAIN);
290             defaultDomain = StringUtils.isNotBlank(defaultDomain) ? defaultDomain : getHostname();
291 
292             // mvn parameter (constructed with username) or git default
293             authorMail = StringUtils.isNotBlank(repo.getUser())
294                     ? repo.getUser() + "@" + defaultDomain
295                     : user.getAuthorEmail();
296         }
297 
298         return new UserInfo(authorName, authorMail);
299     }
300 
301     private String getHostname() {
302         String hostname;
303         try {
304             InetAddress localhost = java.net.InetAddress.getLocalHost();
305             hostname = localhost.getHostName();
306         } catch (UnknownHostException e) {
307             logger.warn(
308                     "failed to resolve hostname to create mail address, " + "defaulting to 'maven-scm-provider-jgit'");
309             hostname = "maven-scm-provider-jgit";
310         }
311         return hostname;
312     }
313 }