forked from reposense/RepoSense
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathReportGenerator.java
More file actions
343 lines (300 loc) · 16.6 KB
/
ReportGenerator.java
File metadata and controls
343 lines (300 loc) · 16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
package reposense.report;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import com.google.gson.JsonSyntaxException;
import reposense.RepoSense;
import reposense.authorship.AuthorshipReporter;
import reposense.authorship.model.AuthorshipSummary;
import reposense.commits.CommitsReporter;
import reposense.commits.model.CommitContributionSummary;
import reposense.git.GitClone;
import reposense.git.GitLsTree;
import reposense.git.GitRevParse;
import reposense.git.GitShortlog;
import reposense.git.exception.GitBranchException;
import reposense.git.exception.GitCloneException;
import reposense.git.exception.InvalidFilePathException;
import reposense.model.Author;
import reposense.model.RepoConfiguration;
import reposense.model.RepoLocation;
import reposense.model.StandaloneConfig;
import reposense.parser.SinceDateArgumentType;
import reposense.parser.StandaloneConfigJsonParser;
import reposense.report.exception.NoAuthorsWithCommitsFoundException;
import reposense.system.LogsManager;
import reposense.util.FileUtil;
import reposense.util.ProgressTracker;
/**
* Contains report generation related functionalities.
*/
public class ReportGenerator {
private static final String REPOSENSE_CONFIG_FOLDER = "_reposense";
private static final String REPOSENSE_CONFIG_FILE = "config.json";
private static final Logger logger = LogsManager.getLogger(ReportGenerator.class);
// zip file which contains all the report template files
private static final String TEMPLATE_FILE = "/templateZip.zip";
private static final String MESSAGE_INVALID_CONFIG_JSON = "%s Ignoring the config provided by %s (%s).";
private static final String MESSAGE_ERROR_CREATING_DIRECTORY =
"Error has occurred while creating repo directory for %s (%s), will skip this repo.";
private static final String MESSAGE_NO_STANDALONE_CONFIG = "%s (%s) does not contain a standalone config file.";
private static final String MESSAGE_IGNORING_STANDALONE_CONFIG = "Ignoring standalone config file in %s (%s).";
private static final String MESSAGE_MALFORMED_STANDALONE_CONFIG = "%s/%s/%s is malformed for %s (%s).";
private static final String MESSAGE_NO_AUTHORS_SPECIFIED =
"%s (%s) has no authors specified, using all authors by default.";
private static final String MESSAGE_NO_AUTHORS_WITH_COMMITS_FOUND =
"No authors found with commits for %s (%s).";
private static final String MESSAGE_START_ANALYSIS = "Analyzing %s (%s)...";
private static final String MESSAGE_COMPLETE_ANALYSIS = "Analysis of %s (%s) completed!";
private static final String MESSAGE_REPORT_GENERATED = "The report is generated at %s";
private static final String MESSAGE_BRANCH_DOES_NOT_EXIST = "Branch %s does not exist in %s! Analysis terminated.";
private static final String LOG_ERROR_CLONING = "Failed to clone from %s";
private static final String LOG_BRANCH_DOES_NOT_EXIST = "Branch \"%s\" does not exist.";
private static final String LOG_BRANCH_CONTAINS_ILLEGAL_FILE_PATH =
"Branch contains file paths with illegal characters and not analyzable.";
private static final String LOG_ERROR_CLONING_OR_BRANCHING = "Exception met while cloning or checking out.";
private static Date earliestSinceDate = null;
private static ProgressTracker progressTracker = null;
/**
* Generates the authorship and commits JSON file for each repo in {@code configs} at {@code outputPath}, as
* well as the summary JSON file of all the repos.
*
* @return the list of file paths that were generated.
* @throws IOException if templateZip.zip does not exists in jar file.
*/
public static List<Path> generateReposReport(List<RepoConfiguration> configs, String outputPath,
String generationDate, Date cliSinceDate, Date untilDate,
boolean isSinceDateProvided, boolean isUntilDateProvided) throws IOException {
InputStream is = RepoSense.class.getResourceAsStream(TEMPLATE_FILE);
FileUtil.copyTemplate(is, outputPath);
earliestSinceDate = null;
progressTracker = new ProgressTracker(configs.size());
cloneAndAnalyzeRepos(configs, outputPath);
Date reportSinceDate = (cliSinceDate.equals(SinceDateArgumentType.ARBITRARY_FIRST_COMMIT_DATE))
? earliestSinceDate : cliSinceDate;
FileUtil.writeJsonFile(
new SummaryJson(configs, generationDate, reportSinceDate, untilDate, isSinceDateProvided,
isUntilDateProvided, RepoSense.getVersion(), ErrorSummary.getInstance().getErrorList()),
getSummaryResultPath(outputPath));
logger.info(String.format(MESSAGE_REPORT_GENERATED, outputPath));
List<Path> reportFoldersAndFiles = new ArrayList<>();
for (RepoConfiguration config : configs) {
reportFoldersAndFiles.add(
Paths.get(outputPath + File.separator + config.getOutputFolderName()).toAbsolutePath());
}
reportFoldersAndFiles.add(Paths.get(outputPath, SummaryJson.SUMMARY_JSON_FILE_NAME));
return reportFoldersAndFiles;
}
/**
* Groups {@code RepoConfiguration} with the same {@code RepoLocation} together so that they are only cloned once.
*/
private static Map<RepoLocation, List<RepoConfiguration>> groupConfigsByRepoLocation(
List<RepoConfiguration> configs) {
Map<RepoLocation, List<RepoConfiguration>> repoLocationMap = new HashMap<>();
for (RepoConfiguration config : configs) {
RepoLocation location = config.getLocation();
if (!repoLocationMap.containsKey(location)) {
repoLocationMap.put(location, new ArrayList<>());
}
repoLocationMap.get(location).add(config);
}
return repoLocationMap;
}
/**
* Clone, analyze and generate the report for repositories in {@code repoLocationMap}.
* Performs analysis and report generation of each repository in parallel with the cloning of the next repository.
*/
private static void cloneAndAnalyzeRepos(List<RepoConfiguration> configs, String outputPath) {
Map<RepoLocation, List<RepoConfiguration>> repoLocationMap = groupConfigsByRepoLocation(configs);
RepoCloner repoCloner = new RepoCloner();
RepoLocation clonedRepoLocation = null;
List<RepoLocation> repoLocationList = new ArrayList<>(repoLocationMap.keySet());
RepoLocation currRepoLocation = repoLocationList.get(0);
repoCloner.cloneBare(repoLocationMap.get(currRepoLocation).get(0));
for (int index = 1; index <= repoLocationList.size(); index++) {
RepoLocation nextRepoLocation = (index < repoLocationList.size()) ? repoLocationList.get(index) : null;
clonedRepoLocation = repoCloner.getClonedRepoLocation();
// Clones the next location while analyzing the previously cloned repos in parallel.
if (nextRepoLocation != null) {
repoCloner.cloneBare(repoLocationMap.get(nextRepoLocation).get(0));
}
if (clonedRepoLocation == null) {
handleCloningFailed(configs, currRepoLocation);
} else {
analyzeRepos(outputPath, configs, repoLocationMap.get(clonedRepoLocation),
repoCloner.getCurrentRepoDefaultBranch());
}
currRepoLocation = nextRepoLocation;
}
repoCloner.cleanup();
}
/**
* Analyzes all repos in {@code configsToAnalyze} and generates their report.
* Also removes {@code configsToAnalyze} that failed to analyze from {@code configs}.
*/
private static void analyzeRepos(String outputPath, List<RepoConfiguration> configs,
List<RepoConfiguration> configsToAnalyze, String defaultBranch) {
Iterator<RepoConfiguration> itr = configsToAnalyze.iterator();
while (itr.hasNext()) {
progressTracker.incrementProgress();
RepoConfiguration configToAnalyze = itr.next();
configToAnalyze.updateBranch(defaultBranch);
Path repoReportDirectory = Paths.get(outputPath, configToAnalyze.getOutputFolderName());
logger.info(
String.format(progressTracker.getProgress() + " "
+ MESSAGE_START_ANALYSIS, configToAnalyze.getLocation(), configToAnalyze.getBranch()));
try {
GitRevParse.assertBranchExists(configToAnalyze, FileUtil.getBareRepoPath(configToAnalyze));
GitLsTree.validateFilePaths(configToAnalyze, FileUtil.getBareRepoPath(configToAnalyze));
GitClone.cloneFromBareAndUpdateBranch(Paths.get(FileUtil.REPOS_ADDRESS), configToAnalyze);
FileUtil.createDirectory(repoReportDirectory);
analyzeRepo(configToAnalyze, repoReportDirectory.toString());
} catch (IOException ioe) {
String logMessage = String.format(MESSAGE_ERROR_CREATING_DIRECTORY,
configToAnalyze.getLocation(), configToAnalyze.getBranch());
logger.log(Level.WARNING, logMessage, ioe);
} catch (GitBranchException gbe) {
logger.log(Level.SEVERE, String.format(MESSAGE_BRANCH_DOES_NOT_EXIST,
configToAnalyze.getBranch(), configToAnalyze.getLocation()), gbe);
handleAnalysisFailed(configs, configToAnalyze,
String.format(LOG_BRANCH_DOES_NOT_EXIST, configToAnalyze.getBranch()));
} catch (InvalidFilePathException ipe) {
handleAnalysisFailed(configs, configToAnalyze, LOG_BRANCH_CONTAINS_ILLEGAL_FILE_PATH);
} catch (GitCloneException gce) {
handleAnalysisFailed(configs, configToAnalyze, LOG_ERROR_CLONING_OR_BRANCHING);
} catch (NoAuthorsWithCommitsFoundException nafe) {
logger.log(Level.WARNING, String.format(MESSAGE_NO_AUTHORS_WITH_COMMITS_FOUND,
configToAnalyze.getLocation(), configToAnalyze.getBranch()));
generateEmptyRepoReport(repoReportDirectory.toString(), Author.NAME_NO_AUTHOR_WITH_COMMITS_FOUND);
}
}
}
/**
* Analyzes repo specified by {@code config} and generates the report.
*/
private static void analyzeRepo(
RepoConfiguration config, String repoReportDirectory) throws NoAuthorsWithCommitsFoundException {
// preprocess the config and repo
updateRepoConfig(config);
updateAuthorList(config);
CommitContributionSummary commitSummary = CommitsReporter.generateCommitSummary(config);
AuthorshipSummary authorshipSummary = AuthorshipReporter.generateAuthorshipSummary(config);
generateIndividualRepoReport(repoReportDirectory, commitSummary, authorshipSummary);
logger.info(String.format(MESSAGE_COMPLETE_ANALYSIS, config.getLocation(), config.getBranch()));
}
/**
* Updates {@code config} with configuration provided by repository if exists.
*/
public static void updateRepoConfig(RepoConfiguration config) {
Path configJsonPath =
Paths.get(config.getRepoRoot(), REPOSENSE_CONFIG_FOLDER, REPOSENSE_CONFIG_FILE).toAbsolutePath();
if (!Files.exists(configJsonPath)) {
logger.info(String.format(MESSAGE_NO_STANDALONE_CONFIG, config.getLocation(), config.getBranch()));
return;
}
if (config.isStandaloneConfigIgnored()) {
logger.info(String.format(MESSAGE_IGNORING_STANDALONE_CONFIG, config.getLocation(), config.getBranch()));
return;
}
try {
StandaloneConfig standaloneConfig = new StandaloneConfigJsonParser().parse(configJsonPath);
config.update(standaloneConfig);
} catch (JsonSyntaxException jse) {
logger.warning(String.format(MESSAGE_MALFORMED_STANDALONE_CONFIG, config.getDisplayName(),
REPOSENSE_CONFIG_FOLDER, REPOSENSE_CONFIG_FILE, config.getLocation(), config.getBranch()));
} catch (IllegalArgumentException iae) {
logger.warning(String.format(MESSAGE_INVALID_CONFIG_JSON,
iae.getMessage(), config.getLocation(), config.getBranch()));
} catch (IOException ioe) {
throw new AssertionError(
"This exception should not happen as we have performed the file existence check.");
}
}
/**
* Find and update {@code config} with all the author identities if author list is empty.
*/
private static void updateAuthorList(RepoConfiguration config) throws NoAuthorsWithCommitsFoundException {
if (config.getAuthorList().isEmpty()) {
logger.info(String.format(MESSAGE_NO_AUTHORS_SPECIFIED, config.getLocation(), config.getBranch()));
List<Author> authorList = GitShortlog.getAuthors(config);
if (authorList.isEmpty()) {
throw new NoAuthorsWithCommitsFoundException();
}
config.setAuthorList(authorList);
}
}
/**
* Adds {@code configs} that were not successfully cloned from {@code failedRepoLocation}
* into the list of errors in the summary report and removes them from the list of {@code configs}.
*/
private static void handleCloningFailed(List<RepoConfiguration> configs, RepoLocation failedRepoLocation) {
List<RepoConfiguration> failedConfigs = configs.stream()
.filter(config -> config.getLocation().equals(failedRepoLocation))
.collect(Collectors.toList());
handleFailedConfigs(configs, failedConfigs, String.format(LOG_ERROR_CLONING, failedRepoLocation));
}
/**
* Adds {@code failedConfig} that failed analysis into the list of errors in the summary report and
* removes {@code failedConfig} from the list of {@code configs}.
*/
private static void handleAnalysisFailed(List<RepoConfiguration> configs, RepoConfiguration failedConfig,
String errorMessage) {
handleFailedConfigs(configs, Collections.singletonList(failedConfig), errorMessage);
}
/**
* Adds {@code failedConfigs} that failed cloning/analysis into the list of errors in the summary report and
* removes {@code failedConfigs} from the list of {@code configs}.
*/
private static void handleFailedConfigs(
List<RepoConfiguration> configs, List<RepoConfiguration> failedConfigs, String errorMessage) {
Iterator<RepoConfiguration> itr = configs.iterator();
while (itr.hasNext()) {
RepoConfiguration config = itr.next();
if (failedConfigs.contains(config)) {
ErrorSummary.getInstance().addErrorMessage(config.getDisplayName(), errorMessage);
itr.remove();
}
}
}
/**
* Generates a report at the {@code repoReportDirectory}.
*/
public static void generateEmptyRepoReport(String repoReportDirectory, String displayName) {
CommitReportJson emptyCommitReportJson = new CommitReportJson(displayName);
FileUtil.writeJsonFile(emptyCommitReportJson, getIndividualCommitsPath(repoReportDirectory));
FileUtil.writeJsonFile(Collections.emptyList(), getIndividualAuthorshipPath(repoReportDirectory));
}
private static void generateIndividualRepoReport(
String repoReportDirectory, CommitContributionSummary commitSummary, AuthorshipSummary authorshipSummary) {
CommitReportJson commitReportJson = new CommitReportJson(commitSummary, authorshipSummary);
FileUtil.writeJsonFile(commitReportJson, getIndividualCommitsPath(repoReportDirectory));
FileUtil.writeJsonFile(authorshipSummary.getFileResults(), getIndividualAuthorshipPath(repoReportDirectory));
}
private static String getSummaryResultPath(String targetFileLocation) {
return targetFileLocation + "/" + SummaryJson.SUMMARY_JSON_FILE_NAME;
}
private static String getIndividualAuthorshipPath(String repoReportDirectory) {
return repoReportDirectory + "/authorship.json";
}
private static String getIndividualCommitsPath(String repoReportDirectory) {
return repoReportDirectory + "/commits.json";
}
public static void setEarliestSinceDate(Date newEarliestSinceDate) {
if (earliestSinceDate == null || newEarliestSinceDate.before(earliestSinceDate)) {
earliestSinceDate = newEarliestSinceDate;
}
}
}