1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugins.changelog;
20
21 import java.io.BufferedOutputStream;
22 import java.io.BufferedReader;
23 import java.io.File;
24 import java.io.FileNotFoundException;
25 import java.io.IOException;
26 import java.io.InputStreamReader;
27 import java.io.OutputStreamWriter;
28 import java.io.StringReader;
29 import java.io.UnsupportedEncodingException;
30 import java.io.Writer;
31 import java.net.URLEncoder;
32 import java.nio.file.Files;
33 import java.text.ParseException;
34 import java.text.SimpleDateFormat;
35 import java.util.ArrayList;
36 import java.util.Collection;
37 import java.util.Date;
38 import java.util.HashMap;
39 import java.util.Iterator;
40 import java.util.LinkedList;
41 import java.util.List;
42 import java.util.Locale;
43 import java.util.Map;
44 import java.util.Properties;
45 import java.util.ResourceBundle;
46 import java.util.StringTokenizer;
47 import java.util.regex.Matcher;
48 import java.util.regex.Pattern;
49
50 import org.apache.maven.doxia.sink.Sink;
51 import org.apache.maven.doxia.siterenderer.Renderer;
52 import org.apache.maven.model.Developer;
53 import org.apache.maven.plugin.MojoExecutionException;
54 import org.apache.maven.plugins.annotations.Component;
55 import org.apache.maven.plugins.annotations.Mojo;
56 import org.apache.maven.plugins.annotations.Parameter;
57 import org.apache.maven.plugins.changelog.scm.provider.svn.svnexe.command.info.SvnInfoCommandExpanded;
58 import org.apache.maven.project.MavenProject;
59 import org.apache.maven.reporting.AbstractMavenReport;
60 import org.apache.maven.reporting.MavenReportException;
61 import org.apache.maven.scm.ChangeFile;
62 import org.apache.maven.scm.ChangeSet;
63 import org.apache.maven.scm.ScmBranch;
64 import org.apache.maven.scm.ScmException;
65 import org.apache.maven.scm.ScmFileSet;
66 import org.apache.maven.scm.ScmResult;
67 import org.apache.maven.scm.ScmRevision;
68 import org.apache.maven.scm.command.changelog.ChangeLogScmResult;
69 import org.apache.maven.scm.command.changelog.ChangeLogSet;
70 import org.apache.maven.scm.command.info.InfoItem;
71 import org.apache.maven.scm.command.info.InfoScmResult;
72 import org.apache.maven.scm.manager.ScmManager;
73 import org.apache.maven.scm.provider.ScmProvider;
74 import org.apache.maven.scm.provider.ScmProviderRepository;
75 import org.apache.maven.scm.provider.ScmProviderRepositoryWithHost;
76 import org.apache.maven.scm.provider.svn.repository.SvnScmProviderRepository;
77 import org.apache.maven.scm.repository.ScmRepository;
78 import org.apache.maven.settings.Server;
79 import org.apache.maven.settings.Settings;
80
81
82
83
84 @Mojo(name = "changelog")
85 public class ChangeLogReport extends AbstractMavenReport {
86
87
88
89
90 private static final String FILE_TOKEN = "%FILE%";
91
92
93
94
95
96 private static final String ISSUE_TOKEN = "%ISSUE%";
97
98
99
100
101
102
103 private static final String REV_TOKEN = "%REV%";
104
105
106
107
108 private static final int DEFAULT_RANGE = 30;
109
110 public static final String DEFAULT_ISSUE_ID_REGEX_PATTERN = "[a-zA-Z]{2,}-\\d+";
111
112 private static final String DEFAULT_ISSUE_LINK_URL = "https://issues.apache.org/jira/browse/" + ISSUE_TOKEN;
113
114
115
116
117
118
119
120 @Parameter(property = "changelog.headingDateFormat", defaultValue = "yyyy-MM-dd")
121 private String headingDateFormat = "yyyy-MM-dd";
122
123
124
125
126 @Parameter(property = "changelog.type", defaultValue = "range", required = true)
127 private String type;
128
129
130
131
132 @Parameter(property = "changelog.range", defaultValue = "-1")
133 private int range;
134
135
136
137
138 @Parameter
139 private List<String> dates;
140
141
142
143
144 @Parameter
145 private List<String> tags;
146
147
148
149
150 @Parameter(property = "changelog.dateFormat", defaultValue = "yyyy-MM-dd HH:mm:ss", required = true)
151 private String dateFormat;
152
153
154
155
156 @Parameter(property = "basedir", required = true)
157 private File basedir;
158
159
160
161
162 @Parameter(defaultValue = "${project.build.directory}/changelog.xml", required = true)
163 private File outputXML;
164
165
166
167
168 @Parameter(property = "outputXMLExpiration", defaultValue = "60", required = true)
169 private int outputXMLExpiration;
170
171
172
173
174 @Parameter(property = "changelog.outputEncoding", defaultValue = "${project.reporting.outputEncoding}")
175 private String outputEncoding;
176
177
178
179
180 @Parameter(property = "username")
181 private String username;
182
183
184
185
186 @Parameter(property = "password")
187 private String password;
188
189
190
191
192 @Parameter(property = "privateKey")
193 private String privateKey;
194
195
196
197
198 @Parameter(property = "passphrase")
199 private String passphrase;
200
201
202
203
204 @Parameter(property = "tagBase")
205 private String tagBase;
206
207
208
209
210 @Parameter(property = "project.scm.url")
211 protected String scmUrl;
212
213
214
215
216
217
218
219 @Parameter(property = "changelog.skip", defaultValue = "false")
220 protected boolean skip;
221
222
223
224
225
226
227 @Parameter(property = "encodeFileUri", defaultValue = "false")
228 protected boolean encodeFileUri;
229
230
231
232
233
234
235 @Parameter
236 private String[] includes;
237
238
239
240
241
242
243 @Parameter
244 private String[] excludes;
245
246
247
248
249 @Parameter(defaultValue = "${project}", readonly = true, required = true)
250 private MavenProject project;
251
252
253
254 @Parameter(property = "settings.offline", required = true, readonly = true)
255 private boolean offline;
256
257
258
259 @Component
260 private ScmManager manager;
261
262
263
264 @Parameter(defaultValue = "${settings}", readonly = true, required = true)
265 private Settings settings;
266
267
268
269
270
271 @Parameter(defaultValue = "connection", required = true)
272 private String connectionType;
273
274
275
276
277 @Parameter(property = "omitFileAndRevision", defaultValue = "false")
278 private boolean omitFileAndRevision;
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295 @Parameter(property = "displayFileDetailUrl", defaultValue = "${project.scm.url}")
296 protected String displayFileDetailUrl;
297
298
299
300
301
302
303
304
305
306
307
308 @Parameter(property = "issueIDRegexPattern", defaultValue = DEFAULT_ISSUE_ID_REGEX_PATTERN, required = true)
309 private String issueIDRegexPattern;
310
311
312
313
314
315
316
317
318
319 @Parameter(property = "issueLinkUrl", defaultValue = DEFAULT_ISSUE_LINK_URL, required = true)
320 private String issueLinkUrl;
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339 @Parameter(property = "displayChangeSetDetailUrl")
340 protected String displayChangeSetDetailUrl;
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367 @Parameter(property = "displayFileRevDetailUrl")
368 protected String displayFileRevDetailUrl;
369
370
371
372
373
374
375 @Parameter(property = "project.developers")
376 protected List<Developer> developers;
377
378
379
380
381 @Parameter
382 private Map<String, String> providerImplementations;
383
384
385 private String rptRepository, rptOneRepoParam, rptMultiRepoParam;
386
387
388 private String connection;
389
390
391 private HashMap<String, Developer> developersById;
392
393
394 private HashMap<String, Developer> developersByName;
395
396
397
398
399 @Parameter
400 private Properties systemProperties;
401
402 private final Pattern sinkFileNamePattern = Pattern.compile("\\\\");
403
404
405
406
407 public void executeReport(Locale locale) throws MavenReportException {
408
409 if (!basedir.exists()) {
410 doGenerateEmptyReport(getBundle(locale), getSink());
411
412 return;
413 }
414
415 if (providerImplementations != null) {
416 for (Map.Entry<String, String> entry : providerImplementations.entrySet()) {
417 String providerType = entry.getKey();
418 String providerImplementation = entry.getValue();
419 getLog().info("Change the default '" + providerType + "' provider implementation to '"
420 + providerImplementation + "'.");
421 manager.setScmProviderImplementation(providerType, providerImplementation);
422 }
423 }
424
425 initializeDefaultConfigurationParameters();
426
427 initializeDeveloperMaps();
428
429 verifySCMTypeParams();
430
431 if (systemProperties != null) {
432
433
434 for (Object o : systemProperties.keySet()) {
435 String key = (String) o;
436
437 String value = systemProperties.getProperty(key);
438
439 System.setProperty(key, value);
440
441 getLog().debug("Setting system property: " + key + '=' + value);
442 }
443 }
444
445 doGenerateReport(getChangedSets(), getBundle(locale), getSink());
446 }
447
448
449
450
451
452 private void initializeDefaultConfigurationParameters() {
453 if (displayFileRevDetailUrl == null || displayFileRevDetailUrl.isEmpty()) {
454 displayFileRevDetailUrl = displayFileDetailUrl;
455 }
456 }
457
458
459
460
461
462 private void initializeDeveloperMaps() {
463 developersById = new HashMap<>();
464 developersByName = new HashMap<>();
465
466 if (developers != null) {
467 for (Developer developer : developers) {
468 developersById.put(developer.getId(), developer);
469 developersByName.put(developer.getName(), developer);
470 }
471 }
472 }
473
474
475
476
477
478
479
480
481 protected List<ChangeLogSet> getChangedSets() throws MavenReportException {
482 List<ChangeLogSet> changelogList = null;
483
484 if (!outputXML.isAbsolute()) {
485 outputXML = new File(project.getBasedir(), outputXML.getPath());
486 }
487
488 if (outputXML.exists()) {
489
490 if (outputXMLExpiration > 0
491 && outputXMLExpiration * 60000L > System.currentTimeMillis() - outputXML.lastModified())
492
493 {
494 try {
495 getLog().info("Using existing changelog.xml...");
496 changelogList = ChangeLog.loadChangedSets(
497 new InputStreamReader(Files.newInputStream(outputXML.toPath()), getOutputEncoding()));
498 } catch (FileNotFoundException e) {
499
500 } catch (Exception e) {
501 throw new MavenReportException("An error occurred while parsing " + outputXML.getAbsolutePath(), e);
502 }
503 }
504 }
505
506 if (changelogList == null) {
507 if (offline) {
508 throw new MavenReportException("This report requires online mode.");
509 }
510
511 getLog().info("Generating changed sets xml to: " + outputXML.getAbsolutePath());
512
513 changelogList = generateChangeSetsFromSCM();
514
515 try {
516 writeChangelogXml(changelogList);
517 } catch (IOException e) {
518 throw new MavenReportException("Can't create " + outputXML.getAbsolutePath(), e);
519 }
520 }
521
522 return changelogList;
523 }
524
525 private void writeChangelogXml(List<ChangeLogSet> changelogList) throws IOException {
526 StringBuilder changelogXml = new StringBuilder();
527
528 changelogXml
529 .append("<?xml version=\"1.0\" encoding=\"")
530 .append(getOutputEncoding())
531 .append("\"?>\n");
532 changelogXml.append("<changelog>");
533
534 for (ChangeLogSet changelogSet : changelogList) {
535 changelogXml.append("\n ");
536
537 String changeset = changelogSet.toXML(getOutputEncoding());
538
539
540 if (changeset.startsWith("<?xml")) {
541 int idx = changeset.indexOf("?>") + 2;
542 changeset = changeset.substring(idx);
543 }
544
545 changelogXml.append(changeset);
546 }
547
548 changelogXml.append("\n</changelog>");
549
550 outputXML.getParentFile().mkdirs();
551 Writer writer = new OutputStreamWriter(
552 new BufferedOutputStream(Files.newOutputStream(outputXML.toPath())), getOutputEncoding());
553 writer.write(changelogXml.toString());
554 writer.flush();
555 writer.close();
556 }
557
558
559
560
561
562
563
564 protected List<ChangeLogSet> generateChangeSetsFromSCM() throws MavenReportException {
565 try {
566 List<ChangeLogSet> changeSets = new ArrayList<>();
567
568 ScmRepository repository = getScmRepository();
569
570 ScmProvider provider = manager.getProviderByRepository(repository);
571
572 ChangeLogScmResult result;
573
574 if ("range".equals(type)) {
575 result = provider.changeLog(
576 repository, new ScmFileSet(basedir), null, null, range, (ScmBranch) null, dateFormat);
577
578 checkResult(result);
579
580 changeSets.add(result.getChangeLog());
581 } else if ("tag".equals(type)) {
582
583 Iterator<String> tagsIter = tags.iterator();
584
585 String startTag = tagsIter.next();
586 String endTag = null;
587
588 if (tagsIter.hasNext()) {
589 while (tagsIter.hasNext()) {
590 endTag = tagsIter.next();
591 String endRevision = getRevisionForTag(endTag, repository, provider);
592 String startRevision = getRevisionForTag(startTag, repository, provider);
593 result = provider.changeLog(
594 repository,
595 new ScmFileSet(basedir),
596 new ScmRevision(startRevision),
597 new ScmRevision(endRevision));
598
599 checkResult(result);
600 result.getChangeLog().setStartVersion(new ScmRevision(startTag));
601 result.getChangeLog().setEndVersion(new ScmRevision(endTag));
602
603 changeSets.add(result.getChangeLog());
604
605 startTag = endTag;
606 }
607 } else {
608 String startRevision = getRevisionForTag(startTag, repository, provider);
609 String endRevision = getRevisionForTag(endTag, repository, provider);
610 result = provider.changeLog(
611 repository,
612 new ScmFileSet(basedir),
613 new ScmRevision(startRevision),
614 new ScmRevision(endRevision));
615
616 checkResult(result);
617 result.getChangeLog().setStartVersion(new ScmRevision(startTag));
618 result.getChangeLog().setEndVersion(null);
619 changeSets.add(result.getChangeLog());
620 }
621 } else if ("date".equals(type)) {
622 Iterator<String> dateIter = dates.iterator();
623
624 String startDate = dateIter.next();
625 String endDate = null;
626
627 if (dateIter.hasNext()) {
628 while (dateIter.hasNext()) {
629 endDate = dateIter.next();
630
631 result = provider.changeLog(
632 repository,
633 new ScmFileSet(basedir),
634 parseDate(startDate),
635 parseDate(endDate),
636 0,
637 (ScmBranch) null);
638
639 checkResult(result);
640
641 changeSets.add(result.getChangeLog());
642
643 startDate = endDate;
644 }
645 } else {
646 result = provider.changeLog(
647 repository,
648 new ScmFileSet(basedir),
649 parseDate(startDate),
650 parseDate(endDate),
651 0,
652 (ScmBranch) null);
653
654 checkResult(result);
655
656 changeSets.add(result.getChangeLog());
657 }
658 } else {
659 throw new MavenReportException("The type '" + type + "' isn't supported.");
660 }
661 filter(changeSets);
662 return changeSets;
663
664 } catch (ScmException e) {
665 throw new MavenReportException("Cannot run changelog command : ", e);
666 } catch (MojoExecutionException e) {
667 throw new MavenReportException("An error has occurred during changelog command : ", e);
668 }
669 }
670
671
672
673
674
675
676
677
678
679
680 private String getRevisionForTag(final String tag, final ScmRepository repository, final ScmProvider provider)
681 throws ScmException {
682 if (repository.getProvider().equals("svn")) {
683 if (tag == null) {
684 return "HEAD";
685 }
686 SvnInfoCommandExpanded infoCommand = new SvnInfoCommandExpanded();
687
688 InfoScmResult infoScmResult = infoCommand.executeInfoTagCommand(
689 (SvnScmProviderRepository) repository.getProviderRepository(),
690 new ScmFileSet(basedir),
691 tag,
692 null,
693 false,
694 null);
695 if (infoScmResult.getInfoItems().isEmpty()) {
696 throw new ScmException("There is no tag named '" + tag + "' in the Subversion repository.");
697 }
698 InfoItem infoItem = infoScmResult.getInfoItems().get(0);
699 String revision = infoItem.getLastChangedRevision();
700 getLog().info(String.format("Resolved tag '%s' to revision '%s'", tag, revision));
701 return revision;
702 }
703 return tag;
704 }
705
706
707
708
709 private void filter(List<ChangeLogSet> changeSets) {
710 List<Pattern> include = compilePatterns(includes);
711 List<Pattern> exclude = compilePatterns(excludes);
712 if (includes == null && excludes == null) {
713 return;
714 }
715 for (ChangeLogSet changeLogSet : changeSets) {
716 List<ChangeSet> set = changeLogSet.getChangeSets();
717 filter(set, include, exclude);
718 }
719 }
720
721 private List<Pattern> compilePatterns(String[] patternArray) {
722 if (patternArray == null) {
723 return new ArrayList<>();
724 }
725 List<Pattern> patterns = new ArrayList<>(patternArray.length);
726 for (String string : patternArray) {
727
728
729
730 string = "\\Q" + string + "\\E";
731 string = string.replace("**", "\\E.?REPLACEMENT?\\Q");
732 string = string.replace("*", "\\E[^/\\\\]?REPLACEMENT?\\Q");
733 string = string.replace("?REPLACEMENT?", "*");
734 string = string.replace("\\Q\\E", "");
735 patterns.add(Pattern.compile(string));
736 }
737 return patterns;
738 }
739
740 private void filter(List<ChangeSet> sets, List<Pattern> includes, List<Pattern> excludes) {
741 Iterator<ChangeSet> it = sets.iterator();
742 while (it.hasNext()) {
743 ChangeSet changeSet = it.next();
744 List<ChangeFile> files = changeSet.getFiles();
745 Iterator<ChangeFile> iterator = files.iterator();
746 while (iterator.hasNext()) {
747 ChangeFile changeFile = iterator.next();
748 String name = changeFile.getName();
749 if (!isIncluded(includes, name) || isExcluded(excludes, name)) {
750 iterator.remove();
751 }
752 }
753 if (files.isEmpty()) {
754 it.remove();
755 }
756 }
757 }
758
759 private boolean isExcluded(List<Pattern> excludes, String name) {
760 if (excludes == null || excludes.isEmpty()) {
761 return false;
762 }
763 for (Pattern pattern : excludes) {
764 if (pattern.matcher(name).matches()) {
765 return true;
766 }
767 }
768 return false;
769 }
770
771 private boolean isIncluded(List<Pattern> includes, String name) {
772 if (includes == null || includes.isEmpty()) {
773 return true;
774 }
775 for (Pattern pattern : includes) {
776 if (pattern.matcher(name).matches()) {
777 return true;
778 }
779 }
780 return false;
781 }
782
783
784
785
786
787
788 private Date parseDate(String date) throws MojoExecutionException {
789 if (date == null || date.trim().isEmpty()) {
790 return null;
791 }
792
793 SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
794
795 try {
796 return formatter.parse(date);
797 } catch (ParseException e) {
798 throw new MojoExecutionException("Please use this date pattern: " + formatter.toLocalizedPattern(), e);
799 }
800 }
801
802 public ScmRepository getScmRepository() throws ScmException {
803 ScmRepository repository;
804
805 try {
806 repository = manager.makeScmRepository(getConnection());
807
808 ScmProviderRepository providerRepo = repository.getProviderRepository();
809
810 if (!(username == null || username.isEmpty())) {
811 providerRepo.setUser(username);
812 }
813
814 if (!(password == null || password.isEmpty())) {
815 providerRepo.setPassword(password);
816 }
817
818 if (repository.getProviderRepository() instanceof ScmProviderRepositoryWithHost) {
819 ScmProviderRepositoryWithHost repo = (ScmProviderRepositoryWithHost) repository.getProviderRepository();
820
821 loadInfosFromSettings(repo);
822
823 if (!(username == null || username.isEmpty())) {
824 repo.setUser(username);
825 }
826
827 if (!(password == null || password.isEmpty())) {
828 repo.setPassword(password);
829 }
830
831 if (!(privateKey == null || privateKey.isEmpty())) {
832 repo.setPrivateKey(privateKey);
833 }
834
835 if (!(passphrase == null || passphrase.isEmpty())) {
836 repo.setPassphrase(passphrase);
837 }
838 }
839
840 if (!(tagBase == null || tagBase.isEmpty())
841 && repository.getProvider().equals("svn")) {
842 SvnScmProviderRepository svnRepo = (SvnScmProviderRepository) repository.getProviderRepository();
843
844 svnRepo.setTagBase(tagBase);
845 }
846 } catch (Exception e) {
847 throw new ScmException("Can't load the scm provider.", e);
848 }
849
850 return repository;
851 }
852
853
854
855
856
857
858 private void loadInfosFromSettings(ScmProviderRepositoryWithHost repo) {
859 if (username == null || password == null) {
860 String host = repo.getHost();
861
862 int port = repo.getPort();
863
864 if (port > 0) {
865 host += ":" + port;
866 }
867
868 Server server = this.settings.getServer(host);
869
870 if (server != null) {
871 if (username == null) {
872 username = this.settings.getServer(host).getUsername();
873 }
874
875 if (password == null) {
876 password = this.settings.getServer(host).getPassword();
877 }
878
879 if (privateKey == null) {
880 privateKey = this.settings.getServer(host).getPrivateKey();
881 }
882
883 if (passphrase == null) {
884 passphrase = this.settings.getServer(host).getPassphrase();
885 }
886 }
887 }
888 }
889
890 public void checkResult(ScmResult result) throws MojoExecutionException {
891 if (!result.isSuccess()) {
892 getLog().error("Provider message:");
893
894 getLog().error(result.getProviderMessage() == null ? "" : result.getProviderMessage());
895
896 getLog().error("Command output:");
897
898 getLog().error(result.getCommandOutput() == null ? "" : result.getCommandOutput());
899
900 throw new MojoExecutionException("Command failed.");
901 }
902 }
903
904
905
906
907
908
909
910 protected String getConnection() throws MavenReportException {
911 if (this.connection != null) {
912 return connection;
913 }
914
915 if (project.getScm() == null) {
916 throw new MavenReportException("SCM Connection is not set.");
917 }
918
919 String scmConnection = project.getScm().getConnection();
920 if ((scmConnection != null && !scmConnection.isEmpty()) && "connection".equalsIgnoreCase(connectionType)) {
921 connection = scmConnection;
922 }
923
924 String scmDeveloper = project.getScm().getDeveloperConnection();
925 if ((scmDeveloper != null && !scmDeveloper.isEmpty())
926 && "developerconnection".equalsIgnoreCase(connectionType)) {
927 connection = scmDeveloper;
928 }
929
930 if (connection == null || connection.isEmpty()) {
931 throw new MavenReportException("SCM Connection is not set.");
932 }
933
934 return connection;
935 }
936
937
938
939
940
941
942 private void verifySCMTypeParams() throws MavenReportException {
943 if ("range".equals(type)) {
944 if (range == -1) {
945 range = DEFAULT_RANGE;
946 }
947 } else if ("date".equals(type)) {
948 if (dates == null) {
949 throw new MavenReportException("The dates parameter is required when type=\"date\". The value "
950 + "should be the absolute date for the start of the log.");
951 }
952 } else if ("tag".equals(type)) {
953 if (tags == null) {
954 throw new MavenReportException("The tags parameter is required when type=\"tag\".");
955 }
956 } else {
957 throw new MavenReportException("The type parameter has an invalid value: " + type
958 + ". The value should be \"range\", \"date\", or \"tag\".");
959 }
960 }
961
962
963
964
965
966
967
968 protected void doGenerateEmptyReport(ResourceBundle bundle, Sink sink) {
969 sink.head();
970 sink.title();
971 sink.text(bundle.getString("report.changelog.header"));
972 sink.title_();
973 sink.head_();
974
975 sink.body();
976 sink.section1();
977
978 sink.sectionTitle1();
979 sink.text(bundle.getString("report.changelog.mainTitle"));
980 sink.sectionTitle1_();
981
982 sink.paragraph();
983 sink.text(bundle.getString("report.changelog.nosources"));
984 sink.paragraph_();
985
986 sink.section1_();
987
988 sink.body_();
989 sink.flush();
990 sink.close();
991 }
992
993
994
995
996
997
998
999
1000 protected void doGenerateReport(List<ChangeLogSet> changeLogSets, ResourceBundle bundle, Sink sink) {
1001 sink.head();
1002 sink.title();
1003 sink.text(bundle.getString("report.changelog.header"));
1004 sink.title_();
1005 sink.head_();
1006
1007 sink.body();
1008 sink.section1();
1009
1010 sink.sectionTitle1();
1011 sink.text(bundle.getString("report.changelog.mainTitle"));
1012 sink.sectionTitle1_();
1013
1014
1015 doSummarySection(changeLogSets, bundle, sink);
1016
1017 for (ChangeLogSet changeLogSet : changeLogSets) {
1018 doChangedSet(changeLogSet, bundle, sink);
1019 }
1020
1021 sink.section1_();
1022 sink.body_();
1023
1024 sink.flush();
1025 sink.close();
1026 }
1027
1028
1029
1030
1031
1032
1033
1034
1035 private void doSummarySection(List<ChangeLogSet> changeLogSets, ResourceBundle bundle, Sink sink) {
1036 sink.paragraph();
1037
1038 sink.text(bundle.getString("report.changelog.ChangedSetsTotal"));
1039 sink.text(": " + changeLogSets.size());
1040
1041 sink.paragraph_();
1042 }
1043
1044
1045
1046
1047
1048
1049
1050
1051 private void doChangedSet(ChangeLogSet set, ResourceBundle bundle, Sink sink) {
1052 sink.section2();
1053
1054 doChangeSetTitle(set, bundle, sink);
1055
1056 doSummary(set, bundle, sink);
1057
1058 doChangedSetTable(set.getChangeSets(), bundle, sink);
1059
1060 sink.section2_();
1061 }
1062
1063
1064
1065
1066
1067
1068
1069
1070 protected void doChangeSetTitle(ChangeLogSet set, ResourceBundle bundle, Sink sink) {
1071 sink.sectionTitle2();
1072
1073 SimpleDateFormat headingDateFormater = new SimpleDateFormat(headingDateFormat);
1074
1075 if ("tag".equals(type)) {
1076 if (set.getStartVersion() == null || set.getStartVersion().getName() == null) {
1077 sink.text(bundle.getString("report.SetTagCreation"));
1078 if (set.getEndVersion() != null && set.getEndVersion().getName() != null) {
1079 sink.text(' ' + bundle.getString("report.SetTagUntil") + " '" + set.getEndVersion() + '\'');
1080 }
1081 } else if (set.getEndVersion() == null || set.getEndVersion().getName() == null) {
1082 sink.text(bundle.getString("report.SetTagSince"));
1083 sink.text(" '" + set.getStartVersion() + '\'');
1084 } else {
1085 sink.text(bundle.getString("report.SetTagBetween"));
1086 sink.text(" '" + set.getStartVersion() + "' " + bundle.getString("report.And") + " '"
1087 + set.getEndVersion() + '\'');
1088 }
1089 } else if (set.getStartDate() == null) {
1090 sink.text(bundle.getString("report.SetRangeUnknown"));
1091 } else if (set.getEndDate() == null) {
1092 sink.text(bundle.getString("report.SetRangeSince"));
1093 sink.text(' ' + headingDateFormater.format(set.getStartDate()));
1094 } else {
1095 sink.text(bundle.getString("report.SetRangeBetween"));
1096 sink.text(' '
1097 + headingDateFormater.format(set.getStartDate())
1098 + ' '
1099 + bundle.getString("report.And")
1100 + ' '
1101 + headingDateFormater.format(set.getEndDate()));
1102 }
1103 sink.sectionTitle2_();
1104 }
1105
1106
1107
1108
1109
1110
1111
1112
1113 protected void doSummary(ChangeLogSet set, ResourceBundle bundle, Sink sink) {
1114 sink.paragraph();
1115 sink.text(bundle.getString("report.TotalCommits"));
1116 sink.text(": " + set.getChangeSets().size());
1117 sink.lineBreak();
1118 sink.text(bundle.getString("report.changelog.FilesChanged"));
1119 sink.text(": " + countFilesChanged(set.getChangeSets()));
1120 sink.paragraph_();
1121 }
1122
1123
1124
1125
1126
1127
1128
1129 protected long countFilesChanged(Collection<ChangeSet> entries) {
1130 if (entries == null) {
1131 return 0;
1132 }
1133
1134 if (entries.isEmpty()) {
1135 return 0;
1136 }
1137
1138 HashMap<String, List<ChangeFile>> fileList = new HashMap<>();
1139
1140 for (ChangeSet entry : entries) {
1141 for (ChangeFile file : entry.getFiles()) {
1142 String name = file.getName();
1143 List<ChangeFile> list = fileList.get(name);
1144
1145 if (list != null) {
1146 list.add(file);
1147 } else {
1148 list = new LinkedList<>();
1149
1150 list.add(file);
1151
1152 fileList.put(name, list);
1153 }
1154 }
1155 }
1156
1157 return fileList.size();
1158 }
1159
1160
1161
1162
1163
1164
1165
1166
1167 private void doChangedSetTable(Collection<ChangeSet> entries, ResourceBundle bundle, Sink sink) {
1168 sink.table();
1169 sink.tableRows(new int[] {Sink.JUSTIFY_LEFT}, false);
1170
1171 sink.tableRow();
1172 sink.tableHeaderCell();
1173 sink.text(bundle.getString("report.changelog.timestamp"));
1174 sink.tableHeaderCell_();
1175 sink.tableHeaderCell();
1176 sink.text(bundle.getString("report.changelog.author"));
1177 sink.tableHeaderCell_();
1178 sink.tableHeaderCell();
1179 sink.text(bundle.getString("report.changelog.details"));
1180 sink.tableHeaderCell_();
1181 sink.tableRow_();
1182
1183 initReportUrls();
1184
1185 List<ChangeSet> sortedEntries = new ArrayList<>(entries);
1186 sortedEntries.sort((changeSet0, changeSet1) -> changeSet1.getDate().compareTo(changeSet0.getDate()));
1187
1188 for (ChangeSet entry : sortedEntries) {
1189 doChangedSetDetail(entry, sink);
1190 }
1191
1192 sink.tableRows_();
1193 sink.table_();
1194 }
1195
1196
1197
1198
1199
1200
1201
1202 private void doChangedSetDetail(ChangeSet entry, Sink sink) {
1203 sink.tableRow();
1204
1205 sink.tableCell();
1206 sink.text(entry.getDateFormatted() + ' ' + entry.getTimeFormatted());
1207 sink.tableCell_();
1208
1209 sink.tableCell();
1210
1211 sinkAuthorDetails(sink, entry.getAuthor());
1212
1213 sink.tableCell_();
1214
1215 sink.tableCell();
1216
1217 if (!omitFileAndRevision) {
1218 doChangedFiles(entry.getFiles(), sink);
1219 sink.lineBreak();
1220 }
1221
1222 StringReader sr = new StringReader(entry.getComment());
1223 BufferedReader br = new BufferedReader(sr);
1224 String line;
1225 try {
1226 if ((issueIDRegexPattern != null && !issueIDRegexPattern.isEmpty())
1227 && (issueLinkUrl != null && !issueLinkUrl.isEmpty())) {
1228 Pattern pattern = Pattern.compile(issueIDRegexPattern);
1229
1230 line = br.readLine();
1231
1232 while (line != null) {
1233 sinkIssueLink(sink, line, pattern);
1234
1235 line = br.readLine();
1236 if (line != null) {
1237 sink.lineBreak();
1238 }
1239 }
1240 } else {
1241 line = br.readLine();
1242
1243 while (line != null) {
1244 sink.text(line);
1245 line = br.readLine();
1246 if (line != null) {
1247 sink.lineBreak();
1248 }
1249 }
1250 }
1251 } catch (IOException e) {
1252 getLog().warn("Unable to read the comment of a ChangeSet as a stream.");
1253 } finally {
1254 try {
1255 br.close();
1256 } catch (IOException e) {
1257 getLog().warn("Unable to close a reader.");
1258 }
1259 sr.close();
1260 }
1261 sink.tableCell_();
1262
1263 sink.tableRow_();
1264 }
1265
1266 private void sinkIssueLink(Sink sink, String line, Pattern pattern) {
1267
1268
1269 Matcher matcher = pattern.matcher(line);
1270
1271 int currLoc = 0;
1272
1273 while (matcher.find()) {
1274 String match = matcher.group();
1275
1276 String link;
1277
1278 if (issueLinkUrl.indexOf(ISSUE_TOKEN) > 0) {
1279 link = issueLinkUrl.replaceAll(ISSUE_TOKEN, match);
1280 } else {
1281 if (issueLinkUrl.endsWith("/")) {
1282 link = issueLinkUrl;
1283 } else {
1284 link = issueLinkUrl + '/';
1285 }
1286
1287 link += match;
1288 }
1289
1290 int startOfMatch = matcher.start();
1291
1292 String unmatchedText = line.substring(currLoc, startOfMatch);
1293
1294 currLoc = matcher.end();
1295
1296 sink.text(unmatchedText);
1297
1298 sink.link(link);
1299 sink.text(match);
1300 sink.link_();
1301 }
1302
1303 sink.text(line.substring(currLoc));
1304 }
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314 protected void sinkAuthorDetails(Sink sink, String author) {
1315 Developer developer = developersById.get(author);
1316
1317 if (developer == null) {
1318 developer = developersByName.get(author);
1319 }
1320
1321 if (developer != null) {
1322 sink.link("team-list.html#" + developer.getId());
1323 sink.text(developer.getName());
1324 sink.link_();
1325 } else {
1326 sink.text(author);
1327 }
1328 }
1329
1330
1331
1332
1333 protected void initReportUrls() {
1334 if (scmUrl != null) {
1335 int idx = scmUrl.indexOf('?');
1336
1337 if (idx > 0) {
1338 rptRepository = scmUrl.substring(0, idx);
1339
1340 if (scmUrl.equals(displayFileDetailUrl)) {
1341 String rptTmpMultiRepoParam = scmUrl.substring(rptRepository.length());
1342
1343 rptOneRepoParam = '?' + rptTmpMultiRepoParam.substring(1);
1344
1345 rptMultiRepoParam = '&' + rptTmpMultiRepoParam.substring(1);
1346 }
1347 } else {
1348 rptRepository = scmUrl;
1349
1350 rptOneRepoParam = "";
1351
1352 rptMultiRepoParam = "";
1353 }
1354 }
1355 }
1356
1357
1358
1359
1360
1361
1362
1363 private void doChangedFiles(List<ChangeFile> files, Sink sink) {
1364 for (ChangeFile file : files) {
1365 sinkLogFile(sink, file.getName(), file.getRevision());
1366 }
1367 }
1368
1369
1370
1371
1372
1373
1374
1375
1376 private void sinkLogFile(Sink sink, String name, String revision) {
1377 try {
1378 String connection = getConnection();
1379
1380 generateLinks(connection, name, revision, sink);
1381 } catch (Exception e) {
1382 getLog().debug(e);
1383
1384 sink.text(name + " v " + revision);
1385 }
1386
1387 sink.lineBreak();
1388 }
1389
1390
1391
1392
1393
1394
1395
1396
1397 protected void generateLinks(String connection, String name, Sink sink) {
1398 generateLinks(connection, name, null, sink);
1399 }
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409 protected void generateLinks(String connection, String name, String revision, Sink sink) {
1410 String linkFile;
1411 String linkRev = null;
1412
1413 if (revision != null) {
1414 linkFile = displayFileRevDetailUrl;
1415 } else {
1416 linkFile = displayFileDetailUrl;
1417 }
1418
1419 if (linkFile != null) {
1420 if (!linkFile.equals(scmUrl)) {
1421 String linkName = name;
1422 if (encodeFileUri) {
1423 try {
1424 linkName = URLEncoder.encode(linkName, "UTF-8");
1425 } catch (UnsupportedEncodingException e) {
1426
1427 }
1428 }
1429
1430
1431
1432 if (linkFile.indexOf(FILE_TOKEN) > 0) {
1433 linkFile = linkFile.replaceAll(FILE_TOKEN, linkName);
1434 } else {
1435
1436
1437
1438 linkFile += linkName;
1439 }
1440
1441
1442
1443 if (revision != null && linkFile.indexOf(REV_TOKEN) > 0) {
1444 linkFile = linkFile.replaceAll(REV_TOKEN, revision);
1445 }
1446 } else if (connection.startsWith("scm:perforce")) {
1447 String path = getAbsolutePath(displayFileDetailUrl, name);
1448 linkFile = path + "?ac=22";
1449 if (revision != null) {
1450 linkRev = path + "?ac=64&rev=" + revision;
1451 }
1452 } else if (connection.startsWith("scm:clearcase")) {
1453 String path = getAbsolutePath(displayFileDetailUrl, name);
1454 linkFile = path + rptOneRepoParam;
1455 } else if (connection.indexOf("cvsmonitor.pl") > 0) {
1456 Pattern cvsMonitorRegex = Pattern.compile("^.*(&module=.*?(?:&|$)).*$");
1457 String module = cvsMonitorRegex.matcher(rptOneRepoParam).replaceAll("$1");
1458 linkFile = displayFileDetailUrl + "?cmd=viewBrowseFile" + module + "&file=" + name;
1459 if (revision != null) {
1460 linkRev = rptRepository + "?cmd=viewBrowseVersion" + module + "&file=" + name + "&version="
1461 + revision;
1462 }
1463 } else {
1464 String path = getAbsolutePath(displayFileDetailUrl, name);
1465 linkFile = path + rptOneRepoParam;
1466 if (revision != null) {
1467 linkRev = path + "?rev=" + revision + "&content-type=text/vnd.viewcvs-markup" + rptMultiRepoParam;
1468 }
1469 }
1470 }
1471
1472 if (linkFile != null) {
1473 sink.link(linkFile);
1474 sinkFileName(name, sink);
1475 sink.link_();
1476 } else {
1477 sinkFileName(name, sink);
1478 }
1479
1480 sink.text(" ");
1481
1482 if (linkRev == null && revision != null && displayChangeSetDetailUrl != null) {
1483 if (displayChangeSetDetailUrl.indexOf(REV_TOKEN) > 0) {
1484 linkRev = displayChangeSetDetailUrl.replaceAll(REV_TOKEN, revision);
1485 } else {
1486 linkRev = displayChangeSetDetailUrl + revision;
1487 }
1488 }
1489
1490 if (linkRev != null) {
1491 sink.link(linkRev);
1492 sink.text("v " + revision);
1493 sink.link_();
1494 } else if (revision != null) {
1495 sink.text("v " + revision);
1496 }
1497 }
1498
1499
1500
1501
1502
1503
1504
1505 private void sinkFileName(String name, Sink sink) {
1506 name = sinkFileNamePattern.matcher(name).replaceAll("/");
1507 int pos = name.lastIndexOf('/');
1508
1509 String head;
1510 String tail;
1511 if (pos < 0) {
1512 head = "";
1513 tail = name;
1514 } else {
1515 head = name.substring(0, pos) + '/';
1516 tail = name.substring(pos + 1);
1517 }
1518
1519 sink.text(head);
1520 sink.bold();
1521 sink.text(tail);
1522 sink.bold_();
1523 }
1524
1525
1526
1527
1528
1529
1530
1531 private String getAbsolutePath(final String base, final String target) {
1532 StringBuilder absPath = new StringBuilder();
1533
1534 StringTokenizer baseTokens =
1535 new StringTokenizer(sinkFileNamePattern.matcher(base).replaceAll("/"), "/", true);
1536
1537 StringTokenizer targetTokens =
1538 new StringTokenizer(sinkFileNamePattern.matcher(target).replaceAll("/"), "/");
1539
1540 String targetRoot = targetTokens.nextToken();
1541
1542 while (baseTokens.hasMoreTokens()) {
1543 String baseToken = baseTokens.nextToken();
1544
1545 if (baseToken.equals(targetRoot)) {
1546 break;
1547 }
1548
1549 absPath.append(baseToken);
1550 }
1551
1552 if (!absPath.toString().endsWith("/")) {
1553 absPath.append("/");
1554 }
1555
1556 String newTarget = target;
1557 if (newTarget.startsWith("/")) {
1558 newTarget = newTarget.substring(1);
1559 }
1560
1561 return absPath + newTarget;
1562 }
1563
1564
1565
1566
1567 protected MavenProject getProject() {
1568 return project;
1569 }
1570
1571
1572
1573
1574 protected String getOutputDirectory() {
1575 if (!outputDirectory.isAbsolute()) {
1576 outputDirectory = new File(project.getBasedir(), outputDirectory.getPath());
1577 }
1578
1579 return outputDirectory.getAbsolutePath();
1580 }
1581
1582
1583
1584
1585
1586
1587 protected String getOutputEncoding() {
1588 return (outputEncoding != null) ? outputEncoding : "UTF-8";
1589 }
1590
1591
1592
1593
1594 protected Renderer getSiteRenderer() {
1595 return siteRenderer;
1596 }
1597
1598
1599
1600
1601 public String getDescription(Locale locale) {
1602 return getBundle(locale).getString("report.changelog.description");
1603 }
1604
1605
1606
1607
1608 public String getName(Locale locale) {
1609 return getBundle(locale).getString("report.changelog.name");
1610 }
1611
1612
1613
1614
1615 public String getOutputName() {
1616 return "changelog";
1617 }
1618
1619
1620
1621
1622
1623 protected ResourceBundle getBundle(Locale locale) {
1624 return ResourceBundle.getBundle("scm-activity", locale, this.getClass().getClassLoader());
1625 }
1626
1627
1628
1629
1630 public boolean canGenerateReport() {
1631 if (offline && !outputXML.exists()) {
1632 return false;
1633 }
1634
1635 return !skip;
1636 }
1637 }