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.report.projectinfo;
20  
21  import javax.inject.Inject;
22  
23  import java.util.ArrayList;
24  import java.util.List;
25  import java.util.Locale;
26  
27  import org.apache.maven.doxia.sink.Sink;
28  import org.apache.maven.model.Model;
29  import org.apache.maven.model.Scm;
30  import org.apache.maven.plugin.logging.Log;
31  import org.apache.maven.plugins.annotations.Mojo;
32  import org.apache.maven.plugins.annotations.Parameter;
33  import org.apache.maven.project.ProjectBuilder;
34  import org.apache.maven.reporting.MavenReportException;
35  import org.apache.maven.repository.RepositorySystem;
36  import org.apache.maven.scm.manager.ScmManager;
37  import org.apache.maven.scm.provider.git.repository.GitScmProviderRepository;
38  import org.apache.maven.scm.provider.hg.repository.HgScmProviderRepository;
39  import org.apache.maven.scm.provider.svn.repository.SvnScmProviderRepository;
40  import org.apache.maven.scm.repository.ScmRepository;
41  import org.codehaus.plexus.i18n.I18N;
42  import org.codehaus.plexus.util.StringUtils;
43  
44  /**
45   * Generates the Project Source Code Management (SCM) report.
46   *
47   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton </a>
48   * @since 2.0
49   */
50  @Mojo(name = "scm")
51  public class ScmReport extends AbstractProjectInfoReport {
52      // ----------------------------------------------------------------------
53      // Mojo parameters
54      // ----------------------------------------------------------------------
55  
56      /**
57       * The directory name to check out right after the SCM URL.
58       */
59      @Parameter(defaultValue = "${project.artifactId}")
60      private String checkoutDirectoryName;
61  
62      /**
63       * The SCM anonymous connection url respecting the SCM URL Format.
64       *
65       * @see <a href="http://maven.apache.org/scm/scm-url-format.html">SCM URL Format</a>
66       * @since 2.1
67       */
68      @Parameter(defaultValue = "${project.scm.connection}")
69      private String anonymousConnection;
70  
71      /**
72       * The SCM developer connection url respecting the SCM URL Format.
73       *
74       * @see <a href="http://maven.apache.org/scm/scm-url-format.html">SCM URL Format</a>
75       * @since 2.1
76       */
77      @Parameter(defaultValue = "${project.scm.developerConnection}")
78      private String developerConnection;
79  
80      /**
81       * The SCM web access url.
82       *
83       * @since 2.1
84       */
85      @Parameter(defaultValue = "${project.scm.url}")
86      private String webAccessUrl;
87  
88      /**
89       * The SCM tag.
90       *
91       * @since 2.8
92       */
93      @Parameter(defaultValue = "${project.scm.tag}")
94      private String scmTag;
95  
96      /**
97       * Maven SCM Manager.
98       */
99      protected final ScmManager scmManager;
100 
101     @Inject
102     public ScmReport(
103             RepositorySystem repositorySystem, I18N i18n, ProjectBuilder projectBuilder, ScmManager scmManager) {
104         super(repositorySystem, i18n, projectBuilder);
105         this.scmManager = scmManager;
106     }
107 
108     // ----------------------------------------------------------------------
109     // Public methods
110     // ----------------------------------------------------------------------
111 
112     @Override
113     public boolean canGenerateReport() throws MavenReportException {
114         boolean result = super.canGenerateReport();
115         if (result && skipEmptyReport) {
116             Scm scm = getProject().getModel().getScm();
117             result = scm != null;
118 
119             if (result
120                     && (anonymousConnection == null || anonymousConnection.isEmpty())
121                     && (developerConnection == null || developerConnection.isEmpty())
122                     && StringUtils.isEmpty(scm.getUrl())) {
123                 result = false;
124             }
125         }
126 
127         return result;
128     }
129 
130     @Override
131     public void executeReport(Locale locale) {
132         ScmRenderer r = new ScmRenderer(
133                 getLog(),
134                 scmManager,
135                 getSink(),
136                 getProject().getModel(),
137                 getI18N(locale),
138                 locale,
139                 checkoutDirectoryName,
140                 webAccessUrl,
141                 anonymousConnection,
142                 developerConnection,
143                 scmTag);
144 
145         r.render();
146     }
147 
148     /** {@inheritDoc} */
149     public String getOutputName() {
150         return "scm";
151     }
152 
153     @Override
154     protected String getI18Nsection() {
155         return "scm";
156     }
157 
158     // ----------------------------------------------------------------------
159     // Private
160     // ----------------------------------------------------------------------
161 
162     /**
163      * Internal renderer class
164      */
165     private static class ScmRenderer extends AbstractProjectInfoRenderer {
166         private static final String LS = System.lineSeparator();
167 
168         private Log log;
169 
170         private Model model;
171 
172         private ScmManager scmManager;
173 
174         /**
175          * To support more SCM
176          */
177         private String anonymousConnection;
178 
179         private String devConnection;
180 
181         private String checkoutDirectoryName;
182 
183         private String webAccessUrl;
184 
185         private String scmTag;
186 
187         ScmRenderer(
188                 Log log,
189                 ScmManager scmManager,
190                 Sink sink,
191                 Model model,
192                 I18N i18n,
193                 Locale locale,
194                 String checkoutDirName,
195                 String webAccessUrl,
196                 String anonymousConnection,
197                 String devConnection,
198                 String scmTag) {
199             super(sink, i18n, locale);
200 
201             this.log = log;
202 
203             this.scmManager = scmManager;
204 
205             this.model = model;
206 
207             this.checkoutDirectoryName = checkoutDirName;
208 
209             this.webAccessUrl = webAccessUrl;
210 
211             this.anonymousConnection = anonymousConnection;
212 
213             this.devConnection = devConnection;
214 
215             this.scmTag = scmTag;
216         }
217 
218         @Override
219         protected String getI18Nsection() {
220             return "scm";
221         }
222 
223         @Override
224         protected void renderBody() {
225             Scm scm = model.getScm();
226             if (scm == null
227                     || (anonymousConnection == null || anonymousConnection.isEmpty())
228                             && (devConnection == null || devConnection.isEmpty())
229                             && StringUtils.isEmpty(scm.getUrl())) {
230                 startSection(getTitle());
231 
232                 paragraph(getI18nString("noscm"));
233 
234                 endSection();
235 
236                 return;
237             }
238 
239             ScmRepository anonymousRepository = getScmRepository(anonymousConnection);
240             ScmRepository devRepository = getScmRepository(devConnection);
241 
242             // Overview section
243             renderOverviewSection(anonymousRepository, devRepository);
244 
245             // Web access section
246             renderWebAccessSection(webAccessUrl);
247 
248             // Anonymous access section if needed
249             renderAnonymousAccessSection(anonymousRepository);
250 
251             // Developer access section
252             renderDeveloperAccessSection(devRepository);
253 
254             // Access from behind a firewall section if needed
255             renderAccessBehindFirewallSection(devRepository);
256 
257             // Access through a proxy section if needed
258             renderAccessThroughProxySection(anonymousRepository, devRepository);
259         }
260 
261         /**
262          * Render the overview section
263          *
264          * @param anonymousRepository the anonymous repository
265          * @param devRepository the developer repository
266          */
267         private void renderOverviewSection(ScmRepository anonymousRepository, ScmRepository devRepository) {
268             startSection(getI18nString("overview.title"));
269 
270             if (isScmSystem(anonymousRepository, "git") || isScmSystem(devRepository, "git")) {
271                 sink.paragraph();
272                 linkPatternedText(getI18nString("git.intro"));
273                 sink.paragraph_();
274             } else if (isScmSystem(anonymousRepository, "hg") || isScmSystem(devRepository, "hg")) {
275                 sink.paragraph();
276                 linkPatternedText(getI18nString("hg.intro"));
277                 sink.paragraph_();
278             } else if (isScmSystem(anonymousRepository, "svn") || isScmSystem(devRepository, "svn")) {
279                 sink.paragraph();
280                 linkPatternedText(getI18nString("svn.intro"));
281                 sink.paragraph_();
282             } else {
283                 paragraph(getI18nString("general.intro"));
284             }
285 
286             endSection();
287         }
288 
289         /**
290          * Render the web access section
291          *
292          * @param scmUrl The URL to the project's browsable repository.
293          */
294         private void renderWebAccessSection(String scmUrl) {
295             startSection(getI18nString("webaccess.title"));
296 
297             if (scmUrl == null || scmUrl.isEmpty()) {
298                 paragraph(getI18nString("webaccess.nourl"));
299             } else {
300                 paragraph(getI18nString("webaccess.url"));
301 
302                 verbatimLink(scmUrl, scmUrl);
303             }
304 
305             endSection();
306         }
307 
308         /**
309          * Render the anonymous access section depending the repository.
310          * <p>
311          * Note: ClearCase, Starteam et Perforce seems to have no anonymous access.
312          * </p>
313          *
314          * @param anonymousRepository the anonymous repository
315          */
316         private void renderAnonymousAccessSection(ScmRepository anonymousRepository) {
317             if (anonymousConnection == null || anonymousConnection.isEmpty()) {
318                 return;
319             }
320 
321             startSection(getI18nString("anonymousaccess.title"));
322 
323             if (anonymousRepository != null && isScmSystem(anonymousRepository, "git")) {
324                 GitScmProviderRepository gitRepo =
325                         (GitScmProviderRepository) anonymousRepository.getProviderRepository();
326 
327                 anonymousAccessGit(gitRepo);
328             } else if (anonymousRepository != null && isScmSystem(anonymousRepository, "hg")) {
329                 HgScmProviderRepository hgRepo = (HgScmProviderRepository) anonymousRepository.getProviderRepository();
330 
331                 anonymousAccessMercurial(hgRepo);
332             } else if (anonymousRepository != null && isScmSystem(anonymousRepository, "svn")) {
333                 SvnScmProviderRepository svnRepo =
334                         (SvnScmProviderRepository) anonymousRepository.getProviderRepository();
335 
336                 anonymousAccessSubversion(svnRepo);
337             } else {
338                 paragraph(getI18nString("anonymousaccess.general.intro"));
339 
340                 verbatimText(anonymousConnection.substring(4));
341             }
342 
343             endSection();
344         }
345 
346         /**
347          * Render the developer access section
348          *
349          * @param devRepository the dev repository
350          */
351         private void renderDeveloperAccessSection(ScmRepository devRepository) {
352             if (devConnection == null || devConnection.isEmpty()) {
353                 return;
354             }
355 
356             startSection(getI18nString("devaccess.title"));
357 
358             if (devRepository != null && isScmSystem(devRepository, "git")) {
359                 GitScmProviderRepository gitRepo = (GitScmProviderRepository) devRepository.getProviderRepository();
360 
361                 developerAccessGit(gitRepo);
362             } else if (devRepository != null && isScmSystem(devRepository, "hg")) {
363                 HgScmProviderRepository hgRepo = (HgScmProviderRepository) devRepository.getProviderRepository();
364 
365                 developerAccessMercurial(hgRepo);
366             } else if (devRepository != null && isScmSystem(devRepository, "svn")) {
367                 SvnScmProviderRepository svnRepo = (SvnScmProviderRepository) devRepository.getProviderRepository();
368 
369                 developerAccessSubversion(svnRepo);
370             } else {
371                 paragraph(getI18nString("devaccess.general.intro"));
372 
373                 verbatimText(devConnection.substring(4));
374             }
375 
376             endSection();
377         }
378 
379         /**
380          * Render the access from behind a firewall section
381          *
382          * @param devRepository the dev repository
383          */
384         private void renderAccessBehindFirewallSection(ScmRepository devRepository) {
385             startSection(getI18nString("accessbehindfirewall.title"));
386 
387             if (devRepository != null && isScmSystem(devRepository, "svn")) {
388                 SvnScmProviderRepository svnRepo = (SvnScmProviderRepository) devRepository.getProviderRepository();
389 
390                 paragraph(getI18nString("accessbehindfirewall.svn.intro"));
391 
392                 verbatimText("$ svn checkout " + svnRepo.getUrl() + " " + checkoutDirectoryName);
393             } else {
394                 paragraph(getI18nString("accessbehindfirewall.general.intro"));
395             }
396 
397             endSection();
398         }
399 
400         /**
401          * Render the access from behind a firewall section
402          *
403          * @param anonymousRepository the anonymous repository
404          * @param devRepository the dev repository
405          */
406         private void renderAccessThroughProxySection(ScmRepository anonymousRepository, ScmRepository devRepository) {
407             if (isScmSystem(anonymousRepository, "svn") || isScmSystem(devRepository, "svn")) {
408                 startSection(getI18nString("accessthroughtproxy.title"));
409 
410                 paragraph(getI18nString("accessthroughtproxy.svn.intro1"));
411                 paragraph(getI18nString("accessthroughtproxy.svn.intro2"));
412                 paragraph(getI18nString("accessthroughtproxy.svn.intro3"));
413 
414                 verbatimText(
415                         "[global]" + LS + "http-proxy-host = your.proxy.name" + LS + "http-proxy-port = 3128" + LS);
416 
417                 endSection();
418             }
419         }
420 
421         // Git
422 
423         private void gitClone(String url) {
424             // in the future, git scm url should support both repository + path: at the moment, require a hack
425             // to remove path added to repository
426             int index = url.indexOf(".git/");
427             if (index > 0) {
428                 url = url.substring(0, index + 4);
429             }
430 
431             boolean head = (scmTag == null || scmTag.isEmpty()) || "HEAD".equals(scmTag);
432             verbatimText("$ git clone " + (head ? "" : ("--branch " + scmTag + ' ')) + url);
433         }
434 
435         /**
436          * Create the documentation to provide an anonymous access with a <code>Git</code> SCM. For example, generate
437          * the following command line:
438          * <p>
439          * git clone uri
440          * </p>
441          *
442          * @param gitRepo
443          */
444         private void anonymousAccessGit(GitScmProviderRepository gitRepo) {
445             sink.paragraph();
446             linkPatternedText(getI18nString("anonymousaccess.git.intro"));
447             sink.paragraph_();
448 
449             gitClone(gitRepo.getFetchUrl());
450         }
451 
452         // Mercurial
453 
454         /**
455          * Create the documentation to provide an anonymous access with a <code>Mercurial</code> SCM. For example,
456          * generate the following command line:
457          * <p>
458          * hg clone uri
459          * </p>
460          *
461          * @param hgRepo
462          */
463         private void anonymousAccessMercurial(HgScmProviderRepository hgRepo) {
464             sink.paragraph();
465             linkPatternedText(getI18nString("anonymousaccess.hg.intro"));
466             sink.paragraph_();
467 
468             verbatimText("$ hg clone " + hgRepo.getURI());
469         }
470 
471         // Git
472 
473         /**
474          * Create the documentation to provide an developer access with a <code>Git</code> SCM. For example, generate
475          * the following command line:
476          * <p>
477          * git clone repo
478          * </p>
479          *
480          * @param gitRepo
481          */
482         private void developerAccessGit(GitScmProviderRepository gitRepo) {
483             sink.paragraph();
484             linkPatternedText(getI18nString("devaccess.git.intro"));
485             sink.paragraph_();
486 
487             gitClone(gitRepo.getPushUrl());
488         }
489 
490         // Mercurial
491 
492         /**
493          * Create the documentation to provide an developer access with a <code>Mercurial</code> SCM. For example,
494          * generate the following command line:
495          * <p>
496          * hg clone repo
497          * </p>
498          *
499          * @param hgRepo
500          */
501         private void developerAccessMercurial(HgScmProviderRepository hgRepo) {
502             sink.paragraph();
503             linkPatternedText(getI18nString("devaccess.hg.intro"));
504             sink.paragraph_();
505 
506             verbatimText("$ hg clone " + hgRepo.getURI());
507         }
508 
509         // Subversion
510 
511         /**
512          * Create the documentation to provide an anonymous access with a <code>Subversion</code>
513          * SCM. For example, generate the following command line:
514          * <p>
515          * svn checkout http://svn.apache.org/repos/asf/maven/components/trunk maven
516          * </p>
517          *
518          * @param svnRepo
519          * @see <a href="http://svnbook.red-bean.com/">http://svnbook.red-bean.com/</a>
520          */
521         private void anonymousAccessSubversion(SvnScmProviderRepository svnRepo) {
522             paragraph(getI18nString("anonymousaccess.svn.intro"));
523 
524             verbatimText("$ svn checkout " + svnRepo.getUrl() + " " + checkoutDirectoryName);
525         }
526 
527         /**
528          * Create the documentation to provide an developer access with a <code>Subversion</code>
529          * SCM. For example, generate the following command line:
530          * <p>
531          * svn checkout https://svn.apache.org/repos/asf/maven/components/trunk maven
532          * </p>
533          * <p>
534          * svn commit --username your-username -m "A message"
535          * </p>
536          *
537          * @param svnRepo
538          * @see <a href="http://svnbook.red-bean.com/">http://svnbook.red-bean.com/</a>
539          */
540         private void developerAccessSubversion(SvnScmProviderRepository svnRepo) {
541             if (svnRepo.getUrl() != null) {
542                 if (svnRepo.getUrl().startsWith("https://")) {
543                     paragraph(getI18nString("devaccess.svn.intro1.https"));
544                 } else if (svnRepo.getUrl().startsWith("svn://")) {
545                     paragraph(getI18nString("devaccess.svn.intro1.svn"));
546                 } else if (svnRepo.getUrl().startsWith("svn+ssh://")) {
547                     paragraph(getI18nString("devaccess.svn.intro1.svnssh"));
548                 } else {
549                     paragraph(getI18nString("devaccess.svn.intro1.other"));
550                 }
551             }
552 
553             StringBuilder sb = new StringBuilder();
554 
555             sb.append("$ svn checkout ").append(svnRepo.getUrl()).append(" ").append(checkoutDirectoryName);
556 
557             verbatimText(sb.toString());
558 
559             paragraph(getI18nString("devaccess.svn.intro2"));
560 
561             sb = new StringBuilder();
562             sb.append("$ svn commit --username your-username -m \"A message\"");
563 
564             verbatimText(sb.toString());
565         }
566 
567         /**
568          * Return a <code>SCM repository</code> defined by a given url
569          *
570          * @param scmUrl an SCM URL
571          * @return a valid SCM repository or null
572          */
573         public ScmRepository getScmRepository(String scmUrl) {
574             if (scmUrl == null || scmUrl.isEmpty()) {
575                 return null;
576             }
577 
578             ScmRepository repo = null;
579             List<String> messages = new ArrayList<>();
580             try {
581                 messages.addAll(scmManager.validateScmRepository(scmUrl));
582             } catch (Exception e) {
583                 messages.add(e.getMessage());
584             }
585 
586             if (!messages.isEmpty()) {
587                 StringBuilder sb = new StringBuilder();
588                 boolean isIntroAdded = false;
589                 for (String msg : messages) {
590                     // Ignore NoSuchScmProviderException msg
591                     // See impl of AbstractScmManager#validateScmRepository()
592                     if (msg.startsWith("No such provider")) {
593                         continue;
594                     }
595 
596                     if (!isIntroAdded) {
597                         sb.append("This SCM url '");
598                         sb.append(scmUrl);
599                         sb.append("' is invalid due to the following errors:");
600                         sb.append(LS);
601                         isIntroAdded = true;
602                     }
603                     sb.append(" * ");
604                     sb.append(msg);
605                     sb.append(LS);
606                 }
607 
608                 if (StringUtils.isNotEmpty(sb.toString())) {
609                     sb.append("For more information about SCM URL Format, please refer to: "
610                             + "http://maven.apache.org/scm/scm-url-format.html");
611 
612                     throw new IllegalArgumentException(sb.toString());
613                 }
614             }
615 
616             try {
617                 repo = scmManager.makeScmRepository(scmUrl);
618             } catch (Exception e) {
619                 // Should be already catched
620                 if (log.isDebugEnabled()) {
621                     log.debug(e.getMessage(), e);
622                 }
623             }
624 
625             return repo;
626         }
627 
628         /**
629          * Convenience method that return true is the defined <code>SCM repository</code> is a known provider.
630          * <p>
631          * Currently, we fully support ClearCase, CVS, Git, Perforce, Mercurial, Starteam and Subversion
632          * by the maven-scm-providers component.
633          * </p>
634          *
635          * @param scmRepository a SCM repository
636          * @param scmProvider a SCM provider name
637          * @return true if the provider of the given SCM repository is equal to the given scm provider.
638          * @see <a href="http://svn.apache.org/repos/asf/maven/scm/trunk/maven-scm-providers/">maven-scm-providers</a>
639          */
640         private static boolean isScmSystem(ScmRepository scmRepository, String scmProvider) {
641             if (scmProvider == null || scmProvider.isEmpty()) {
642                 return false;
643             }
644             return scmRepository != null && scmProvider.equalsIgnoreCase(scmRepository.getProvider());
645         }
646     }
647 }