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.shared.release.phase;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.io.File;
26  import java.io.IOException;
27  import java.nio.file.LinkOption;
28  import java.nio.file.Path;
29  import java.nio.file.Paths;
30  import java.util.List;
31  
32  import org.apache.maven.project.MavenProject;
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.ScmFileSet;
37  import org.apache.maven.scm.ScmTag;
38  import org.apache.maven.scm.command.checkout.CheckOutScmResult;
39  import org.apache.maven.scm.manager.NoSuchScmProviderException;
40  import org.apache.maven.scm.provider.ScmProvider;
41  import org.apache.maven.scm.repository.ScmRepository;
42  import org.apache.maven.scm.repository.ScmRepositoryException;
43  import org.apache.maven.shared.release.ReleaseExecutionException;
44  import org.apache.maven.shared.release.ReleaseFailureException;
45  import org.apache.maven.shared.release.ReleaseResult;
46  import org.apache.maven.shared.release.config.ReleaseDescriptor;
47  import org.apache.maven.shared.release.env.ReleaseEnvironment;
48  import org.apache.maven.shared.release.scm.ReleaseScmCommandException;
49  import org.apache.maven.shared.release.scm.ReleaseScmRepositoryException;
50  import org.apache.maven.shared.release.scm.ScmRepositoryConfigurator;
51  import org.apache.maven.shared.release.util.ReleaseUtil;
52  import org.codehaus.plexus.util.FileUtils;
53  
54  import static java.util.Objects.requireNonNull;
55  import static org.apache.maven.shared.utils.logging.MessageUtils.buffer;
56  
57  /**
58   * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
59   */
60  @Singleton
61  @Named("checkout-project-from-scm")
62  public class CheckoutProjectFromScm extends AbstractReleasePhase {
63      /**
64       * Tool that gets a configured SCM repository from release configuration.
65       */
66      private final ScmRepositoryConfigurator scmRepositoryConfigurator;
67  
68      @Inject
69      public CheckoutProjectFromScm(ScmRepositoryConfigurator scmRepositoryConfigurator) {
70          this.scmRepositoryConfigurator = requireNonNull(scmRepositoryConfigurator);
71      }
72  
73      @Override
74      public ReleaseResult execute(
75              ReleaseDescriptor releaseDescriptor,
76              ReleaseEnvironment releaseEnvironment,
77              List<MavenProject> reactorProjects)
78              throws ReleaseExecutionException, ReleaseFailureException {
79          ReleaseResult releaseResult;
80  
81          if (releaseDescriptor.isLocalCheckout()) {
82              // in the release phase we have to change the checkout URL
83              // to do a local checkout instead of going over the network.
84  
85              // the first step is a bit tricky, we need to know which provider! like e.g. "scm:jgit:http://"
86              // the offset of 4 is because 'scm:' has 4 characters...
87              String providerPart = releaseDescriptor
88                      .getScmSourceUrl()
89                      .substring(0, releaseDescriptor.getScmSourceUrl().indexOf(':', 4));
90  
91              String scmPath = releaseDescriptor.getWorkingDirectory();
92  
93              // now we iteratively try to checkout.
94              // if the local checkout fails, then we might be in a subdirectory
95              // and need to walk a few directories up.
96              do {
97                  try {
98                      if (scmPath.startsWith("/")) {
99                          // cut off the first '/'
100                         scmPath = scmPath.substring(1);
101                     }
102 
103                     String scmUrl = providerPart + ":file:///" + scmPath;
104                     releaseDescriptor.setScmSourceUrl(scmUrl);
105                     getLogger().info("Performing a LOCAL checkout from " + releaseDescriptor.getScmSourceUrl());
106 
107                     releaseResult = performCheckout(releaseDescriptor, releaseEnvironment, reactorProjects);
108                 } catch (ScmException scmEx) {
109                     // the checkout from _this_ directory failed
110                     releaseResult = null;
111                 }
112 
113                 if (releaseResult == null || releaseResult.getResultCode() == ReleaseResult.ERROR) {
114                     // this means that there is no SCM repo in this directory
115                     // thus we try to step one directory up
116                     releaseResult = null;
117 
118                     // remove last sub-directory path
119                     int lastSlashPos = scmPath.lastIndexOf(File.separator);
120                     if (lastSlashPos > 0) {
121                         scmPath = scmPath.substring(0, lastSlashPos);
122                     } else {
123                         throw new ReleaseExecutionException("could not perform a local checkout");
124                     }
125                 }
126             } while (releaseResult == null);
127         } else {
128             // when there is no localCheckout, then we just do a standard SCM checkout.
129             try {
130                 releaseResult = performCheckout(releaseDescriptor, releaseEnvironment, reactorProjects);
131             } catch (ScmException e) {
132                 releaseResult = new ReleaseResult();
133                 releaseResult.setResultCode(ReleaseResult.ERROR);
134                 logError(releaseResult, e.getMessage());
135 
136                 throw new ReleaseExecutionException(
137                         "An error is occurred in the checkout process: " + e.getMessage(), e);
138             }
139         }
140 
141         return releaseResult;
142     }
143 
144     private ReleaseResult performCheckout(
145             ReleaseDescriptor releaseDescriptor,
146             ReleaseEnvironment releaseEnvironment,
147             List<MavenProject> reactorProjects)
148             throws ReleaseExecutionException, ReleaseFailureException, ScmException {
149         ReleaseResult result = new ReleaseResult();
150 
151         logInfo(result, "Checking out the project to perform the release ...");
152 
153         ScmRepository repository;
154         ScmProvider provider;
155 
156         try {
157             repository = scmRepositoryConfigurator.getConfiguredRepository(
158                     releaseDescriptor, releaseEnvironment.getSettings());
159 
160             provider = scmRepositoryConfigurator.getRepositoryProvider(repository);
161         } catch (ScmRepositoryException e) {
162             result.setResultCode(ReleaseResult.ERROR);
163             logError(result, e.getMessage());
164 
165             throw new ReleaseScmRepositoryException(e.getMessage(), e.getValidationMessages());
166         } catch (NoSuchScmProviderException e) {
167             result.setResultCode(ReleaseResult.ERROR);
168             logError(result, e.getMessage());
169 
170             throw new ReleaseExecutionException("Unable to configure SCM repository: " + e.getMessage(), e);
171         }
172 
173         MavenProject rootProject = ReleaseUtil.getRootProject(reactorProjects);
174 
175         // TODO: sanity check that it is not . or .. or lower
176         File checkoutDirectory =
177                 FileUtils.resolveFile(rootProject.getBasedir(), releaseDescriptor.getCheckoutDirectory());
178 
179         if (checkoutDirectory.exists()) {
180             try {
181                 FileUtils.deleteDirectory(checkoutDirectory);
182             } catch (IOException e) {
183                 result.setResultCode(ReleaseResult.ERROR);
184                 logError(result, e.getMessage());
185 
186                 throw new ReleaseExecutionException("Unable to remove old checkout directory: " + e.getMessage(), e);
187             }
188         }
189 
190         checkoutDirectory.mkdirs();
191 
192         CommandParameters commandParameters = new CommandParameters();
193         commandParameters.setString(
194                 CommandParameter.SHALLOW,
195                 Boolean.valueOf(releaseDescriptor.isScmShallowClone()).toString());
196 
197         CheckOutScmResult scmResult = provider.checkOut(
198                 repository,
199                 new ScmFileSet(checkoutDirectory),
200                 new ScmTag(releaseDescriptor.getScmReleaseLabel()),
201                 commandParameters);
202 
203         if (releaseDescriptor.isLocalCheckout() && !scmResult.isSuccess()) {
204             // this is not beautiful but needed to indicate that the execute() method
205             // should continue in the parent directory
206             return null;
207         }
208 
209         String scmRelativePathProjectDirectory = scmResult.getRelativePathProjectDirectory();
210         if (scmRelativePathProjectDirectory == null || scmRelativePathProjectDirectory.isEmpty()) {
211             Path workingDirectory = Paths.get(releaseDescriptor.getWorkingDirectory());
212 
213             Path rootProjectBasedir;
214             try {
215                 rootProjectBasedir = rootProject.getBasedir().toPath().toRealPath(LinkOption.NOFOLLOW_LINKS);
216             } catch (IOException e) {
217                 throw new ReleaseExecutionException(e.getMessage(), e);
218             }
219 
220             scmRelativePathProjectDirectory =
221                     workingDirectory.relativize(rootProjectBasedir).toString();
222         }
223         releaseDescriptor.setScmRelativePathProjectDirectory(scmRelativePathProjectDirectory);
224 
225         if (!scmResult.isSuccess()) {
226             result.setResultCode(ReleaseResult.ERROR);
227             logError(result, scmResult.getProviderMessage());
228 
229             throw new ReleaseScmCommandException("Unable to checkout from SCM", scmResult);
230         }
231 
232         result.setResultCode(ReleaseResult.SUCCESS);
233 
234         return result;
235     }
236 
237     @Override
238     public ReleaseResult simulate(
239             ReleaseDescriptor releaseDescriptor,
240             ReleaseEnvironment releaseEnvironment,
241             List<MavenProject> reactorProjects)
242             throws ReleaseExecutionException, ReleaseFailureException {
243         ReleaseResult result = new ReleaseResult();
244 
245         MavenProject rootProject = ReleaseUtil.getRootProject(reactorProjects);
246         File checkoutDirectory =
247                 FileUtils.resolveFile(rootProject.getBasedir(), releaseDescriptor.getCheckoutDirectory());
248 
249         if (releaseDescriptor.isLocalCheckout()) {
250             logInfo(
251                     result,
252                     "The project would have a " + buffer().strong("local") + " check out to perform the release from "
253                             + checkoutDirectory + "...");
254         } else {
255             logInfo(
256                     result,
257                     "The project would be checked out to perform the release from " + checkoutDirectory + "...");
258         }
259 
260         result.setResultCode(ReleaseResult.SUCCESS);
261         return result;
262     }
263 }