From 1b6e79b64075519838d8df47ac21d36f06f33f19 Mon Sep 17 00:00:00 2001 From: George Tay Date: Mon, 15 Apr 2024 14:32:10 +0800 Subject: [PATCH 01/42] Implement one-stop config file --- build.gradle | 6 +- config/report-config.yaml | 43 +++++++ docs/ug/configFiles.md | 16 ++- docs/ug/report-config.json | 3 - docs/ug/report-config.yaml | 11 ++ src/main/java/reposense/RepoSense.java | 2 +- .../java/reposense/model/CliArguments.java | 5 +- .../reposense/model/ReportConfiguration.java | 13 -- .../model/reportconfig/ReportBranchData.java | 121 ++++++++++++++++++ .../reportconfig/ReportConfiguration.java | 57 +++++++++ .../reportconfig/ReportGroupDetails.java | 49 +++++++ .../reportconfig/ReportGroupNameAndGlobs.java | 69 ++++++++++ .../reportconfig/ReportRepoConfiguration.java | 93 ++++++++++++++ .../java/reposense/parser/ArgsParser.java | 7 +- .../parser/ReportConfigJsonParser.java | 34 ----- .../parser/ReportConfigYamlParser.java | 59 +++++++++ .../reposense/report/ReportGenerator.java | 10 +- .../java/reposense/report/SummaryJson.java | 7 +- .../ConfigSystemTest/report-config.json | 1 - .../ConfigSystemTest/report-config.yaml | 43 +++++++ .../reportconfig/ReportBranchDataTest.java | 56 ++++++++ .../reportconfig/ReportConfigurationTest.java | 28 ++++ .../reportconfig/ReportGroupDetailsTest.java | 23 ++++ .../ReportGroupNameAndGlobsTest.java | 22 ++++ .../ReportRepoConfigurationTest.java | 46 +++++++ .../parser/ReportConfigJsonParserTest.java | 38 ------ .../parser/ReportConfigYamlParserTest.java | 44 +++++++ .../report-config-empty.json | 3 - .../report-config-invalid.json | 3 - .../report-config-valid.json | 3 - .../report-config-empty.yaml | 0 .../report-config-invalid.yaml | 36 ++++++ .../report-config-valid.yaml | 44 +++++++ 33 files changed, 878 insertions(+), 117 deletions(-) create mode 100644 config/report-config.yaml delete mode 100644 docs/ug/report-config.json create mode 100644 docs/ug/report-config.yaml delete mode 100644 src/main/java/reposense/model/ReportConfiguration.java create mode 100644 src/main/java/reposense/model/reportconfig/ReportBranchData.java create mode 100644 src/main/java/reposense/model/reportconfig/ReportConfiguration.java create mode 100644 src/main/java/reposense/model/reportconfig/ReportGroupDetails.java create mode 100644 src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java create mode 100644 src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java delete mode 100644 src/main/java/reposense/parser/ReportConfigJsonParser.java create mode 100644 src/main/java/reposense/parser/ReportConfigYamlParser.java delete mode 100644 src/systemtest/resources/ConfigSystemTest/report-config.json create mode 100644 src/systemtest/resources/ConfigSystemTest/report-config.yaml create mode 100644 src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java create mode 100644 src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java create mode 100644 src/test/java/reposense/model/reportconfig/ReportGroupDetailsTest.java create mode 100644 src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java create mode 100644 src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java delete mode 100644 src/test/java/reposense/parser/ReportConfigJsonParserTest.java create mode 100644 src/test/java/reposense/parser/ReportConfigYamlParserTest.java delete mode 100644 src/test/resources/ReportConfigJsonParserTest/report-config-empty.json delete mode 100644 src/test/resources/ReportConfigJsonParserTest/report-config-invalid.json delete mode 100644 src/test/resources/ReportConfigJsonParserTest/report-config-valid.json create mode 100644 src/test/resources/ReportConfigYamlParserTest/report-config-empty.yaml create mode 100644 src/test/resources/ReportConfigYamlParserTest/report-config-invalid.yaml create mode 100644 src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml diff --git a/build.gradle b/build.gradle index 95e377018b..2cda2181b8 100644 --- a/build.gradle +++ b/build.gradle @@ -18,8 +18,8 @@ mainClassName = 'reposense.RepoSense' node.download = false // The Liferay Node Gradle Plugin will use the system PATH to find the Node/npm executable. -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 +sourceCompatibility = JavaVersion.VERSION_11 +targetCompatibility = JavaVersion.VERSION_11 repositories { mavenCentral() @@ -38,6 +38,8 @@ dependencies { implementation group: 'org.apache.ant', name: 'ant', version: '1.10.12' implementation group: 'org.apache.commons', name: 'commons-csv', version: '1.9.0' implementation group: 'org.fusesource.jansi', name: 'jansi', version: '2.4.0' + implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.17.0' + implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.17.0' testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: jUnitVersion testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion diff --git a/config/report-config.yaml b/config/report-config.yaml new file mode 100644 index 0000000000..4e430c7404 --- /dev/null +++ b/config/report-config.yaml @@ -0,0 +1,43 @@ +title: RepoSense Report +group-details: + - repo: https://github.com/user/repo/tree/fake-branch + groups: + - group-name: code + globs: + - "**.java" + - group-name: tests + globs: + - "src/test**" + - group-name: docs + globs: + - "docs**" + - "**.adoc" + - "**.md" +repos: + - repo: https://github.com/user/repo/tree/fake-branch + author-emails: + - john@john.com + - johny@mail.com + - j@domain.com + author-git-host-id: johnDoe + author-display-name: John Doe + author-git-author-name: my home PC + branches: + - branch: main + file-formats: + - override:java + - md + - fxml + ignore-glob-list: + - "docs**" + ignore-standalone-config: true + ignore-commits-list: + - 2fb6b9b2dd9fa40bf0f9815da2cb0ae8731436c7 + - c5a6dc774e22099cd9ddeb0faff1e75f9cf4f151 + - cd7f610e0becbdf331d5231887d8010a689f87c7 + - 768015345e70f06add2a8b7d1f901dc07bf70582 + ignore-authors-list: + - author1 + - author2 + is-shallow-cloning: true + is-find-previous-authors: false diff --git a/docs/ug/configFiles.md b/docs/ug/configFiles.md index 1797f8e4ea..2b942673c1 100644 --- a/docs/ug/configFiles.md +++ b/docs/ug/configFiles.md @@ -107,12 +107,22 @@ e.g.: `example.java` in `example-repo` can either be in the `test` group or the -## `report-config.json` +## `report-config.yaml` -You can optionally use `report-config.json` to customize report generation by providing the following information. ([example](report-config.json)) +You can also optionally use a `report-config.json` file to quickly define the repository information for the repositories you are interested in tracking and generate your very own code portfolio. + +View this [example](report-config.yaml) for a better understanding of what repository information is required. **Fields to provide**: -* `title`: Title of the generated report, which is also the title of the deployed dashboard. Default: "RepoSense Report" +* `repoUrl`: The URL to your repository of interest +* `reportTitle`: Title of the generated report, which is also the title of the deployed dashboard. Default: "RepoSense Report" +* `authorDisplayName`: Name of the author to track in the repository. +* `authorGithubId`: The GitHub username of the author to track in the repository. +* `branches`: A list of branches with their associated blurbs. + * `name`: Name of the branch to track + * `blurb`: Blurb to include with the branch +* `startDate`: The start date of analysis for RepoSense. Default: "2020-01-01" +* `endDate`: The end date of analysis for RepoSense. Default: "9999-12-32" diff --git a/docs/ug/report-config.json b/docs/ug/report-config.json deleted file mode 100644 index 8165c98ac4..0000000000 --- a/docs/ug/report-config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "CS2103 RepoSense Report" -} diff --git a/docs/ug/report-config.yaml b/docs/ug/report-config.yaml new file mode 100644 index 0000000000..d7fed5f8fe --- /dev/null +++ b/docs/ug/report-config.yaml @@ -0,0 +1,11 @@ +repoUrl: github.com/user/repo +reportTitle: RepoSense Report +authorDisplayName: Sample Author +authorGithubId: Sample Author Github ID +branches: + - name: Branch 1 + blurb: Blurb 1 + - name: Branch 2 + blurb: Blurb 2 +startDate: 2020-01-01 +endDate: 9999-12-31 diff --git a/src/main/java/reposense/RepoSense.java b/src/main/java/reposense/RepoSense.java index 20a8c9188d..4132a90969 100644 --- a/src/main/java/reposense/RepoSense.java +++ b/src/main/java/reposense/RepoSense.java @@ -12,8 +12,8 @@ import reposense.git.GitConfig; import reposense.model.CliArguments; import reposense.model.RepoConfiguration; -import reposense.model.ReportConfiguration; import reposense.model.RunConfigurationDecider; +import reposense.model.reportconfig.ReportConfiguration; import reposense.parser.ArgsParser; import reposense.parser.exceptions.InvalidCsvException; import reposense.parser.exceptions.InvalidHeaderException; diff --git a/src/main/java/reposense/model/CliArguments.java b/src/main/java/reposense/model/CliArguments.java index 0b70dd2b0e..f78e9cd24b 100644 --- a/src/main/java/reposense/model/CliArguments.java +++ b/src/main/java/reposense/model/CliArguments.java @@ -7,11 +7,12 @@ import java.util.List; import java.util.Objects; +import reposense.model.reportconfig.ReportConfiguration; import reposense.parser.ArgsParser; import reposense.parser.AuthorConfigCsvParser; import reposense.parser.GroupConfigCsvParser; import reposense.parser.RepoConfigCsvParser; -import reposense.parser.ReportConfigJsonParser; +import reposense.parser.ReportConfigYamlParser; /** * Represents command line arguments user supplied when running the program. @@ -441,7 +442,7 @@ public Builder configFolderPath(Path configFolderPath) { this.cliArguments.groupConfigFilePath = configFolderPath.resolve( GroupConfigCsvParser.GROUP_CONFIG_FILENAME); this.cliArguments.reportConfigFilePath = configFolderPath.resolve( - ReportConfigJsonParser.REPORT_CONFIG_FILENAME); + ReportConfigYamlParser.REPORT_CONFIG_FILENAME); return this; } diff --git a/src/main/java/reposense/model/ReportConfiguration.java b/src/main/java/reposense/model/ReportConfiguration.java deleted file mode 100644 index 4f6e166c34..0000000000 --- a/src/main/java/reposense/model/ReportConfiguration.java +++ /dev/null @@ -1,13 +0,0 @@ -package reposense.model; - -/** - * Represents configuration information from JSON config file for generated report. - */ -public class ReportConfiguration { - private static final String DEFAULT_TITLE = "RepoSense Report"; - private String title; - - public String getTitle() { - return (title == null) ? DEFAULT_TITLE : title; - } -} diff --git a/src/main/java/reposense/model/reportconfig/ReportBranchData.java b/src/main/java/reposense/model/reportconfig/ReportBranchData.java new file mode 100644 index 0000000000..687577b863 --- /dev/null +++ b/src/main/java/reposense/model/reportconfig/ReportBranchData.java @@ -0,0 +1,121 @@ +package reposense.model.reportconfig; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a single entry of a branch in the YAML config file. + */ +public class ReportBranchData { + public static final String DEFAULT_BRANCH = "main"; + public static final List DEFAULT_FILE_FORMATS = List.of( + "override:java", "md", "fxml" + ); + public static final List DEFAULT_IGNORE_GLOB_LIST = List.of( + "docs**" + ); + public static final List DEFAULT_IGNORE_COMMITS_LIST = List.of( + "2fb6b9b2dd9fa40bf0f9815da2cb0ae8731436c7", + "c5a6dc774e22099cd9ddeb0faff1e75f9cf4f151", + "cd7f610e0becbdf331d5231887d8010a689f87c7", + "768015345e70f06add2a8b7d1f901dc07bf70582" + ); + public static final List DEFAULT_IGNORE_AUTHORS_LIST = List.of( + "author1", + "author2" + ); + public static final boolean DEFAULT_IS_FIND_PREVIOUS_AUTHOR = false; + public static final boolean DEFAULT_IS_SHALLOW_CLONING = true; + public static final boolean DEFAULT_IS_IGNORE_STANDALONE_CONFIG = true; + + public static final ReportBranchData DEFAULT_INSTANCE = new ReportBranchData(); + + static { + DEFAULT_INSTANCE.branch = ReportBranchData.DEFAULT_BRANCH; + DEFAULT_INSTANCE.fileFormats = ReportBranchData.DEFAULT_FILE_FORMATS; + DEFAULT_INSTANCE.ignoreGlobList = ReportBranchData.DEFAULT_IGNORE_GLOB_LIST; + DEFAULT_INSTANCE.ignoreCommitList = ReportBranchData.DEFAULT_IGNORE_COMMITS_LIST; + DEFAULT_INSTANCE.ignoreAuthorList = ReportBranchData.DEFAULT_IGNORE_AUTHORS_LIST; + DEFAULT_INSTANCE.isFindPreviousAuthor = ReportBranchData.DEFAULT_IS_FIND_PREVIOUS_AUTHOR; + DEFAULT_INSTANCE.isShallowCloning = ReportBranchData.DEFAULT_IS_SHALLOW_CLONING; + DEFAULT_INSTANCE.isIgnoreStandaloneConfig = ReportBranchData.DEFAULT_IS_IGNORE_STANDALONE_CONFIG; + } + + @JsonProperty("branch") + private String branch; + + @JsonProperty("file-formats") + private List fileFormats; + + @JsonProperty("ignore-glob-list") + private List ignoreGlobList; + + @JsonProperty("ignore-standalone-config") + private Boolean isIgnoreStandaloneConfig; + + @JsonProperty("ignore-commits-list") + private List ignoreCommitList; + + @JsonProperty("ignore-authors-list") + private List ignoreAuthorList; + + @JsonProperty("is-shallow-cloning") + private Boolean isShallowCloning; + + @JsonProperty("is-find-previous-authors") + private Boolean isFindPreviousAuthor; + + public String getBranch() { + return branch == null ? DEFAULT_BRANCH : branch; + } + + public List getFileFormats() { + return fileFormats == null ? DEFAULT_FILE_FORMATS : fileFormats; + } + + public List getIgnoreGlobList() { + return ignoreGlobList == null ? DEFAULT_IGNORE_GLOB_LIST : fileFormats; + } + + public boolean getIsIgnoreStandaloneConfig() { + return isIgnoreStandaloneConfig == null ? DEFAULT_IS_IGNORE_STANDALONE_CONFIG : isIgnoreStandaloneConfig; + } + + public List getIgnoreCommitList() { + return ignoreCommitList == null ? DEFAULT_IGNORE_COMMITS_LIST : ignoreCommitList; + } + + public List getIgnoreAuthorList() { + return ignoreAuthorList == null ? DEFAULT_IGNORE_AUTHORS_LIST : ignoreAuthorList; + } + + public boolean getIsShallowCloning() { + return isShallowCloning == null ? DEFAULT_IS_SHALLOW_CLONING : isShallowCloning; + } + + public boolean getIsFindPreviousAuthor() { + return isFindPreviousAuthor == null ? DEFAULT_IS_FIND_PREVIOUS_AUTHOR : isFindPreviousAuthor; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof ReportBranchData) { + ReportBranchData rbd = (ReportBranchData) obj; + return this.getBranch().equals(rbd.getBranch()) + && this.getFileFormats().equals(rbd.getFileFormats()) + && this.getIgnoreGlobList().equals(rbd.getIgnoreGlobList()) + && this.getIsIgnoreStandaloneConfig() == rbd.getIsIgnoreStandaloneConfig() + && this.getIgnoreCommitList().equals(rbd.getIgnoreCommitList()) + && this.getIgnoreAuthorList().equals(rbd.getIgnoreAuthorList()) + && this.getIsShallowCloning() == rbd.getIsShallowCloning() + && this.getIsFindPreviousAuthor() == rbd.getIsFindPreviousAuthor(); + } + + return false; + } +} diff --git a/src/main/java/reposense/model/reportconfig/ReportConfiguration.java b/src/main/java/reposense/model/reportconfig/ReportConfiguration.java new file mode 100644 index 0000000000..fcaf682598 --- /dev/null +++ b/src/main/java/reposense/model/reportconfig/ReportConfiguration.java @@ -0,0 +1,57 @@ +package reposense.model.reportconfig; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Class that contains information on a report's configurations. + * This class is used mainly for quickly setting up one's personal code portfolio. + */ +public class ReportConfiguration { + public static final String DEFAULT_TITLE = "RepoSense Report"; + public static final List DEFAULT_REPORT_GROUP_DETAILS = new ArrayList<>(); + public static final List DEFAULT_REPORT_REPO_CONFIGS = new ArrayList<>(); + + static { + DEFAULT_REPORT_REPO_CONFIGS.add(ReportRepoConfiguration.DEFAULT_INSTANCE); + DEFAULT_REPORT_GROUP_DETAILS.add(ReportGroupDetails.DEFAULT_INSTANCE); + } + + @JsonProperty("title") + private String title; + + @JsonProperty("group-details") + private List groupDetails; + + @JsonProperty("repos") + private List reportRepoConfigurations; + + public String getTitle() { + return title == null ? DEFAULT_TITLE : title; + } + + public List getGroupDetails() { + return groupDetails == null ? DEFAULT_REPORT_GROUP_DETAILS : groupDetails; + } + + public List getReportRepoConfigurations() { + return reportRepoConfigurations == null ? DEFAULT_REPORT_REPO_CONFIGS : reportRepoConfigurations; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof ReportConfiguration) { + ReportConfiguration rc = (ReportConfiguration) obj; + return rc.getTitle().equals(this.getTitle()) + && rc.getReportRepoConfigurations().equals(this.getReportRepoConfigurations()); + } + + return false; + } +} diff --git a/src/main/java/reposense/model/reportconfig/ReportGroupDetails.java b/src/main/java/reposense/model/reportconfig/ReportGroupDetails.java new file mode 100644 index 0000000000..8310e86f49 --- /dev/null +++ b/src/main/java/reposense/model/reportconfig/ReportGroupDetails.java @@ -0,0 +1,49 @@ +package reposense.model.reportconfig; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Contains information about the group contained in a particular repo. + */ +public class ReportGroupDetails { + public static final String DEFAULT_REPO = "https://github.com/user/repo/tree/fake-branch"; + public static final List DEFAULT_NAMES_AND_GLOBS = + ReportGroupNameAndGlobs.DEFAULT_INSTANCES; + public static final ReportGroupDetails DEFAULT_INSTANCE = new ReportGroupDetails(); + + static { + DEFAULT_INSTANCE.repo = DEFAULT_REPO; + DEFAULT_INSTANCE.reportGroupNameAndGlobsList = ReportGroupNameAndGlobs.DEFAULT_INSTANCES; + } + + @JsonProperty("repo") + private String repo; + + @JsonProperty("groups") + private List reportGroupNameAndGlobsList; + + public String getRepo() { + return repo == null ? DEFAULT_REPO : repo; + } + + public List getReportGroupNameAndGlobsList() { + return reportGroupNameAndGlobsList == null ? DEFAULT_NAMES_AND_GLOBS : reportGroupNameAndGlobsList; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof ReportGroupDetails) { + ReportGroupDetails rgd = (ReportGroupDetails) obj; + return this.getRepo().equals(rgd.getRepo()) + && this.getReportGroupNameAndGlobsList().equals(rgd.getReportGroupNameAndGlobsList()); + } + + return false; + } +} diff --git a/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java b/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java new file mode 100644 index 0000000000..f3403d31ab --- /dev/null +++ b/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java @@ -0,0 +1,69 @@ +package reposense.model.reportconfig; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Contains details about each report group and the corresponding globs. + */ +public class ReportGroupNameAndGlobs { + public static final String DEFAULT_GROUP_NAME = "code"; + public static final List DEFAULT_GLOBS = List.of( + "**.java" + ); + public static final List DEFAULT_INSTANCES = new ArrayList<>(); + + static { + ReportGroupNameAndGlobs rg1 = new ReportGroupNameAndGlobs(); + rg1.groupName = "code"; + rg1.globs = List.of("**.java"); + + ReportGroupNameAndGlobs rg2 = new ReportGroupNameAndGlobs(); + rg2.groupName = "tests"; + rg2.globs = List.of("src/test**"); + + ReportGroupNameAndGlobs rg3 = new ReportGroupNameAndGlobs(); + rg3.groupName = "docs"; + rg3.globs = List.of("docs**", "**.adoc", "**.md"); + + DEFAULT_INSTANCES.add(rg1); + DEFAULT_INSTANCES.add(rg2); + DEFAULT_INSTANCES.add(rg3); + } + + @JsonProperty("group-name") + private String groupName; + + @JsonProperty("globs") + private List globs; + + public String getGroupName() { + return groupName == null ? DEFAULT_GROUP_NAME : groupName; + } + + public List getGlobs() { + return globs == null ? DEFAULT_GLOBS : globs; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof ReportGroupNameAndGlobs) { + ReportGroupNameAndGlobs rgnag = (ReportGroupNameAndGlobs) obj; + return rgnag.getGroupName().equals(this.getGroupName()) + && rgnag.getGlobs().equals(this.getGlobs()); + } + + return false; + } + + @Override + public String toString() { + return "RGNAG { group-name: " + this.groupName + ", globs: " + this.globs + "}"; + } +} diff --git a/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java new file mode 100644 index 0000000000..3a90009cad --- /dev/null +++ b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java @@ -0,0 +1,93 @@ +package reposense.model.reportconfig; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a single repository configuration in the overall report + * configuration. + */ +public class ReportRepoConfiguration { + public static final String DEFAULT_REPO = "https://github.com/user/repo"; + public static final List DEFAULT_AUTHOR_EMAIL = List.of( + "john@john.com", "johny@mail.com", "j@domain.com" + ); + public static final String DEFAULT_GIT_HOST_ID = "johnDoe"; + public static final String DEFAULT_DISPLAY_NAME = "John Doe"; + public static final String DEFAULT_GIT_AUTHOR_NAME = "my home PC"; + public static final List DEFAULT_BRANCHES = List.of( + ReportBranchData.DEFAULT_INSTANCE + ); + public static final ReportRepoConfiguration DEFAULT_INSTANCE = new ReportRepoConfiguration(); + + static { + DEFAULT_INSTANCE.repo = DEFAULT_REPO; + DEFAULT_INSTANCE.authorEmails = DEFAULT_AUTHOR_EMAIL; + DEFAULT_INSTANCE.authorGitHostId = DEFAULT_GIT_HOST_ID; + DEFAULT_INSTANCE.authorDisplayName = DEFAULT_DISPLAY_NAME; + DEFAULT_INSTANCE.authorGitAuthorName = DEFAULT_GIT_AUTHOR_NAME; + DEFAULT_INSTANCE.branches = DEFAULT_BRANCHES; + } + + @JsonProperty("repo") + private String repo; + + @JsonProperty("author-emails") + private List authorEmails; + + @JsonProperty("author-git-host-id") + private String authorGitHostId; + + @JsonProperty("author-display-name") + private String authorDisplayName; + + @JsonProperty("author-git-author-name") + private String authorGitAuthorName; + + @JsonProperty("branches") + private List branches; + + public String getRepo() { + return repo == null ? DEFAULT_REPO : repo; + } + + public List getAuthorEmails() { + return authorEmails == null ? DEFAULT_AUTHOR_EMAIL : authorEmails; + } + + public String getAuthorGitHostId() { + return authorGitHostId == null ? DEFAULT_GIT_HOST_ID : authorGitHostId; + } + + public String getAuthorDisplayName() { + return authorDisplayName == null ? DEFAULT_DISPLAY_NAME : authorDisplayName; + } + + public String getAuthorGitAuthorName() { + return authorGitAuthorName == null ? DEFAULT_GIT_AUTHOR_NAME : authorGitAuthorName; + } + + public List getBranches() { + return branches == null ? DEFAULT_BRANCHES : branches; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof ReportRepoConfiguration) { + ReportRepoConfiguration rrc = (ReportRepoConfiguration) obj; + return rrc.getRepo().equals(this.getRepo()) + && rrc.getAuthorEmails().equals(this.getAuthorEmails()) + && rrc.getAuthorGitHostId().equals(this.getAuthorGitHostId()) + && rrc.getAuthorDisplayName().equals(this.getAuthorDisplayName()) + && rrc.getAuthorGitAuthorName().equals(this.getAuthorGitAuthorName()) + && rrc.getBranches().equals(this.getBranches()); + } + + return false; + } +} diff --git a/src/main/java/reposense/parser/ArgsParser.java b/src/main/java/reposense/parser/ArgsParser.java index 245be1a6a3..7c8e493d92 100644 --- a/src/main/java/reposense/parser/ArgsParser.java +++ b/src/main/java/reposense/parser/ArgsParser.java @@ -27,7 +27,7 @@ import reposense.RepoSense; import reposense.model.CliArguments; import reposense.model.FileType; -import reposense.model.ReportConfiguration; +import reposense.model.reportconfig.ReportConfiguration; import reposense.parser.exceptions.ParseException; import reposense.parser.types.AlphanumericArgumentType; import reposense.parser.types.AnalysisThreadsArgumentType; @@ -361,10 +361,10 @@ private static void addReportConfigToBuilder(CliArguments.Builder builder, Names // Report config is ignored if --repos is provided if (locations == null) { - Path reportConfigFilePath = configFolderPath.resolve(ReportConfigJsonParser.REPORT_CONFIG_FILENAME); + Path reportConfigFilePath = configFolderPath.resolve(ReportConfigYamlParser.REPORT_CONFIG_FILENAME); try { - reportConfig = new ReportConfigJsonParser().parse(reportConfigFilePath); + reportConfig = new ReportConfigYamlParser().parse(reportConfigFilePath); } catch (JsonSyntaxException jse) { logger.warning(String.format(MESSAGE_INVALID_CONFIG_PATH, reportConfigFilePath)); } catch (IllegalArgumentException iae) { @@ -374,6 +374,7 @@ private static void addReportConfigToBuilder(CliArguments.Builder builder, Names // Ignore exception as the file is optional. } } + builder.reportConfiguration(reportConfig); } diff --git a/src/main/java/reposense/parser/ReportConfigJsonParser.java b/src/main/java/reposense/parser/ReportConfigJsonParser.java deleted file mode 100644 index f200fc712a..0000000000 --- a/src/main/java/reposense/parser/ReportConfigJsonParser.java +++ /dev/null @@ -1,34 +0,0 @@ -package reposense.parser; - -import java.io.IOException; -import java.lang.reflect.Type; -import java.nio.file.Path; - -import com.google.gson.reflect.TypeToken; - -import reposense.model.ReportConfiguration; - -/** - * Parses json file from {@link Path} and creates a new {@link ReportConfiguration} object. - */ -public class ReportConfigJsonParser extends JsonParser { - public static final String REPORT_CONFIG_FILENAME = "report-config.json"; - - /** - * Gets the type of {@link ReportConfiguration} for json conversion. - */ - @Override - public Type getType() { - return new TypeToken(){}.getType(); - } - - /** - * Converts json file from the given {@code path} and returns a {@link ReportConfiguration} object. - * - * @throws IOException if {@code path} is invalid. - */ - @Override - public ReportConfiguration parse(Path path) throws IOException { - return fromJson(path); - } -} diff --git a/src/main/java/reposense/parser/ReportConfigYamlParser.java b/src/main/java/reposense/parser/ReportConfigYamlParser.java new file mode 100644 index 0000000000..d584d8d40b --- /dev/null +++ b/src/main/java/reposense/parser/ReportConfigYamlParser.java @@ -0,0 +1,59 @@ +package reposense.parser; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Path; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.google.gson.Gson; + +import reposense.model.reportconfig.ReportConfiguration; +import reposense.system.LogsManager; + +/** + * YAML Parser for report-config.yaml files. + */ +public class ReportConfigYamlParser extends JsonParser { + public static final String REPORT_CONFIG_FILENAME = "report-config.yaml"; + private static final Logger logger = LogsManager.getLogger(ReportConfigYamlParser.class); + + @Override + public Type getType() { + return ReportConfiguration.class; + } + + @Override + public ReportConfiguration parse(Path path) throws IOException { + return this.fromJson(null, path, null); + } + + @Override + protected ReportConfiguration fromJson(Path path) throws IOException { + return this.fromJson(null, path, null); + } + + @Override + protected ReportConfiguration fromJson(Gson gson, Path path, Type type) throws IOException , JsonMappingException { + // adapted from https://www.baeldung.com/jackson-yaml + ReportConfiguration reportConfigation; + + try { + logger.log(Level.INFO, "Parsing report-config.yaml file..."); + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + mapper.findAndRegisterModules(); + reportConfigation = mapper.readValue(new File(path.toString()), ReportConfiguration.class); + logger.log(Level.INFO, "report-config.yaml file parsed successfully!"); + } catch (IOException ioe) { + // if the parse fails for any reason, the default config file is used instead + logger.log(Level.WARNING, "Error parsing report-config.yaml: " + ioe.getMessage(), ioe); + reportConfigation = new ReportConfiguration(); + } + + return reportConfigation; + } +} diff --git a/src/main/java/reposense/report/ReportGenerator.java b/src/main/java/reposense/report/ReportGenerator.java index 39d87a343e..913b427765 100644 --- a/src/main/java/reposense/report/ReportGenerator.java +++ b/src/main/java/reposense/report/ReportGenerator.java @@ -44,8 +44,8 @@ import reposense.model.CommitHash; import reposense.model.RepoConfiguration; import reposense.model.RepoLocation; -import reposense.model.ReportConfiguration; import reposense.model.StandaloneConfig; +import reposense.model.reportconfig.ReportConfiguration; import reposense.parser.StandaloneConfigJsonParser; import reposense.report.exception.NoAuthorsWithCommitsFoundException; import reposense.system.LogsManager; @@ -114,10 +114,10 @@ public class ReportGenerator { * @throws IOException if templateZip.zip does not exists in jar file. */ public List generateReposReport(List configs, String outputPath, String assetsPath, - ReportConfiguration reportConfig, String generationDate, LocalDateTime cliSinceDate, - LocalDateTime untilDate, boolean isSinceDateProvided, boolean isUntilDateProvided, int numCloningThreads, - int numAnalysisThreads, Supplier reportGenerationTimeProvider, ZoneId zoneId, - boolean shouldFreshClone) throws IOException { + ReportConfiguration reportConfig, String generationDate, LocalDateTime cliSinceDate, + LocalDateTime untilDate, boolean isSinceDateProvided, boolean isUntilDateProvided, int numCloningThreads, + int numAnalysisThreads, Supplier reportGenerationTimeProvider, ZoneId zoneId, + boolean shouldFreshClone) throws IOException { prepareTemplateFile(outputPath); if (Files.exists(Paths.get(assetsPath))) { FileUtil.copyDirectoryContents(assetsPath, outputPath, assetsFilesWhiteList); diff --git a/src/main/java/reposense/report/SummaryJson.java b/src/main/java/reposense/report/SummaryJson.java index 4d264f938f..80961aff9c 100644 --- a/src/main/java/reposense/report/SummaryJson.java +++ b/src/main/java/reposense/report/SummaryJson.java @@ -7,8 +7,8 @@ import java.util.Set; import reposense.model.RepoConfiguration; -import reposense.model.ReportConfiguration; import reposense.model.SupportedDomainUrlMap; +import reposense.model.reportconfig.ReportConfiguration; /** * Represents the structure of summary.json file in reposense-report folder. @@ -30,8 +30,9 @@ public class SummaryJson { private final Map> supportedDomainUrlMap; public SummaryJson(List repos, ReportConfiguration reportConfig, String reportGeneratedTime, - LocalDateTime sinceDate, LocalDateTime untilDate, boolean isSinceDateProvided, boolean isUntilDateProvided, - String repoSenseVersion, Set> errorSet, String reportGenerationTime, ZoneId zoneId) { + LocalDateTime sinceDate, LocalDateTime untilDate, boolean isSinceDateProvided, + boolean isUntilDateProvided, String repoSenseVersion, Set> errorSet, + String reportGenerationTime, ZoneId zoneId) { this.repos = repos; this.reportGeneratedTime = reportGeneratedTime; this.reportGenerationTime = reportGenerationTime; diff --git a/src/systemtest/resources/ConfigSystemTest/report-config.json b/src/systemtest/resources/ConfigSystemTest/report-config.json deleted file mode 100644 index c2aa7ed0bd..0000000000 --- a/src/systemtest/resources/ConfigSystemTest/report-config.json +++ /dev/null @@ -1 +0,0 @@ -{"title": "RepoSense Report Test Title"} diff --git a/src/systemtest/resources/ConfigSystemTest/report-config.yaml b/src/systemtest/resources/ConfigSystemTest/report-config.yaml new file mode 100644 index 0000000000..1708f569bd --- /dev/null +++ b/src/systemtest/resources/ConfigSystemTest/report-config.yaml @@ -0,0 +1,43 @@ +title: RepoSense Report Test Title +group-details: + - repo: https://github.com/user/repo/tree/fake-branch + groups: + - group-name: code + globs: + - "**.java" + - group-name: tests + globs: + - "src/test**" + - group-name: docs + globs: + - "docs**" + - "**.adoc" + - "**.md" +repos: + - repo: https://github.com/user/repo/tree/fake-branch + author-emails: + - john@john.com + - johny@mail.com + - j@domain.com + author-git-host-id: johnDoe + author-display-name: John Doe + author-git-author-name: my home PC + branches: + - branch: main + file-formats: + - override:java + - md + - fxml + ignore-glob-list: + - "docs**" + ignore-standalone-config: true + ignore-commits-list: + - 2fb6b9b2dd9fa40bf0f9815da2cb0ae8731436c7 + - c5a6dc774e22099cd9ddeb0faff1e75f9cf4f151 + - cd7f610e0becbdf331d5231887d8010a689f87c7 + - 768015345e70f06add2a8b7d1f901dc07bf70582 + ignore-authors-list: + - author1 + - author2 + is-shallow-cloning: true + is-find-previous-authors: false diff --git a/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java new file mode 100644 index 0000000000..70302084c2 --- /dev/null +++ b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java @@ -0,0 +1,56 @@ +package reposense.model.reportconfig; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ReportBranchDataTest { + @Test + public void getBranch_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportBranchData().getBranch(), ReportBranchData.DEFAULT_BRANCH); + } + + @Test + public void getFileFormats_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportBranchData().getFileFormats(), ReportBranchData.DEFAULT_FILE_FORMATS); + } + + @Test + public void getIgnoreGlobList_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportBranchData().getIgnoreGlobList(), ReportBranchData.DEFAULT_IGNORE_GLOB_LIST); + } + + @Test + public void getIsIgnoreStandaloneConfig_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportBranchData().getIsIgnoreStandaloneConfig(), + ReportBranchData.DEFAULT_IS_IGNORE_STANDALONE_CONFIG); + } + + @Test + public void getIgnoreCommitList_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportBranchData().getIgnoreCommitList(), + ReportBranchData.DEFAULT_IGNORE_COMMITS_LIST); + } + + @Test + public void getIgnoreAuthorList_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportBranchData().getIgnoreAuthorList(), + ReportBranchData.DEFAULT_IGNORE_AUTHORS_LIST); + } + + @Test + public void getIsShallowCloning_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportBranchData().getIsShallowCloning(), + ReportBranchData.DEFAULT_IS_SHALLOW_CLONING); + } + + @Test + public void getIsFindPreviousAuthor_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportBranchData().getIsFindPreviousAuthor(), + ReportBranchData.DEFAULT_IS_FIND_PREVIOUS_AUTHOR); + } + + @Test + public void equals_defaultInstancesAreEqual_success() { + Assertions.assertEquals(new ReportBranchData(), new ReportBranchData()); + } +} diff --git a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java new file mode 100644 index 0000000000..19a5fad8b3 --- /dev/null +++ b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java @@ -0,0 +1,28 @@ +package reposense.model.reportconfig; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ReportConfigurationTest { + @Test + public void getTitle_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportConfiguration().getTitle(), ReportConfiguration.DEFAULT_TITLE); + } + + @Test + public void getReportRepoConfigurations_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportConfiguration().getReportRepoConfigurations(), + ReportConfiguration.DEFAULT_REPORT_REPO_CONFIGS); + } + + @Test + public void getGroupDetails_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportConfiguration().getGroupDetails(), + ReportConfiguration.DEFAULT_REPORT_GROUP_DETAILS); + } + + @Test + public void equals_defaultInstancesAreEqual_success() { + Assertions.assertEquals(new ReportConfiguration(), new ReportConfiguration()); + } +} diff --git a/src/test/java/reposense/model/reportconfig/ReportGroupDetailsTest.java b/src/test/java/reposense/model/reportconfig/ReportGroupDetailsTest.java new file mode 100644 index 0000000000..d3df44380f --- /dev/null +++ b/src/test/java/reposense/model/reportconfig/ReportGroupDetailsTest.java @@ -0,0 +1,23 @@ +package reposense.model.reportconfig; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ReportGroupDetailsTest { + @Test + public void getRepo_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportGroupDetails().getRepo(), ReportGroupDetails.DEFAULT_REPO); + } + + @Test + public void getReportGroupNameAndGlobsList_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportGroupDetails().getReportGroupNameAndGlobsList(), + ReportGroupDetails.DEFAULT_NAMES_AND_GLOBS); + } + + @Test + public void equals_defaultInstancesAreEqual_success() { + Assertions.assertEquals(new ReportGroupDetails(), new ReportGroupDetails()); + Assertions.assertEquals(new ReportGroupDetails(), ReportGroupDetails.DEFAULT_INSTANCE); + } +} diff --git a/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java b/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java new file mode 100644 index 0000000000..e44028c85e --- /dev/null +++ b/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java @@ -0,0 +1,22 @@ +package reposense.model.reportconfig; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ReportGroupNameAndGlobsTest { + @Test + public void getRepo_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportGroupNameAndGlobs().getGroupName(), ReportGroupNameAndGlobs.DEFAULT_GROUP_NAME); + } + + @Test + public void getReportGroupNameAndGlobsList_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportGroupNameAndGlobs().getGlobs(), + ReportGroupNameAndGlobs.DEFAULT_GLOBS); + } + + @Test + public void equals_defaultInstancesAreEqual_success() { + Assertions.assertEquals(new ReportGroupNameAndGlobs(), new ReportGroupNameAndGlobs()); + } +} diff --git a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java new file mode 100644 index 0000000000..679e9ea88f --- /dev/null +++ b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java @@ -0,0 +1,46 @@ +package reposense.model.reportconfig; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ReportRepoConfigurationTest { + @Test + public void getRepo_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportRepoConfiguration().getRepo(), ReportRepoConfiguration.DEFAULT_REPO); + } + + @Test + public void getAuthorEmails_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportRepoConfiguration().getAuthorEmails(), + ReportRepoConfiguration.DEFAULT_AUTHOR_EMAIL); + } + + @Test + public void getAuthorGitHostId_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportRepoConfiguration().getAuthorGitHostId(), + ReportRepoConfiguration.DEFAULT_GIT_HOST_ID); + } + + @Test + public void getAuthorDisplayName_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportRepoConfiguration().getAuthorDisplayName(), + ReportRepoConfiguration.DEFAULT_DISPLAY_NAME); + } + + @Test + public void getAuthorGitAuthorName_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportRepoConfiguration().getAuthorGitAuthorName(), + ReportRepoConfiguration.DEFAULT_GIT_AUTHOR_NAME); + } + + @Test + public void getBranches_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportRepoConfiguration().getBranches(), ReportRepoConfiguration.DEFAULT_BRANCHES); + } + + @Test + public void equals_defaultInstancesAreEqual_success() { + Assertions.assertEquals(new ReportRepoConfiguration(), new ReportRepoConfiguration()); + Assertions.assertEquals(new ReportRepoConfiguration(), ReportRepoConfiguration.DEFAULT_INSTANCE); + } +} diff --git a/src/test/java/reposense/parser/ReportConfigJsonParserTest.java b/src/test/java/reposense/parser/ReportConfigJsonParserTest.java deleted file mode 100644 index 44005ad8d2..0000000000 --- a/src/test/java/reposense/parser/ReportConfigJsonParserTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package reposense.parser; - -import static reposense.util.TestUtil.loadResource; - -import java.nio.file.Path; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import reposense.model.ReportConfiguration; - -public class ReportConfigJsonParserTest { - - private static final Path VALID_REPORT_CONFIG = loadResource( - ReportConfigJsonParserTest.class, "ReportConfigJsonParserTest/report-config-valid.json"); - private static final Path INVALID_REPORT_CONFIG = loadResource( - ReportConfigJsonParserTest.class, "ReportConfigJsonParserTest/report-config-invalid.json"); - private static final Path EMPTY_REPORT_CONFIG = loadResource( - ReportConfigJsonParserTest.class, "ReportConfigJsonParserTest/report-config-empty.json"); - private static final String DEFAULT_TITLE = "RepoSense Report"; - - @Test - public void reportConfig_parseEmptyJsonFile_getDefaultTitle() throws Exception { - ReportConfiguration reportConfig = new ReportConfigJsonParser().parse(EMPTY_REPORT_CONFIG); - Assertions.assertEquals(reportConfig.getTitle(), DEFAULT_TITLE); - } - @Test - public void reportConfig_parseInvalidJsonFile_getDefaultTitle() throws Exception { - ReportConfiguration reportConfig = new ReportConfigJsonParser().parse(INVALID_REPORT_CONFIG); - Assertions.assertEquals(reportConfig.getTitle(), DEFAULT_TITLE); - } - - @Test - public void reportConfig_parseValidJsonFile_getCustomTitle() throws Exception { - ReportConfiguration reportConfig = new ReportConfigJsonParser().parse(VALID_REPORT_CONFIG); - Assertions.assertNotEquals(reportConfig.getTitle(), DEFAULT_TITLE); - } -} diff --git a/src/test/java/reposense/parser/ReportConfigYamlParserTest.java b/src/test/java/reposense/parser/ReportConfigYamlParserTest.java new file mode 100644 index 0000000000..79e440b09e --- /dev/null +++ b/src/test/java/reposense/parser/ReportConfigYamlParserTest.java @@ -0,0 +1,44 @@ +package reposense.parser; + +import static reposense.util.TestUtil.loadResource; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import reposense.model.reportconfig.ReportConfiguration; + +public class ReportConfigYamlParserTest { + + private static final Path VALID_REPORT_CONFIG = loadResource( + ReportConfigYamlParserTest.class, "ReportConfigYamlParserTest/report-config-valid.yaml"); + private static final Path INVALID_REPORT_CONFIG = loadResource( + ReportConfigYamlParserTest.class, "ReportConfigYamlParserTest/report-config-invalid.yaml"); + private static final Path EMPTY_REPORT_CONFIG = loadResource( + ReportConfigYamlParserTest.class, "ReportConfigYamlParserTest/report-config-empty.yaml"); + + @Test + public void reportConfig_parseEmptyYamlFile_getDefaultTitle() throws Exception { + ReportConfiguration reportConfig = new ReportConfigYamlParser().parse(EMPTY_REPORT_CONFIG); + Assertions.assertEquals(reportConfig.getTitle(), ReportConfiguration.DEFAULT_TITLE); + Assertions.assertEquals(reportConfig.getReportRepoConfigurations(), + ReportConfiguration.DEFAULT_REPORT_REPO_CONFIGS); + } + + @Test + public void reportConfig_parseInvalidYamlFile_getDefaultTitle() throws Exception { + ReportConfiguration reportConfig = new ReportConfigYamlParser().parse(INVALID_REPORT_CONFIG); + Assertions.assertEquals(reportConfig.getTitle(), ReportConfiguration.DEFAULT_TITLE); + Assertions.assertEquals(reportConfig.getReportRepoConfigurations(), + ReportConfiguration.DEFAULT_REPORT_REPO_CONFIGS); + } + + @Test + public void reportConfig_parseValidYamlFile_getCustomTitle() throws Exception { + ReportConfiguration reportConfig = new ReportConfigYamlParser().parse(VALID_REPORT_CONFIG); + Assertions.assertNotEquals(reportConfig.getTitle(), ReportConfiguration.DEFAULT_TITLE); + Assertions.assertNotEquals(reportConfig.getReportRepoConfigurations(), + ReportConfiguration.DEFAULT_REPORT_REPO_CONFIGS); + } +} diff --git a/src/test/resources/ReportConfigJsonParserTest/report-config-empty.json b/src/test/resources/ReportConfigJsonParserTest/report-config-empty.json deleted file mode 100644 index 0db3279e44..0000000000 --- a/src/test/resources/ReportConfigJsonParserTest/report-config-empty.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} diff --git a/src/test/resources/ReportConfigJsonParserTest/report-config-invalid.json b/src/test/resources/ReportConfigJsonParserTest/report-config-invalid.json deleted file mode 100644 index fd26202fa2..0000000000 --- a/src/test/resources/ReportConfigJsonParserTest/report-config-invalid.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "ti": "!@#$%^&*()<>:" -} diff --git a/src/test/resources/ReportConfigJsonParserTest/report-config-valid.json b/src/test/resources/ReportConfigJsonParserTest/report-config-valid.json deleted file mode 100644 index ba731c2add..0000000000 --- a/src/test/resources/ReportConfigJsonParserTest/report-config-valid.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "!@#$%^&*()<>:" -} diff --git a/src/test/resources/ReportConfigYamlParserTest/report-config-empty.yaml b/src/test/resources/ReportConfigYamlParserTest/report-config-empty.yaml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/test/resources/ReportConfigYamlParserTest/report-config-invalid.yaml b/src/test/resources/ReportConfigYamlParserTest/report-config-invalid.yaml new file mode 100644 index 0000000000..fda2a639be --- /dev/null +++ b/src/test/resources/ReportConfigYamlParserTest/report-config-invalid.yaml @@ -0,0 +1,36 @@ +title: RepoSense Report +group-details: + - repo: https://github.com/user/repo/tree/fake-branch + grou + - group-name: code + globs: + - "**.java" + - group-name: tests + globs: + - "src/test**" + - group-name: docs + globs: + - "docs**" + - "**.adoc" + - "**.md" +os: + - repo: https://github.com/user/repo/tree/fake-branch + author-emails: + - john@john.com + - johny@mail.com + - j@domain.com + author-git-author-name: my home PC + branches: + - branch: main + file-formats: + - override:java + - md + - fxml + ignore-glob-list: + - "docs**" + ignore-standalone-config: true + ignore-author-list: + - author1 + - author2 + is-shallow-cloning: true + is-find-previous-authors: false diff --git a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml new file mode 100644 index 0000000000..746b0493f6 --- /dev/null +++ b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml @@ -0,0 +1,44 @@ +title: RepoSense Report Repo Test +group-details: + - repo: https://github.com/user/repo/tree/fake-branch + groups: + - group-name: code + globs: + - "**.java" + - group-name: tests + globs: + - "src/test**" + - group-name: docs + globs: + - "docs**" + - "**.adoc" + - "**.md" +repos: + - repo: https://github.com/user/repo/tree/fake-branch + author-emails: + - john@john.com + - johny@mail.com + - j@domain.com + author-git-host-id: johnDoe + author-display-name: John Doe + author-git-author-name: my home PC + branches: + - branch: main + file-formats: + - override:java + - md + - fxml + ignore-glob-list: + - "docs**" + ignore-standalone-config: true + ignore-commits-list: + - 2fb6b9b2dd9fa40bf0f9815da2cb0ae8731436c7 + - c5a6dc774e22099cd9ddeb0faff1e75f9cf4f151 + - cd7f610e0becbdf331d5231887d8010a689f87c7 + - 768015345e70f06add2a8b7d1f901dc07bf70582 + ignore-authors-list: + - author1 + - author2 + is-shallow-cloning: true + is-find-previous-authors: false + From b76b29e90e160e4c5a2fd72ba1b366f721f7a21f Mon Sep 17 00:00:00 2001 From: George Tay Date: Sat, 27 Apr 2024 16:22:25 +0800 Subject: [PATCH 02/42] Update report-config.yaml file --- docs/ug/configFiles.md | 2 +- docs/ug/report-config.yaml | 54 +++++++++++++++---- .../java/reposense/parser/ArgsParser.java | 6 +-- 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/docs/ug/configFiles.md b/docs/ug/configFiles.md index 2b942673c1..46f95d4851 100644 --- a/docs/ug/configFiles.md +++ b/docs/ug/configFiles.md @@ -109,7 +109,7 @@ e.g.: `example.java` in `example-repo` can either be in the `test` group or the ## `report-config.yaml` -You can also optionally use a `report-config.json` file to quickly define the repository information for the repositories you are interested in tracking and generate your very own code portfolio. +You can also optionally use a `report-config.yaml` file to quickly define the repository information for the repositories you are interested in tracking and generate your very own code portfolio. View this [example](report-config.yaml) for a better understanding of what repository information is required. diff --git a/docs/ug/report-config.yaml b/docs/ug/report-config.yaml index d7fed5f8fe..4e430c7404 100644 --- a/docs/ug/report-config.yaml +++ b/docs/ug/report-config.yaml @@ -1,11 +1,43 @@ -repoUrl: github.com/user/repo -reportTitle: RepoSense Report -authorDisplayName: Sample Author -authorGithubId: Sample Author Github ID -branches: - - name: Branch 1 - blurb: Blurb 1 - - name: Branch 2 - blurb: Blurb 2 -startDate: 2020-01-01 -endDate: 9999-12-31 +title: RepoSense Report +group-details: + - repo: https://github.com/user/repo/tree/fake-branch + groups: + - group-name: code + globs: + - "**.java" + - group-name: tests + globs: + - "src/test**" + - group-name: docs + globs: + - "docs**" + - "**.adoc" + - "**.md" +repos: + - repo: https://github.com/user/repo/tree/fake-branch + author-emails: + - john@john.com + - johny@mail.com + - j@domain.com + author-git-host-id: johnDoe + author-display-name: John Doe + author-git-author-name: my home PC + branches: + - branch: main + file-formats: + - override:java + - md + - fxml + ignore-glob-list: + - "docs**" + ignore-standalone-config: true + ignore-commits-list: + - 2fb6b9b2dd9fa40bf0f9815da2cb0ae8731436c7 + - c5a6dc774e22099cd9ddeb0faff1e75f9cf4f151 + - cd7f610e0becbdf331d5231887d8010a689f87c7 + - 768015345e70f06add2a8b7d1f901dc07bf70582 + ignore-authors-list: + - author1 + - author2 + is-shallow-cloning: true + is-find-previous-authors: false diff --git a/src/main/java/reposense/parser/ArgsParser.java b/src/main/java/reposense/parser/ArgsParser.java index 7c8e493d92..efe37cee65 100644 --- a/src/main/java/reposense/parser/ArgsParser.java +++ b/src/main/java/reposense/parser/ArgsParser.java @@ -89,7 +89,7 @@ public class ArgsParser { private static final String MESSAGE_USING_DEFAULT_CONFIG_PATH = "Config path not provided, using the config folder as default."; private static final String MESSAGE_INVALID_CONFIG_PATH = "%s is malformed."; - private static final String MESSAGE_INVALID_CONFIG_JSON = "%s Ignoring the report config provided."; + private static final String MESSAGE_INVALID_CONFIG_YAML = "%s Ignoring the report config provided."; private static final String MESSAGE_SINCE_D1_WITH_PERIOD = "You may be using --since d1 with the --period flag. " + "This may result in an incorrect date range being analysed."; private static final String MESSAGE_SINCE_DATE_LATER_THAN_UNTIL_DATE = @@ -368,9 +368,9 @@ private static void addReportConfigToBuilder(CliArguments.Builder builder, Names } catch (JsonSyntaxException jse) { logger.warning(String.format(MESSAGE_INVALID_CONFIG_PATH, reportConfigFilePath)); } catch (IllegalArgumentException iae) { - logger.warning(String.format(MESSAGE_INVALID_CONFIG_JSON, iae.getMessage())); + logger.warning(String.format(MESSAGE_INVALID_CONFIG_YAML, iae.getMessage())); } catch (IOException ioe) { - // IOException thrown as report-config.json is not found. + // IOException thrown as report-config.yaml is not found. // Ignore exception as the file is optional. } } From 65cf7ddeab9565b3e485f91d03bb1d4507f8ddef Mon Sep 17 00:00:00 2001 From: sopa301 <96387349+sopa301@users.noreply.github.com> Date: Tue, 7 May 2024 12:48:08 +0800 Subject: [PATCH 03/42] Update config file in cypress --- frontend/cypress/config/report-config.json | 3 --- frontend/cypress/config/report-config.yaml | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 frontend/cypress/config/report-config.json create mode 100644 frontend/cypress/config/report-config.yaml diff --git a/frontend/cypress/config/report-config.json b/frontend/cypress/config/report-config.json deleted file mode 100644 index ea98e619e3..0000000000 --- a/frontend/cypress/config/report-config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "RepoSense Test Report" -} diff --git a/frontend/cypress/config/report-config.yaml b/frontend/cypress/config/report-config.yaml new file mode 100644 index 0000000000..a530aa7a91 --- /dev/null +++ b/frontend/cypress/config/report-config.yaml @@ -0,0 +1 @@ +title: RepoSense Report From 0a4b72d173f1de5c8f05f373cb87a6b82a1fc991 Mon Sep 17 00:00:00 2001 From: sopa301 <96387349+sopa301@users.noreply.github.com> Date: Tue, 7 May 2024 12:49:03 +0800 Subject: [PATCH 04/42] Fix title name --- frontend/cypress/config/report-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/cypress/config/report-config.yaml b/frontend/cypress/config/report-config.yaml index a530aa7a91..a5d9bb7897 100644 --- a/frontend/cypress/config/report-config.yaml +++ b/frontend/cypress/config/report-config.yaml @@ -1 +1 @@ -title: RepoSense Report +title: RepoSense Test Report From ab3aff4d8d69395ca1dd7155e8b6ac93bd238e90 Mon Sep 17 00:00:00 2001 From: George Tay Date: Sun, 12 May 2024 19:41:52 +0800 Subject: [PATCH 05/42] Update report-config.yaml format --- config/report-config.yaml | 38 ++++---- docs/ug/cli.md | 2 +- docs/ug/configFiles.md | 38 ++++---- .../reportconfig/ReportAuthorDetails.java | 94 +++++++++++++++++++ .../model/reportconfig/ReportBranchData.java | 13 +++ .../reportconfig/ReportRepoConfiguration.java | 42 --------- .../ConfigSystemTest/report-config.yaml | 15 +-- .../reportconfig/ReportAuthorDetailsTest.java | 30 ++++++ .../ReportRepoConfigurationTest.java | 24 ----- .../report-config-valid.yaml | 15 +-- 10 files changed, 192 insertions(+), 119 deletions(-) create mode 100644 src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java create mode 100644 src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java diff --git a/config/report-config.yaml b/config/report-config.yaml index 4e430c7404..b5914b25ce 100644 --- a/config/report-config.yaml +++ b/config/report-config.yaml @@ -1,6 +1,6 @@ title: RepoSense Report group-details: - - repo: https://github.com/user/repo/tree/fake-branch + - repo: https://github.com/reposense/testrepo-Delta.git groups: - group-name: code globs: @@ -14,30 +14,30 @@ group-details: - "**.adoc" - "**.md" repos: - - repo: https://github.com/user/repo/tree/fake-branch - author-emails: - - john@john.com - - johny@mail.com - - j@domain.com - author-git-host-id: johnDoe - author-display-name: John Doe - author-git-author-name: my home PC + - repo: https://github.com/reposense/testrepo-Delta.git branches: - - branch: main + - branch: master + authors: + - author-git-host-id: fzdy1914 + author-display-name: WANG CHAO + author-git-author-name: WANG CHAO + author-emails: + - 1229983126@qq.com + - author-git-host-id: FH-30 + author-display-name: Francis Hodianto + author-git-author-name: Francis Hodianto + author-emails: + - file-formats: - override:java - md - fxml ignore-glob-list: - - "docs**" - ignore-standalone-config: true + - + ignore-standalone-config: false ignore-commits-list: - - 2fb6b9b2dd9fa40bf0f9815da2cb0ae8731436c7 - - c5a6dc774e22099cd9ddeb0faff1e75f9cf4f151 - - cd7f610e0becbdf331d5231887d8010a689f87c7 - - 768015345e70f06add2a8b7d1f901dc07bf70582 + - ignore-authors-list: - - author1 - - author2 - is-shallow-cloning: true + - + is-shallow-cloning: false is-find-previous-authors: false diff --git a/docs/ug/cli.md b/docs/ug/cli.md index 550713969c..d596af8594 100644 --- a/docs/ug/cli.md +++ b/docs/ug/cli.md @@ -68,7 +68,7 @@ partial credit.
**`--config CONFIG_DIRECTORY`**: Specifies that config files located in `CONFIG_DIRECTORY` should be used to customize the report. -* Parameter: `CONFIG_DIRECTORY` The directory containing the config files. Should contain a `repo-config.csv` file. Optionally, can contain an `author-config.csv` file or/and a `group-config.csv` file or/and a `report-config.json` file. +* Parameter: `CONFIG_DIRECTORY` The directory containing the config files. Should contain a `repo-config.csv` file. Optionally, can contain an `author-config.csv` file or/and a `group-config.csv` file or/and a `report-config.yaml` file. * Alias: `-c` * Example: `java -jar RepoSense.jar --config ./config` diff --git a/docs/ug/configFiles.md b/docs/ug/configFiles.md index 46f95d4851..3c9585fb54 100644 --- a/docs/ug/configFiles.md +++ b/docs/ug/configFiles.md @@ -31,20 +31,20 @@ Given below are the details of the various config files used by RepoSense. **`repo-config.csv` file contains repo-level config data.** Each row represents a repository's configuration ([example](repo-config.csv)). -| Column Name | Explanation | -|-------------|-------------| -| Repository's Location {{ mandatory }} | The `Remote Repo URL` or `Disk Path` to the git repository e.g., `https://github.com/foo/bar.git` or `C:\Users\user\Desktop\GitHub\foo\bar` | -| Branch | The branch to analyze in the target repository e.g., `master`. Default: the default branch of the repo | -| File formats*+ | The file extensions to analyze. Binary file formats, such as `png` and `jpg`, will be automatically labelled as the file type `binary` in the generated report. Default: all file formats | -| Find Previous Authors | Enter **`yes`** to utilize Git blame's ignore revisions functionality, RepoSense will attempt to blame the line changes caused by commits in the ignore commit list to the previous authors who altered those lines (if available). | -| Ignore Glob List*+ | The list of file path globs to ignore during analysis for each author e.g., `test/**;temp/**`. Refer to the [_glob format_](https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob) for the path glob syntax. | -| Ignore standalone config | To ignore the standalone config file (if any) in target repository, enter **`yes`**. If the cell is empty, the standalone config file in the repo (if any) will take precedence over configurations provided in the csv files. | -| Ignore Commits List*+ | The list of commits to ignore during analysis. For accurate results, the commits should be provided with their full hash. Additionally, a range of commits can be specified using the `..` notation e.g. `abc123..def456` (both inclusive). | -| Ignore Authors List*+ | The list of authors to ignore during analysis. Authors should be specified by their [Git Author Name](#a-note-about-git-author-name). | -| Shallow Cloning | Enter **`yes`** to clone the repository using Git's shallow cloning functionality. This option can significantly reduce the time taken to clone large repositories. However, the option should ideally be disabled for smaller repositories where the `.git` file is smaller than 500 MB, as it would create overhead. | -| File Size Limit+ | Enter a file size limit for the repository in bytes as a single number without units (for a size limit of 1MB for example, enter 1000000). This file size limit will override the default file size limit (500KB). Files exceeding the file size limit will be marked as ignored and only the file name and line count will be reflected in the report. | -| Ignore File Size Limit | Enter **`yes`** to ignore both the default file size limit and the file size limit possibly set by the user in `repo-config.csv`. | -| Skip Ignored File Analysis | Enter **`yes`** to ignore analysis of files exceeding the file size limit entirely. If file analysis is skipped, all information about the file will be omitted from the generated report. This option can significantly improve report generation time. | +| Column Name | Explanation | +|---------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Repository's Location {{ mandatory }} | The `Remote Repo URL` or `Disk Path` to the git repository e.g., `https://github.com/foo/bar.git` or `C:\Users\user\Desktop\GitHub\foo\bar` | +| Branch | The branch to analyze in the target repository e.g., `master`. Default: the default branch of the repo | +| File formats*+ | The file extensions to analyze. Binary file formats, such as `png` and `jpg`, will be automatically labelled as the file type `binary` in the generated report. Default: all file formats | +| Find Previous Authors | Enter **`yes`** to utilize Git blame's ignore revisions functionality, RepoSense will attempt to blame the line changes caused by commits in the ignore commit list to the previous authors who altered those lines (if available). | +| Ignore Glob List*+ | The list of file path globs to ignore during analysis for each author e.g., `test/**;temp/**`. Refer to the [_glob format_](https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob) for the path glob syntax. | +| Ignore standalone config | To ignore the standalone config file (if any) in target repository, enter **`yes`**. If the cell is empty, the standalone config file in the repo (if any) will take precedence over configurations provided in the csv files. | +| Ignore Commits List*+ | The list of commits to ignore during analysis. For accurate results, the commits should be provided with their full hash. Additionally, a range of commits can be specified using the `..` notation e.g. `abc123..def456` (both inclusive). | +| Ignore Authors List*+ | The list of authors to ignore during analysis. Authors should be specified by their [Git Author Name](#a-note-about-git-author-name). | +| Shallow Cloning | Enter **`yes`** to clone the repository using Git's shallow cloning functionality. This option can significantly reduce the time taken to clone large repositories. However, the option should ideally be disabled for smaller repositories where the `.git` file is smaller than 500 MB, as it would create overhead. | +| File Size Limit+ | Enter a file size limit for the repository in bytes as a single number without units (for a size limit of 1MB for example, enter 1000000). This file size limit will override the default file size limit (500KB). Files exceeding the file size limit will be marked as ignored and only the file name and line count will be reflected in the report. | +| Ignore File Size Limit | Enter **`yes`** to ignore both the default file size limit and the file size limit possibly set by the user in `repo-config.csv`. | +| Skip Ignored File Analysis | Enter **`yes`** to ignore analysis of files exceeding the file size limit entirely. If file analysis is skipped, all information about the file will be omitted from the generated report. This option can significantly improve report generation time. | The Shallow Cloning option is incompatible with the "--last-modified-date" CLI flag. @@ -94,11 +94,11 @@ If `author-config.csv` is not given and the repo has not provided author details Optionally, you can provide a `group-config.csv`(which should be in the same directory as `repo-config.csv` file) to provide details on any custom groupings for files in specified repositories ([example](group-config.csv)). It should contain the following columns: -| Column Name | Explanation | -|-------------|-------------| -| Repository's Location | Same as `repo-config.csv`. Default: all the repos in `repo-config.csv` | -| Group Name {{ mandatory }} | Name of the group, e.g.,`test`. | -| Globs * {{ mandatory }} | The list of file path globs to include for specified group, e.g.,`**/test/*;**.java`. | +| Column Name | Explanation | +|----------------------------|---------------------------------------------------------------------------------------| +| Repository's Location | Same as `repo-config.csv`. Default: all the repos in `repo-config.csv` | +| Group Name {{ mandatory }} | Name of the group, e.g.,`test`. | +| Globs * {{ mandatory }} | The list of file path globs to include for specified group, e.g.,`**/test/*;**.java`. | * **Multi-value column**: multiple values can be entered in this column using a semicolon `;` as the separator. diff --git a/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java b/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java new file mode 100644 index 0000000000..af5d41662f --- /dev/null +++ b/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java @@ -0,0 +1,94 @@ +package reposense.model.reportconfig; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents an author's details in the report-config.yaml file. + */ +public class ReportAuthorDetails { + public static final List DEFAULT_INSTANCES = new ArrayList<>(); + + public static final List DEFAULT_AUTHOR_EMAIL = List.of("john@john.com", "johny@mail.com", "j@domain.com"); + public static final String DEFAULT_GIT_HOST_ID = "johnDoe"; + public static final String DEFAULT_DISPLAY_NAME = "John Doe"; + public static final String DEFAULT_GIT_AUTHOR_NAME = "my home PC"; + + private static final List DEFAULT_AUTHOR_EMAIL_1 = List.of("1229983126@qq.com"); + private static final String DEFAULT_GIT_HOST_ID_1 = "fzdy1914"; + private static final String DEFAULT_DISPLAY_NAME_1 = "WANG CHAO"; + private static final String DEFAULT_GIT_AUTHOR_NAME_1 = "WANG CHAO"; + private static final List DEFAULT_AUTHOR_EMAIL_2 = List.of(""); + private static final String DEFAULT_GIT_HOST_ID_2 = "FH-30"; + private static final String DEFAULT_DISPLAY_NAME_2 = "Francis Hodianto"; + private static final String DEFAULT_GIT_AUTHOR_NAME_2 = "Francis Hodianto"; + + static { + ReportAuthorDetails rad1 = new ReportAuthorDetails(); + ReportAuthorDetails rad2 = new ReportAuthorDetails(); + + rad1.authorEmails = DEFAULT_AUTHOR_EMAIL_1; + rad1.authorGitHostId = DEFAULT_GIT_HOST_ID_1; + rad1.authorGitAuthorName = DEFAULT_GIT_AUTHOR_NAME_1; + rad1.authorDisplayName = DEFAULT_DISPLAY_NAME_1; + + rad2.authorEmails = DEFAULT_AUTHOR_EMAIL_2; + rad2.authorGitHostId = DEFAULT_GIT_HOST_ID_2; + rad2.authorGitAuthorName = DEFAULT_GIT_AUTHOR_NAME_2; + rad2.authorDisplayName = DEFAULT_DISPLAY_NAME_2; + + DEFAULT_INSTANCES.add(rad1); + DEFAULT_INSTANCES.add(rad2); + } + + @JsonProperty("author-emails") + private List authorEmails; + + @JsonProperty("author-git-host-id") + private String authorGitHostId; + + @JsonProperty("author-display-name") + private String authorDisplayName; + + @JsonProperty("author-git-author-name") + private String authorGitAuthorName; + + public ReportAuthorDetails() { + + } + + public List getAuthorEmails() { + return authorEmails == null ? DEFAULT_AUTHOR_EMAIL : authorEmails; + } + + public String getAuthorGitHostId() { + return authorGitHostId == null ? DEFAULT_GIT_HOST_ID : authorGitHostId; + } + + public String getAuthorDisplayName() { + return authorDisplayName == null ? DEFAULT_DISPLAY_NAME : authorDisplayName; + } + + public String getAuthorGitAuthorName() { + return authorGitAuthorName == null ? DEFAULT_GIT_AUTHOR_NAME : authorGitAuthorName; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof ReportAuthorDetails) { + ReportAuthorDetails rad = (ReportAuthorDetails) obj; + return rad.getAuthorEmails().equals(this.getAuthorEmails()) + && rad.getAuthorGitHostId().equals(this.getAuthorGitHostId()) + && rad.getAuthorDisplayName().equals(this.getAuthorDisplayName()) + && rad.getAuthorGitAuthorName().equals(this.getAuthorGitAuthorName()); + } + + return false; + } +} diff --git a/src/main/java/reposense/model/reportconfig/ReportBranchData.java b/src/main/java/reposense/model/reportconfig/ReportBranchData.java index 687577b863..88cf71ef62 100644 --- a/src/main/java/reposense/model/reportconfig/ReportBranchData.java +++ b/src/main/java/reposense/model/reportconfig/ReportBranchData.java @@ -1,5 +1,6 @@ package reposense.model.reportconfig; +import java.util.ArrayList; import java.util.List; import com.fasterxml.jackson.annotation.JsonProperty; @@ -9,6 +10,9 @@ */ public class ReportBranchData { public static final String DEFAULT_BRANCH = "main"; + public static final List DEFAULT_REPORT_AUTHOR_DETAILS = new ArrayList<>( + ReportAuthorDetails.DEFAULT_INSTANCES + ); public static final List DEFAULT_FILE_FORMATS = List.of( "override:java", "md", "fxml" ); @@ -40,11 +44,15 @@ public class ReportBranchData { DEFAULT_INSTANCE.isFindPreviousAuthor = ReportBranchData.DEFAULT_IS_FIND_PREVIOUS_AUTHOR; DEFAULT_INSTANCE.isShallowCloning = ReportBranchData.DEFAULT_IS_SHALLOW_CLONING; DEFAULT_INSTANCE.isIgnoreStandaloneConfig = ReportBranchData.DEFAULT_IS_IGNORE_STANDALONE_CONFIG; + DEFAULT_INSTANCE.reportAuthorDetails = ReportAuthorDetails.DEFAULT_INSTANCES; } @JsonProperty("branch") private String branch; + @JsonProperty("authors") + private List reportAuthorDetails; + @JsonProperty("file-formats") private List fileFormats; @@ -70,6 +78,10 @@ public String getBranch() { return branch == null ? DEFAULT_BRANCH : branch; } + public List getReportAuthorDetails() { + return reportAuthorDetails == null ? DEFAULT_REPORT_AUTHOR_DETAILS : reportAuthorDetails; + } + public List getFileFormats() { return fileFormats == null ? DEFAULT_FILE_FORMATS : fileFormats; } @@ -107,6 +119,7 @@ public boolean equals(Object obj) { if (obj instanceof ReportBranchData) { ReportBranchData rbd = (ReportBranchData) obj; return this.getBranch().equals(rbd.getBranch()) + && this.getReportAuthorDetails().equals(rbd.getReportAuthorDetails()) && this.getFileFormats().equals(rbd.getFileFormats()) && this.getIgnoreGlobList().equals(rbd.getIgnoreGlobList()) && this.getIsIgnoreStandaloneConfig() == rbd.getIsIgnoreStandaloneConfig() diff --git a/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java index 3a90009cad..375ce95653 100644 --- a/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java +++ b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java @@ -10,12 +10,6 @@ */ public class ReportRepoConfiguration { public static final String DEFAULT_REPO = "https://github.com/user/repo"; - public static final List DEFAULT_AUTHOR_EMAIL = List.of( - "john@john.com", "johny@mail.com", "j@domain.com" - ); - public static final String DEFAULT_GIT_HOST_ID = "johnDoe"; - public static final String DEFAULT_DISPLAY_NAME = "John Doe"; - public static final String DEFAULT_GIT_AUTHOR_NAME = "my home PC"; public static final List DEFAULT_BRANCHES = List.of( ReportBranchData.DEFAULT_INSTANCE ); @@ -23,28 +17,12 @@ public class ReportRepoConfiguration { static { DEFAULT_INSTANCE.repo = DEFAULT_REPO; - DEFAULT_INSTANCE.authorEmails = DEFAULT_AUTHOR_EMAIL; - DEFAULT_INSTANCE.authorGitHostId = DEFAULT_GIT_HOST_ID; - DEFAULT_INSTANCE.authorDisplayName = DEFAULT_DISPLAY_NAME; - DEFAULT_INSTANCE.authorGitAuthorName = DEFAULT_GIT_AUTHOR_NAME; DEFAULT_INSTANCE.branches = DEFAULT_BRANCHES; } @JsonProperty("repo") private String repo; - @JsonProperty("author-emails") - private List authorEmails; - - @JsonProperty("author-git-host-id") - private String authorGitHostId; - - @JsonProperty("author-display-name") - private String authorDisplayName; - - @JsonProperty("author-git-author-name") - private String authorGitAuthorName; - @JsonProperty("branches") private List branches; @@ -52,22 +30,6 @@ public String getRepo() { return repo == null ? DEFAULT_REPO : repo; } - public List getAuthorEmails() { - return authorEmails == null ? DEFAULT_AUTHOR_EMAIL : authorEmails; - } - - public String getAuthorGitHostId() { - return authorGitHostId == null ? DEFAULT_GIT_HOST_ID : authorGitHostId; - } - - public String getAuthorDisplayName() { - return authorDisplayName == null ? DEFAULT_DISPLAY_NAME : authorDisplayName; - } - - public String getAuthorGitAuthorName() { - return authorGitAuthorName == null ? DEFAULT_GIT_AUTHOR_NAME : authorGitAuthorName; - } - public List getBranches() { return branches == null ? DEFAULT_BRANCHES : branches; } @@ -81,10 +43,6 @@ public boolean equals(Object obj) { if (obj instanceof ReportRepoConfiguration) { ReportRepoConfiguration rrc = (ReportRepoConfiguration) obj; return rrc.getRepo().equals(this.getRepo()) - && rrc.getAuthorEmails().equals(this.getAuthorEmails()) - && rrc.getAuthorGitHostId().equals(this.getAuthorGitHostId()) - && rrc.getAuthorDisplayName().equals(this.getAuthorDisplayName()) - && rrc.getAuthorGitAuthorName().equals(this.getAuthorGitAuthorName()) && rrc.getBranches().equals(this.getBranches()); } diff --git a/src/systemtest/resources/ConfigSystemTest/report-config.yaml b/src/systemtest/resources/ConfigSystemTest/report-config.yaml index 1708f569bd..95399c0930 100644 --- a/src/systemtest/resources/ConfigSystemTest/report-config.yaml +++ b/src/systemtest/resources/ConfigSystemTest/report-config.yaml @@ -15,15 +15,16 @@ group-details: - "**.md" repos: - repo: https://github.com/user/repo/tree/fake-branch - author-emails: - - john@john.com - - johny@mail.com - - j@domain.com - author-git-host-id: johnDoe - author-display-name: John Doe - author-git-author-name: my home PC branches: - branch: main + authors: + - author-git-host-id: johnDoe + author-display-name: John Doe + author-git-author-name: my home PC + author-emails: + - john@john.com + - johny@mail.com + - j@domain.com file-formats: - override:java - md diff --git a/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java b/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java new file mode 100644 index 0000000000..1e066fbde1 --- /dev/null +++ b/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java @@ -0,0 +1,30 @@ +package reposense.model.reportconfig; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ReportAuthorDetailsTest { + @Test + public void getAuthorEmails_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportAuthorDetails().getAuthorEmails(), + ReportAuthorDetails.DEFAULT_AUTHOR_EMAIL); + } + + @Test + public void getAuthorGitHostId_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportAuthorDetails().getAuthorGitHostId(), + ReportAuthorDetails.DEFAULT_GIT_HOST_ID); + } + + @Test + public void getAuthorDisplayName_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportAuthorDetails().getAuthorDisplayName(), + ReportAuthorDetails.DEFAULT_DISPLAY_NAME); + } + + @Test + public void getAuthorGitAuthorName_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportAuthorDetails().getAuthorGitAuthorName(), + ReportAuthorDetails.DEFAULT_GIT_AUTHOR_NAME); + } +} diff --git a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java index 679e9ea88f..0c5b589e3c 100644 --- a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java @@ -9,30 +9,6 @@ public void getRepo_equalsDefaultReturnValue_success() { Assertions.assertSame(new ReportRepoConfiguration().getRepo(), ReportRepoConfiguration.DEFAULT_REPO); } - @Test - public void getAuthorEmails_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportRepoConfiguration().getAuthorEmails(), - ReportRepoConfiguration.DEFAULT_AUTHOR_EMAIL); - } - - @Test - public void getAuthorGitHostId_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportRepoConfiguration().getAuthorGitHostId(), - ReportRepoConfiguration.DEFAULT_GIT_HOST_ID); - } - - @Test - public void getAuthorDisplayName_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportRepoConfiguration().getAuthorDisplayName(), - ReportRepoConfiguration.DEFAULT_DISPLAY_NAME); - } - - @Test - public void getAuthorGitAuthorName_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportRepoConfiguration().getAuthorGitAuthorName(), - ReportRepoConfiguration.DEFAULT_GIT_AUTHOR_NAME); - } - @Test public void getBranches_equalsDefaultReturnValue_success() { Assertions.assertSame(new ReportRepoConfiguration().getBranches(), ReportRepoConfiguration.DEFAULT_BRANCHES); diff --git a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml index 746b0493f6..5ff5b91dc0 100644 --- a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml +++ b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml @@ -15,15 +15,16 @@ group-details: - "**.md" repos: - repo: https://github.com/user/repo/tree/fake-branch - author-emails: - - john@john.com - - johny@mail.com - - j@domain.com - author-git-host-id: johnDoe - author-display-name: John Doe - author-git-author-name: my home PC branches: - branch: main + authors: + - author-git-host-id: johnDoe + author-display-name: John Doe + author-git-author-name: my home PC + author-emails: + - john@john.com + - johny@mail.com + - j@domain.com file-formats: - override:java - md From c20e450c6169ff93ba744615948c6c9d920b2fb6 Mon Sep 17 00:00:00 2001 From: George Tay Date: Sun, 12 May 2024 20:18:25 +0800 Subject: [PATCH 06/42] Update docs for report-config.yaml --- docs/ug/configFiles.md | 33 ++++++++++++++++++++++++--------- docs/ug/report-config.yaml | 38 +++++++++++++++++++------------------- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/docs/ug/configFiles.md b/docs/ug/configFiles.md index 3c9585fb54..2b12def07d 100644 --- a/docs/ug/configFiles.md +++ b/docs/ug/configFiles.md @@ -113,16 +113,31 @@ You can also optionally use a `report-config.yaml` file to quickly define the re View this [example](report-config.yaml) for a better understanding of what repository information is required. +Note: All fields should be defined, and left blank if not used. + **Fields to provide**: -* `repoUrl`: The URL to your repository of interest -* `reportTitle`: Title of the generated report, which is also the title of the deployed dashboard. Default: "RepoSense Report" -* `authorDisplayName`: Name of the author to track in the repository. -* `authorGithubId`: The GitHub username of the author to track in the repository. -* `branches`: A list of branches with their associated blurbs. - * `name`: Name of the branch to track - * `blurb`: Blurb to include with the branch -* `startDate`: The start date of analysis for RepoSense. Default: "2020-01-01" -* `endDate`: The end date of analysis for RepoSense. Default: "9999-12-32" +* `title`: Title of the generated report, which is also the title of the deployed dashboard. Default: "RepoSense Report". +* `group-details`: Details regarding the different custom groupings of files in your repository (refer to the section on `group-config.csv` for more information). + * `repo`: The `Remote Repo URL` or `Disk Path` to the git repository. + * `groups`: A list of the different custom groupings. + * `group-name`: Name of the group. + * `globs`: The list of file path globs to include for specified group. +* `repos`: A list of repositories to include for analysis. + * `repo`: The URL to your repository of interest. + * `branches`: A list of branches to analyse for each repository. + * `branch`: The name of the branch. + * `authors`: A list of authors to analyse on the branch. + * `author-git-host-id`: Git host username of the author. + * `author-display-name`: Display name of the author. + * `author-git-author-name`: Author's Git host name. + * `author-emails`: A list of emails associated with an author. + * `file-formats`: File formats to analyze. + * `ignore-glob-list`: Folders/files to ignore, specified using the [_glob format_](https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob). + * `ignore-standalone-config`: Ignores the different standalone configuration files when analysing this branch + * `ignore-commits-list`: The list of commits to ignore during analysis. For accurate results, the commits should be provided with their full hash. Additionally, a range of commits can be specified using the `..` notation e.g. `abc123..def456` (both inclusive). + * `ignore-authors-list`: The list of authors to ignore during analysis. Authors specified in `authors` field or `author-config.csv` will be also be omitted if they are in this list. Authors should be specified by their [Git Author Name](#a-note-about-git-author-name). + * `is-shallow-cloning`: Indicates whether to clone the repository using Git's shallow cloning functionality. + * `is-find-previous-authors`: Indicates whether to find previous authors of the repository/branch. diff --git a/docs/ug/report-config.yaml b/docs/ug/report-config.yaml index 4e430c7404..b5914b25ce 100644 --- a/docs/ug/report-config.yaml +++ b/docs/ug/report-config.yaml @@ -1,6 +1,6 @@ title: RepoSense Report group-details: - - repo: https://github.com/user/repo/tree/fake-branch + - repo: https://github.com/reposense/testrepo-Delta.git groups: - group-name: code globs: @@ -14,30 +14,30 @@ group-details: - "**.adoc" - "**.md" repos: - - repo: https://github.com/user/repo/tree/fake-branch - author-emails: - - john@john.com - - johny@mail.com - - j@domain.com - author-git-host-id: johnDoe - author-display-name: John Doe - author-git-author-name: my home PC + - repo: https://github.com/reposense/testrepo-Delta.git branches: - - branch: main + - branch: master + authors: + - author-git-host-id: fzdy1914 + author-display-name: WANG CHAO + author-git-author-name: WANG CHAO + author-emails: + - 1229983126@qq.com + - author-git-host-id: FH-30 + author-display-name: Francis Hodianto + author-git-author-name: Francis Hodianto + author-emails: + - file-formats: - override:java - md - fxml ignore-glob-list: - - "docs**" - ignore-standalone-config: true + - + ignore-standalone-config: false ignore-commits-list: - - 2fb6b9b2dd9fa40bf0f9815da2cb0ae8731436c7 - - c5a6dc774e22099cd9ddeb0faff1e75f9cf4f151 - - cd7f610e0becbdf331d5231887d8010a689f87c7 - - 768015345e70f06add2a8b7d1f901dc07bf70582 + - ignore-authors-list: - - author1 - - author2 - is-shallow-cloning: true + - + is-shallow-cloning: false is-find-previous-authors: false From 66cd7190e3169ecab759b0309313b77eccd6263b Mon Sep 17 00:00:00 2001 From: George Tay Date: Sun, 12 May 2024 20:21:25 +0800 Subject: [PATCH 07/42] Update URLs --- src/systemtest/resources/ConfigSystemTest/report-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/systemtest/resources/ConfigSystemTest/report-config.yaml b/src/systemtest/resources/ConfigSystemTest/report-config.yaml index 95399c0930..5d7a6d7079 100644 --- a/src/systemtest/resources/ConfigSystemTest/report-config.yaml +++ b/src/systemtest/resources/ConfigSystemTest/report-config.yaml @@ -1,6 +1,6 @@ title: RepoSense Report Test Title group-details: - - repo: https://github.com/user/repo/tree/fake-branch + - repo: https://github.com/user/repo.git groups: - group-name: code globs: @@ -14,7 +14,7 @@ group-details: - "**.adoc" - "**.md" repos: - - repo: https://github.com/user/repo/tree/fake-branch + - repo: https://github.com/user/repo.git branches: - branch: main authors: From 18772f07d32cce2e2748b5c094ea2e8f120fa848 Mon Sep 17 00:00:00 2001 From: George Tay Date: Sun, 9 Jun 2024 14:12:42 +0800 Subject: [PATCH 08/42] Update YAML Format --- config/report-config.yaml | 4 +- .../reportconfig/ReportConfiguration.java | 14 ++-- .../reportconfig/ReportGroupDetails.java | 49 ----------- .../reportconfig/ReportRepoConfiguration.java | 15 ++++ .../parser/ReportConfigYamlParser.java | 24 ++---- .../java/reposense/parser/YamlParser.java | 32 +++++++ .../30daysFromUntilDate/expected/summary.json | 83 ++++++++++++++++++- .../expected/summary.json | 83 ++++++++++++++++++- .../expected/summary.json | 83 ++++++++++++++++++- .../ConfigSystemTest/report-config.yaml | 6 +- .../expected/summary.json | 83 ++++++++++++++++++- .../expected/summary.json | 83 ++++++++++++++++++- .../expected/summary.json | 83 ++++++++++++++++++- .../reportconfig/ReportConfigurationTest.java | 6 -- .../reportconfig/ReportGroupDetailsTest.java | 23 ----- .../report-config-invalid.yaml | 40 ++++----- .../report-config-valid.yaml | 40 ++++----- 17 files changed, 590 insertions(+), 161 deletions(-) delete mode 100644 src/main/java/reposense/model/reportconfig/ReportGroupDetails.java create mode 100644 src/main/java/reposense/parser/YamlParser.java delete mode 100644 src/test/java/reposense/model/reportconfig/ReportGroupDetailsTest.java diff --git a/config/report-config.yaml b/config/report-config.yaml index b5914b25ce..71b1bc3e00 100644 --- a/config/report-config.yaml +++ b/config/report-config.yaml @@ -1,5 +1,5 @@ title: RepoSense Report -group-details: +repos: - repo: https://github.com/reposense/testrepo-Delta.git groups: - group-name: code @@ -13,8 +13,6 @@ group-details: - "docs**" - "**.adoc" - "**.md" -repos: - - repo: https://github.com/reposense/testrepo-Delta.git branches: - branch: master authors: diff --git a/src/main/java/reposense/model/reportconfig/ReportConfiguration.java b/src/main/java/reposense/model/reportconfig/ReportConfiguration.java index fcaf682598..b91f4d4a08 100644 --- a/src/main/java/reposense/model/reportconfig/ReportConfiguration.java +++ b/src/main/java/reposense/model/reportconfig/ReportConfiguration.java @@ -11,20 +11,15 @@ */ public class ReportConfiguration { public static final String DEFAULT_TITLE = "RepoSense Report"; - public static final List DEFAULT_REPORT_GROUP_DETAILS = new ArrayList<>(); public static final List DEFAULT_REPORT_REPO_CONFIGS = new ArrayList<>(); static { DEFAULT_REPORT_REPO_CONFIGS.add(ReportRepoConfiguration.DEFAULT_INSTANCE); - DEFAULT_REPORT_GROUP_DETAILS.add(ReportGroupDetails.DEFAULT_INSTANCE); } @JsonProperty("title") private String title; - @JsonProperty("group-details") - private List groupDetails; - @JsonProperty("repos") private List reportRepoConfigurations; @@ -32,10 +27,6 @@ public String getTitle() { return title == null ? DEFAULT_TITLE : title; } - public List getGroupDetails() { - return groupDetails == null ? DEFAULT_REPORT_GROUP_DETAILS : groupDetails; - } - public List getReportRepoConfigurations() { return reportRepoConfigurations == null ? DEFAULT_REPORT_REPO_CONFIGS : reportRepoConfigurations; } @@ -54,4 +45,9 @@ public boolean equals(Object obj) { return false; } + + @Override + public String toString() { + return title + "\n" + reportRepoConfigurations; + } } diff --git a/src/main/java/reposense/model/reportconfig/ReportGroupDetails.java b/src/main/java/reposense/model/reportconfig/ReportGroupDetails.java deleted file mode 100644 index 8310e86f49..0000000000 --- a/src/main/java/reposense/model/reportconfig/ReportGroupDetails.java +++ /dev/null @@ -1,49 +0,0 @@ -package reposense.model.reportconfig; - -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Contains information about the group contained in a particular repo. - */ -public class ReportGroupDetails { - public static final String DEFAULT_REPO = "https://github.com/user/repo/tree/fake-branch"; - public static final List DEFAULT_NAMES_AND_GLOBS = - ReportGroupNameAndGlobs.DEFAULT_INSTANCES; - public static final ReportGroupDetails DEFAULT_INSTANCE = new ReportGroupDetails(); - - static { - DEFAULT_INSTANCE.repo = DEFAULT_REPO; - DEFAULT_INSTANCE.reportGroupNameAndGlobsList = ReportGroupNameAndGlobs.DEFAULT_INSTANCES; - } - - @JsonProperty("repo") - private String repo; - - @JsonProperty("groups") - private List reportGroupNameAndGlobsList; - - public String getRepo() { - return repo == null ? DEFAULT_REPO : repo; - } - - public List getReportGroupNameAndGlobsList() { - return reportGroupNameAndGlobsList == null ? DEFAULT_NAMES_AND_GLOBS : reportGroupNameAndGlobsList; - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - - if (obj instanceof ReportGroupDetails) { - ReportGroupDetails rgd = (ReportGroupDetails) obj; - return this.getRepo().equals(rgd.getRepo()) - && this.getReportGroupNameAndGlobsList().equals(rgd.getReportGroupNameAndGlobsList()); - } - - return false; - } -} diff --git a/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java index 375ce95653..9d14d2a5df 100644 --- a/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java +++ b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java @@ -10,6 +10,7 @@ */ public class ReportRepoConfiguration { public static final String DEFAULT_REPO = "https://github.com/user/repo"; + public static final List DEFAULT_GROUP_DETAILS = ReportGroupNameAndGlobs.DEFAULT_INSTANCES; public static final List DEFAULT_BRANCHES = List.of( ReportBranchData.DEFAULT_INSTANCE ); @@ -17,12 +18,16 @@ public class ReportRepoConfiguration { static { DEFAULT_INSTANCE.repo = DEFAULT_REPO; + DEFAULT_INSTANCE.groups = DEFAULT_GROUP_DETAILS; DEFAULT_INSTANCE.branches = DEFAULT_BRANCHES; } @JsonProperty("repo") private String repo; + @JsonProperty("groups") + private List groups; + @JsonProperty("branches") private List branches; @@ -30,6 +35,10 @@ public String getRepo() { return repo == null ? DEFAULT_REPO : repo; } + public List getGroupDetails() { + return groups == null ? DEFAULT_GROUP_DETAILS : groups; + } + public List getBranches() { return branches == null ? DEFAULT_BRANCHES : branches; } @@ -43,9 +52,15 @@ public boolean equals(Object obj) { if (obj instanceof ReportRepoConfiguration) { ReportRepoConfiguration rrc = (ReportRepoConfiguration) obj; return rrc.getRepo().equals(this.getRepo()) + && rrc.getGroupDetails().equals(this.getGroupDetails()) && rrc.getBranches().equals(this.getBranches()); } return false; } + + @Override + public String toString() { + return repo + "\n" + groups + "\n" + branches; + } } diff --git a/src/main/java/reposense/parser/ReportConfigYamlParser.java b/src/main/java/reposense/parser/ReportConfigYamlParser.java index d584d8d40b..1cdceaa13a 100644 --- a/src/main/java/reposense/parser/ReportConfigYamlParser.java +++ b/src/main/java/reposense/parser/ReportConfigYamlParser.java @@ -5,40 +5,32 @@ import java.lang.reflect.Type; import java.nio.file.Path; import java.util.logging.Level; -import java.util.logging.Logger; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.google.gson.Gson; import reposense.model.reportconfig.ReportConfiguration; -import reposense.system.LogsManager; /** * YAML Parser for report-config.yaml files. */ -public class ReportConfigYamlParser extends JsonParser { +public class ReportConfigYamlParser extends YamlParser { public static final String REPORT_CONFIG_FILENAME = "report-config.yaml"; - private static final Logger logger = LogsManager.getLogger(ReportConfigYamlParser.class); @Override public Type getType() { return ReportConfiguration.class; } + /** + * Parses the YAML file at {@code path}. + * + * @param path Path to the YAML file + * @return Parsed {@code ReportConfiguration} object + * @throws IOException if the provided path is invalid + */ @Override public ReportConfiguration parse(Path path) throws IOException { - return this.fromJson(null, path, null); - } - - @Override - protected ReportConfiguration fromJson(Path path) throws IOException { - return this.fromJson(null, path, null); - } - - @Override - protected ReportConfiguration fromJson(Gson gson, Path path, Type type) throws IOException , JsonMappingException { // adapted from https://www.baeldung.com/jackson-yaml ReportConfiguration reportConfigation; diff --git a/src/main/java/reposense/parser/YamlParser.java b/src/main/java/reposense/parser/YamlParser.java new file mode 100644 index 0000000000..5c47407e18 --- /dev/null +++ b/src/main/java/reposense/parser/YamlParser.java @@ -0,0 +1,32 @@ +package reposense.parser; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.file.Path; +import java.util.logging.Logger; + +import reposense.system.LogsManager; + +/** + * Represents a YAML parser that is able to parse a YAML file from a {@link Path} into an object of + * type {@code T}. + * + * @param Type {@code T} that this parser can parse and return as an object + */ +public abstract class YamlParser { + protected static final Logger logger = LogsManager.getLogger(YamlParser.class); + + /** + * Returns the type of {@code T} for YAML file conversion. + */ + public abstract Type getType(); + + /** + * Converts the YAML file from the given {@code path} into object of type {@code T} and return it. + * + * @param path Path to the YAML file + * @return Parsed object of type {@code T} + * @throws IOException if the {@code path} is invalid + */ + public abstract T parse(Path path) throws IOException; +} diff --git a/src/systemtest/resources/ConfigSystemTest/30daysFromUntilDate/expected/summary.json b/src/systemtest/resources/ConfigSystemTest/30daysFromUntilDate/expected/summary.json index c809a19ae0..b4a4475f8f 100644 --- a/src/systemtest/resources/ConfigSystemTest/30daysFromUntilDate/expected/summary.json +++ b/src/systemtest/resources/ConfigSystemTest/30daysFromUntilDate/expected/summary.json @@ -1 +1,82 @@ -{"reportGeneratedTime":"Tue Jul 24 17:45:15 SGT 2018","reportGenerationTime":"15 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report Test Title","repos":[{"location":{"location":"https://github.com/reposense/testrepo-Alpha.git","repoName":"testrepo-Alpha","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Alpha[master]","outputFolderName":"reposense_testrepo-Alpha_master"},{"location":{"location":"https://github.com/reposense/testrepo-Beta.git","repoName":"testrepo-Beta","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Beta[master]","outputFolderName":"reposense_testrepo-Beta_master"},{"location":{"location":"https://github.com/reposense/testrepo-Charlie.git","repoName":"testrepo-Charlie","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Charlie[master]","outputFolderName":"reposense_testrepo-Charlie_master"},{"location":{"location":"https://github.com/reposense/testrepo-Delta.git","repoName":"testrepo-Delta","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Delta[master]","outputFolderName":"reposense_testrepo-Delta_master"}],"errorSet":[{"repoName":"ttps://github.com/reposense/testrepo-Beta.git","errorMessage":"ttps://github.com/reposense/testrepo-Beta.git is an invalid remote URL."},{"repoName":"reposense/testrepo-Delta[nonExistentBranch]","errorMessage":"Branch \"nonExistentBranch\" does not exist."}],"sinceDate":"2017-10-01","untilDate":"2017-11-01","isSinceDateProvided":false,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}}} +{ + "zoneId": "Asia/Singapore", + "reportTitle": "RepoSense Report", + "repos": [ + { + "location": { + "location": "https://github.com/reposense/testrepo-Alpha.git", + "repoName": "testrepo-Alpha", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Alpha[master]", + "outputFolderName": "reposense_testrepo-Alpha_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Beta.git", + "repoName": "testrepo-Beta", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Beta[master]", + "outputFolderName": "reposense_testrepo-Beta_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Charlie.git", + "repoName": "testrepo-Charlie", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Charlie[master]", + "outputFolderName": "reposense_testrepo-Charlie_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Delta.git", + "repoName": "testrepo-Delta", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Delta[master]", + "outputFolderName": "reposense_testrepo-Delta_master" + } + ], + "errorSet": [ + { + "repoName": "ttps://github.com/reposense/testrepo-Beta.git", + "errorMessage": "ttps://github.com/reposense/testrepo-Beta.git is an invalid remote URL." + }, + { + "repoName": "reposense/testrepo-Delta[nonExistentBranch]", + "errorMessage": "Branch \"nonExistentBranch\" does not exist." + } + ], + "sinceDate": "2017-10-01", + "untilDate": "2017-11-01", + "isSinceDateProvided": false, + "isUntilDateProvided": true, + "supportedDomainUrlMap": { + "NOT_RECOGNIZED": { + "BRANCH": "", + "REPO_URL": "UNSUPPORTED", + "BASE_URL": "UNSUPPORTED", + "HISTORY_PATH": "", + "COMMIT_PATH": "", + "BLAME_PATH": "" + }, + "github": { + "BRANCH": "tree/$BRANCH", + "REPO_URL": "https://github.com/$ORGANIZATION/$REPO_NAME/", + "BASE_URL": "https://github.com/", + "HISTORY_PATH": "commits/$BRANCH/$FILE_PATH", + "COMMIT_PATH": "commit/$COMMIT_HASH", + "BLAME_PATH": "blame/$BRANCH/$FILE_PATH" + } + } +} diff --git a/src/systemtest/resources/ConfigSystemTest/30daysFromUntilDateFindPreviousAuthors/expected/summary.json b/src/systemtest/resources/ConfigSystemTest/30daysFromUntilDateFindPreviousAuthors/expected/summary.json index c809a19ae0..b4a4475f8f 100644 --- a/src/systemtest/resources/ConfigSystemTest/30daysFromUntilDateFindPreviousAuthors/expected/summary.json +++ b/src/systemtest/resources/ConfigSystemTest/30daysFromUntilDateFindPreviousAuthors/expected/summary.json @@ -1 +1,82 @@ -{"reportGeneratedTime":"Tue Jul 24 17:45:15 SGT 2018","reportGenerationTime":"15 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report Test Title","repos":[{"location":{"location":"https://github.com/reposense/testrepo-Alpha.git","repoName":"testrepo-Alpha","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Alpha[master]","outputFolderName":"reposense_testrepo-Alpha_master"},{"location":{"location":"https://github.com/reposense/testrepo-Beta.git","repoName":"testrepo-Beta","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Beta[master]","outputFolderName":"reposense_testrepo-Beta_master"},{"location":{"location":"https://github.com/reposense/testrepo-Charlie.git","repoName":"testrepo-Charlie","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Charlie[master]","outputFolderName":"reposense_testrepo-Charlie_master"},{"location":{"location":"https://github.com/reposense/testrepo-Delta.git","repoName":"testrepo-Delta","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Delta[master]","outputFolderName":"reposense_testrepo-Delta_master"}],"errorSet":[{"repoName":"ttps://github.com/reposense/testrepo-Beta.git","errorMessage":"ttps://github.com/reposense/testrepo-Beta.git is an invalid remote URL."},{"repoName":"reposense/testrepo-Delta[nonExistentBranch]","errorMessage":"Branch \"nonExistentBranch\" does not exist."}],"sinceDate":"2017-10-01","untilDate":"2017-11-01","isSinceDateProvided":false,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}}} +{ + "zoneId": "Asia/Singapore", + "reportTitle": "RepoSense Report", + "repos": [ + { + "location": { + "location": "https://github.com/reposense/testrepo-Alpha.git", + "repoName": "testrepo-Alpha", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Alpha[master]", + "outputFolderName": "reposense_testrepo-Alpha_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Beta.git", + "repoName": "testrepo-Beta", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Beta[master]", + "outputFolderName": "reposense_testrepo-Beta_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Charlie.git", + "repoName": "testrepo-Charlie", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Charlie[master]", + "outputFolderName": "reposense_testrepo-Charlie_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Delta.git", + "repoName": "testrepo-Delta", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Delta[master]", + "outputFolderName": "reposense_testrepo-Delta_master" + } + ], + "errorSet": [ + { + "repoName": "ttps://github.com/reposense/testrepo-Beta.git", + "errorMessage": "ttps://github.com/reposense/testrepo-Beta.git is an invalid remote URL." + }, + { + "repoName": "reposense/testrepo-Delta[nonExistentBranch]", + "errorMessage": "Branch \"nonExistentBranch\" does not exist." + } + ], + "sinceDate": "2017-10-01", + "untilDate": "2017-11-01", + "isSinceDateProvided": false, + "isUntilDateProvided": true, + "supportedDomainUrlMap": { + "NOT_RECOGNIZED": { + "BRANCH": "", + "REPO_URL": "UNSUPPORTED", + "BASE_URL": "UNSUPPORTED", + "HISTORY_PATH": "", + "COMMIT_PATH": "", + "BLAME_PATH": "" + }, + "github": { + "BRANCH": "tree/$BRANCH", + "REPO_URL": "https://github.com/$ORGANIZATION/$REPO_NAME/", + "BASE_URL": "https://github.com/", + "HISTORY_PATH": "commits/$BRANCH/$FILE_PATH", + "COMMIT_PATH": "commit/$COMMIT_HASH", + "BLAME_PATH": "blame/$BRANCH/$FILE_PATH" + } + } +} diff --git a/src/systemtest/resources/ConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/summary.json b/src/systemtest/resources/ConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/summary.json index c809a19ae0..b4a4475f8f 100644 --- a/src/systemtest/resources/ConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/summary.json +++ b/src/systemtest/resources/ConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/summary.json @@ -1 +1,82 @@ -{"reportGeneratedTime":"Tue Jul 24 17:45:15 SGT 2018","reportGenerationTime":"15 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report Test Title","repos":[{"location":{"location":"https://github.com/reposense/testrepo-Alpha.git","repoName":"testrepo-Alpha","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Alpha[master]","outputFolderName":"reposense_testrepo-Alpha_master"},{"location":{"location":"https://github.com/reposense/testrepo-Beta.git","repoName":"testrepo-Beta","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Beta[master]","outputFolderName":"reposense_testrepo-Beta_master"},{"location":{"location":"https://github.com/reposense/testrepo-Charlie.git","repoName":"testrepo-Charlie","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Charlie[master]","outputFolderName":"reposense_testrepo-Charlie_master"},{"location":{"location":"https://github.com/reposense/testrepo-Delta.git","repoName":"testrepo-Delta","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Delta[master]","outputFolderName":"reposense_testrepo-Delta_master"}],"errorSet":[{"repoName":"ttps://github.com/reposense/testrepo-Beta.git","errorMessage":"ttps://github.com/reposense/testrepo-Beta.git is an invalid remote URL."},{"repoName":"reposense/testrepo-Delta[nonExistentBranch]","errorMessage":"Branch \"nonExistentBranch\" does not exist."}],"sinceDate":"2017-10-01","untilDate":"2017-11-01","isSinceDateProvided":false,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}}} +{ + "zoneId": "Asia/Singapore", + "reportTitle": "RepoSense Report", + "repos": [ + { + "location": { + "location": "https://github.com/reposense/testrepo-Alpha.git", + "repoName": "testrepo-Alpha", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Alpha[master]", + "outputFolderName": "reposense_testrepo-Alpha_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Beta.git", + "repoName": "testrepo-Beta", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Beta[master]", + "outputFolderName": "reposense_testrepo-Beta_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Charlie.git", + "repoName": "testrepo-Charlie", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Charlie[master]", + "outputFolderName": "reposense_testrepo-Charlie_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Delta.git", + "repoName": "testrepo-Delta", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Delta[master]", + "outputFolderName": "reposense_testrepo-Delta_master" + } + ], + "errorSet": [ + { + "repoName": "ttps://github.com/reposense/testrepo-Beta.git", + "errorMessage": "ttps://github.com/reposense/testrepo-Beta.git is an invalid remote URL." + }, + { + "repoName": "reposense/testrepo-Delta[nonExistentBranch]", + "errorMessage": "Branch \"nonExistentBranch\" does not exist." + } + ], + "sinceDate": "2017-10-01", + "untilDate": "2017-11-01", + "isSinceDateProvided": false, + "isUntilDateProvided": true, + "supportedDomainUrlMap": { + "NOT_RECOGNIZED": { + "BRANCH": "", + "REPO_URL": "UNSUPPORTED", + "BASE_URL": "UNSUPPORTED", + "HISTORY_PATH": "", + "COMMIT_PATH": "", + "BLAME_PATH": "" + }, + "github": { + "BRANCH": "tree/$BRANCH", + "REPO_URL": "https://github.com/$ORGANIZATION/$REPO_NAME/", + "BASE_URL": "https://github.com/", + "HISTORY_PATH": "commits/$BRANCH/$FILE_PATH", + "COMMIT_PATH": "commit/$COMMIT_HASH", + "BLAME_PATH": "blame/$BRANCH/$FILE_PATH" + } + } +} diff --git a/src/systemtest/resources/ConfigSystemTest/report-config.yaml b/src/systemtest/resources/ConfigSystemTest/report-config.yaml index 5d7a6d7079..1c6fed2d3c 100644 --- a/src/systemtest/resources/ConfigSystemTest/report-config.yaml +++ b/src/systemtest/resources/ConfigSystemTest/report-config.yaml @@ -1,5 +1,5 @@ -title: RepoSense Report Test Title -group-details: +title: RepoSense Report +repos: - repo: https://github.com/user/repo.git groups: - group-name: code @@ -13,8 +13,6 @@ group-details: - "docs**" - "**.adoc" - "**.md" -repos: - - repo: https://github.com/user/repo.git branches: - branch: main authors: diff --git a/src/systemtest/resources/ConfigSystemTest/sinceBeginningDateRange/expected/summary.json b/src/systemtest/resources/ConfigSystemTest/sinceBeginningDateRange/expected/summary.json index f0d69e7d5b..629c28d968 100644 --- a/src/systemtest/resources/ConfigSystemTest/sinceBeginningDateRange/expected/summary.json +++ b/src/systemtest/resources/ConfigSystemTest/sinceBeginningDateRange/expected/summary.json @@ -1 +1,82 @@ -{"reportGeneratedTime":"Tue Jul 24 17:45:15 SGT 2018","reportGenerationTime":"15 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report Test Title","repos":[{"location":{"location":"https://github.com/reposense/testrepo-Alpha.git","repoName":"testrepo-Alpha","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Alpha[master]","outputFolderName":"reposense_testrepo-Alpha_master"},{"location":{"location":"https://github.com/reposense/testrepo-Beta.git","repoName":"testrepo-Beta","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Beta[master]","outputFolderName":"reposense_testrepo-Beta_master"},{"location":{"location":"https://github.com/reposense/testrepo-Charlie.git","repoName":"testrepo-Charlie","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Charlie[master]","outputFolderName":"reposense_testrepo-Charlie_master"},{"location":{"location":"https://github.com/reposense/testrepo-Delta.git","repoName":"testrepo-Delta","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Delta[master]","outputFolderName":"reposense_testrepo-Delta_master"}],"errorSet":[{"repoName":"ttps://github.com/reposense/testrepo-Beta.git","errorMessage":"ttps://github.com/reposense/testrepo-Beta.git is an invalid remote URL."},{"repoName":"reposense/testrepo-Delta[nonExistentBranch]","errorMessage":"Branch \"nonExistentBranch\" does not exist."}],"sinceDate":"2017-09-30","untilDate":"2019-03-02","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}}} +{ + "zoneId": "Asia/Singapore", + "reportTitle": "RepoSense Report", + "repos": [ + { + "location": { + "location": "https://github.com/reposense/testrepo-Alpha.git", + "repoName": "testrepo-Alpha", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Alpha[master]", + "outputFolderName": "reposense_testrepo-Alpha_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Beta.git", + "repoName": "testrepo-Beta", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Beta[master]", + "outputFolderName": "reposense_testrepo-Beta_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Charlie.git", + "repoName": "testrepo-Charlie", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Charlie[master]", + "outputFolderName": "reposense_testrepo-Charlie_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Delta.git", + "repoName": "testrepo-Delta", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Delta[master]", + "outputFolderName": "reposense_testrepo-Delta_master" + } + ], + "errorSet": [ + { + "repoName": "ttps://github.com/reposense/testrepo-Beta.git", + "errorMessage": "ttps://github.com/reposense/testrepo-Beta.git is an invalid remote URL." + }, + { + "repoName": "reposense/testrepo-Delta[nonExistentBranch]", + "errorMessage": "Branch \"nonExistentBranch\" does not exist." + } + ], + "sinceDate": "2017-09-30", + "untilDate": "2019-03-02", + "isSinceDateProvided": true, + "isUntilDateProvided": true, + "supportedDomainUrlMap": { + "NOT_RECOGNIZED": { + "BRANCH": "", + "REPO_URL": "UNSUPPORTED", + "BASE_URL": "UNSUPPORTED", + "HISTORY_PATH": "", + "COMMIT_PATH": "", + "BLAME_PATH": "" + }, + "github": { + "BRANCH": "tree/$BRANCH", + "REPO_URL": "https://github.com/$ORGANIZATION/$REPO_NAME/", + "BASE_URL": "https://github.com/", + "HISTORY_PATH": "commits/$BRANCH/$FILE_PATH", + "COMMIT_PATH": "commit/$COMMIT_HASH", + "BLAME_PATH": "blame/$BRANCH/$FILE_PATH" + } + } +} diff --git a/src/systemtest/resources/ConfigSystemTest/sinceBeginningDateRangeFindPreviousAuthors/expected/summary.json b/src/systemtest/resources/ConfigSystemTest/sinceBeginningDateRangeFindPreviousAuthors/expected/summary.json index f0d69e7d5b..629c28d968 100644 --- a/src/systemtest/resources/ConfigSystemTest/sinceBeginningDateRangeFindPreviousAuthors/expected/summary.json +++ b/src/systemtest/resources/ConfigSystemTest/sinceBeginningDateRangeFindPreviousAuthors/expected/summary.json @@ -1 +1,82 @@ -{"reportGeneratedTime":"Tue Jul 24 17:45:15 SGT 2018","reportGenerationTime":"15 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report Test Title","repos":[{"location":{"location":"https://github.com/reposense/testrepo-Alpha.git","repoName":"testrepo-Alpha","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Alpha[master]","outputFolderName":"reposense_testrepo-Alpha_master"},{"location":{"location":"https://github.com/reposense/testrepo-Beta.git","repoName":"testrepo-Beta","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Beta[master]","outputFolderName":"reposense_testrepo-Beta_master"},{"location":{"location":"https://github.com/reposense/testrepo-Charlie.git","repoName":"testrepo-Charlie","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Charlie[master]","outputFolderName":"reposense_testrepo-Charlie_master"},{"location":{"location":"https://github.com/reposense/testrepo-Delta.git","repoName":"testrepo-Delta","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Delta[master]","outputFolderName":"reposense_testrepo-Delta_master"}],"errorSet":[{"repoName":"ttps://github.com/reposense/testrepo-Beta.git","errorMessage":"ttps://github.com/reposense/testrepo-Beta.git is an invalid remote URL."},{"repoName":"reposense/testrepo-Delta[nonExistentBranch]","errorMessage":"Branch \"nonExistentBranch\" does not exist."}],"sinceDate":"2017-09-30","untilDate":"2019-03-02","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}}} +{ + "zoneId": "Asia/Singapore", + "reportTitle": "RepoSense Report", + "repos": [ + { + "location": { + "location": "https://github.com/reposense/testrepo-Alpha.git", + "repoName": "testrepo-Alpha", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Alpha[master]", + "outputFolderName": "reposense_testrepo-Alpha_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Beta.git", + "repoName": "testrepo-Beta", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Beta[master]", + "outputFolderName": "reposense_testrepo-Beta_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Charlie.git", + "repoName": "testrepo-Charlie", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Charlie[master]", + "outputFolderName": "reposense_testrepo-Charlie_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Delta.git", + "repoName": "testrepo-Delta", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Delta[master]", + "outputFolderName": "reposense_testrepo-Delta_master" + } + ], + "errorSet": [ + { + "repoName": "ttps://github.com/reposense/testrepo-Beta.git", + "errorMessage": "ttps://github.com/reposense/testrepo-Beta.git is an invalid remote URL." + }, + { + "repoName": "reposense/testrepo-Delta[nonExistentBranch]", + "errorMessage": "Branch \"nonExistentBranch\" does not exist." + } + ], + "sinceDate": "2017-09-30", + "untilDate": "2019-03-02", + "isSinceDateProvided": true, + "isUntilDateProvided": true, + "supportedDomainUrlMap": { + "NOT_RECOGNIZED": { + "BRANCH": "", + "REPO_URL": "UNSUPPORTED", + "BASE_URL": "UNSUPPORTED", + "HISTORY_PATH": "", + "COMMIT_PATH": "", + "BLAME_PATH": "" + }, + "github": { + "BRANCH": "tree/$BRANCH", + "REPO_URL": "https://github.com/$ORGANIZATION/$REPO_NAME/", + "BASE_URL": "https://github.com/", + "HISTORY_PATH": "commits/$BRANCH/$FILE_PATH", + "COMMIT_PATH": "commit/$COMMIT_HASH", + "BLAME_PATH": "blame/$BRANCH/$FILE_PATH" + } + } +} diff --git a/src/systemtest/resources/ConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/summary.json b/src/systemtest/resources/ConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/summary.json index f0d69e7d5b..629c28d968 100644 --- a/src/systemtest/resources/ConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/summary.json +++ b/src/systemtest/resources/ConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/summary.json @@ -1 +1,82 @@ -{"reportGeneratedTime":"Tue Jul 24 17:45:15 SGT 2018","reportGenerationTime":"15 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report Test Title","repos":[{"location":{"location":"https://github.com/reposense/testrepo-Alpha.git","repoName":"testrepo-Alpha","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Alpha[master]","outputFolderName":"reposense_testrepo-Alpha_master"},{"location":{"location":"https://github.com/reposense/testrepo-Beta.git","repoName":"testrepo-Beta","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Beta[master]","outputFolderName":"reposense_testrepo-Beta_master"},{"location":{"location":"https://github.com/reposense/testrepo-Charlie.git","repoName":"testrepo-Charlie","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Charlie[master]","outputFolderName":"reposense_testrepo-Charlie_master"},{"location":{"location":"https://github.com/reposense/testrepo-Delta.git","repoName":"testrepo-Delta","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/testrepo-Delta[master]","outputFolderName":"reposense_testrepo-Delta_master"}],"errorSet":[{"repoName":"ttps://github.com/reposense/testrepo-Beta.git","errorMessage":"ttps://github.com/reposense/testrepo-Beta.git is an invalid remote URL."},{"repoName":"reposense/testrepo-Delta[nonExistentBranch]","errorMessage":"Branch \"nonExistentBranch\" does not exist."}],"sinceDate":"2017-09-30","untilDate":"2019-03-02","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}}} +{ + "zoneId": "Asia/Singapore", + "reportTitle": "RepoSense Report", + "repos": [ + { + "location": { + "location": "https://github.com/reposense/testrepo-Alpha.git", + "repoName": "testrepo-Alpha", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Alpha[master]", + "outputFolderName": "reposense_testrepo-Alpha_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Beta.git", + "repoName": "testrepo-Beta", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Beta[master]", + "outputFolderName": "reposense_testrepo-Beta_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Charlie.git", + "repoName": "testrepo-Charlie", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Charlie[master]", + "outputFolderName": "reposense_testrepo-Charlie_master" + }, + { + "location": { + "location": "https://github.com/reposense/testrepo-Delta.git", + "repoName": "testrepo-Delta", + "organization": "reposense", + "domainName": "github" + }, + "branch": "master", + "displayName": "reposense/testrepo-Delta[master]", + "outputFolderName": "reposense_testrepo-Delta_master" + } + ], + "errorSet": [ + { + "repoName": "ttps://github.com/reposense/testrepo-Beta.git", + "errorMessage": "ttps://github.com/reposense/testrepo-Beta.git is an invalid remote URL." + }, + { + "repoName": "reposense/testrepo-Delta[nonExistentBranch]", + "errorMessage": "Branch \"nonExistentBranch\" does not exist." + } + ], + "sinceDate": "2017-09-30", + "untilDate": "2019-03-02", + "isSinceDateProvided": true, + "isUntilDateProvided": true, + "supportedDomainUrlMap": { + "NOT_RECOGNIZED": { + "BRANCH": "", + "REPO_URL": "UNSUPPORTED", + "BASE_URL": "UNSUPPORTED", + "HISTORY_PATH": "", + "COMMIT_PATH": "", + "BLAME_PATH": "" + }, + "github": { + "BRANCH": "tree/$BRANCH", + "REPO_URL": "https://github.com/$ORGANIZATION/$REPO_NAME/", + "BASE_URL": "https://github.com/", + "HISTORY_PATH": "commits/$BRANCH/$FILE_PATH", + "COMMIT_PATH": "commit/$COMMIT_HASH", + "BLAME_PATH": "blame/$BRANCH/$FILE_PATH" + } + } +} diff --git a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java index 19a5fad8b3..935b0872ff 100644 --- a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java @@ -15,12 +15,6 @@ public void getReportRepoConfigurations_equalsDefaultReturnValue_success() { ReportConfiguration.DEFAULT_REPORT_REPO_CONFIGS); } - @Test - public void getGroupDetails_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportConfiguration().getGroupDetails(), - ReportConfiguration.DEFAULT_REPORT_GROUP_DETAILS); - } - @Test public void equals_defaultInstancesAreEqual_success() { Assertions.assertEquals(new ReportConfiguration(), new ReportConfiguration()); diff --git a/src/test/java/reposense/model/reportconfig/ReportGroupDetailsTest.java b/src/test/java/reposense/model/reportconfig/ReportGroupDetailsTest.java deleted file mode 100644 index d3df44380f..0000000000 --- a/src/test/java/reposense/model/reportconfig/ReportGroupDetailsTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package reposense.model.reportconfig; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class ReportGroupDetailsTest { - @Test - public void getRepo_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportGroupDetails().getRepo(), ReportGroupDetails.DEFAULT_REPO); - } - - @Test - public void getReportGroupNameAndGlobsList_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportGroupDetails().getReportGroupNameAndGlobsList(), - ReportGroupDetails.DEFAULT_NAMES_AND_GLOBS); - } - - @Test - public void equals_defaultInstancesAreEqual_success() { - Assertions.assertEquals(new ReportGroupDetails(), new ReportGroupDetails()); - Assertions.assertEquals(new ReportGroupDetails(), ReportGroupDetails.DEFAULT_INSTANCE); - } -} diff --git a/src/test/resources/ReportConfigYamlParserTest/report-config-invalid.yaml b/src/test/resources/ReportConfigYamlParserTest/report-config-invalid.yaml index fda2a639be..bd6058f6c4 100644 --- a/src/test/resources/ReportConfigYamlParserTest/report-config-invalid.yaml +++ b/src/test/resources/ReportConfigYamlParserTest/report-config-invalid.yaml @@ -1,36 +1,30 @@ -title: RepoSense Report -group-details: - - repo: https://github.com/user/repo/tree/fake-branch - grou - - group-name: code +titles: RepoSense Report +repo: + - repos: https://github.com/reposense/testrepo-Delta.git + group: + - grame: code globs: - "**.java" - group-name: tests globs: - "src/test**" - group-name: docs - globs: + gl: - "docs**" - "**.adoc" - "**.md" -os: - - repo: https://github.com/user/repo/tree/fake-branch - author-emails: - - john@john.com - - johny@mail.com - - j@domain.com - author-git-author-name: my home PC - branches: - - branch: main + bhes: + - branch: master + autho:thor-name: WANG CHAO + author-emails: + - 1229983126@qq.com + - author-git-host-id: FH-30 + author-display-name: Francis Hodianto + author-git-author-name: Francis Hodianto + author-emails: + - file-formats: - override:java - md - - fxml - ignore-glob-list: - - "docs**" - ignore-standalone-config: true - ignore-author-list: - - author1 - - author2 - is-shallow-cloning: true + is-shallow-cloning: false is-find-previous-authors: false diff --git a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml index 5ff5b91dc0..dea7a91415 100644 --- a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml +++ b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml @@ -1,6 +1,6 @@ -title: RepoSense Report Repo Test -group-details: - - repo: https://github.com/user/repo/tree/fake-branch +title: RepoSense Report Test Repo +repos: + - repo: https://github.com/reposense/testrepo-Delta.git groups: - group-name: code globs: @@ -13,33 +13,29 @@ group-details: - "docs**" - "**.adoc" - "**.md" -repos: - - repo: https://github.com/user/repo/tree/fake-branch branches: - - branch: main + - branch: master authors: - - author-git-host-id: johnDoe - author-display-name: John Doe - author-git-author-name: my home PC + - author-git-host-id: fzdy1914 + author-display-name: WANG CHAO + author-git-author-name: WANG CHAO author-emails: - - john@john.com - - johny@mail.com - - j@domain.com + - 1229983126@qq.com + - author-git-host-id: FH-30 + author-display-name: Francis Hodianto + author-git-author-name: Francis Hodianto + author-emails: + - file-formats: - override:java - md - fxml ignore-glob-list: - - "docs**" - ignore-standalone-config: true + - + ignore-standalone-config: false ignore-commits-list: - - 2fb6b9b2dd9fa40bf0f9815da2cb0ae8731436c7 - - c5a6dc774e22099cd9ddeb0faff1e75f9cf4f151 - - cd7f610e0becbdf331d5231887d8010a689f87c7 - - 768015345e70f06add2a8b7d1f901dc07bf70582 + - ignore-authors-list: - - author1 - - author2 - is-shallow-cloning: true + - + is-shallow-cloning: false is-find-previous-authors: false - From 587f1528f1d3400683e6c589be85d77f8cb0619f Mon Sep 17 00:00:00 2001 From: George Tay Date: Sat, 29 Jun 2024 22:02:26 +0800 Subject: [PATCH 09/42] Update YAML config file format --- config/report-config.yaml | 21 +---- .../reportconfig/ReportAuthorDetails.java | 94 ------------------- .../model/reportconfig/ReportBranchData.java | 79 ++-------------- .../ConfigSystemTest/report-config.yaml | 21 +---- .../reportconfig/ReportAuthorDetailsTest.java | 30 ------ .../reportconfig/ReportBranchDataTest.java | 29 ------ .../report-config-valid.yaml | 21 +---- 7 files changed, 12 insertions(+), 283 deletions(-) delete mode 100644 src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java delete mode 100644 src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java diff --git a/config/report-config.yaml b/config/report-config.yaml index 71b1bc3e00..d7bda4334c 100644 --- a/config/report-config.yaml +++ b/config/report-config.yaml @@ -15,27 +15,8 @@ repos: - "**.md" branches: - branch: master - authors: - - author-git-host-id: fzdy1914 - author-display-name: WANG CHAO - author-git-author-name: WANG CHAO - author-emails: - - 1229983126@qq.com - - author-git-host-id: FH-30 - author-display-name: Francis Hodianto - author-git-author-name: Francis Hodianto - author-emails: - - - file-formats: - - override:java - - md - - fxml + blurb: ignore-glob-list: - - ignore-standalone-config: false - ignore-commits-list: - - ignore-authors-list: - - is-shallow-cloning: false - is-find-previous-authors: false diff --git a/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java b/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java deleted file mode 100644 index af5d41662f..0000000000 --- a/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java +++ /dev/null @@ -1,94 +0,0 @@ -package reposense.model.reportconfig; - -import java.util.ArrayList; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Represents an author's details in the report-config.yaml file. - */ -public class ReportAuthorDetails { - public static final List DEFAULT_INSTANCES = new ArrayList<>(); - - public static final List DEFAULT_AUTHOR_EMAIL = List.of("john@john.com", "johny@mail.com", "j@domain.com"); - public static final String DEFAULT_GIT_HOST_ID = "johnDoe"; - public static final String DEFAULT_DISPLAY_NAME = "John Doe"; - public static final String DEFAULT_GIT_AUTHOR_NAME = "my home PC"; - - private static final List DEFAULT_AUTHOR_EMAIL_1 = List.of("1229983126@qq.com"); - private static final String DEFAULT_GIT_HOST_ID_1 = "fzdy1914"; - private static final String DEFAULT_DISPLAY_NAME_1 = "WANG CHAO"; - private static final String DEFAULT_GIT_AUTHOR_NAME_1 = "WANG CHAO"; - private static final List DEFAULT_AUTHOR_EMAIL_2 = List.of(""); - private static final String DEFAULT_GIT_HOST_ID_2 = "FH-30"; - private static final String DEFAULT_DISPLAY_NAME_2 = "Francis Hodianto"; - private static final String DEFAULT_GIT_AUTHOR_NAME_2 = "Francis Hodianto"; - - static { - ReportAuthorDetails rad1 = new ReportAuthorDetails(); - ReportAuthorDetails rad2 = new ReportAuthorDetails(); - - rad1.authorEmails = DEFAULT_AUTHOR_EMAIL_1; - rad1.authorGitHostId = DEFAULT_GIT_HOST_ID_1; - rad1.authorGitAuthorName = DEFAULT_GIT_AUTHOR_NAME_1; - rad1.authorDisplayName = DEFAULT_DISPLAY_NAME_1; - - rad2.authorEmails = DEFAULT_AUTHOR_EMAIL_2; - rad2.authorGitHostId = DEFAULT_GIT_HOST_ID_2; - rad2.authorGitAuthorName = DEFAULT_GIT_AUTHOR_NAME_2; - rad2.authorDisplayName = DEFAULT_DISPLAY_NAME_2; - - DEFAULT_INSTANCES.add(rad1); - DEFAULT_INSTANCES.add(rad2); - } - - @JsonProperty("author-emails") - private List authorEmails; - - @JsonProperty("author-git-host-id") - private String authorGitHostId; - - @JsonProperty("author-display-name") - private String authorDisplayName; - - @JsonProperty("author-git-author-name") - private String authorGitAuthorName; - - public ReportAuthorDetails() { - - } - - public List getAuthorEmails() { - return authorEmails == null ? DEFAULT_AUTHOR_EMAIL : authorEmails; - } - - public String getAuthorGitHostId() { - return authorGitHostId == null ? DEFAULT_GIT_HOST_ID : authorGitHostId; - } - - public String getAuthorDisplayName() { - return authorDisplayName == null ? DEFAULT_DISPLAY_NAME : authorDisplayName; - } - - public String getAuthorGitAuthorName() { - return authorGitAuthorName == null ? DEFAULT_GIT_AUTHOR_NAME : authorGitAuthorName; - } - - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - - if (obj instanceof ReportAuthorDetails) { - ReportAuthorDetails rad = (ReportAuthorDetails) obj; - return rad.getAuthorEmails().equals(this.getAuthorEmails()) - && rad.getAuthorGitHostId().equals(this.getAuthorGitHostId()) - && rad.getAuthorDisplayName().equals(this.getAuthorDisplayName()) - && rad.getAuthorGitAuthorName().equals(this.getAuthorGitAuthorName()); - } - - return false; - } -} diff --git a/src/main/java/reposense/model/reportconfig/ReportBranchData.java b/src/main/java/reposense/model/reportconfig/ReportBranchData.java index 88cf71ef62..27d7e07377 100644 --- a/src/main/java/reposense/model/reportconfig/ReportBranchData.java +++ b/src/main/java/reposense/model/reportconfig/ReportBranchData.java @@ -1,6 +1,5 @@ package reposense.model.reportconfig; -import java.util.ArrayList; import java.util.List; import com.fasterxml.jackson.annotation.JsonProperty; @@ -10,106 +9,51 @@ */ public class ReportBranchData { public static final String DEFAULT_BRANCH = "main"; - public static final List DEFAULT_REPORT_AUTHOR_DETAILS = new ArrayList<>( - ReportAuthorDetails.DEFAULT_INSTANCES - ); - public static final List DEFAULT_FILE_FORMATS = List.of( - "override:java", "md", "fxml" - ); + public static final String DEFAULT_BLURB = ""; public static final List DEFAULT_IGNORE_GLOB_LIST = List.of( "docs**" ); - public static final List DEFAULT_IGNORE_COMMITS_LIST = List.of( - "2fb6b9b2dd9fa40bf0f9815da2cb0ae8731436c7", - "c5a6dc774e22099cd9ddeb0faff1e75f9cf4f151", - "cd7f610e0becbdf331d5231887d8010a689f87c7", - "768015345e70f06add2a8b7d1f901dc07bf70582" - ); public static final List DEFAULT_IGNORE_AUTHORS_LIST = List.of( "author1", "author2" ); - public static final boolean DEFAULT_IS_FIND_PREVIOUS_AUTHOR = false; - public static final boolean DEFAULT_IS_SHALLOW_CLONING = true; - public static final boolean DEFAULT_IS_IGNORE_STANDALONE_CONFIG = true; - public static final ReportBranchData DEFAULT_INSTANCE = new ReportBranchData(); static { DEFAULT_INSTANCE.branch = ReportBranchData.DEFAULT_BRANCH; - DEFAULT_INSTANCE.fileFormats = ReportBranchData.DEFAULT_FILE_FORMATS; + DEFAULT_INSTANCE.blurb = ReportBranchData.DEFAULT_BLURB; DEFAULT_INSTANCE.ignoreGlobList = ReportBranchData.DEFAULT_IGNORE_GLOB_LIST; - DEFAULT_INSTANCE.ignoreCommitList = ReportBranchData.DEFAULT_IGNORE_COMMITS_LIST; DEFAULT_INSTANCE.ignoreAuthorList = ReportBranchData.DEFAULT_IGNORE_AUTHORS_LIST; - DEFAULT_INSTANCE.isFindPreviousAuthor = ReportBranchData.DEFAULT_IS_FIND_PREVIOUS_AUTHOR; - DEFAULT_INSTANCE.isShallowCloning = ReportBranchData.DEFAULT_IS_SHALLOW_CLONING; - DEFAULT_INSTANCE.isIgnoreStandaloneConfig = ReportBranchData.DEFAULT_IS_IGNORE_STANDALONE_CONFIG; - DEFAULT_INSTANCE.reportAuthorDetails = ReportAuthorDetails.DEFAULT_INSTANCES; } @JsonProperty("branch") private String branch; - @JsonProperty("authors") - private List reportAuthorDetails; - - @JsonProperty("file-formats") - private List fileFormats; + @JsonProperty("blurb") + private String blurb; @JsonProperty("ignore-glob-list") private List ignoreGlobList; - @JsonProperty("ignore-standalone-config") - private Boolean isIgnoreStandaloneConfig; - - @JsonProperty("ignore-commits-list") - private List ignoreCommitList; - @JsonProperty("ignore-authors-list") private List ignoreAuthorList; - @JsonProperty("is-shallow-cloning") - private Boolean isShallowCloning; - - @JsonProperty("is-find-previous-authors") - private Boolean isFindPreviousAuthor; - public String getBranch() { return branch == null ? DEFAULT_BRANCH : branch; } - public List getReportAuthorDetails() { - return reportAuthorDetails == null ? DEFAULT_REPORT_AUTHOR_DETAILS : reportAuthorDetails; - } - - public List getFileFormats() { - return fileFormats == null ? DEFAULT_FILE_FORMATS : fileFormats; + public String getBlurb() { + return blurb == null ? DEFAULT_BLURB : blurb; } public List getIgnoreGlobList() { - return ignoreGlobList == null ? DEFAULT_IGNORE_GLOB_LIST : fileFormats; - } - - public boolean getIsIgnoreStandaloneConfig() { - return isIgnoreStandaloneConfig == null ? DEFAULT_IS_IGNORE_STANDALONE_CONFIG : isIgnoreStandaloneConfig; - } - - public List getIgnoreCommitList() { - return ignoreCommitList == null ? DEFAULT_IGNORE_COMMITS_LIST : ignoreCommitList; + return ignoreGlobList == null ? DEFAULT_IGNORE_GLOB_LIST : ignoreGlobList; } public List getIgnoreAuthorList() { return ignoreAuthorList == null ? DEFAULT_IGNORE_AUTHORS_LIST : ignoreAuthorList; } - public boolean getIsShallowCloning() { - return isShallowCloning == null ? DEFAULT_IS_SHALLOW_CLONING : isShallowCloning; - } - - public boolean getIsFindPreviousAuthor() { - return isFindPreviousAuthor == null ? DEFAULT_IS_FIND_PREVIOUS_AUTHOR : isFindPreviousAuthor; - } - @Override public boolean equals(Object obj) { if (obj == this) { @@ -119,14 +63,9 @@ public boolean equals(Object obj) { if (obj instanceof ReportBranchData) { ReportBranchData rbd = (ReportBranchData) obj; return this.getBranch().equals(rbd.getBranch()) - && this.getReportAuthorDetails().equals(rbd.getReportAuthorDetails()) - && this.getFileFormats().equals(rbd.getFileFormats()) + && this.getBlurb().equals(rbd.getBlurb()) && this.getIgnoreGlobList().equals(rbd.getIgnoreGlobList()) - && this.getIsIgnoreStandaloneConfig() == rbd.getIsIgnoreStandaloneConfig() - && this.getIgnoreCommitList().equals(rbd.getIgnoreCommitList()) - && this.getIgnoreAuthorList().equals(rbd.getIgnoreAuthorList()) - && this.getIsShallowCloning() == rbd.getIsShallowCloning() - && this.getIsFindPreviousAuthor() == rbd.getIsFindPreviousAuthor(); + && this.getIgnoreAuthorList().equals(rbd.getIgnoreAuthorList()); } return false; diff --git a/src/systemtest/resources/ConfigSystemTest/report-config.yaml b/src/systemtest/resources/ConfigSystemTest/report-config.yaml index f0eb63aafc..fdcdf7cb9f 100644 --- a/src/systemtest/resources/ConfigSystemTest/report-config.yaml +++ b/src/systemtest/resources/ConfigSystemTest/report-config.yaml @@ -15,28 +15,9 @@ repos: - "**.md" branches: - branch: main - authors: - - author-git-host-id: johnDoe - author-display-name: John Doe - author-git-author-name: my home PC - author-emails: - - john@john.com - - johny@mail.com - - j@domain.com - file-formats: - - override:java - - md - - fxml + blurb: ignore-glob-list: - "docs**" - ignore-standalone-config: true - ignore-commits-list: - - 2fb6b9b2dd9fa40bf0f9815da2cb0ae8731436c7 - - c5a6dc774e22099cd9ddeb0faff1e75f9cf4f151 - - cd7f610e0becbdf331d5231887d8010a689f87c7 - - 768015345e70f06add2a8b7d1f901dc07bf70582 ignore-authors-list: - author1 - author2 - is-shallow-cloning: true - is-find-previous-authors: false diff --git a/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java b/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java deleted file mode 100644 index 1e066fbde1..0000000000 --- a/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package reposense.model.reportconfig; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class ReportAuthorDetailsTest { - @Test - public void getAuthorEmails_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportAuthorDetails().getAuthorEmails(), - ReportAuthorDetails.DEFAULT_AUTHOR_EMAIL); - } - - @Test - public void getAuthorGitHostId_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportAuthorDetails().getAuthorGitHostId(), - ReportAuthorDetails.DEFAULT_GIT_HOST_ID); - } - - @Test - public void getAuthorDisplayName_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportAuthorDetails().getAuthorDisplayName(), - ReportAuthorDetails.DEFAULT_DISPLAY_NAME); - } - - @Test - public void getAuthorGitAuthorName_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportAuthorDetails().getAuthorGitAuthorName(), - ReportAuthorDetails.DEFAULT_GIT_AUTHOR_NAME); - } -} diff --git a/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java index 70302084c2..79b9a49ef6 100644 --- a/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java @@ -9,46 +9,17 @@ public void getBranch_equalsDefaultReturnValue_success() { Assertions.assertSame(new ReportBranchData().getBranch(), ReportBranchData.DEFAULT_BRANCH); } - @Test - public void getFileFormats_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportBranchData().getFileFormats(), ReportBranchData.DEFAULT_FILE_FORMATS); - } - @Test public void getIgnoreGlobList_equalsDefaultReturnValue_success() { Assertions.assertSame(new ReportBranchData().getIgnoreGlobList(), ReportBranchData.DEFAULT_IGNORE_GLOB_LIST); } - @Test - public void getIsIgnoreStandaloneConfig_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportBranchData().getIsIgnoreStandaloneConfig(), - ReportBranchData.DEFAULT_IS_IGNORE_STANDALONE_CONFIG); - } - - @Test - public void getIgnoreCommitList_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportBranchData().getIgnoreCommitList(), - ReportBranchData.DEFAULT_IGNORE_COMMITS_LIST); - } - @Test public void getIgnoreAuthorList_equalsDefaultReturnValue_success() { Assertions.assertSame(new ReportBranchData().getIgnoreAuthorList(), ReportBranchData.DEFAULT_IGNORE_AUTHORS_LIST); } - @Test - public void getIsShallowCloning_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportBranchData().getIsShallowCloning(), - ReportBranchData.DEFAULT_IS_SHALLOW_CLONING); - } - - @Test - public void getIsFindPreviousAuthor_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportBranchData().getIsFindPreviousAuthor(), - ReportBranchData.DEFAULT_IS_FIND_PREVIOUS_AUTHOR); - } - @Test public void equals_defaultInstancesAreEqual_success() { Assertions.assertEquals(new ReportBranchData(), new ReportBranchData()); diff --git a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml index dea7a91415..3957c75ec8 100644 --- a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml +++ b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml @@ -15,27 +15,8 @@ repos: - "**.md" branches: - branch: master - authors: - - author-git-host-id: fzdy1914 - author-display-name: WANG CHAO - author-git-author-name: WANG CHAO - author-emails: - - 1229983126@qq.com - - author-git-host-id: FH-30 - author-display-name: Francis Hodianto - author-git-author-name: Francis Hodianto - author-emails: - - - file-formats: - - override:java - - md - - fxml + blurb: ignore-glob-list: - - ignore-standalone-config: false - ignore-commits-list: - - ignore-authors-list: - - is-shallow-cloning: false - is-find-previous-authors: false From cc151f489c0ca295ee0c16b97596ca3359f23a54 Mon Sep 17 00:00:00 2001 From: George Tay Date: Sun, 14 Jul 2024 17:41:33 +0800 Subject: [PATCH 10/42] Revert "Update YAML config file format" This reverts commit 587f1528f1d3400683e6c589be85d77f8cb0619f. --- config/report-config.yaml | 21 ++++- .../reportconfig/ReportAuthorDetails.java | 94 +++++++++++++++++++ .../model/reportconfig/ReportBranchData.java | 79 ++++++++++++++-- .../ConfigSystemTest/report-config.yaml | 21 ++++- .../reportconfig/ReportAuthorDetailsTest.java | 30 ++++++ .../reportconfig/ReportBranchDataTest.java | 29 ++++++ .../report-config-valid.yaml | 21 ++++- 7 files changed, 283 insertions(+), 12 deletions(-) create mode 100644 src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java create mode 100644 src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java diff --git a/config/report-config.yaml b/config/report-config.yaml index d7bda4334c..71b1bc3e00 100644 --- a/config/report-config.yaml +++ b/config/report-config.yaml @@ -15,8 +15,27 @@ repos: - "**.md" branches: - branch: master - blurb: + authors: + - author-git-host-id: fzdy1914 + author-display-name: WANG CHAO + author-git-author-name: WANG CHAO + author-emails: + - 1229983126@qq.com + - author-git-host-id: FH-30 + author-display-name: Francis Hodianto + author-git-author-name: Francis Hodianto + author-emails: + - + file-formats: + - override:java + - md + - fxml ignore-glob-list: - + ignore-standalone-config: false + ignore-commits-list: + - ignore-authors-list: - + is-shallow-cloning: false + is-find-previous-authors: false diff --git a/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java b/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java new file mode 100644 index 0000000000..af5d41662f --- /dev/null +++ b/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java @@ -0,0 +1,94 @@ +package reposense.model.reportconfig; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents an author's details in the report-config.yaml file. + */ +public class ReportAuthorDetails { + public static final List DEFAULT_INSTANCES = new ArrayList<>(); + + public static final List DEFAULT_AUTHOR_EMAIL = List.of("john@john.com", "johny@mail.com", "j@domain.com"); + public static final String DEFAULT_GIT_HOST_ID = "johnDoe"; + public static final String DEFAULT_DISPLAY_NAME = "John Doe"; + public static final String DEFAULT_GIT_AUTHOR_NAME = "my home PC"; + + private static final List DEFAULT_AUTHOR_EMAIL_1 = List.of("1229983126@qq.com"); + private static final String DEFAULT_GIT_HOST_ID_1 = "fzdy1914"; + private static final String DEFAULT_DISPLAY_NAME_1 = "WANG CHAO"; + private static final String DEFAULT_GIT_AUTHOR_NAME_1 = "WANG CHAO"; + private static final List DEFAULT_AUTHOR_EMAIL_2 = List.of(""); + private static final String DEFAULT_GIT_HOST_ID_2 = "FH-30"; + private static final String DEFAULT_DISPLAY_NAME_2 = "Francis Hodianto"; + private static final String DEFAULT_GIT_AUTHOR_NAME_2 = "Francis Hodianto"; + + static { + ReportAuthorDetails rad1 = new ReportAuthorDetails(); + ReportAuthorDetails rad2 = new ReportAuthorDetails(); + + rad1.authorEmails = DEFAULT_AUTHOR_EMAIL_1; + rad1.authorGitHostId = DEFAULT_GIT_HOST_ID_1; + rad1.authorGitAuthorName = DEFAULT_GIT_AUTHOR_NAME_1; + rad1.authorDisplayName = DEFAULT_DISPLAY_NAME_1; + + rad2.authorEmails = DEFAULT_AUTHOR_EMAIL_2; + rad2.authorGitHostId = DEFAULT_GIT_HOST_ID_2; + rad2.authorGitAuthorName = DEFAULT_GIT_AUTHOR_NAME_2; + rad2.authorDisplayName = DEFAULT_DISPLAY_NAME_2; + + DEFAULT_INSTANCES.add(rad1); + DEFAULT_INSTANCES.add(rad2); + } + + @JsonProperty("author-emails") + private List authorEmails; + + @JsonProperty("author-git-host-id") + private String authorGitHostId; + + @JsonProperty("author-display-name") + private String authorDisplayName; + + @JsonProperty("author-git-author-name") + private String authorGitAuthorName; + + public ReportAuthorDetails() { + + } + + public List getAuthorEmails() { + return authorEmails == null ? DEFAULT_AUTHOR_EMAIL : authorEmails; + } + + public String getAuthorGitHostId() { + return authorGitHostId == null ? DEFAULT_GIT_HOST_ID : authorGitHostId; + } + + public String getAuthorDisplayName() { + return authorDisplayName == null ? DEFAULT_DISPLAY_NAME : authorDisplayName; + } + + public String getAuthorGitAuthorName() { + return authorGitAuthorName == null ? DEFAULT_GIT_AUTHOR_NAME : authorGitAuthorName; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj instanceof ReportAuthorDetails) { + ReportAuthorDetails rad = (ReportAuthorDetails) obj; + return rad.getAuthorEmails().equals(this.getAuthorEmails()) + && rad.getAuthorGitHostId().equals(this.getAuthorGitHostId()) + && rad.getAuthorDisplayName().equals(this.getAuthorDisplayName()) + && rad.getAuthorGitAuthorName().equals(this.getAuthorGitAuthorName()); + } + + return false; + } +} diff --git a/src/main/java/reposense/model/reportconfig/ReportBranchData.java b/src/main/java/reposense/model/reportconfig/ReportBranchData.java index 27d7e07377..88cf71ef62 100644 --- a/src/main/java/reposense/model/reportconfig/ReportBranchData.java +++ b/src/main/java/reposense/model/reportconfig/ReportBranchData.java @@ -1,5 +1,6 @@ package reposense.model.reportconfig; +import java.util.ArrayList; import java.util.List; import com.fasterxml.jackson.annotation.JsonProperty; @@ -9,51 +10,106 @@ */ public class ReportBranchData { public static final String DEFAULT_BRANCH = "main"; - public static final String DEFAULT_BLURB = ""; + public static final List DEFAULT_REPORT_AUTHOR_DETAILS = new ArrayList<>( + ReportAuthorDetails.DEFAULT_INSTANCES + ); + public static final List DEFAULT_FILE_FORMATS = List.of( + "override:java", "md", "fxml" + ); public static final List DEFAULT_IGNORE_GLOB_LIST = List.of( "docs**" ); + public static final List DEFAULT_IGNORE_COMMITS_LIST = List.of( + "2fb6b9b2dd9fa40bf0f9815da2cb0ae8731436c7", + "c5a6dc774e22099cd9ddeb0faff1e75f9cf4f151", + "cd7f610e0becbdf331d5231887d8010a689f87c7", + "768015345e70f06add2a8b7d1f901dc07bf70582" + ); public static final List DEFAULT_IGNORE_AUTHORS_LIST = List.of( "author1", "author2" ); + public static final boolean DEFAULT_IS_FIND_PREVIOUS_AUTHOR = false; + public static final boolean DEFAULT_IS_SHALLOW_CLONING = true; + public static final boolean DEFAULT_IS_IGNORE_STANDALONE_CONFIG = true; + public static final ReportBranchData DEFAULT_INSTANCE = new ReportBranchData(); static { DEFAULT_INSTANCE.branch = ReportBranchData.DEFAULT_BRANCH; - DEFAULT_INSTANCE.blurb = ReportBranchData.DEFAULT_BLURB; + DEFAULT_INSTANCE.fileFormats = ReportBranchData.DEFAULT_FILE_FORMATS; DEFAULT_INSTANCE.ignoreGlobList = ReportBranchData.DEFAULT_IGNORE_GLOB_LIST; + DEFAULT_INSTANCE.ignoreCommitList = ReportBranchData.DEFAULT_IGNORE_COMMITS_LIST; DEFAULT_INSTANCE.ignoreAuthorList = ReportBranchData.DEFAULT_IGNORE_AUTHORS_LIST; + DEFAULT_INSTANCE.isFindPreviousAuthor = ReportBranchData.DEFAULT_IS_FIND_PREVIOUS_AUTHOR; + DEFAULT_INSTANCE.isShallowCloning = ReportBranchData.DEFAULT_IS_SHALLOW_CLONING; + DEFAULT_INSTANCE.isIgnoreStandaloneConfig = ReportBranchData.DEFAULT_IS_IGNORE_STANDALONE_CONFIG; + DEFAULT_INSTANCE.reportAuthorDetails = ReportAuthorDetails.DEFAULT_INSTANCES; } @JsonProperty("branch") private String branch; - @JsonProperty("blurb") - private String blurb; + @JsonProperty("authors") + private List reportAuthorDetails; + + @JsonProperty("file-formats") + private List fileFormats; @JsonProperty("ignore-glob-list") private List ignoreGlobList; + @JsonProperty("ignore-standalone-config") + private Boolean isIgnoreStandaloneConfig; + + @JsonProperty("ignore-commits-list") + private List ignoreCommitList; + @JsonProperty("ignore-authors-list") private List ignoreAuthorList; + @JsonProperty("is-shallow-cloning") + private Boolean isShallowCloning; + + @JsonProperty("is-find-previous-authors") + private Boolean isFindPreviousAuthor; + public String getBranch() { return branch == null ? DEFAULT_BRANCH : branch; } - public String getBlurb() { - return blurb == null ? DEFAULT_BLURB : blurb; + public List getReportAuthorDetails() { + return reportAuthorDetails == null ? DEFAULT_REPORT_AUTHOR_DETAILS : reportAuthorDetails; + } + + public List getFileFormats() { + return fileFormats == null ? DEFAULT_FILE_FORMATS : fileFormats; } public List getIgnoreGlobList() { - return ignoreGlobList == null ? DEFAULT_IGNORE_GLOB_LIST : ignoreGlobList; + return ignoreGlobList == null ? DEFAULT_IGNORE_GLOB_LIST : fileFormats; + } + + public boolean getIsIgnoreStandaloneConfig() { + return isIgnoreStandaloneConfig == null ? DEFAULT_IS_IGNORE_STANDALONE_CONFIG : isIgnoreStandaloneConfig; + } + + public List getIgnoreCommitList() { + return ignoreCommitList == null ? DEFAULT_IGNORE_COMMITS_LIST : ignoreCommitList; } public List getIgnoreAuthorList() { return ignoreAuthorList == null ? DEFAULT_IGNORE_AUTHORS_LIST : ignoreAuthorList; } + public boolean getIsShallowCloning() { + return isShallowCloning == null ? DEFAULT_IS_SHALLOW_CLONING : isShallowCloning; + } + + public boolean getIsFindPreviousAuthor() { + return isFindPreviousAuthor == null ? DEFAULT_IS_FIND_PREVIOUS_AUTHOR : isFindPreviousAuthor; + } + @Override public boolean equals(Object obj) { if (obj == this) { @@ -63,9 +119,14 @@ public boolean equals(Object obj) { if (obj instanceof ReportBranchData) { ReportBranchData rbd = (ReportBranchData) obj; return this.getBranch().equals(rbd.getBranch()) - && this.getBlurb().equals(rbd.getBlurb()) + && this.getReportAuthorDetails().equals(rbd.getReportAuthorDetails()) + && this.getFileFormats().equals(rbd.getFileFormats()) && this.getIgnoreGlobList().equals(rbd.getIgnoreGlobList()) - && this.getIgnoreAuthorList().equals(rbd.getIgnoreAuthorList()); + && this.getIsIgnoreStandaloneConfig() == rbd.getIsIgnoreStandaloneConfig() + && this.getIgnoreCommitList().equals(rbd.getIgnoreCommitList()) + && this.getIgnoreAuthorList().equals(rbd.getIgnoreAuthorList()) + && this.getIsShallowCloning() == rbd.getIsShallowCloning() + && this.getIsFindPreviousAuthor() == rbd.getIsFindPreviousAuthor(); } return false; diff --git a/src/systemtest/resources/ConfigSystemTest/report-config.yaml b/src/systemtest/resources/ConfigSystemTest/report-config.yaml index fdcdf7cb9f..f0eb63aafc 100644 --- a/src/systemtest/resources/ConfigSystemTest/report-config.yaml +++ b/src/systemtest/resources/ConfigSystemTest/report-config.yaml @@ -15,9 +15,28 @@ repos: - "**.md" branches: - branch: main - blurb: + authors: + - author-git-host-id: johnDoe + author-display-name: John Doe + author-git-author-name: my home PC + author-emails: + - john@john.com + - johny@mail.com + - j@domain.com + file-formats: + - override:java + - md + - fxml ignore-glob-list: - "docs**" + ignore-standalone-config: true + ignore-commits-list: + - 2fb6b9b2dd9fa40bf0f9815da2cb0ae8731436c7 + - c5a6dc774e22099cd9ddeb0faff1e75f9cf4f151 + - cd7f610e0becbdf331d5231887d8010a689f87c7 + - 768015345e70f06add2a8b7d1f901dc07bf70582 ignore-authors-list: - author1 - author2 + is-shallow-cloning: true + is-find-previous-authors: false diff --git a/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java b/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java new file mode 100644 index 0000000000..1e066fbde1 --- /dev/null +++ b/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java @@ -0,0 +1,30 @@ +package reposense.model.reportconfig; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ReportAuthorDetailsTest { + @Test + public void getAuthorEmails_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportAuthorDetails().getAuthorEmails(), + ReportAuthorDetails.DEFAULT_AUTHOR_EMAIL); + } + + @Test + public void getAuthorGitHostId_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportAuthorDetails().getAuthorGitHostId(), + ReportAuthorDetails.DEFAULT_GIT_HOST_ID); + } + + @Test + public void getAuthorDisplayName_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportAuthorDetails().getAuthorDisplayName(), + ReportAuthorDetails.DEFAULT_DISPLAY_NAME); + } + + @Test + public void getAuthorGitAuthorName_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportAuthorDetails().getAuthorGitAuthorName(), + ReportAuthorDetails.DEFAULT_GIT_AUTHOR_NAME); + } +} diff --git a/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java index 79b9a49ef6..70302084c2 100644 --- a/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java @@ -9,17 +9,46 @@ public void getBranch_equalsDefaultReturnValue_success() { Assertions.assertSame(new ReportBranchData().getBranch(), ReportBranchData.DEFAULT_BRANCH); } + @Test + public void getFileFormats_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportBranchData().getFileFormats(), ReportBranchData.DEFAULT_FILE_FORMATS); + } + @Test public void getIgnoreGlobList_equalsDefaultReturnValue_success() { Assertions.assertSame(new ReportBranchData().getIgnoreGlobList(), ReportBranchData.DEFAULT_IGNORE_GLOB_LIST); } + @Test + public void getIsIgnoreStandaloneConfig_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportBranchData().getIsIgnoreStandaloneConfig(), + ReportBranchData.DEFAULT_IS_IGNORE_STANDALONE_CONFIG); + } + + @Test + public void getIgnoreCommitList_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportBranchData().getIgnoreCommitList(), + ReportBranchData.DEFAULT_IGNORE_COMMITS_LIST); + } + @Test public void getIgnoreAuthorList_equalsDefaultReturnValue_success() { Assertions.assertSame(new ReportBranchData().getIgnoreAuthorList(), ReportBranchData.DEFAULT_IGNORE_AUTHORS_LIST); } + @Test + public void getIsShallowCloning_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportBranchData().getIsShallowCloning(), + ReportBranchData.DEFAULT_IS_SHALLOW_CLONING); + } + + @Test + public void getIsFindPreviousAuthor_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportBranchData().getIsFindPreviousAuthor(), + ReportBranchData.DEFAULT_IS_FIND_PREVIOUS_AUTHOR); + } + @Test public void equals_defaultInstancesAreEqual_success() { Assertions.assertEquals(new ReportBranchData(), new ReportBranchData()); diff --git a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml index 3957c75ec8..dea7a91415 100644 --- a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml +++ b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml @@ -15,8 +15,27 @@ repos: - "**.md" branches: - branch: master - blurb: + authors: + - author-git-host-id: fzdy1914 + author-display-name: WANG CHAO + author-git-author-name: WANG CHAO + author-emails: + - 1229983126@qq.com + - author-git-host-id: FH-30 + author-display-name: Francis Hodianto + author-git-author-name: Francis Hodianto + author-emails: + - + file-formats: + - override:java + - md + - fxml ignore-glob-list: - + ignore-standalone-config: false + ignore-commits-list: + - ignore-authors-list: - + is-shallow-cloning: false + is-find-previous-authors: false From 645b4c78023a636b9c79dc355b6247b175eac289 Mon Sep 17 00:00:00 2001 From: George Tay Date: Sun, 14 Jul 2024 18:25:08 +0800 Subject: [PATCH 11/42] Update Report Configuration YAML File Format --- config/report-config.yaml | 10 +-- .../model/reportconfig/ReportBranchData.java | 61 +------------------ .../reportconfig/ReportRepoConfiguration.java | 10 +++ .../ConfigSystemTest/report-config.yaml | 13 +--- .../reportconfig/ReportBranchDataTest.java | 28 --------- .../ReportRepoConfigurationTest.java | 5 ++ .../report-config-invalid.yaml | 1 + .../report-config-valid.yaml | 10 +-- 8 files changed, 21 insertions(+), 117 deletions(-) diff --git a/config/report-config.yaml b/config/report-config.yaml index 71b1bc3e00..e410a3629a 100644 --- a/config/report-config.yaml +++ b/config/report-config.yaml @@ -1,6 +1,7 @@ title: RepoSense Report repos: - repo: https://github.com/reposense/testrepo-Delta.git + blurb: "My project" groups: - group-name: code globs: @@ -26,16 +27,7 @@ repos: author-git-author-name: Francis Hodianto author-emails: - - file-formats: - - override:java - - md - - fxml ignore-glob-list: - - ignore-standalone-config: false - ignore-commits-list: - - ignore-authors-list: - - is-shallow-cloning: false - is-find-previous-authors: false diff --git a/src/main/java/reposense/model/reportconfig/ReportBranchData.java b/src/main/java/reposense/model/reportconfig/ReportBranchData.java index 88cf71ef62..b77db292f8 100644 --- a/src/main/java/reposense/model/reportconfig/ReportBranchData.java +++ b/src/main/java/reposense/model/reportconfig/ReportBranchData.java @@ -13,37 +13,20 @@ public class ReportBranchData { public static final List DEFAULT_REPORT_AUTHOR_DETAILS = new ArrayList<>( ReportAuthorDetails.DEFAULT_INSTANCES ); - public static final List DEFAULT_FILE_FORMATS = List.of( - "override:java", "md", "fxml" - ); public static final List DEFAULT_IGNORE_GLOB_LIST = List.of( "docs**" ); - public static final List DEFAULT_IGNORE_COMMITS_LIST = List.of( - "2fb6b9b2dd9fa40bf0f9815da2cb0ae8731436c7", - "c5a6dc774e22099cd9ddeb0faff1e75f9cf4f151", - "cd7f610e0becbdf331d5231887d8010a689f87c7", - "768015345e70f06add2a8b7d1f901dc07bf70582" - ); public static final List DEFAULT_IGNORE_AUTHORS_LIST = List.of( "author1", "author2" ); - public static final boolean DEFAULT_IS_FIND_PREVIOUS_AUTHOR = false; - public static final boolean DEFAULT_IS_SHALLOW_CLONING = true; - public static final boolean DEFAULT_IS_IGNORE_STANDALONE_CONFIG = true; public static final ReportBranchData DEFAULT_INSTANCE = new ReportBranchData(); static { DEFAULT_INSTANCE.branch = ReportBranchData.DEFAULT_BRANCH; - DEFAULT_INSTANCE.fileFormats = ReportBranchData.DEFAULT_FILE_FORMATS; DEFAULT_INSTANCE.ignoreGlobList = ReportBranchData.DEFAULT_IGNORE_GLOB_LIST; - DEFAULT_INSTANCE.ignoreCommitList = ReportBranchData.DEFAULT_IGNORE_COMMITS_LIST; DEFAULT_INSTANCE.ignoreAuthorList = ReportBranchData.DEFAULT_IGNORE_AUTHORS_LIST; - DEFAULT_INSTANCE.isFindPreviousAuthor = ReportBranchData.DEFAULT_IS_FIND_PREVIOUS_AUTHOR; - DEFAULT_INSTANCE.isShallowCloning = ReportBranchData.DEFAULT_IS_SHALLOW_CLONING; - DEFAULT_INSTANCE.isIgnoreStandaloneConfig = ReportBranchData.DEFAULT_IS_IGNORE_STANDALONE_CONFIG; DEFAULT_INSTANCE.reportAuthorDetails = ReportAuthorDetails.DEFAULT_INSTANCES; } @@ -53,27 +36,12 @@ public class ReportBranchData { @JsonProperty("authors") private List reportAuthorDetails; - @JsonProperty("file-formats") - private List fileFormats; - @JsonProperty("ignore-glob-list") private List ignoreGlobList; - @JsonProperty("ignore-standalone-config") - private Boolean isIgnoreStandaloneConfig; - - @JsonProperty("ignore-commits-list") - private List ignoreCommitList; - @JsonProperty("ignore-authors-list") private List ignoreAuthorList; - @JsonProperty("is-shallow-cloning") - private Boolean isShallowCloning; - - @JsonProperty("is-find-previous-authors") - private Boolean isFindPreviousAuthor; - public String getBranch() { return branch == null ? DEFAULT_BRANCH : branch; } @@ -82,34 +50,14 @@ public List getReportAuthorDetails() { return reportAuthorDetails == null ? DEFAULT_REPORT_AUTHOR_DETAILS : reportAuthorDetails; } - public List getFileFormats() { - return fileFormats == null ? DEFAULT_FILE_FORMATS : fileFormats; - } - public List getIgnoreGlobList() { - return ignoreGlobList == null ? DEFAULT_IGNORE_GLOB_LIST : fileFormats; - } - - public boolean getIsIgnoreStandaloneConfig() { - return isIgnoreStandaloneConfig == null ? DEFAULT_IS_IGNORE_STANDALONE_CONFIG : isIgnoreStandaloneConfig; - } - - public List getIgnoreCommitList() { - return ignoreCommitList == null ? DEFAULT_IGNORE_COMMITS_LIST : ignoreCommitList; + return ignoreGlobList == null ? DEFAULT_IGNORE_GLOB_LIST : ignoreGlobList; } public List getIgnoreAuthorList() { return ignoreAuthorList == null ? DEFAULT_IGNORE_AUTHORS_LIST : ignoreAuthorList; } - public boolean getIsShallowCloning() { - return isShallowCloning == null ? DEFAULT_IS_SHALLOW_CLONING : isShallowCloning; - } - - public boolean getIsFindPreviousAuthor() { - return isFindPreviousAuthor == null ? DEFAULT_IS_FIND_PREVIOUS_AUTHOR : isFindPreviousAuthor; - } - @Override public boolean equals(Object obj) { if (obj == this) { @@ -120,13 +68,8 @@ public boolean equals(Object obj) { ReportBranchData rbd = (ReportBranchData) obj; return this.getBranch().equals(rbd.getBranch()) && this.getReportAuthorDetails().equals(rbd.getReportAuthorDetails()) - && this.getFileFormats().equals(rbd.getFileFormats()) && this.getIgnoreGlobList().equals(rbd.getIgnoreGlobList()) - && this.getIsIgnoreStandaloneConfig() == rbd.getIsIgnoreStandaloneConfig() - && this.getIgnoreCommitList().equals(rbd.getIgnoreCommitList()) - && this.getIgnoreAuthorList().equals(rbd.getIgnoreAuthorList()) - && this.getIsShallowCloning() == rbd.getIsShallowCloning() - && this.getIsFindPreviousAuthor() == rbd.getIsFindPreviousAuthor(); + && this.getIgnoreAuthorList().equals(rbd.getIgnoreAuthorList()); } return false; diff --git a/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java index 9d14d2a5df..f817c7b913 100644 --- a/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java +++ b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java @@ -10,6 +10,7 @@ */ public class ReportRepoConfiguration { public static final String DEFAULT_REPO = "https://github.com/user/repo"; + public static final String DEFAULT_BLURB = "My project"; public static final List DEFAULT_GROUP_DETAILS = ReportGroupNameAndGlobs.DEFAULT_INSTANCES; public static final List DEFAULT_BRANCHES = List.of( ReportBranchData.DEFAULT_INSTANCE @@ -18,6 +19,7 @@ public class ReportRepoConfiguration { static { DEFAULT_INSTANCE.repo = DEFAULT_REPO; + DEFAULT_INSTANCE.blurb = DEFAULT_BLURB; DEFAULT_INSTANCE.groups = DEFAULT_GROUP_DETAILS; DEFAULT_INSTANCE.branches = DEFAULT_BRANCHES; } @@ -25,6 +27,9 @@ public class ReportRepoConfiguration { @JsonProperty("repo") private String repo; + @JsonProperty("blurb") + private String blurb; + @JsonProperty("groups") private List groups; @@ -35,6 +40,10 @@ public String getRepo() { return repo == null ? DEFAULT_REPO : repo; } + public String getBlurb() { + return blurb == null ? DEFAULT_BLURB : blurb; + } + public List getGroupDetails() { return groups == null ? DEFAULT_GROUP_DETAILS : groups; } @@ -52,6 +61,7 @@ public boolean equals(Object obj) { if (obj instanceof ReportRepoConfiguration) { ReportRepoConfiguration rrc = (ReportRepoConfiguration) obj; return rrc.getRepo().equals(this.getRepo()) + && rrc.getBlurb().equals(this.getBlurb()) && rrc.getGroupDetails().equals(this.getGroupDetails()) && rrc.getBranches().equals(this.getBranches()); } diff --git a/src/systemtest/resources/ConfigSystemTest/report-config.yaml b/src/systemtest/resources/ConfigSystemTest/report-config.yaml index f0eb63aafc..fc5e2ee765 100644 --- a/src/systemtest/resources/ConfigSystemTest/report-config.yaml +++ b/src/systemtest/resources/ConfigSystemTest/report-config.yaml @@ -1,6 +1,7 @@ title: RepoSense Report Test Title repos: - repo: https://github.com/user/repo.git + blurb: "My project" groups: - group-name: code globs: @@ -23,20 +24,8 @@ repos: - john@john.com - johny@mail.com - j@domain.com - file-formats: - - override:java - - md - - fxml ignore-glob-list: - "docs**" - ignore-standalone-config: true - ignore-commits-list: - - 2fb6b9b2dd9fa40bf0f9815da2cb0ae8731436c7 - - c5a6dc774e22099cd9ddeb0faff1e75f9cf4f151 - - cd7f610e0becbdf331d5231887d8010a689f87c7 - - 768015345e70f06add2a8b7d1f901dc07bf70582 ignore-authors-list: - author1 - author2 - is-shallow-cloning: true - is-find-previous-authors: false diff --git a/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java index 70302084c2..72da4d190e 100644 --- a/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java @@ -9,27 +9,11 @@ public void getBranch_equalsDefaultReturnValue_success() { Assertions.assertSame(new ReportBranchData().getBranch(), ReportBranchData.DEFAULT_BRANCH); } - @Test - public void getFileFormats_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportBranchData().getFileFormats(), ReportBranchData.DEFAULT_FILE_FORMATS); - } - @Test public void getIgnoreGlobList_equalsDefaultReturnValue_success() { Assertions.assertSame(new ReportBranchData().getIgnoreGlobList(), ReportBranchData.DEFAULT_IGNORE_GLOB_LIST); } - @Test - public void getIsIgnoreStandaloneConfig_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportBranchData().getIsIgnoreStandaloneConfig(), - ReportBranchData.DEFAULT_IS_IGNORE_STANDALONE_CONFIG); - } - - @Test - public void getIgnoreCommitList_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportBranchData().getIgnoreCommitList(), - ReportBranchData.DEFAULT_IGNORE_COMMITS_LIST); - } @Test public void getIgnoreAuthorList_equalsDefaultReturnValue_success() { @@ -37,18 +21,6 @@ public void getIgnoreAuthorList_equalsDefaultReturnValue_success() { ReportBranchData.DEFAULT_IGNORE_AUTHORS_LIST); } - @Test - public void getIsShallowCloning_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportBranchData().getIsShallowCloning(), - ReportBranchData.DEFAULT_IS_SHALLOW_CLONING); - } - - @Test - public void getIsFindPreviousAuthor_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportBranchData().getIsFindPreviousAuthor(), - ReportBranchData.DEFAULT_IS_FIND_PREVIOUS_AUTHOR); - } - @Test public void equals_defaultInstancesAreEqual_success() { Assertions.assertEquals(new ReportBranchData(), new ReportBranchData()); diff --git a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java index 0c5b589e3c..fbc4174bd3 100644 --- a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java @@ -9,6 +9,11 @@ public void getRepo_equalsDefaultReturnValue_success() { Assertions.assertSame(new ReportRepoConfiguration().getRepo(), ReportRepoConfiguration.DEFAULT_REPO); } + @Test + public void getBlurb_equalsDefaultReturnValue_success() { + Assertions.assertSame(new ReportRepoConfiguration().getBlurb(), ReportRepoConfiguration.DEFAULT_BLURB); + } + @Test public void getBranches_equalsDefaultReturnValue_success() { Assertions.assertSame(new ReportRepoConfiguration().getBranches(), ReportRepoConfiguration.DEFAULT_BRANCHES); diff --git a/src/test/resources/ReportConfigYamlParserTest/report-config-invalid.yaml b/src/test/resources/ReportConfigYamlParserTest/report-config-invalid.yaml index bd6058f6c4..f4b2300daf 100644 --- a/src/test/resources/ReportConfigYamlParserTest/report-config-invalid.yaml +++ b/src/test/resources/ReportConfigYamlParserTest/report-config-invalid.yaml @@ -1,6 +1,7 @@ titles: RepoSense Report repo: - repos: https://github.com/reposense/testrepo-Delta.git + blurbs: "The main branch" group: - grame: code globs: diff --git a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml index dea7a91415..ff8994253d 100644 --- a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml +++ b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml @@ -1,6 +1,7 @@ title: RepoSense Report Test Repo repos: - repo: https://github.com/reposense/testrepo-Delta.git + blurb: "The main branch" groups: - group-name: code globs: @@ -26,16 +27,7 @@ repos: author-git-author-name: Francis Hodianto author-emails: - - file-formats: - - override:java - - md - - fxml ignore-glob-list: - - ignore-standalone-config: false - ignore-commits-list: - - ignore-authors-list: - - is-shallow-cloning: false - is-find-previous-authors: false From 42d167de4562d466600f1f30aeffec0aa330a16b Mon Sep 17 00:00:00 2001 From: George Tay Date: Mon, 15 Jul 2024 09:20:33 +0800 Subject: [PATCH 12/42] Implement One-Stop Config File Feature --- config/report-config.yaml | 2 +- src/main/java/reposense/model/BlurbMap.java | 5 ++ .../java/reposense/model/CliArguments.java | 45 ++++++++++- .../model/OneStopConfigRunConfiguration.java | 77 ++++++++++++++++++ .../model/RunConfigurationDecider.java | 5 ++ .../model/reportconfig/ReportBranchData.java | 10 +++ .../reportconfig/ReportConfiguration.java | 37 +++++++-- .../reportconfig/ReportGroupNameAndGlobs.java | 19 +++-- .../reportconfig/ReportRepoConfiguration.java | 79 ++++++++++++++++--- .../java/reposense/parser/ArgsParser.java | 5 +- .../ConfigSystemTest/report-config.yaml | 2 +- .../ReportRepoConfigurationTest.java | 5 -- .../report-config-valid.yaml | 2 +- 13 files changed, 260 insertions(+), 33 deletions(-) create mode 100644 src/main/java/reposense/model/OneStopConfigRunConfiguration.java diff --git a/config/report-config.yaml b/config/report-config.yaml index e410a3629a..968a172699 100644 --- a/config/report-config.yaml +++ b/config/report-config.yaml @@ -1,7 +1,6 @@ title: RepoSense Report repos: - repo: https://github.com/reposense/testrepo-Delta.git - blurb: "My project" groups: - group-name: code globs: @@ -16,6 +15,7 @@ repos: - "**.md" branches: - branch: master + blurb: "My project" authors: - author-git-host-id: fzdy1914 author-display-name: WANG CHAO diff --git a/src/main/java/reposense/model/BlurbMap.java b/src/main/java/reposense/model/BlurbMap.java index 478a3d0c1f..94874f1c30 100644 --- a/src/main/java/reposense/model/BlurbMap.java +++ b/src/main/java/reposense/model/BlurbMap.java @@ -16,6 +16,11 @@ public BlurbMap() { this.urlBlurbMap = new HashMap<>(); } + /** + * Return a copy of the mapping between the repo URL to the associated blurb. + * + * @return a {@code Map} containing the mapping between the repo URL to the associated blurb. + */ public Map getAllMappings() { return new HashMap<>(this.urlBlurbMap); } diff --git a/src/main/java/reposense/model/CliArguments.java b/src/main/java/reposense/model/CliArguments.java index dad1d3563a..3e8e0eafd6 100644 --- a/src/main/java/reposense/model/CliArguments.java +++ b/src/main/java/reposense/model/CliArguments.java @@ -40,6 +40,8 @@ public class CliArguments { private double originalityThreshold; private boolean isTestMode = ArgsParser.DEFAULT_IS_TEST_MODE; private boolean isFreshClonePerformed = ArgsParser.DEFAULT_SHOULD_FRESH_CLONE; + private boolean isBlurbMapOverriding; + private boolean isOneStopConfigSpecified; private List locations; private boolean isViewModeOnly; @@ -167,6 +169,14 @@ public BlurbMap getBlurbMap() { return blurbMap; } + public boolean isBlurbMapOverriding() { + return isBlurbMapOverriding; + } + + public boolean isOneStopConfigSpecified() { + return isOneStopConfigSpecified; + } + public boolean isViewModeOnly() { return isViewModeOnly; } @@ -219,7 +229,8 @@ public boolean equals(Object other) { && Objects.equals(this.reportConfigFilePath, otherCliArguments.reportConfigFilePath) && Objects.equals(this.blurbMap, otherCliArguments.blurbMap) && this.isAuthorshipAnalyzed == otherCliArguments.isAuthorshipAnalyzed - && Objects.equals(this.originalityThreshold, otherCliArguments.originalityThreshold); + && Objects.equals(this.originalityThreshold, otherCliArguments.originalityThreshold) + && this.isBlurbMapOverriding == otherCliArguments.isBlurbMapOverriding; } /** @@ -504,6 +515,38 @@ public Builder blurbMap(BlurbMap blurbMap) { return this; } + /** + * Mark the {@code blurbMap} as overriding. + */ + public Builder markAsBlurbMapOverriding() { + this.cliArguments.isBlurbMapOverriding = true; + return this; + } + + /** + * Unmark the {@code blurbMap} as overriding. + */ + public Builder unmarkAsBlurbMapOverriding() { + this.cliArguments.isBlurbMapOverriding = false; + return this; + } + + /** + * Mark the {@code oneStopConfig} as specified. + */ + public Builder markAsOneStopConfigSpecified() { + this.cliArguments.isOneStopConfigSpecified = true; + return this; + } + + /** + * Unmark the {@code oneStopConfig} as specified. + */ + public Builder unmarkAsOneStopConfigSpecified() { + this.cliArguments.isOneStopConfigSpecified = false; + return this; + } + /** * Builds CliArguments. * diff --git a/src/main/java/reposense/model/OneStopConfigRunConfiguration.java b/src/main/java/reposense/model/OneStopConfigRunConfiguration.java new file mode 100644 index 0000000000..5e61ca64a2 --- /dev/null +++ b/src/main/java/reposense/model/OneStopConfigRunConfiguration.java @@ -0,0 +1,77 @@ +package reposense.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import reposense.model.reportconfig.ReportAuthorDetails; +import reposense.model.reportconfig.ReportBranchData; +import reposense.model.reportconfig.ReportConfiguration; +import reposense.model.reportconfig.ReportRepoConfiguration; +import reposense.parser.exceptions.InvalidLocationException; +import reposense.system.LogsManager; + +/** + * Represents RepoSense run configured by the one-stop configuration file. + */ +public class OneStopConfigRunConfiguration implements RunConfiguration { + private static final Logger logger = LogsManager.getLogger(OneStopConfigRunConfiguration.class); + + private final CliArguments cliArguments; + + public OneStopConfigRunConfiguration(CliArguments cliArguments) { + this.cliArguments = cliArguments; + } + + /** + * Constructs a list of {@link RepoConfiguration}. + * + * @throws InvalidLocationException if the location specified in the config file is invalid. + */ + @Override + public List getRepoConfigurations() throws InvalidLocationException { + ReportConfiguration reportConfiguration = this.cliArguments.getReportConfiguration(); + List repoConfigs = new ArrayList<>(); + List authorConfigs = new ArrayList<>(); + List groupConfigs = new ArrayList<>(); + + for (ReportRepoConfiguration rrc : reportConfiguration.getReportRepoConfigurations()) { + logger.info("Parsing " + rrc.getRepo() + "..."); + RepoLocation repoLocation = new RepoLocation(rrc.getRepo()); + groupConfigs.add(rrc.getGroupConfiguration(repoLocation)); + + // iterate for each branch and for each author + for (ReportBranchData rbd : rrc.getBranches()) { + logger.info("Parsing " + rbd.getBranch() + "..."); + for (ReportAuthorDetails rad : rbd.getReportAuthorDetails()) { + logger.info("Parsing " + rad.getAuthorGitHostId() + "..."); + // create the repoconfiguration object + // we will need to add the group configuration details to the repo configuration builder + RepoConfiguration.Builder builder = new RepoConfiguration.Builder() + .location(repoLocation); + + // set the relevant branch details for the repo + builder = builder.branch(rbd.getBranch()) + .ignoreGlobList(rbd.getIgnoreGlobList()) + .ignoredAuthorsList(rbd.getIgnoreAuthorList()); + + // prepare the author details + Author author = new Author(rad.getAuthorGitHostId()); + author.setEmails(rad.getAuthorEmails()); + author.setDisplayName(rad.getAuthorDisplayName()); + author.setAuthorAliases(List.of(rad.getAuthorGitAuthorName())); + authorConfigs.add(new AuthorConfiguration(repoLocation, rbd.getBranch())); + + repoConfigs.add(builder.build()); + } + } + } + + logger.info("Merging author, group and repo configurations..."); + RepoConfiguration.merge(repoConfigs, authorConfigs); + RepoConfiguration.setGroupConfigsToRepos(repoConfigs, groupConfigs); + + logger.info("Finished parsing OneStopConfigRunConfiguration!"); + return repoConfigs; + } +} diff --git a/src/main/java/reposense/model/RunConfigurationDecider.java b/src/main/java/reposense/model/RunConfigurationDecider.java index c4bd07c85b..9fd6cb2c20 100644 --- a/src/main/java/reposense/model/RunConfigurationDecider.java +++ b/src/main/java/reposense/model/RunConfigurationDecider.java @@ -6,8 +6,13 @@ public class RunConfigurationDecider { public static RunConfiguration getRunConfiguration(CliArguments cliArguments) { if (cliArguments.getLocations() != null) { + if (cliArguments.isOneStopConfigSpecified()) { + return new OneStopConfigRunConfiguration(cliArguments); + } + return new CliRunConfiguration(cliArguments); } + return new ConfigRunConfiguration(cliArguments); } } diff --git a/src/main/java/reposense/model/reportconfig/ReportBranchData.java b/src/main/java/reposense/model/reportconfig/ReportBranchData.java index b77db292f8..f1027640f0 100644 --- a/src/main/java/reposense/model/reportconfig/ReportBranchData.java +++ b/src/main/java/reposense/model/reportconfig/ReportBranchData.java @@ -10,6 +10,7 @@ */ public class ReportBranchData { public static final String DEFAULT_BRANCH = "main"; + public static final String DEFAULT_BLURB = "My project"; public static final List DEFAULT_REPORT_AUTHOR_DETAILS = new ArrayList<>( ReportAuthorDetails.DEFAULT_INSTANCES ); @@ -25,6 +26,7 @@ public class ReportBranchData { static { DEFAULT_INSTANCE.branch = ReportBranchData.DEFAULT_BRANCH; + DEFAULT_INSTANCE.blurb = DEFAULT_BLURB; DEFAULT_INSTANCE.ignoreGlobList = ReportBranchData.DEFAULT_IGNORE_GLOB_LIST; DEFAULT_INSTANCE.ignoreAuthorList = ReportBranchData.DEFAULT_IGNORE_AUTHORS_LIST; DEFAULT_INSTANCE.reportAuthorDetails = ReportAuthorDetails.DEFAULT_INSTANCES; @@ -33,6 +35,9 @@ public class ReportBranchData { @JsonProperty("branch") private String branch; + @JsonProperty("blurb") + private String blurb; + @JsonProperty("authors") private List reportAuthorDetails; @@ -46,6 +51,10 @@ public String getBranch() { return branch == null ? DEFAULT_BRANCH : branch; } + public String getBlurb() { + return blurb == null ? DEFAULT_BLURB : blurb; + } + public List getReportAuthorDetails() { return reportAuthorDetails == null ? DEFAULT_REPORT_AUTHOR_DETAILS : reportAuthorDetails; } @@ -67,6 +76,7 @@ public boolean equals(Object obj) { if (obj instanceof ReportBranchData) { ReportBranchData rbd = (ReportBranchData) obj; return this.getBranch().equals(rbd.getBranch()) + && this.getBlurb().equals(rbd.getBlurb()) && this.getReportAuthorDetails().equals(rbd.getReportAuthorDetails()) && this.getIgnoreGlobList().equals(rbd.getIgnoreGlobList()) && this.getIgnoreAuthorList().equals(rbd.getIgnoreAuthorList()); diff --git a/src/main/java/reposense/model/reportconfig/ReportConfiguration.java b/src/main/java/reposense/model/reportconfig/ReportConfiguration.java index b91f4d4a08..8315b25b16 100644 --- a/src/main/java/reposense/model/reportconfig/ReportConfiguration.java +++ b/src/main/java/reposense/model/reportconfig/ReportConfiguration.java @@ -2,9 +2,14 @@ import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import com.fasterxml.jackson.annotation.JsonProperty; +import reposense.model.BlurbMap; +import reposense.system.LogsManager; + /** * Class that contains information on a report's configurations. * This class is used mainly for quickly setting up one's personal code portfolio. @@ -12,9 +17,14 @@ public class ReportConfiguration { public static final String DEFAULT_TITLE = "RepoSense Report"; public static final List DEFAULT_REPORT_REPO_CONFIGS = new ArrayList<>(); + public static final ReportConfiguration DEFAULT_INSTANCE = new ReportConfiguration(); + + private static final Logger logger = LogsManager.getLogger(ReportConfiguration.class); static { DEFAULT_REPORT_REPO_CONFIGS.add(ReportRepoConfiguration.DEFAULT_INSTANCE); + ReportConfiguration.DEFAULT_INSTANCE.title = DEFAULT_TITLE; + ReportConfiguration.DEFAULT_INSTANCE.reportRepoConfigurations = DEFAULT_REPORT_REPO_CONFIGS; } @JsonProperty("title") @@ -23,6 +33,28 @@ public class ReportConfiguration { @JsonProperty("repos") private List reportRepoConfigurations; + /** + * Converts the {@code ReportRepoConfiguration} list into a {@code BlurbMap}. + * + * @return {@code BlurbMap} containing the repository name and its associated blurb. + */ + public BlurbMap getBlurbMap() { + BlurbMap blurbMap = new BlurbMap(); + + for (ReportRepoConfiguration repoConfig : reportRepoConfigurations) { + try { + for (ReportRepoConfiguration.MapEntry repoNameBlurbPair + : repoConfig.getFullyQualifiedRepoNamesWithBlurbs()) { + blurbMap.withRecord(repoNameBlurbPair.getKey(), repoNameBlurbPair.getValue()); + } + } catch (IllegalArgumentException ex) { + logger.log(Level.WARNING, ex.getMessage(), ex); + } + } + + return blurbMap; + } + public String getTitle() { return title == null ? DEFAULT_TITLE : title; } @@ -45,9 +77,4 @@ public boolean equals(Object obj) { return false; } - - @Override - public String toString() { - return title + "\n" + reportRepoConfigurations; - } } diff --git a/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java b/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java index f3403d31ab..ba78c61c6c 100644 --- a/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java +++ b/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java @@ -5,6 +5,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; +import reposense.model.FileType; + /** * Contains details about each report group and the corresponding globs. */ @@ -47,6 +49,18 @@ public List getGlobs() { return globs == null ? DEFAULT_GLOBS : globs; } + /** + * Converts this {@code ReportGroupNameAndGlobs} into a {@code FileType}. + * + * @return Adapted {@code FileType} object. + */ + public FileType toFileType() { + return new FileType( + this.getGroupName(), + this.getGlobs() + ); + } + @Override public boolean equals(Object obj) { if (obj == this) { @@ -61,9 +75,4 @@ public boolean equals(Object obj) { return false; } - - @Override - public String toString() { - return "RGNAG { group-name: " + this.groupName + ", globs: " + this.globs + "}"; - } } diff --git a/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java index f817c7b913..5c24b59b54 100644 --- a/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java +++ b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java @@ -1,16 +1,21 @@ package reposense.model.reportconfig; +import java.util.ArrayList; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import com.fasterxml.jackson.annotation.JsonProperty; +import reposense.model.GroupConfiguration; +import reposense.model.RepoLocation; + /** * Represents a single repository configuration in the overall report * configuration. */ public class ReportRepoConfiguration { public static final String DEFAULT_REPO = "https://github.com/user/repo"; - public static final String DEFAULT_BLURB = "My project"; public static final List DEFAULT_GROUP_DETAILS = ReportGroupNameAndGlobs.DEFAULT_INSTANCES; public static final List DEFAULT_BRANCHES = List.of( ReportBranchData.DEFAULT_INSTANCE @@ -19,7 +24,6 @@ public class ReportRepoConfiguration { static { DEFAULT_INSTANCE.repo = DEFAULT_REPO; - DEFAULT_INSTANCE.blurb = DEFAULT_BLURB; DEFAULT_INSTANCE.groups = DEFAULT_GROUP_DETAILS; DEFAULT_INSTANCE.branches = DEFAULT_BRANCHES; } @@ -27,27 +31,82 @@ public class ReportRepoConfiguration { @JsonProperty("repo") private String repo; - @JsonProperty("blurb") - private String blurb; - @JsonProperty("groups") private List groups; @JsonProperty("branches") private List branches; + /** + * Represents a single mapped entry between a fully qualified repository name and its associated blurb. + */ + public static final class MapEntry { + private final String key; + private final String value; + + public MapEntry(String key, String value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + } + public String getRepo() { return repo == null ? DEFAULT_REPO : repo; } - public String getBlurb() { - return blurb == null ? DEFAULT_BLURB : blurb; + /** + * Returns the repository name from the {@code repo} URL. Each entry is represented by a list containing + * the fully qualified repository name and the associated blurb. + * + * @return {@code List} containing the fully qualified repository name and the associated blurb. + * @throws IllegalArgumentException if the {@code repo} URL is not in the correct format. + */ + public List getFullyQualifiedRepoNamesWithBlurbs() { + List repoNames = new ArrayList<>(); + Pattern pattern = Pattern.compile(".git"); + Matcher matcher = pattern.matcher(getRepo()); + + if (!matcher.find()) { + // no point continuing if there are no blurbs + throw new IllegalArgumentException("Repo URL: " + this.getRepo() + " is not in the correct format. " + + "Skipping..."); + } + + for (ReportBranchData rbd : this.getBranches()) { + String qualifiedName = getRepo().substring(0, matcher.start()) + "/" + rbd.getBranch(); + repoNames.add(new MapEntry(qualifiedName, rbd.getBlurb())); + } + + return repoNames; } public List getGroupDetails() { return groups == null ? DEFAULT_GROUP_DETAILS : groups; } + /** + * Merges all {@code ReportGroupNameAndGlobs} objects in {@code groups} into a {@code GroupConfiguration} object. + * + * @param location {@code RepoLocation} object. + * @return Adapted {@code GroupConfiguration} object. + */ + public GroupConfiguration getGroupConfiguration(RepoLocation location) { + GroupConfiguration groupConfiguration = new GroupConfiguration(location); + + this.getGroupDetails() + .forEach(x -> groupConfiguration.addGroup(x.toFileType())); + + return groupConfiguration; + } + public List getBranches() { return branches == null ? DEFAULT_BRANCHES : branches; } @@ -61,16 +120,10 @@ public boolean equals(Object obj) { if (obj instanceof ReportRepoConfiguration) { ReportRepoConfiguration rrc = (ReportRepoConfiguration) obj; return rrc.getRepo().equals(this.getRepo()) - && rrc.getBlurb().equals(this.getBlurb()) && rrc.getGroupDetails().equals(this.getGroupDetails()) && rrc.getBranches().equals(this.getBranches()); } return false; } - - @Override - public String toString() { - return repo + "\n" + groups + "\n" + branches; - } } diff --git a/src/main/java/reposense/parser/ArgsParser.java b/src/main/java/reposense/parser/ArgsParser.java index 0529c350ce..5ef3ae7e82 100644 --- a/src/main/java/reposense/parser/ArgsParser.java +++ b/src/main/java/reposense/parser/ArgsParser.java @@ -390,6 +390,7 @@ private static void addReportConfigToBuilder(CliArguments.Builder builder, Names try { reportConfig = new ReportConfigYamlParser().parse(reportConfigFilePath); + builder = builder.markAsOneStopConfigSpecified(); } catch (JsonSyntaxException jse) { logger.warning(String.format(MESSAGE_INVALID_CONFIG_PATH, reportConfigFilePath)); } catch (IllegalArgumentException iae) { @@ -397,6 +398,7 @@ private static void addReportConfigToBuilder(CliArguments.Builder builder, Names } catch (IOException ioe) { // IOException thrown as report-config.yaml is not found. // Ignore exception as the file is optional. + builder = builder.unmarkAsOneStopConfigSpecified(); } } @@ -418,11 +420,12 @@ private static void addBlurbMapToBuilder(CliArguments.Builder builder, Namespace try { blurbMap = new BlurbMarkdownParser(blurbConfigPath).parse(); + builder = builder.markAsBlurbMapOverriding(); } catch (InvalidMarkdownException ex) { logger.warning(String.format(MESSAGE_INVALID_MARKDOWN_BLURBS, ex.getMessage())); } catch (IOException ioe) { // IOException thrown as blurbs.md is not found. - // Ignore exception as the file is optional. + builder = builder.unmarkAsBlurbMapOverriding(); } builder.blurbMap(blurbMap); diff --git a/src/systemtest/resources/ConfigSystemTest/report-config.yaml b/src/systemtest/resources/ConfigSystemTest/report-config.yaml index fc5e2ee765..925686c29f 100644 --- a/src/systemtest/resources/ConfigSystemTest/report-config.yaml +++ b/src/systemtest/resources/ConfigSystemTest/report-config.yaml @@ -1,7 +1,6 @@ title: RepoSense Report Test Title repos: - repo: https://github.com/user/repo.git - blurb: "My project" groups: - group-name: code globs: @@ -16,6 +15,7 @@ repos: - "**.md" branches: - branch: main + blurb: "My project" authors: - author-git-host-id: johnDoe author-display-name: John Doe diff --git a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java index fbc4174bd3..0c5b589e3c 100644 --- a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java @@ -9,11 +9,6 @@ public void getRepo_equalsDefaultReturnValue_success() { Assertions.assertSame(new ReportRepoConfiguration().getRepo(), ReportRepoConfiguration.DEFAULT_REPO); } - @Test - public void getBlurb_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportRepoConfiguration().getBlurb(), ReportRepoConfiguration.DEFAULT_BLURB); - } - @Test public void getBranches_equalsDefaultReturnValue_success() { Assertions.assertSame(new ReportRepoConfiguration().getBranches(), ReportRepoConfiguration.DEFAULT_BRANCHES); diff --git a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml index ff8994253d..934f50536a 100644 --- a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml +++ b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml @@ -1,7 +1,6 @@ title: RepoSense Report Test Repo repos: - repo: https://github.com/reposense/testrepo-Delta.git - blurb: "The main branch" groups: - group-name: code globs: @@ -16,6 +15,7 @@ repos: - "**.md" branches: - branch: master + blurb: "The main branch" authors: - author-git-host-id: fzdy1914 author-display-name: WANG CHAO From c020ec84cf9cffbd677991b19d100b40e15b1956 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Fri, 14 Feb 2025 23:44:27 +0800 Subject: [PATCH 13/42] Implement MVP of one stop config file and add file size limit --- config/report-config.yaml | 9 ---- src/main/java/reposense/RepoSense.java | 2 +- .../java/reposense/model/CliArguments.java | 53 ++++++++----------- .../model/OneStopConfigRunConfiguration.java | 29 ++++++---- .../model/RunConfigurationDecider.java | 8 +-- .../reportconfig/ReportAuthorDetails.java | 13 ++--- .../model/reportconfig/ReportBranchData.java | 27 +++++----- .../reportconfig/ReportConfiguration.java | 4 ++ .../reportconfig/ReportGroupNameAndGlobs.java | 8 +-- .../reportconfig/ReportRepoConfiguration.java | 10 ++-- .../java/reposense/parser/ArgsParser.java | 8 +-- .../parser/ReportConfigYamlParser.java | 24 ++++----- 12 files changed, 87 insertions(+), 108 deletions(-) diff --git a/config/report-config.yaml b/config/report-config.yaml index 968a172699..bb1f3321e6 100644 --- a/config/report-config.yaml +++ b/config/report-config.yaml @@ -17,17 +17,8 @@ repos: - branch: master blurb: "My project" authors: - - author-git-host-id: fzdy1914 - author-display-name: WANG CHAO - author-git-author-name: WANG CHAO - author-emails: - - 1229983126@qq.com - author-git-host-id: FH-30 author-display-name: Francis Hodianto author-git-author-name: Francis Hodianto author-emails: - - - ignore-glob-list: - - ignore-authors-list: - - diff --git a/src/main/java/reposense/RepoSense.java b/src/main/java/reposense/RepoSense.java index 80d410d3b5..8266f6c006 100644 --- a/src/main/java/reposense/RepoSense.java +++ b/src/main/java/reposense/RepoSense.java @@ -54,7 +54,7 @@ public static void main(String[] args) { configs = RunConfigurationDecider.getRunConfiguration(cliArguments).getRepoConfigurations(); reportConfig = cliArguments.getReportConfiguration(); - blurbMap = cliArguments.getBlurbMap(); + blurbMap = cliArguments.mergeWithRepoConfigBlurbMap(); RepoConfiguration.setFormatsToRepoConfigs(configs, cliArguments.getFormats()); RepoConfiguration.setDatesToRepoConfigs(configs, cliArguments.getSinceDate(), cliArguments.getUntilDate()); diff --git a/src/main/java/reposense/model/CliArguments.java b/src/main/java/reposense/model/CliArguments.java index 3e8e0eafd6..2a98c45658 100644 --- a/src/main/java/reposense/model/CliArguments.java +++ b/src/main/java/reposense/model/CliArguments.java @@ -5,6 +5,7 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.util.List; +import java.util.Map; import java.util.Objects; import reposense.model.reportconfig.ReportConfiguration; @@ -40,8 +41,7 @@ public class CliArguments { private double originalityThreshold; private boolean isTestMode = ArgsParser.DEFAULT_IS_TEST_MODE; private boolean isFreshClonePerformed = ArgsParser.DEFAULT_SHOULD_FRESH_CLONE; - private boolean isBlurbMapOverriding; - private boolean isOneStopConfigSpecified; + private boolean isOneStopConfigFilePresent; private List locations; private boolean isViewModeOnly; @@ -169,12 +169,21 @@ public BlurbMap getBlurbMap() { return blurbMap; } - public boolean isBlurbMapOverriding() { - return isBlurbMapOverriding; + /** + * Merges the {@code blurbMap} from the blurbs file with the blurb map in {@code reportConfiguration}. + * + * @return the merged blurb map. + */ + public BlurbMap mergeWithRepoConfigBlurbMap() { + BlurbMap repoConfigBlurbMap = reportConfiguration.getBlurbMap(); + for (Map.Entry entry : blurbMap.getAllMappings().entrySet()) { + repoConfigBlurbMap.withRecord(entry.getKey(), entry.getValue()); + } + return repoConfigBlurbMap; } - public boolean isOneStopConfigSpecified() { - return isOneStopConfigSpecified; + public boolean isOneStopConfigFilePresent() { + return isOneStopConfigFilePresent; } public boolean isViewModeOnly() { @@ -230,7 +239,7 @@ public boolean equals(Object other) { && Objects.equals(this.blurbMap, otherCliArguments.blurbMap) && this.isAuthorshipAnalyzed == otherCliArguments.isAuthorshipAnalyzed && Objects.equals(this.originalityThreshold, otherCliArguments.originalityThreshold) - && this.isBlurbMapOverriding == otherCliArguments.isBlurbMapOverriding; + && this.isOneStopConfigFilePresent == otherCliArguments.isOneStopConfigFilePresent; } /** @@ -515,37 +524,17 @@ public Builder blurbMap(BlurbMap blurbMap) { return this; } - /** - * Mark the {@code blurbMap} as overriding. - */ - public Builder markAsBlurbMapOverriding() { - this.cliArguments.isBlurbMapOverriding = true; - return this; - } /** - * Unmark the {@code blurbMap} as overriding. - */ - public Builder unmarkAsBlurbMapOverriding() { - this.cliArguments.isBlurbMapOverriding = false; - return this; - } - - /** - * Mark the {@code oneStopConfig} as specified. + * Adds the {@code isOneStopConfigFilePresent} to CliArguments. + * + * @param isOneStopConfigFilePresent Is blurb map overriding. */ - public Builder markAsOneStopConfigSpecified() { - this.cliArguments.isOneStopConfigSpecified = true; + public Builder isOneStopConfigFilePresent(boolean isOneStopConfigFilePresent) { + this.cliArguments.isOneStopConfigFilePresent = isOneStopConfigFilePresent; return this; } - /** - * Unmark the {@code oneStopConfig} as specified. - */ - public Builder unmarkAsOneStopConfigSpecified() { - this.cliArguments.isOneStopConfigSpecified = false; - return this; - } /** * Builds CliArguments. diff --git a/src/main/java/reposense/model/OneStopConfigRunConfiguration.java b/src/main/java/reposense/model/OneStopConfigRunConfiguration.java index 5e61ca64a2..7a42555383 100644 --- a/src/main/java/reposense/model/OneStopConfigRunConfiguration.java +++ b/src/main/java/reposense/model/OneStopConfigRunConfiguration.java @@ -43,27 +43,36 @@ public List getRepoConfigurations() throws InvalidLocationExc // iterate for each branch and for each author for (ReportBranchData rbd : rrc.getBranches()) { logger.info("Parsing " + rbd.getBranch() + "..."); + // create the repoconfiguration object + // we will need to add the group configuration details to the repo configuration builder + RepoConfiguration.Builder builder = new RepoConfiguration.Builder() + .location(repoLocation); + + // set the relevant branch details for the repo + builder = builder.branch(rbd.getBranch()) + .ignoreGlobList(rbd.getIgnoreGlobList()) + .ignoredAuthorsList(rbd.getIgnoreAuthorList()) + .fileSizeLimit(rbd.getFileSizeLimit()) + .isStandaloneConfigIgnored(true); // remove this when we deprecated the standalone config + + + AuthorConfiguration authorConfiguration = new AuthorConfiguration(repoLocation, rbd.getBranch()); for (ReportAuthorDetails rad : rbd.getReportAuthorDetails()) { logger.info("Parsing " + rad.getAuthorGitHostId() + "..."); - // create the repoconfiguration object - // we will need to add the group configuration details to the repo configuration builder - RepoConfiguration.Builder builder = new RepoConfiguration.Builder() - .location(repoLocation); - // set the relevant branch details for the repo - builder = builder.branch(rbd.getBranch()) - .ignoreGlobList(rbd.getIgnoreGlobList()) - .ignoredAuthorsList(rbd.getIgnoreAuthorList()); // prepare the author details Author author = new Author(rad.getAuthorGitHostId()); author.setEmails(rad.getAuthorEmails()); author.setDisplayName(rad.getAuthorDisplayName()); author.setAuthorAliases(List.of(rad.getAuthorGitAuthorName())); - authorConfigs.add(new AuthorConfiguration(repoLocation, rbd.getBranch())); - repoConfigs.add(builder.build()); + authorConfiguration.addAuthor(author); + + } + authorConfigs.add(authorConfiguration); + repoConfigs.add(builder.build()); } } diff --git a/src/main/java/reposense/model/RunConfigurationDecider.java b/src/main/java/reposense/model/RunConfigurationDecider.java index 9fd6cb2c20..cda6e8ccf4 100644 --- a/src/main/java/reposense/model/RunConfigurationDecider.java +++ b/src/main/java/reposense/model/RunConfigurationDecider.java @@ -6,13 +6,13 @@ public class RunConfigurationDecider { public static RunConfiguration getRunConfiguration(CliArguments cliArguments) { if (cliArguments.getLocations() != null) { - if (cliArguments.isOneStopConfigSpecified()) { - return new OneStopConfigRunConfiguration(cliArguments); - } - return new CliRunConfiguration(cliArguments); } + if (cliArguments.isOneStopConfigFilePresent()) { + return new OneStopConfigRunConfiguration(cliArguments); + } + return new ConfigRunConfiguration(cliArguments); } } diff --git a/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java b/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java index af5d41662f..5205bb91e8 100644 --- a/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java +++ b/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java @@ -11,16 +11,16 @@ public class ReportAuthorDetails { public static final List DEFAULT_INSTANCES = new ArrayList<>(); - public static final List DEFAULT_AUTHOR_EMAIL = List.of("john@john.com", "johny@mail.com", "j@domain.com"); - public static final String DEFAULT_GIT_HOST_ID = "johnDoe"; - public static final String DEFAULT_DISPLAY_NAME = "John Doe"; - public static final String DEFAULT_GIT_AUTHOR_NAME = "my home PC"; + public static final List DEFAULT_AUTHOR_EMAIL = List.of(); + public static final String DEFAULT_GIT_HOST_ID = ""; + public static final String DEFAULT_DISPLAY_NAME = ""; + public static final String DEFAULT_GIT_AUTHOR_NAME = ""; private static final List DEFAULT_AUTHOR_EMAIL_1 = List.of("1229983126@qq.com"); private static final String DEFAULT_GIT_HOST_ID_1 = "fzdy1914"; private static final String DEFAULT_DISPLAY_NAME_1 = "WANG CHAO"; private static final String DEFAULT_GIT_AUTHOR_NAME_1 = "WANG CHAO"; - private static final List DEFAULT_AUTHOR_EMAIL_2 = List.of(""); + private static final List DEFAULT_AUTHOR_EMAIL_2 = List.of("123@gmail.com"); private static final String DEFAULT_GIT_HOST_ID_2 = "FH-30"; private static final String DEFAULT_DISPLAY_NAME_2 = "Francis Hodianto"; private static final String DEFAULT_GIT_AUTHOR_NAME_2 = "Francis Hodianto"; @@ -38,9 +38,6 @@ public class ReportAuthorDetails { rad2.authorGitHostId = DEFAULT_GIT_HOST_ID_2; rad2.authorGitAuthorName = DEFAULT_GIT_AUTHOR_NAME_2; rad2.authorDisplayName = DEFAULT_DISPLAY_NAME_2; - - DEFAULT_INSTANCES.add(rad1); - DEFAULT_INSTANCES.add(rad2); } @JsonProperty("author-emails") diff --git a/src/main/java/reposense/model/reportconfig/ReportBranchData.java b/src/main/java/reposense/model/reportconfig/ReportBranchData.java index f1027640f0..77dd07823d 100644 --- a/src/main/java/reposense/model/reportconfig/ReportBranchData.java +++ b/src/main/java/reposense/model/reportconfig/ReportBranchData.java @@ -10,17 +10,11 @@ */ public class ReportBranchData { public static final String DEFAULT_BRANCH = "main"; - public static final String DEFAULT_BLURB = "My project"; - public static final List DEFAULT_REPORT_AUTHOR_DETAILS = new ArrayList<>( - ReportAuthorDetails.DEFAULT_INSTANCES - ); - public static final List DEFAULT_IGNORE_GLOB_LIST = List.of( - "docs**" - ); - public static final List DEFAULT_IGNORE_AUTHORS_LIST = List.of( - "author1", - "author2" - ); + public static final String DEFAULT_BLURB = ""; + public static final List DEFAULT_REPORT_AUTHOR_DETAILS = new ArrayList<>(); + public static final List DEFAULT_IGNORE_GLOB_LIST = List.of(); + public static final List DEFAULT_IGNORE_AUTHORS_LIST = List.of(); + public static final Long DEFAULT_FILE_SIZE_LIMIT = 1000000L; public static final ReportBranchData DEFAULT_INSTANCE = new ReportBranchData(); @@ -30,6 +24,7 @@ public class ReportBranchData { DEFAULT_INSTANCE.ignoreGlobList = ReportBranchData.DEFAULT_IGNORE_GLOB_LIST; DEFAULT_INSTANCE.ignoreAuthorList = ReportBranchData.DEFAULT_IGNORE_AUTHORS_LIST; DEFAULT_INSTANCE.reportAuthorDetails = ReportAuthorDetails.DEFAULT_INSTANCES; + DEFAULT_INSTANCE.fileSizeLimit = DEFAULT_FILE_SIZE_LIMIT; } @JsonProperty("branch") @@ -47,6 +42,9 @@ public class ReportBranchData { @JsonProperty("ignore-authors-list") private List ignoreAuthorList; + @JsonProperty("file-size-limit") + private Long fileSizeLimit; + public String getBranch() { return branch == null ? DEFAULT_BRANCH : branch; } @@ -67,6 +65,10 @@ public List getIgnoreAuthorList() { return ignoreAuthorList == null ? DEFAULT_IGNORE_AUTHORS_LIST : ignoreAuthorList; } + public Long getFileSizeLimit() { + return fileSizeLimit == null ? DEFAULT_FILE_SIZE_LIMIT : fileSizeLimit; + } + @Override public boolean equals(Object obj) { if (obj == this) { @@ -79,7 +81,8 @@ public boolean equals(Object obj) { && this.getBlurb().equals(rbd.getBlurb()) && this.getReportAuthorDetails().equals(rbd.getReportAuthorDetails()) && this.getIgnoreGlobList().equals(rbd.getIgnoreGlobList()) - && this.getIgnoreAuthorList().equals(rbd.getIgnoreAuthorList()); + && this.getIgnoreAuthorList().equals(rbd.getIgnoreAuthorList()) + && this.getFileSizeLimit().equals(rbd.getFileSizeLimit()); } return false; diff --git a/src/main/java/reposense/model/reportconfig/ReportConfiguration.java b/src/main/java/reposense/model/reportconfig/ReportConfiguration.java index 8315b25b16..d499582d9a 100644 --- a/src/main/java/reposense/model/reportconfig/ReportConfiguration.java +++ b/src/main/java/reposense/model/reportconfig/ReportConfiguration.java @@ -41,6 +41,10 @@ public class ReportConfiguration { public BlurbMap getBlurbMap() { BlurbMap blurbMap = new BlurbMap(); + if (reportRepoConfigurations == null) { + return blurbMap; + } + for (ReportRepoConfiguration repoConfig : reportRepoConfigurations) { try { for (ReportRepoConfiguration.MapEntry repoNameBlurbPair diff --git a/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java b/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java index ba78c61c6c..97fc496f21 100644 --- a/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java +++ b/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java @@ -12,9 +12,7 @@ */ public class ReportGroupNameAndGlobs { public static final String DEFAULT_GROUP_NAME = "code"; - public static final List DEFAULT_GLOBS = List.of( - "**.java" - ); + public static final List DEFAULT_GLOBS = List.of(); public static final List DEFAULT_INSTANCES = new ArrayList<>(); static { @@ -29,10 +27,6 @@ public class ReportGroupNameAndGlobs { ReportGroupNameAndGlobs rg3 = new ReportGroupNameAndGlobs(); rg3.groupName = "docs"; rg3.globs = List.of("docs**", "**.adoc", "**.md"); - - DEFAULT_INSTANCES.add(rg1); - DEFAULT_INSTANCES.add(rg2); - DEFAULT_INSTANCES.add(rg3); } @JsonProperty("group-name") diff --git a/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java index 5c24b59b54..bd973aebe1 100644 --- a/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java +++ b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java @@ -16,10 +16,8 @@ */ public class ReportRepoConfiguration { public static final String DEFAULT_REPO = "https://github.com/user/repo"; - public static final List DEFAULT_GROUP_DETAILS = ReportGroupNameAndGlobs.DEFAULT_INSTANCES; - public static final List DEFAULT_BRANCHES = List.of( - ReportBranchData.DEFAULT_INSTANCE - ); + public static final List DEFAULT_GROUP_DETAILS = List.of(); + public static final List DEFAULT_BRANCHES = List.of(); public static final ReportRepoConfiguration DEFAULT_INSTANCE = new ReportRepoConfiguration(); static { @@ -71,7 +69,7 @@ public String getRepo() { */ public List getFullyQualifiedRepoNamesWithBlurbs() { List repoNames = new ArrayList<>(); - Pattern pattern = Pattern.compile(".git"); + Pattern pattern = Pattern.compile("\\.git$"); Matcher matcher = pattern.matcher(getRepo()); if (!matcher.find()) { @@ -81,7 +79,7 @@ public List getFullyQualifiedRepoNamesWithBlurbs() { } for (ReportBranchData rbd : this.getBranches()) { - String qualifiedName = getRepo().substring(0, matcher.start()) + "/" + rbd.getBranch(); + String qualifiedName = getRepo().substring(0, matcher.start()) + "/tree/" + rbd.getBranch(); repoNames.add(new MapEntry(qualifiedName, rbd.getBlurb())); } diff --git a/src/main/java/reposense/parser/ArgsParser.java b/src/main/java/reposense/parser/ArgsParser.java index 5ef3ae7e82..c8b447b966 100644 --- a/src/main/java/reposense/parser/ArgsParser.java +++ b/src/main/java/reposense/parser/ArgsParser.java @@ -9,6 +9,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.logging.Level; import java.util.logging.Logger; import com.google.gson.JsonSyntaxException; @@ -390,7 +391,7 @@ private static void addReportConfigToBuilder(CliArguments.Builder builder, Names try { reportConfig = new ReportConfigYamlParser().parse(reportConfigFilePath); - builder = builder.markAsOneStopConfigSpecified(); + builder.isOneStopConfigFilePresent(true); } catch (JsonSyntaxException jse) { logger.warning(String.format(MESSAGE_INVALID_CONFIG_PATH, reportConfigFilePath)); } catch (IllegalArgumentException iae) { @@ -398,7 +399,8 @@ private static void addReportConfigToBuilder(CliArguments.Builder builder, Names } catch (IOException ioe) { // IOException thrown as report-config.yaml is not found. // Ignore exception as the file is optional. - builder = builder.unmarkAsOneStopConfigSpecified(); + logger.log(Level.WARNING, "Error parsing report-config.yaml: " + ioe.getMessage(), ioe); + builder.isOneStopConfigFilePresent(false); } } @@ -420,12 +422,10 @@ private static void addBlurbMapToBuilder(CliArguments.Builder builder, Namespace try { blurbMap = new BlurbMarkdownParser(blurbConfigPath).parse(); - builder = builder.markAsBlurbMapOverriding(); } catch (InvalidMarkdownException ex) { logger.warning(String.format(MESSAGE_INVALID_MARKDOWN_BLURBS, ex.getMessage())); } catch (IOException ioe) { // IOException thrown as blurbs.md is not found. - builder = builder.unmarkAsBlurbMapOverriding(); } builder.blurbMap(blurbMap); diff --git a/src/main/java/reposense/parser/ReportConfigYamlParser.java b/src/main/java/reposense/parser/ReportConfigYamlParser.java index 1cdceaa13a..85f0268b40 100644 --- a/src/main/java/reposense/parser/ReportConfigYamlParser.java +++ b/src/main/java/reposense/parser/ReportConfigYamlParser.java @@ -32,20 +32,14 @@ public Type getType() { @Override public ReportConfiguration parse(Path path) throws IOException { // adapted from https://www.baeldung.com/jackson-yaml - ReportConfiguration reportConfigation; - - try { - logger.log(Level.INFO, "Parsing report-config.yaml file..."); - ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - mapper.findAndRegisterModules(); - reportConfigation = mapper.readValue(new File(path.toString()), ReportConfiguration.class); - logger.log(Level.INFO, "report-config.yaml file parsed successfully!"); - } catch (IOException ioe) { - // if the parse fails for any reason, the default config file is used instead - logger.log(Level.WARNING, "Error parsing report-config.yaml: " + ioe.getMessage(), ioe); - reportConfigation = new ReportConfiguration(); - } - - return reportConfigation; + ReportConfiguration reportConfiguration; + + logger.log(Level.INFO, "Parsing report-config.yaml file..."); + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + mapper.findAndRegisterModules(); + reportConfiguration = mapper.readValue(new File(path.toString()), ReportConfiguration.class); + logger.log(Level.INFO, "report-config.yaml file parsed successfully!"); + + return reportConfiguration; } } From 978f36750a7267f7c0680350a90f8bec8a69825a Mon Sep 17 00:00:00 2001 From: LiYuan Date: Mon, 17 Feb 2025 02:32:07 +0800 Subject: [PATCH 14/42] Refactor classes to remove default instances --- config/report-config.yaml | 4 +- .../reportconfig/ReportAuthorDetails.java | 56 ++++++------------- .../model/reportconfig/ReportBranchData.java | 54 +++++++++--------- .../reportconfig/ReportConfiguration.java | 29 +++++----- .../reportconfig/ReportGroupNameAndGlobs.java | 22 +------- .../reportconfig/ReportRepoConfiguration.java | 33 +++++------ .../reportconfig/ReportAuthorDetailsTest.java | 29 +--------- .../reportconfig/ReportBranchDataTest.java | 10 ++-- .../reportconfig/ReportConfigurationTest.java | 8 ++- .../ReportGroupNameAndGlobsTest.java | 6 +- .../ReportRepoConfigurationTest.java | 7 ++- .../parser/ReportConfigYamlParserTest.java | 19 ++++--- .../report-config-invalid.yaml | 27 ++++----- .../report-config-valid.yaml | 13 +---- 14 files changed, 117 insertions(+), 200 deletions(-) diff --git a/config/report-config.yaml b/config/report-config.yaml index bb1f3321e6..c440ecba8f 100644 --- a/config/report-config.yaml +++ b/config/report-config.yaml @@ -1,4 +1,3 @@ -title: RepoSense Report repos: - repo: https://github.com/reposense/testrepo-Delta.git groups: @@ -18,7 +17,8 @@ repos: blurb: "My project" authors: - author-git-host-id: FH-30 - author-display-name: Francis Hodianto + author-display-name: Francis Hodianto (FH-30) author-git-author-name: Francis Hodianto author-emails: ignore-authors-list: + - jordancjq \ No newline at end of file diff --git a/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java b/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java index 5205bb91e8..175f574ca6 100644 --- a/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java +++ b/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java @@ -3,73 +3,48 @@ import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; /** * Represents an author's details in the report-config.yaml file. */ public class ReportAuthorDetails { - public static final List DEFAULT_INSTANCES = new ArrayList<>(); - - public static final List DEFAULT_AUTHOR_EMAIL = List.of(); - public static final String DEFAULT_GIT_HOST_ID = ""; - public static final String DEFAULT_DISPLAY_NAME = ""; - public static final String DEFAULT_GIT_AUTHOR_NAME = ""; - - private static final List DEFAULT_AUTHOR_EMAIL_1 = List.of("1229983126@qq.com"); - private static final String DEFAULT_GIT_HOST_ID_1 = "fzdy1914"; - private static final String DEFAULT_DISPLAY_NAME_1 = "WANG CHAO"; - private static final String DEFAULT_GIT_AUTHOR_NAME_1 = "WANG CHAO"; - private static final List DEFAULT_AUTHOR_EMAIL_2 = List.of("123@gmail.com"); - private static final String DEFAULT_GIT_HOST_ID_2 = "FH-30"; - private static final String DEFAULT_DISPLAY_NAME_2 = "Francis Hodianto"; - private static final String DEFAULT_GIT_AUTHOR_NAME_2 = "Francis Hodianto"; - - static { - ReportAuthorDetails rad1 = new ReportAuthorDetails(); - ReportAuthorDetails rad2 = new ReportAuthorDetails(); - - rad1.authorEmails = DEFAULT_AUTHOR_EMAIL_1; - rad1.authorGitHostId = DEFAULT_GIT_HOST_ID_1; - rad1.authorGitAuthorName = DEFAULT_GIT_AUTHOR_NAME_1; - rad1.authorDisplayName = DEFAULT_DISPLAY_NAME_1; - - rad2.authorEmails = DEFAULT_AUTHOR_EMAIL_2; - rad2.authorGitHostId = DEFAULT_GIT_HOST_ID_2; - rad2.authorGitAuthorName = DEFAULT_GIT_AUTHOR_NAME_2; - rad2.authorDisplayName = DEFAULT_DISPLAY_NAME_2; - } - @JsonProperty("author-emails") private List authorEmails; - @JsonProperty("author-git-host-id") private String authorGitHostId; - @JsonProperty("author-display-name") private String authorDisplayName; - @JsonProperty("author-git-author-name") private String authorGitAuthorName; - public ReportAuthorDetails() { - + @JsonCreator + public ReportAuthorDetails( + @JsonProperty("author-emails") List authorEmails, + @JsonProperty("author-git-host-id") String authorGitHostId, + @JsonProperty("author-display-name") String authorDisplayName, + @JsonProperty("author-git-author-name") String authorGitAuthorName) { + this.authorEmails = authorEmails == null ? new ArrayList<>() : authorEmails; + this.authorGitHostId = authorGitHostId == null ? "" : authorGitHostId; + this.authorDisplayName = authorDisplayName == null ? "" : authorDisplayName; + this.authorGitAuthorName = authorGitAuthorName == null ? "" : authorGitAuthorName; } public List getAuthorEmails() { - return authorEmails == null ? DEFAULT_AUTHOR_EMAIL : authorEmails; + return authorEmails; } public String getAuthorGitHostId() { - return authorGitHostId == null ? DEFAULT_GIT_HOST_ID : authorGitHostId; + return authorGitHostId; } public String getAuthorDisplayName() { - return authorDisplayName == null ? DEFAULT_DISPLAY_NAME : authorDisplayName; + return authorDisplayName; } public String getAuthorGitAuthorName() { - return authorGitAuthorName == null ? DEFAULT_GIT_AUTHOR_NAME : authorGitAuthorName; + return authorGitAuthorName; } @Override @@ -89,3 +64,4 @@ public boolean equals(Object obj) { return false; } } + diff --git a/src/main/java/reposense/model/reportconfig/ReportBranchData.java b/src/main/java/reposense/model/reportconfig/ReportBranchData.java index 77dd07823d..9ff14e3020 100644 --- a/src/main/java/reposense/model/reportconfig/ReportBranchData.java +++ b/src/main/java/reposense/model/reportconfig/ReportBranchData.java @@ -3,70 +3,68 @@ import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; /** * Represents a single entry of a branch in the YAML config file. */ public class ReportBranchData { - public static final String DEFAULT_BRANCH = "main"; - public static final String DEFAULT_BLURB = ""; - public static final List DEFAULT_REPORT_AUTHOR_DETAILS = new ArrayList<>(); - public static final List DEFAULT_IGNORE_GLOB_LIST = List.of(); - public static final List DEFAULT_IGNORE_AUTHORS_LIST = List.of(); + public static final String DEFAULT_BRANCH = "HEAD"; public static final Long DEFAULT_FILE_SIZE_LIMIT = 1000000L; - public static final ReportBranchData DEFAULT_INSTANCE = new ReportBranchData(); - - static { - DEFAULT_INSTANCE.branch = ReportBranchData.DEFAULT_BRANCH; - DEFAULT_INSTANCE.blurb = DEFAULT_BLURB; - DEFAULT_INSTANCE.ignoreGlobList = ReportBranchData.DEFAULT_IGNORE_GLOB_LIST; - DEFAULT_INSTANCE.ignoreAuthorList = ReportBranchData.DEFAULT_IGNORE_AUTHORS_LIST; - DEFAULT_INSTANCE.reportAuthorDetails = ReportAuthorDetails.DEFAULT_INSTANCES; - DEFAULT_INSTANCE.fileSizeLimit = DEFAULT_FILE_SIZE_LIMIT; - } - - @JsonProperty("branch") private String branch; - @JsonProperty("blurb") private String blurb; - @JsonProperty("authors") private List reportAuthorDetails; - @JsonProperty("ignore-glob-list") private List ignoreGlobList; - @JsonProperty("ignore-authors-list") private List ignoreAuthorList; - @JsonProperty("file-size-limit") private Long fileSizeLimit; + public ReportBranchData() {} + + @JsonCreator + public ReportBranchData( + @JsonProperty("branch") String branch, + @JsonProperty("blurb") String blurb, + @JsonProperty("authors") List reportAuthorDetails, + @JsonProperty("ignore-glob-list") List ignoreGlobList, + @JsonProperty("ignore-authors-list") List ignoreAuthorList, + @JsonProperty("file-size-limit") Long fileSizeLimit) { + this.branch = branch == null ? DEFAULT_BRANCH : branch; + this.blurb = blurb == null ? "" : blurb; + this.reportAuthorDetails = reportAuthorDetails == null ? new ArrayList<>() : reportAuthorDetails; + this.ignoreGlobList = ignoreGlobList == null ? new ArrayList<>() : ignoreGlobList; + this.ignoreAuthorList = ignoreAuthorList == null ? new ArrayList<>() : ignoreAuthorList; + this.fileSizeLimit = fileSizeLimit == null ? DEFAULT_FILE_SIZE_LIMIT : fileSizeLimit; + } + public String getBranch() { - return branch == null ? DEFAULT_BRANCH : branch; + return branch; } public String getBlurb() { - return blurb == null ? DEFAULT_BLURB : blurb; + return blurb; } public List getReportAuthorDetails() { - return reportAuthorDetails == null ? DEFAULT_REPORT_AUTHOR_DETAILS : reportAuthorDetails; + return reportAuthorDetails; } public List getIgnoreGlobList() { - return ignoreGlobList == null ? DEFAULT_IGNORE_GLOB_LIST : ignoreGlobList; + return ignoreGlobList; } public List getIgnoreAuthorList() { - return ignoreAuthorList == null ? DEFAULT_IGNORE_AUTHORS_LIST : ignoreAuthorList; + return ignoreAuthorList; } public Long getFileSizeLimit() { - return fileSizeLimit == null ? DEFAULT_FILE_SIZE_LIMIT : fileSizeLimit; + return fileSizeLimit; } @Override diff --git a/src/main/java/reposense/model/reportconfig/ReportConfiguration.java b/src/main/java/reposense/model/reportconfig/ReportConfiguration.java index d499582d9a..be760c78db 100644 --- a/src/main/java/reposense/model/reportconfig/ReportConfiguration.java +++ b/src/main/java/reposense/model/reportconfig/ReportConfiguration.java @@ -5,6 +5,7 @@ import java.util.logging.Level; import java.util.logging.Logger; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import reposense.model.BlurbMap; @@ -16,23 +17,23 @@ */ public class ReportConfiguration { public static final String DEFAULT_TITLE = "RepoSense Report"; - public static final List DEFAULT_REPORT_REPO_CONFIGS = new ArrayList<>(); - public static final ReportConfiguration DEFAULT_INSTANCE = new ReportConfiguration(); private static final Logger logger = LogsManager.getLogger(ReportConfiguration.class); - static { - DEFAULT_REPORT_REPO_CONFIGS.add(ReportRepoConfiguration.DEFAULT_INSTANCE); - ReportConfiguration.DEFAULT_INSTANCE.title = DEFAULT_TITLE; - ReportConfiguration.DEFAULT_INSTANCE.reportRepoConfigurations = DEFAULT_REPORT_REPO_CONFIGS; - } - - @JsonProperty("title") private String title; - @JsonProperty("repos") private List reportRepoConfigurations; + public ReportConfiguration() {} + + @JsonCreator + public ReportConfiguration( + @JsonProperty("title") String title, + @JsonProperty("repos") List reportRepoConfigurations) { + this.title = title == null ? DEFAULT_TITLE : title; + this.reportRepoConfigurations = reportRepoConfigurations == null ? new ArrayList<>() : reportRepoConfigurations; + } + /** * Converts the {@code ReportRepoConfiguration} list into a {@code BlurbMap}. * @@ -41,10 +42,6 @@ public class ReportConfiguration { public BlurbMap getBlurbMap() { BlurbMap blurbMap = new BlurbMap(); - if (reportRepoConfigurations == null) { - return blurbMap; - } - for (ReportRepoConfiguration repoConfig : reportRepoConfigurations) { try { for (ReportRepoConfiguration.MapEntry repoNameBlurbPair @@ -60,11 +57,11 @@ public BlurbMap getBlurbMap() { } public String getTitle() { - return title == null ? DEFAULT_TITLE : title; + return title; } public List getReportRepoConfigurations() { - return reportRepoConfigurations == null ? DEFAULT_REPORT_REPO_CONFIGS : reportRepoConfigurations; + return reportRepoConfigurations; } @Override diff --git a/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java b/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java index 97fc496f21..fb53df595a 100644 --- a/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java +++ b/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java @@ -1,6 +1,5 @@ package reposense.model.reportconfig; -import java.util.ArrayList; import java.util.List; import com.fasterxml.jackson.annotation.JsonProperty; @@ -11,23 +10,6 @@ * Contains details about each report group and the corresponding globs. */ public class ReportGroupNameAndGlobs { - public static final String DEFAULT_GROUP_NAME = "code"; - public static final List DEFAULT_GLOBS = List.of(); - public static final List DEFAULT_INSTANCES = new ArrayList<>(); - - static { - ReportGroupNameAndGlobs rg1 = new ReportGroupNameAndGlobs(); - rg1.groupName = "code"; - rg1.globs = List.of("**.java"); - - ReportGroupNameAndGlobs rg2 = new ReportGroupNameAndGlobs(); - rg2.groupName = "tests"; - rg2.globs = List.of("src/test**"); - - ReportGroupNameAndGlobs rg3 = new ReportGroupNameAndGlobs(); - rg3.groupName = "docs"; - rg3.globs = List.of("docs**", "**.adoc", "**.md"); - } @JsonProperty("group-name") private String groupName; @@ -36,11 +18,11 @@ public class ReportGroupNameAndGlobs { private List globs; public String getGroupName() { - return groupName == null ? DEFAULT_GROUP_NAME : groupName; + return groupName; } public List getGlobs() { - return globs == null ? DEFAULT_GLOBS : globs; + return globs; } /** diff --git a/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java index bd973aebe1..a8154db422 100644 --- a/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java +++ b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java @@ -5,6 +5,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import reposense.model.GroupConfiguration; @@ -15,26 +16,26 @@ * configuration. */ public class ReportRepoConfiguration { - public static final String DEFAULT_REPO = "https://github.com/user/repo"; - public static final List DEFAULT_GROUP_DETAILS = List.of(); - public static final List DEFAULT_BRANCHES = List.of(); - public static final ReportRepoConfiguration DEFAULT_INSTANCE = new ReportRepoConfiguration(); - - static { - DEFAULT_INSTANCE.repo = DEFAULT_REPO; - DEFAULT_INSTANCE.groups = DEFAULT_GROUP_DETAILS; - DEFAULT_INSTANCE.branches = DEFAULT_BRANCHES; - } - @JsonProperty("repo") private String repo; - @JsonProperty("groups") private List groups; - @JsonProperty("branches") private List branches; + public ReportRepoConfiguration() {} + + @JsonCreator + public ReportRepoConfiguration( + @JsonProperty("repo") String repo, + @JsonProperty("groups") List groups, + @JsonProperty("branches") List branches) { + this.repo = repo == null ? "" : repo; + this.groups = groups == null ? new ArrayList<>() : groups; + this.branches = branches == null ? new ArrayList<>() : branches; + } + + /** * Represents a single mapped entry between a fully qualified repository name and its associated blurb. */ @@ -57,7 +58,7 @@ public String getValue() { } public String getRepo() { - return repo == null ? DEFAULT_REPO : repo; + return repo; } /** @@ -87,7 +88,7 @@ public List getFullyQualifiedRepoNamesWithBlurbs() { } public List getGroupDetails() { - return groups == null ? DEFAULT_GROUP_DETAILS : groups; + return groups; } /** @@ -106,7 +107,7 @@ public GroupConfiguration getGroupConfiguration(RepoLocation location) { } public List getBranches() { - return branches == null ? DEFAULT_BRANCHES : branches; + return branches; } @Override diff --git a/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java b/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java index 1e066fbde1..03aba7d6ae 100644 --- a/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java @@ -1,30 +1,3 @@ package reposense.model.reportconfig; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class ReportAuthorDetailsTest { - @Test - public void getAuthorEmails_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportAuthorDetails().getAuthorEmails(), - ReportAuthorDetails.DEFAULT_AUTHOR_EMAIL); - } - - @Test - public void getAuthorGitHostId_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportAuthorDetails().getAuthorGitHostId(), - ReportAuthorDetails.DEFAULT_GIT_HOST_ID); - } - - @Test - public void getAuthorDisplayName_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportAuthorDetails().getAuthorDisplayName(), - ReportAuthorDetails.DEFAULT_DISPLAY_NAME); - } - - @Test - public void getAuthorGitAuthorName_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportAuthorDetails().getAuthorGitAuthorName(), - ReportAuthorDetails.DEFAULT_GIT_AUTHOR_NAME); - } -} +public class ReportAuthorDetailsTest { } diff --git a/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java index 72da4d190e..e4789bdbac 100644 --- a/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java @@ -1,24 +1,26 @@ package reposense.model.reportconfig; +import java.util.ArrayList; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class ReportBranchDataTest { @Test public void getBranch_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportBranchData().getBranch(), ReportBranchData.DEFAULT_BRANCH); + Assertions.assertSame(ReportBranchData.DEFAULT_BRANCH, new ReportBranchData().getBranch()); } @Test public void getIgnoreGlobList_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportBranchData().getIgnoreGlobList(), ReportBranchData.DEFAULT_IGNORE_GLOB_LIST); + Assertions.assertSame(new ArrayList<>(), new ReportBranchData().getIgnoreGlobList()); } @Test public void getIgnoreAuthorList_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportBranchData().getIgnoreAuthorList(), - ReportBranchData.DEFAULT_IGNORE_AUTHORS_LIST); + Assertions.assertSame(new ArrayList<>(), + new ReportBranchData().getIgnoreAuthorList()); } @Test diff --git a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java index 935b0872ff..d2b0bb2c4e 100644 --- a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java @@ -1,18 +1,20 @@ package reposense.model.reportconfig; +import java.util.ArrayList; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class ReportConfigurationTest { @Test public void getTitle_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportConfiguration().getTitle(), ReportConfiguration.DEFAULT_TITLE); + Assertions.assertSame(ReportConfiguration.DEFAULT_TITLE, new ReportConfiguration().getTitle()); } @Test public void getReportRepoConfigurations_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportConfiguration().getReportRepoConfigurations(), - ReportConfiguration.DEFAULT_REPORT_REPO_CONFIGS); + Assertions.assertSame(new ArrayList<>(), + new ReportConfiguration().getReportRepoConfigurations()); } @Test diff --git a/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java b/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java index e44028c85e..d203a7a2f2 100644 --- a/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java @@ -6,13 +6,13 @@ public class ReportGroupNameAndGlobsTest { @Test public void getRepo_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportGroupNameAndGlobs().getGroupName(), ReportGroupNameAndGlobs.DEFAULT_GROUP_NAME); + Assertions.assertSame(null, new ReportGroupNameAndGlobs().getGroupName()); } @Test public void getReportGroupNameAndGlobsList_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportGroupNameAndGlobs().getGlobs(), - ReportGroupNameAndGlobs.DEFAULT_GLOBS); + Assertions.assertSame(null, + new ReportGroupNameAndGlobs().getGlobs()); } @Test diff --git a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java index 0c5b589e3c..0b5bd348b8 100644 --- a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java @@ -1,22 +1,23 @@ package reposense.model.reportconfig; +import java.util.ArrayList; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class ReportRepoConfigurationTest { @Test public void getRepo_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportRepoConfiguration().getRepo(), ReportRepoConfiguration.DEFAULT_REPO); + Assertions.assertSame("", new ReportRepoConfiguration().getRepo()); } @Test public void getBranches_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ReportRepoConfiguration().getBranches(), ReportRepoConfiguration.DEFAULT_BRANCHES); + Assertions.assertSame(new ArrayList<>(), new ReportRepoConfiguration().getBranches()); } @Test public void equals_defaultInstancesAreEqual_success() { Assertions.assertEquals(new ReportRepoConfiguration(), new ReportRepoConfiguration()); - Assertions.assertEquals(new ReportRepoConfiguration(), ReportRepoConfiguration.DEFAULT_INSTANCE); } } diff --git a/src/test/java/reposense/parser/ReportConfigYamlParserTest.java b/src/test/java/reposense/parser/ReportConfigYamlParserTest.java index 79e440b09e..03d10e4139 100644 --- a/src/test/java/reposense/parser/ReportConfigYamlParserTest.java +++ b/src/test/java/reposense/parser/ReportConfigYamlParserTest.java @@ -3,6 +3,7 @@ import static reposense.util.TestUtil.loadResource; import java.nio.file.Path; +import java.util.ArrayList; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -21,24 +22,24 @@ public class ReportConfigYamlParserTest { @Test public void reportConfig_parseEmptyYamlFile_getDefaultTitle() throws Exception { ReportConfiguration reportConfig = new ReportConfigYamlParser().parse(EMPTY_REPORT_CONFIG); - Assertions.assertEquals(reportConfig.getTitle(), ReportConfiguration.DEFAULT_TITLE); - Assertions.assertEquals(reportConfig.getReportRepoConfigurations(), - ReportConfiguration.DEFAULT_REPORT_REPO_CONFIGS); + Assertions.assertEquals(ReportConfiguration.DEFAULT_TITLE, reportConfig.getTitle()); + Assertions.assertEquals(new ArrayList<>(), + reportConfig.getReportRepoConfigurations()); } @Test public void reportConfig_parseInvalidYamlFile_getDefaultTitle() throws Exception { ReportConfiguration reportConfig = new ReportConfigYamlParser().parse(INVALID_REPORT_CONFIG); - Assertions.assertEquals(reportConfig.getTitle(), ReportConfiguration.DEFAULT_TITLE); - Assertions.assertEquals(reportConfig.getReportRepoConfigurations(), - ReportConfiguration.DEFAULT_REPORT_REPO_CONFIGS); + Assertions.assertEquals(ReportConfiguration.DEFAULT_TITLE, reportConfig.getTitle()); + Assertions.assertEquals(new ArrayList<>(), + reportConfig.getReportRepoConfigurations()); } @Test public void reportConfig_parseValidYamlFile_getCustomTitle() throws Exception { ReportConfiguration reportConfig = new ReportConfigYamlParser().parse(VALID_REPORT_CONFIG); - Assertions.assertNotEquals(reportConfig.getTitle(), ReportConfiguration.DEFAULT_TITLE); - Assertions.assertNotEquals(reportConfig.getReportRepoConfigurations(), - ReportConfiguration.DEFAULT_REPORT_REPO_CONFIGS); + Assertions.assertNotEquals(ReportConfiguration.DEFAULT_TITLE, reportConfig.getTitle()); + Assertions.assertEquals(new ArrayList<>(), + reportConfig.getReportRepoConfigurations()); } } diff --git a/src/test/resources/ReportConfigYamlParserTest/report-config-invalid.yaml b/src/test/resources/ReportConfigYamlParserTest/report-config-invalid.yaml index f4b2300daf..ea5be78365 100644 --- a/src/test/resources/ReportConfigYamlParserTest/report-config-invalid.yaml +++ b/src/test/resources/ReportConfigYamlParserTest/report-config-invalid.yaml @@ -1,31 +1,24 @@ -titles: RepoSense Report -repo: - - repos: https://github.com/reposense/testrepo-Delta.git - blurbs: "The main branch" - group: - - grame: code +title: RepoSense Report +repos: + - repo: https://github.com/reposense/testrepo-Delta.git + groups: + - group-name: code globs: - "**.java" - group-name: tests globs: - "src/test**" - group-name: docs - gl: + globs: - "docs**" - "**.adoc" - "**.md" - bhes: + branches: - branch: master - autho:thor-name: WANG CHAO - author-emails: - - 1229983126@qq.com + blurb: "My project" + aut:::thors: - author-git-host-id: FH-30 author-display-name: Francis Hodianto author-git-author-name: Francis Hodianto author-emails: - - - file-formats: - - override:java - - md - is-shallow-cloning: false - is-find-previous-authors: false + ignore-authors-list: diff --git a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml index 934f50536a..bb1f3321e6 100644 --- a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml +++ b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml @@ -1,4 +1,4 @@ -title: RepoSense Report Test Repo +title: RepoSense Report repos: - repo: https://github.com/reposense/testrepo-Delta.git groups: @@ -15,19 +15,10 @@ repos: - "**.md" branches: - branch: master - blurb: "The main branch" + blurb: "My project" authors: - - author-git-host-id: fzdy1914 - author-display-name: WANG CHAO - author-git-author-name: WANG CHAO - author-emails: - - 1229983126@qq.com - author-git-host-id: FH-30 author-display-name: Francis Hodianto author-git-author-name: Francis Hodianto author-emails: - - - ignore-glob-list: - - ignore-authors-list: - - From 02380ae6ff8f0a3b4ef6fda6b00c78c86122d06b Mon Sep 17 00:00:00 2001 From: LiYuan Date: Tue, 18 Feb 2025 11:47:56 +0800 Subject: [PATCH 15/42] Handle empty fields and add tests --- config/report-config.yaml | 6 +-- .../java/reposense/model/CliArguments.java | 7 ++- .../model/OneStopConfigRunConfiguration.java | 9 +--- .../reportconfig/ReportAuthorDetails.java | 14 +++--- .../model/reportconfig/ReportBranchData.java | 14 +++--- .../reportconfig/ReportConfiguration.java | 6 ++- .../reportconfig/ReportGroupNameAndGlobs.java | 31 +++++++++++-- .../reportconfig/ReportRepoConfiguration.java | 14 +++--- .../java/reposense/parser/ArgsParser.java | 2 + .../reportconfig/ReportAuthorDetailsTest.java | 35 +++++++++++++- .../reportconfig/ReportBranchDataTest.java | 38 +++++++++------ .../reportconfig/ReportConfigurationTest.java | 15 +++--- .../ReportGroupNameAndGlobsTest.java | 22 ++++++--- .../ReportRepoConfigurationTest.java | 46 ++++++++++++++++--- .../parser/ReportConfigYamlParserTest.java | 45 ++++++++++++------ .../report-config-valid.yaml | 16 +++---- 16 files changed, 220 insertions(+), 100 deletions(-) diff --git a/config/report-config.yaml b/config/report-config.yaml index c440ecba8f..62f2009bf6 100644 --- a/config/report-config.yaml +++ b/config/report-config.yaml @@ -1,3 +1,4 @@ +title: RepoSense Report repos: - repo: https://github.com/reposense/testrepo-Delta.git groups: @@ -17,8 +18,7 @@ repos: blurb: "My project" authors: - author-git-host-id: FH-30 - author-display-name: Francis Hodianto (FH-30) + author-display-name: Francis Hodianto author-git-author-name: Francis Hodianto author-emails: - ignore-authors-list: - - jordancjq \ No newline at end of file + ignore-authors-list: \ No newline at end of file diff --git a/src/main/java/reposense/model/CliArguments.java b/src/main/java/reposense/model/CliArguments.java index 2a98c45658..eab2dea02a 100644 --- a/src/main/java/reposense/model/CliArguments.java +++ b/src/main/java/reposense/model/CliArguments.java @@ -165,16 +165,15 @@ public ReportConfiguration getReportConfiguration() { return reportConfiguration; } - public BlurbMap getBlurbMap() { - return blurbMap; - } - /** * Merges the {@code blurbMap} from the blurbs file with the blurb map in {@code reportConfiguration}. * * @return the merged blurb map. */ public BlurbMap mergeWithRepoConfigBlurbMap() { + if (!isOneStopConfigFilePresent) { + return blurbMap; + } BlurbMap repoConfigBlurbMap = reportConfiguration.getBlurbMap(); for (Map.Entry entry : blurbMap.getAllMappings().entrySet()) { repoConfigBlurbMap.withRecord(entry.getKey(), entry.getValue()); diff --git a/src/main/java/reposense/model/OneStopConfigRunConfiguration.java b/src/main/java/reposense/model/OneStopConfigRunConfiguration.java index 7a42555383..b17838ed7b 100644 --- a/src/main/java/reposense/model/OneStopConfigRunConfiguration.java +++ b/src/main/java/reposense/model/OneStopConfigRunConfiguration.java @@ -40,28 +40,22 @@ public List getRepoConfigurations() throws InvalidLocationExc RepoLocation repoLocation = new RepoLocation(rrc.getRepo()); groupConfigs.add(rrc.getGroupConfiguration(repoLocation)); - // iterate for each branch and for each author for (ReportBranchData rbd : rrc.getBranches()) { logger.info("Parsing " + rbd.getBranch() + "..."); - // create the repoconfiguration object - // we will need to add the group configuration details to the repo configuration builder + RepoConfiguration.Builder builder = new RepoConfiguration.Builder() .location(repoLocation); - // set the relevant branch details for the repo builder = builder.branch(rbd.getBranch()) .ignoreGlobList(rbd.getIgnoreGlobList()) .ignoredAuthorsList(rbd.getIgnoreAuthorList()) .fileSizeLimit(rbd.getFileSizeLimit()) .isStandaloneConfigIgnored(true); // remove this when we deprecated the standalone config - AuthorConfiguration authorConfiguration = new AuthorConfiguration(repoLocation, rbd.getBranch()); for (ReportAuthorDetails rad : rbd.getReportAuthorDetails()) { logger.info("Parsing " + rad.getAuthorGitHostId() + "..."); - - // prepare the author details Author author = new Author(rad.getAuthorGitHostId()); author.setEmails(rad.getAuthorEmails()); author.setDisplayName(rad.getAuthorDisplayName()); @@ -69,7 +63,6 @@ public List getRepoConfigurations() throws InvalidLocationExc authorConfiguration.addAuthor(author); - } authorConfigs.add(authorConfiguration); repoConfigs.add(builder.build()); diff --git a/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java b/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java index 175f574ca6..d3a637ad8e 100644 --- a/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java +++ b/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java @@ -10,14 +10,13 @@ * Represents an author's details in the report-config.yaml file. */ public class ReportAuthorDetails { + private final List authorEmails; - private List authorEmails; + private final String authorGitHostId; - private String authorGitHostId; + private final String authorDisplayName; - private String authorDisplayName; - - private String authorGitAuthorName; + private final String authorGitAuthorName; @JsonCreator public ReportAuthorDetails( @@ -25,8 +24,11 @@ public ReportAuthorDetails( @JsonProperty("author-git-host-id") String authorGitHostId, @JsonProperty("author-display-name") String authorDisplayName, @JsonProperty("author-git-author-name") String authorGitAuthorName) { + if (authorGitHostId == null) { + throw new IllegalArgumentException("Author Git Host ID cannot be empty."); + } + this.authorGitHostId = authorGitHostId; this.authorEmails = authorEmails == null ? new ArrayList<>() : authorEmails; - this.authorGitHostId = authorGitHostId == null ? "" : authorGitHostId; this.authorDisplayName = authorDisplayName == null ? "" : authorDisplayName; this.authorGitAuthorName = authorGitAuthorName == null ? "" : authorGitAuthorName; } diff --git a/src/main/java/reposense/model/reportconfig/ReportBranchData.java b/src/main/java/reposense/model/reportconfig/ReportBranchData.java index 9ff14e3020..447e5287f2 100644 --- a/src/main/java/reposense/model/reportconfig/ReportBranchData.java +++ b/src/main/java/reposense/model/reportconfig/ReportBranchData.java @@ -13,19 +13,17 @@ public class ReportBranchData { public static final String DEFAULT_BRANCH = "HEAD"; public static final Long DEFAULT_FILE_SIZE_LIMIT = 1000000L; - private String branch; + private final String branch; - private String blurb; + private final String blurb; - private List reportAuthorDetails; + private final List reportAuthorDetails; - private List ignoreGlobList; + private final List ignoreGlobList; - private List ignoreAuthorList; + private final List ignoreAuthorList; - private Long fileSizeLimit; - - public ReportBranchData() {} + private final Long fileSizeLimit; @JsonCreator public ReportBranchData( diff --git a/src/main/java/reposense/model/reportconfig/ReportConfiguration.java b/src/main/java/reposense/model/reportconfig/ReportConfiguration.java index be760c78db..d342c62421 100644 --- a/src/main/java/reposense/model/reportconfig/ReportConfiguration.java +++ b/src/main/java/reposense/model/reportconfig/ReportConfiguration.java @@ -1,6 +1,5 @@ package reposense.model.reportconfig; -import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -30,8 +29,11 @@ public ReportConfiguration() {} public ReportConfiguration( @JsonProperty("title") String title, @JsonProperty("repos") List reportRepoConfigurations) { + if (reportRepoConfigurations == null) { + throw new IllegalArgumentException("ReportConfiguration must contain at least one repository."); + } this.title = title == null ? DEFAULT_TITLE : title; - this.reportRepoConfigurations = reportRepoConfigurations == null ? new ArrayList<>() : reportRepoConfigurations; + this.reportRepoConfigurations = reportRepoConfigurations; } /** diff --git a/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java b/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java index fb53df595a..1da9bfd056 100644 --- a/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java +++ b/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java @@ -2,6 +2,7 @@ import java.util.List; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import reposense.model.FileType; @@ -10,12 +11,34 @@ * Contains details about each report group and the corresponding globs. */ public class ReportGroupNameAndGlobs { + private final String groupName; - @JsonProperty("group-name") - private String groupName; + private final List globs; - @JsonProperty("globs") - private List globs; + @JsonCreator + public ReportGroupNameAndGlobs( + @JsonProperty("group-name") String groupName, + @JsonProperty("globs") List globs) { + validate(groupName, globs); + this.groupName = groupName; + this.globs = globs; + } + + /** + * Validates the group name and globs. + * + * @param groupName the name of the group. + * @param globs the list of globs. + * @throws IllegalArgumentException if the group name or globs is invalid. + */ + private static void validate(String groupName, List globs) throws IllegalArgumentException { + if (groupName == null) { + throw new IllegalArgumentException("Group name cannot be empty."); + } + if (globs == null) { + throw new IllegalArgumentException("Globs cannot be empty."); + } + } public String getGroupName() { return groupName; diff --git a/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java index a8154db422..d5a8f31b18 100644 --- a/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java +++ b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java @@ -17,25 +17,25 @@ */ public class ReportRepoConfiguration { - private String repo; + private final String repo; - private List groups; + private final List groups; - private List branches; - - public ReportRepoConfiguration() {} + private final List branches; @JsonCreator public ReportRepoConfiguration( @JsonProperty("repo") String repo, @JsonProperty("groups") List groups, @JsonProperty("branches") List branches) { - this.repo = repo == null ? "" : repo; + if (repo == null) { + throw new IllegalArgumentException("Repo URL cannot be empty."); + } + this.repo = repo; this.groups = groups == null ? new ArrayList<>() : groups; this.branches = branches == null ? new ArrayList<>() : branches; } - /** * Represents a single mapped entry between a fully qualified repository name and its associated blurb. */ diff --git a/src/main/java/reposense/parser/ArgsParser.java b/src/main/java/reposense/parser/ArgsParser.java index c8b447b966..959ec174b7 100644 --- a/src/main/java/reposense/parser/ArgsParser.java +++ b/src/main/java/reposense/parser/ArgsParser.java @@ -394,8 +394,10 @@ private static void addReportConfigToBuilder(CliArguments.Builder builder, Names builder.isOneStopConfigFilePresent(true); } catch (JsonSyntaxException jse) { logger.warning(String.format(MESSAGE_INVALID_CONFIG_PATH, reportConfigFilePath)); + builder.isOneStopConfigFilePresent(false); } catch (IllegalArgumentException iae) { logger.warning(String.format(MESSAGE_INVALID_CONFIG_YAML, iae.getMessage())); + builder.isOneStopConfigFilePresent(false); } catch (IOException ioe) { // IOException thrown as report-config.yaml is not found. // Ignore exception as the file is optional. diff --git a/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java b/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java index 03aba7d6ae..15c2bb949e 100644 --- a/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java @@ -1,3 +1,36 @@ package reposense.model.reportconfig; -public class ReportAuthorDetailsTest { } +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ReportAuthorDetailsTest { + @Test + public void constructor_withValidInputs_success() { + List emails = List.of("test@example.com", "test2@example.com"); + ReportAuthorDetails details = new ReportAuthorDetails( + emails, + "Git Host Id", + "Display Name", + "Git Author" + ); + + Assertions.assertEquals(emails, details.getAuthorEmails()); + Assertions.assertEquals("Git Host Id", details.getAuthorGitHostId()); + Assertions.assertEquals("Display Name", details.getAuthorDisplayName()); + Assertions.assertEquals("Git Author", details.getAuthorGitAuthorName()); + } + + @Test + public void constructor_nullGitHostId_throwsIllegalArgumentException() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + new ReportAuthorDetails( + List.of("test@example.com"), + null, + "Display Name", + "Git Author" + ); + }); + } +} diff --git a/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java index e4789bdbac..8bfcf3037d 100644 --- a/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java @@ -1,30 +1,40 @@ package reposense.model.reportconfig; import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class ReportBranchDataTest { @Test - public void getBranch_equalsDefaultReturnValue_success() { - Assertions.assertSame(ReportBranchData.DEFAULT_BRANCH, new ReportBranchData().getBranch()); - } - - @Test - public void getIgnoreGlobList_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ArrayList<>(), new ReportBranchData().getIgnoreGlobList()); - } + public void constructor_withValidInputs_success() { + String branch = "main"; + String blurb = "Test blurb"; + List authors = null; + List ignoreGlobs = List.of("*.log"); + List ignoreAuthors = List.of("bot"); + Long fileSize = 2000000L; + ReportBranchData data = new ReportBranchData(branch, blurb, authors, ignoreGlobs, ignoreAuthors, fileSize); - @Test - public void getIgnoreAuthorList_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ArrayList<>(), - new ReportBranchData().getIgnoreAuthorList()); + Assertions.assertEquals(branch, data.getBranch()); + Assertions.assertEquals(blurb, data.getBlurb()); + Assertions.assertEquals(new ArrayList<>(), data.getReportAuthorDetails()); + Assertions.assertEquals(ignoreGlobs, data.getIgnoreGlobList()); + Assertions.assertEquals(ignoreAuthors, data.getIgnoreAuthorList()); + Assertions.assertEquals(fileSize, data.getFileSizeLimit()); } @Test - public void equals_defaultInstancesAreEqual_success() { - Assertions.assertEquals(new ReportBranchData(), new ReportBranchData()); + public void constructor_withNullInputs_shouldUseDefaultValues() { + ReportBranchData data = new ReportBranchData(null, null, null, null, null, null); + + Assertions.assertEquals(ReportBranchData.DEFAULT_BRANCH, data.getBranch()); + Assertions.assertEquals("", data.getBlurb()); + Assertions.assertEquals(new ArrayList<>(), data.getReportAuthorDetails()); + Assertions.assertEquals(new ArrayList<>(), data.getIgnoreGlobList()); + Assertions.assertEquals(new ArrayList<>(), data.getIgnoreAuthorList()); + Assertions.assertEquals(ReportBranchData.DEFAULT_FILE_SIZE_LIMIT, data.getFileSizeLimit()); } } diff --git a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java index d2b0bb2c4e..9f15be7ebc 100644 --- a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java @@ -7,18 +7,19 @@ public class ReportConfigurationTest { @Test - public void getTitle_equalsDefaultReturnValue_success() { - Assertions.assertSame(ReportConfiguration.DEFAULT_TITLE, new ReportConfiguration().getTitle()); + public void constructor_withValidInputs_success() { + ReportConfiguration reportConfiguration = new ReportConfiguration("My Report", new ArrayList<>()); + Assertions.assertNotNull(reportConfiguration); } @Test - public void getReportRepoConfigurations_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ArrayList<>(), - new ReportConfiguration().getReportRepoConfigurations()); + public void constructor_nullReportRepoConfigurations_throwsIllegalArgumentException() { + Assertions.assertThrows(IllegalArgumentException.class, () -> new ReportConfiguration(null, null)); } @Test - public void equals_defaultInstancesAreEqual_success() { - Assertions.assertEquals(new ReportConfiguration(), new ReportConfiguration()); + public void getTitle_equalsDefaultReturnValue_success() { + Assertions.assertSame(ReportConfiguration.DEFAULT_TITLE, + new ReportConfiguration(null, new ArrayList<>()).getTitle()); } } diff --git a/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java b/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java index d203a7a2f2..788c3b2921 100644 --- a/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java @@ -1,22 +1,30 @@ package reposense.model.reportconfig; +import java.util.List; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class ReportGroupNameAndGlobsTest { @Test - public void getRepo_equalsDefaultReturnValue_success() { - Assertions.assertSame(null, new ReportGroupNameAndGlobs().getGroupName()); + public void constructor_withValidInputs_success() { + ReportGroupNameAndGlobs reportGroupNameAndGlobs = new ReportGroupNameAndGlobs("My Group", List.of("code")); + Assertions.assertNotNull(reportGroupNameAndGlobs); + } + + @Test + public void constructor_nullGroupName_throwsIllegalArgumentException() { + Assertions.assertThrows(IllegalArgumentException.class, () -> new ReportGroupNameAndGlobs(null, + List.of("code"))); } @Test - public void getReportGroupNameAndGlobsList_equalsDefaultReturnValue_success() { - Assertions.assertSame(null, - new ReportGroupNameAndGlobs().getGlobs()); + public void constructor_nullGlobs_throwsIllegalArgumentException() { + Assertions.assertThrows(IllegalArgumentException.class, () -> new ReportGroupNameAndGlobs("My Group", null)); } @Test - public void equals_defaultInstancesAreEqual_success() { - Assertions.assertEquals(new ReportGroupNameAndGlobs(), new ReportGroupNameAndGlobs()); + public void constructor_nullGroupNameAndNullGlobs_throwsIllegalArgumentException() { + Assertions.assertThrows(IllegalArgumentException.class, () -> new ReportGroupNameAndGlobs(null, null)); } } diff --git a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java index 0b5bd348b8..5d877f80a6 100644 --- a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java @@ -1,23 +1,57 @@ package reposense.model.reportconfig; import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; public class ReportRepoConfigurationTest { @Test - public void getRepo_equalsDefaultReturnValue_success() { - Assertions.assertSame("", new ReportRepoConfiguration().getRepo()); + void constructor_nullRepo_throwsIllegalArgumentException() { + Assertions.assertThrows(IllegalArgumentException.class, () -> + new ReportRepoConfiguration(null, null, null)); } @Test - public void getBranches_equalsDefaultReturnValue_success() { - Assertions.assertSame(new ArrayList<>(), new ReportRepoConfiguration().getBranches()); + void constructor_nullGroupsAndBranches_createsEmptyLists() { + ReportRepoConfiguration config = new ReportRepoConfiguration("https://github.com/test/repo.git", null, null); + + Assertions.assertNotNull(config.getGroupDetails()); + Assertions.assertTrue(config.getGroupDetails().isEmpty()); + Assertions.assertNotNull(config.getBranches()); + Assertions.assertTrue(config.getBranches().isEmpty()); } @Test - public void equals_defaultInstancesAreEqual_success() { - Assertions.assertEquals(new ReportRepoConfiguration(), new ReportRepoConfiguration()); + void getFullyQualifiedRepoNamesWithBlurbs_validUrl_returnsCorrectMapping() { + List branches = List.of( + new ReportBranchData("main", "Main branch", null, null, null, null), + new ReportBranchData("dev", "Development branch", null, null, null, null) + ); + ReportRepoConfiguration config = new ReportRepoConfiguration( + "https://github.com/test/repo.git", + new ArrayList<>(), + branches + ); + + List result = config.getFullyQualifiedRepoNamesWithBlurbs(); + + Assertions.assertEquals(2, result.size()); + Assertions.assertEquals("https://github.com/test/repo/tree/main", result.get(0).getKey()); + Assertions.assertEquals("Main branch", result.get(0).getValue()); + Assertions.assertEquals("https://github.com/test/repo/tree/dev", result.get(1).getKey()); + Assertions.assertEquals("Development branch", result.get(1).getValue()); + } + + @Test + void getFullyQualifiedRepoNamesWithBlurbs_invalidUrl_throwsIllegalArgumentException() { + ReportRepoConfiguration config = new ReportRepoConfiguration( + "https://github.com/test/repo", // Missing .git + new ArrayList<>(), + new ArrayList<>() + ); + + Assertions.assertThrows(IllegalArgumentException.class, () -> config.getFullyQualifiedRepoNamesWithBlurbs()); } } diff --git a/src/test/java/reposense/parser/ReportConfigYamlParserTest.java b/src/test/java/reposense/parser/ReportConfigYamlParserTest.java index 03d10e4139..6b0d876fdf 100644 --- a/src/test/java/reposense/parser/ReportConfigYamlParserTest.java +++ b/src/test/java/reposense/parser/ReportConfigYamlParserTest.java @@ -4,11 +4,17 @@ import java.nio.file.Path; import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import reposense.model.reportconfig.ReportAuthorDetails; +import reposense.model.reportconfig.ReportBranchData; import reposense.model.reportconfig.ReportConfiguration; +import reposense.model.reportconfig.ReportGroupNameAndGlobs; +import reposense.model.reportconfig.ReportRepoConfiguration; public class ReportConfigYamlParserTest { @@ -19,27 +25,38 @@ public class ReportConfigYamlParserTest { private static final Path EMPTY_REPORT_CONFIG = loadResource( ReportConfigYamlParserTest.class, "ReportConfigYamlParserTest/report-config-empty.yaml"); + private static ReportConfiguration expectedReportConfig; + + @BeforeAll + public static void setUp() { + List groups = new ArrayList<>(); + groups.add(new ReportGroupNameAndGlobs("code", List.of("**.java"))); + + ReportAuthorDetails author = new ReportAuthorDetails(List.of("fh@gmail.com"), "FH-30", + "Francis Hodianto", "Francis Hodianto"); + + ReportBranchData branch = new ReportBranchData("master", "My project", List.of(author), + List.of("**.java"), List.of("bot"), 2000000L); + + ReportRepoConfiguration repo = new ReportRepoConfiguration("https://github.com/reposense/testrepo-Delta.git", + groups, List.of(branch)); + + expectedReportConfig = new ReportConfiguration("Test RepoSense Report", List.of(repo)); + } + @Test - public void reportConfig_parseEmptyYamlFile_getDefaultTitle() throws Exception { - ReportConfiguration reportConfig = new ReportConfigYamlParser().parse(EMPTY_REPORT_CONFIG); - Assertions.assertEquals(ReportConfiguration.DEFAULT_TITLE, reportConfig.getTitle()); - Assertions.assertEquals(new ArrayList<>(), - reportConfig.getReportRepoConfigurations()); + public void reportConfig_parseEmptyYamlFile_throwsIoException() { + Assertions.assertThrows(Exception.class, () -> new ReportConfigYamlParser().parse(EMPTY_REPORT_CONFIG)); } @Test - public void reportConfig_parseInvalidYamlFile_getDefaultTitle() throws Exception { - ReportConfiguration reportConfig = new ReportConfigYamlParser().parse(INVALID_REPORT_CONFIG); - Assertions.assertEquals(ReportConfiguration.DEFAULT_TITLE, reportConfig.getTitle()); - Assertions.assertEquals(new ArrayList<>(), - reportConfig.getReportRepoConfigurations()); + public void reportConfig_parseInvalidYamlFile_throwsIoException() { + Assertions.assertThrows(Exception.class, () -> new ReportConfigYamlParser().parse(INVALID_REPORT_CONFIG)); } @Test - public void reportConfig_parseValidYamlFile_getCustomTitle() throws Exception { + public void reportConfig_parseValidYamlFile_success() throws Exception { ReportConfiguration reportConfig = new ReportConfigYamlParser().parse(VALID_REPORT_CONFIG); - Assertions.assertNotEquals(ReportConfiguration.DEFAULT_TITLE, reportConfig.getTitle()); - Assertions.assertEquals(new ArrayList<>(), - reportConfig.getReportRepoConfigurations()); + Assertions.assertEquals(expectedReportConfig, reportConfig); } } diff --git a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml index bb1f3321e6..f7a2d2d33c 100644 --- a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml +++ b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml @@ -1,18 +1,10 @@ -title: RepoSense Report +title: Test RepoSense Report repos: - repo: https://github.com/reposense/testrepo-Delta.git groups: - group-name: code globs: - "**.java" - - group-name: tests - globs: - - "src/test**" - - group-name: docs - globs: - - "docs**" - - "**.adoc" - - "**.md" branches: - branch: master blurb: "My project" @@ -21,4 +13,10 @@ repos: author-display-name: Francis Hodianto author-git-author-name: Francis Hodianto author-emails: + - fh@gmail.com ignore-authors-list: + - bot + ignore-glob-list: + - "**.java" + file-size-limit: 2000000 + From 1fcb40e50a393e4483a8593762b2e3f297c7c651 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Fri, 21 Feb 2025 02:09:46 +0800 Subject: [PATCH 16/42] Add documentation and refactoring --- docs/ug/cli.md | 2 +- docs/ug/configFiles.md | 16 +++----- docs/ug/report-config.yaml | 39 +++++-------------- frontend/cypress/config/report-config.yaml | 25 +++++++++++- src/main/java/reposense/RepoSense.java | 2 +- .../java/reposense/model/CliArguments.java | 28 ++++--------- .../model/RunConfigurationDecider.java | 2 +- .../reportconfig/ReportConfiguration.java | 6 +-- .../java/reposense/parser/ArgsParser.java | 11 ++---- .../java/reposense/report/SummaryJson.java | 2 +- .../ConfigSystemTest/report-config.yaml | 33 ++++++---------- .../model/RunConfigurationDeciderTest.java | 2 +- .../reportconfig/ReportConfigurationTest.java | 5 --- .../ReportRepoConfigurationTest.java | 4 +- .../parser/ReportConfigYamlParserTest.java | 27 ++++++++++--- .../report-config-valid.yaml | 2 +- 16 files changed, 92 insertions(+), 114 deletions(-) diff --git a/docs/ug/cli.md b/docs/ug/cli.md index 1530c8a1bf..5971af7c8b 100644 --- a/docs/ug/cli.md +++ b/docs/ug/cli.md @@ -68,7 +68,7 @@ partial credit.
**`--config CONFIG_DIRECTORY`**: Specifies that config files located in `CONFIG_DIRECTORY` should be used to customize the report. -* Parameter: `CONFIG_DIRECTORY` The directory containing the config files. Should contain a `repo-config.csv` file. Optionally, can contain an `author-config.csv` file or/and a `group-config.csv` file or/and a `report-config.yaml` file. +* Parameter: `CONFIG_DIRECTORY` The directory containing the config files. Should contain a `repo-config.csv` file. Optionally, can contain an `author-config.csv` file or/and a `group-config.csv` file or/and a `report-config.yaml` file or/and a `blurbs.md` file. * Alias: `-c` * Example: `java -jar RepoSense.jar --config ./config` diff --git a/docs/ug/configFiles.md b/docs/ug/configFiles.md index b6977df56b..e86436eff6 100644 --- a/docs/ug/configFiles.md +++ b/docs/ug/configFiles.md @@ -113,29 +113,25 @@ You can also optionally use a `report-config.yaml` file to quickly define the re View this [example](report-config.yaml) for a better understanding of what repository information is required. -Note: All fields should be defined, and left blank if not used. +Note: All fields are optional unless specified otherwise. **Fields to provide**: * `title`: Title of the generated report, which is also the title of the deployed dashboard. Default: "RepoSense Report". * `repos`: A list of repositories to include for analysis. - * `repo`: The URL to your repository of interest. + * `repo` {{ mandatory }}: The URL to your repository of interest. * `groups`: A list of the different custom groupings. - * `group-name`: Name of the group. - * `globs`: The list of file path globs to include for specified group. + * `group-name` {{ mandatory }}: Name of the group. + * `globs` {{ mandatory }}: The list of file path globs to include for specified group. * `branches`: A list of branches to analyse for each repository. * `branch`: The name of the branch. * `authors`: A list of authors to analyse on the branch. - * `author-git-host-id`: Git host username of the author. + * `author-git-host-id` {{ mandatory }}: Git host username of the author. * `author-display-name`: Display name of the author. * `author-git-author-name`: Author's Git host name. * `author-emails`: A list of emails associated with an author. - * `file-formats`: File formats to analyze. * `ignore-glob-list`: Folders/files to ignore, specified using the [_glob format_](https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob). - * `ignore-standalone-config`: Ignores the different standalone configuration files when analysing this branch - * `ignore-commits-list`: The list of commits to ignore during analysis. For accurate results, the commits should be provided with their full hash. Additionally, a range of commits can be specified using the `..` notation e.g. `abc123..def456` (both inclusive). * `ignore-authors-list`: The list of authors to ignore during analysis. Authors specified in `authors` field or `author-config.csv` will be also be omitted if they are in this list. Authors should be specified by their [Git Author Name](#a-note-about-git-author-name). - * `is-shallow-cloning`: Indicates whether to clone the repository using Git's shallow cloning functionality. - * `is-find-previous-authors`: Indicates whether to find previous authors of the repository/branch. + * `file-size-limit`: A file size limit for the repository in bytes as a single number without units, that will override the default file size limit. If not specified, the default file size limit will continue to be used. diff --git a/docs/ug/report-config.yaml b/docs/ug/report-config.yaml index b5914b25ce..f9c2e7e454 100644 --- a/docs/ug/report-config.yaml +++ b/docs/ug/report-config.yaml @@ -1,43 +1,22 @@ -title: RepoSense Report -group-details: +title: Test RepoSense Report +repos: - repo: https://github.com/reposense/testrepo-Delta.git groups: - group-name: code globs: - "**.java" - - group-name: tests - globs: - - "src/test**" - - group-name: docs - globs: - - "docs**" - - "**.adoc" - - "**.md" -repos: - - repo: https://github.com/reposense/testrepo-Delta.git branches: - branch: master + blurb: "My project" authors: - - author-git-host-id: fzdy1914 - author-display-name: WANG CHAO - author-git-author-name: WANG CHAO - author-emails: - - 1229983126@qq.com - author-git-host-id: FH-30 author-display-name: Francis Hodianto author-git-author-name: Francis Hodianto author-emails: - - - file-formats: - - override:java - - md - - fxml - ignore-glob-list: - - - ignore-standalone-config: false - ignore-commits-list: - - + - fh@gmail.com ignore-authors-list: - - - is-shallow-cloning: false - is-find-previous-authors: false + - bot + ignore-glob-list: + - "**.md" + file-size-limit: 2000000 + diff --git a/frontend/cypress/config/report-config.yaml b/frontend/cypress/config/report-config.yaml index a5d9bb7897..62f2009bf6 100644 --- a/frontend/cypress/config/report-config.yaml +++ b/frontend/cypress/config/report-config.yaml @@ -1 +1,24 @@ -title: RepoSense Test Report +title: RepoSense Report +repos: + - repo: https://github.com/reposense/testrepo-Delta.git + groups: + - group-name: code + globs: + - "**.java" + - group-name: tests + globs: + - "src/test**" + - group-name: docs + globs: + - "docs**" + - "**.adoc" + - "**.md" + branches: + - branch: master + blurb: "My project" + authors: + - author-git-host-id: FH-30 + author-display-name: Francis Hodianto + author-git-author-name: Francis Hodianto + author-emails: + ignore-authors-list: \ No newline at end of file diff --git a/src/main/java/reposense/RepoSense.java b/src/main/java/reposense/RepoSense.java index 8266f6c006..79b1822d04 100644 --- a/src/main/java/reposense/RepoSense.java +++ b/src/main/java/reposense/RepoSense.java @@ -54,7 +54,7 @@ public static void main(String[] args) { configs = RunConfigurationDecider.getRunConfiguration(cliArguments).getRepoConfigurations(); reportConfig = cliArguments.getReportConfiguration(); - blurbMap = cliArguments.mergeWithRepoConfigBlurbMap(); + blurbMap = cliArguments.mergeWithReportConfigBlurbMap(); RepoConfiguration.setFormatsToRepoConfigs(configs, cliArguments.getFormats()); RepoConfiguration.setDatesToRepoConfigs(configs, cliArguments.getSinceDate(), cliArguments.getUntilDate()); diff --git a/src/main/java/reposense/model/CliArguments.java b/src/main/java/reposense/model/CliArguments.java index eab2dea02a..80b051d46f 100644 --- a/src/main/java/reposense/model/CliArguments.java +++ b/src/main/java/reposense/model/CliArguments.java @@ -41,7 +41,6 @@ public class CliArguments { private double originalityThreshold; private boolean isTestMode = ArgsParser.DEFAULT_IS_TEST_MODE; private boolean isFreshClonePerformed = ArgsParser.DEFAULT_SHOULD_FRESH_CLONE; - private boolean isOneStopConfigFilePresent; private List locations; private boolean isViewModeOnly; @@ -170,8 +169,8 @@ public ReportConfiguration getReportConfiguration() { * * @return the merged blurb map. */ - public BlurbMap mergeWithRepoConfigBlurbMap() { - if (!isOneStopConfigFilePresent) { + public BlurbMap mergeWithReportConfigBlurbMap() { + if (reportConfiguration == null) { return blurbMap; } BlurbMap repoConfigBlurbMap = reportConfiguration.getBlurbMap(); @@ -181,10 +180,6 @@ public BlurbMap mergeWithRepoConfigBlurbMap() { return repoConfigBlurbMap; } - public boolean isOneStopConfigFilePresent() { - return isOneStopConfigFilePresent; - } - public boolean isViewModeOnly() { return isViewModeOnly; } @@ -197,6 +192,10 @@ public double getOriginalityThreshold() { return originalityThreshold; } + public boolean isReportConfigRepositoriesConfigured() { + return reportConfiguration != null && !reportConfiguration.getReportRepoConfigurations().isEmpty(); + } + @Override public boolean equals(Object other) { // short circuit if same object @@ -237,8 +236,7 @@ public boolean equals(Object other) { && Objects.equals(this.reportConfigFilePath, otherCliArguments.reportConfigFilePath) && Objects.equals(this.blurbMap, otherCliArguments.blurbMap) && this.isAuthorshipAnalyzed == otherCliArguments.isAuthorshipAnalyzed - && Objects.equals(this.originalityThreshold, otherCliArguments.originalityThreshold) - && this.isOneStopConfigFilePresent == otherCliArguments.isOneStopConfigFilePresent; + && Objects.equals(this.originalityThreshold, otherCliArguments.originalityThreshold); } /** @@ -523,18 +521,6 @@ public Builder blurbMap(BlurbMap blurbMap) { return this; } - - /** - * Adds the {@code isOneStopConfigFilePresent} to CliArguments. - * - * @param isOneStopConfigFilePresent Is blurb map overriding. - */ - public Builder isOneStopConfigFilePresent(boolean isOneStopConfigFilePresent) { - this.cliArguments.isOneStopConfigFilePresent = isOneStopConfigFilePresent; - return this; - } - - /** * Builds CliArguments. * diff --git a/src/main/java/reposense/model/RunConfigurationDecider.java b/src/main/java/reposense/model/RunConfigurationDecider.java index cda6e8ccf4..af283a5437 100644 --- a/src/main/java/reposense/model/RunConfigurationDecider.java +++ b/src/main/java/reposense/model/RunConfigurationDecider.java @@ -9,7 +9,7 @@ public static RunConfiguration getRunConfiguration(CliArguments cliArguments) { return new CliRunConfiguration(cliArguments); } - if (cliArguments.isOneStopConfigFilePresent()) { + if (cliArguments.isReportConfigRepositoriesConfigured()) { return new OneStopConfigRunConfiguration(cliArguments); } diff --git a/src/main/java/reposense/model/reportconfig/ReportConfiguration.java b/src/main/java/reposense/model/reportconfig/ReportConfiguration.java index d342c62421..be760c78db 100644 --- a/src/main/java/reposense/model/reportconfig/ReportConfiguration.java +++ b/src/main/java/reposense/model/reportconfig/ReportConfiguration.java @@ -1,5 +1,6 @@ package reposense.model.reportconfig; +import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -29,11 +30,8 @@ public ReportConfiguration() {} public ReportConfiguration( @JsonProperty("title") String title, @JsonProperty("repos") List reportRepoConfigurations) { - if (reportRepoConfigurations == null) { - throw new IllegalArgumentException("ReportConfiguration must contain at least one repository."); - } this.title = title == null ? DEFAULT_TITLE : title; - this.reportRepoConfigurations = reportRepoConfigurations; + this.reportRepoConfigurations = reportRepoConfigurations == null ? new ArrayList<>() : reportRepoConfigurations; } /** diff --git a/src/main/java/reposense/parser/ArgsParser.java b/src/main/java/reposense/parser/ArgsParser.java index 959ec174b7..8f8aacb857 100644 --- a/src/main/java/reposense/parser/ArgsParser.java +++ b/src/main/java/reposense/parser/ArgsParser.java @@ -381,7 +381,6 @@ public static CliArguments parse(String[] args) throws HelpScreenException, Pars * @param results Parsed results of the user-supplied CLI arguments. */ private static void addReportConfigToBuilder(CliArguments.Builder builder, Namespace results) { - ReportConfiguration reportConfig = new ReportConfiguration(); List locations = results.get(REPO_FLAGS[0]); Path configFolderPath = results.get(CONFIG_FLAGS[0]); @@ -390,23 +389,18 @@ private static void addReportConfigToBuilder(CliArguments.Builder builder, Names Path reportConfigFilePath = configFolderPath.resolve(ReportConfigYamlParser.REPORT_CONFIG_FILENAME); try { - reportConfig = new ReportConfigYamlParser().parse(reportConfigFilePath); - builder.isOneStopConfigFilePresent(true); + ReportConfiguration reportConfig = new ReportConfigYamlParser().parse(reportConfigFilePath); + builder.reportConfiguration(reportConfig); } catch (JsonSyntaxException jse) { logger.warning(String.format(MESSAGE_INVALID_CONFIG_PATH, reportConfigFilePath)); - builder.isOneStopConfigFilePresent(false); } catch (IllegalArgumentException iae) { logger.warning(String.format(MESSAGE_INVALID_CONFIG_YAML, iae.getMessage())); - builder.isOneStopConfigFilePresent(false); } catch (IOException ioe) { // IOException thrown as report-config.yaml is not found. // Ignore exception as the file is optional. logger.log(Level.WARNING, "Error parsing report-config.yaml: " + ioe.getMessage(), ioe); - builder.isOneStopConfigFilePresent(false); } } - - builder.reportConfiguration(reportConfig); } /** @@ -428,6 +422,7 @@ private static void addBlurbMapToBuilder(CliArguments.Builder builder, Namespace logger.warning(String.format(MESSAGE_INVALID_MARKDOWN_BLURBS, ex.getMessage())); } catch (IOException ioe) { // IOException thrown as blurbs.md is not found. + // Ignore exception as the file is optional. } builder.blurbMap(blurbMap); diff --git a/src/main/java/reposense/report/SummaryJson.java b/src/main/java/reposense/report/SummaryJson.java index f62b43caaa..4cd7c32419 100644 --- a/src/main/java/reposense/report/SummaryJson.java +++ b/src/main/java/reposense/report/SummaryJson.java @@ -40,7 +40,7 @@ public SummaryJson(List repos, ReportConfiguration reportConf this.repos = repos; this.reportGeneratedTime = reportGeneratedTime; this.reportGenerationTime = reportGenerationTime; - this.reportTitle = reportConfig.getTitle(); + this.reportTitle = reportConfig == null ? "" : reportConfig.getTitle(); this.sinceDate = sinceDate; this.untilDate = untilDate; this.isSinceDateProvided = isSinceDateProvided; diff --git a/src/systemtest/resources/ConfigSystemTest/report-config.yaml b/src/systemtest/resources/ConfigSystemTest/report-config.yaml index 925686c29f..f9c2e7e454 100644 --- a/src/systemtest/resources/ConfigSystemTest/report-config.yaml +++ b/src/systemtest/resources/ConfigSystemTest/report-config.yaml @@ -1,31 +1,22 @@ -title: RepoSense Report Test Title +title: Test RepoSense Report repos: - - repo: https://github.com/user/repo.git + - repo: https://github.com/reposense/testrepo-Delta.git groups: - group-name: code globs: - "**.java" - - group-name: tests - globs: - - "src/test**" - - group-name: docs - globs: - - "docs**" - - "**.adoc" - - "**.md" branches: - - branch: main + - branch: master blurb: "My project" authors: - - author-git-host-id: johnDoe - author-display-name: John Doe - author-git-author-name: my home PC + - author-git-host-id: FH-30 + author-display-name: Francis Hodianto + author-git-author-name: Francis Hodianto author-emails: - - john@john.com - - johny@mail.com - - j@domain.com - ignore-glob-list: - - "docs**" + - fh@gmail.com ignore-authors-list: - - author1 - - author2 + - bot + ignore-glob-list: + - "**.md" + file-size-limit: 2000000 + diff --git a/src/test/java/reposense/model/RunConfigurationDeciderTest.java b/src/test/java/reposense/model/RunConfigurationDeciderTest.java index 66c9aa246d..8ea8af1595 100644 --- a/src/test/java/reposense/model/RunConfigurationDeciderTest.java +++ b/src/test/java/reposense/model/RunConfigurationDeciderTest.java @@ -78,6 +78,6 @@ public void parse_reposAndConfigNotSpecified_returnsConfigRunConfiguration() thr .build(); CliArguments cliArguments = ArgsParser.parse(translateCommandline(input)); RunConfiguration runConfiguration = getRunConfiguration(cliArguments); - Assertions.assertTrue(runConfiguration instanceof ConfigRunConfiguration); + Assertions.assertFalse(runConfiguration instanceof CliRunConfiguration); } } diff --git a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java index 9f15be7ebc..d0304d580e 100644 --- a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java @@ -12,11 +12,6 @@ public void constructor_withValidInputs_success() { Assertions.assertNotNull(reportConfiguration); } - @Test - public void constructor_nullReportRepoConfigurations_throwsIllegalArgumentException() { - Assertions.assertThrows(IllegalArgumentException.class, () -> new ReportConfiguration(null, null)); - } - @Test public void getTitle_equalsDefaultReturnValue_success() { Assertions.assertSame(ReportConfiguration.DEFAULT_TITLE, diff --git a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java index 5d877f80a6..856f5d5b52 100644 --- a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java @@ -48,8 +48,8 @@ void getFullyQualifiedRepoNamesWithBlurbs_validUrl_returnsCorrectMapping() { void getFullyQualifiedRepoNamesWithBlurbs_invalidUrl_throwsIllegalArgumentException() { ReportRepoConfiguration config = new ReportRepoConfiguration( "https://github.com/test/repo", // Missing .git - new ArrayList<>(), - new ArrayList<>() + null, + null ); Assertions.assertThrows(IllegalArgumentException.class, () -> config.getFullyQualifiedRepoNamesWithBlurbs()); diff --git a/src/test/java/reposense/parser/ReportConfigYamlParserTest.java b/src/test/java/reposense/parser/ReportConfigYamlParserTest.java index 6b0d876fdf..7daa0abf4d 100644 --- a/src/test/java/reposense/parser/ReportConfigYamlParserTest.java +++ b/src/test/java/reposense/parser/ReportConfigYamlParserTest.java @@ -30,18 +30,33 @@ public class ReportConfigYamlParserTest { @BeforeAll public static void setUp() { List groups = new ArrayList<>(); - groups.add(new ReportGroupNameAndGlobs("code", List.of("**.java"))); + List globPatterns = new ArrayList<>(); + globPatterns.add("**.java"); + groups.add(new ReportGroupNameAndGlobs("code", globPatterns)); - ReportAuthorDetails author = new ReportAuthorDetails(List.of("fh@gmail.com"), "FH-30", + List emailList = new ArrayList<>(); + emailList.add("fh@gmail.com"); + ReportAuthorDetails author = new ReportAuthorDetails(emailList, "FH-30", "Francis Hodianto", "Francis Hodianto"); - ReportBranchData branch = new ReportBranchData("master", "My project", List.of(author), - List.of("**.java"), List.of("bot"), 2000000L); + List authorList = new ArrayList<>(); + authorList.add(author); + List ignoreGlobList = new ArrayList<>(); + ignoreGlobList.add("**.md"); + List ignoreAuthorList = new ArrayList<>(); + ignoreAuthorList.add("bot"); + + ReportBranchData branch = new ReportBranchData("master", "My project", authorList, + ignoreGlobList, ignoreAuthorList, 2000000L); + List branches = new ArrayList<>(); + branches.add(branch); ReportRepoConfiguration repo = new ReportRepoConfiguration("https://github.com/reposense/testrepo-Delta.git", - groups, List.of(branch)); + groups, branches); + List repos = new ArrayList<>(); + repos.add(repo); - expectedReportConfig = new ReportConfiguration("Test RepoSense Report", List.of(repo)); + expectedReportConfig = new ReportConfiguration("Test RepoSense Report", repos); } @Test diff --git a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml index f7a2d2d33c..f9c2e7e454 100644 --- a/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml +++ b/src/test/resources/ReportConfigYamlParserTest/report-config-valid.yaml @@ -17,6 +17,6 @@ repos: ignore-authors-list: - bot ignore-glob-list: - - "**.java" + - "**.md" file-size-limit: 2000000 From 9393180f89a8c77457c5d056f9470b00a37fecaf Mon Sep 17 00:00:00 2001 From: LiYuan Date: Sat, 22 Feb 2025 02:31:27 +0800 Subject: [PATCH 17/42] Fix code to pass environmentalChecks and add blurb field documentation --- config/report-config.yaml | 2 +- docs/ug/configFiles.md | 2 ++ docs/ug/report-config.yaml | 1 - frontend/cypress/config/report-config.yaml | 2 +- src/main/java/reposense/model/CliArguments.java | 2 +- .../reposense/model/OneStopConfigRunConfiguration.java | 9 ++++----- .../java/reposense/model/RunConfigurationDecider.java | 2 +- .../reposense/model/RunConfigurationDeciderTest.java | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/config/report-config.yaml b/config/report-config.yaml index 62f2009bf6..bb1f3321e6 100644 --- a/config/report-config.yaml +++ b/config/report-config.yaml @@ -21,4 +21,4 @@ repos: author-display-name: Francis Hodianto author-git-author-name: Francis Hodianto author-emails: - ignore-authors-list: \ No newline at end of file + ignore-authors-list: diff --git a/docs/ug/configFiles.md b/docs/ug/configFiles.md index e86436eff6..dbb8c31864 100644 --- a/docs/ug/configFiles.md +++ b/docs/ug/configFiles.md @@ -110,6 +110,7 @@ e.g.: `example.java` in `example-repo` can either be in the `test` group or the ## `report-config.yaml` You can also optionally use a `report-config.yaml` file to quickly define the repository information for the repositories you are interested in tracking and generate your very own code portfolio. +The configurations of this file will override the configurations in the csv files if the file is present and correctly formatted. View this [example](report-config.yaml) for a better understanding of what repository information is required. @@ -124,6 +125,7 @@ Note: All fields are optional unless specified otherwise. * `globs` {{ mandatory }}: The list of file path globs to include for specified group. * `branches`: A list of branches to analyse for each repository. * `branch`: The name of the branch. + * `blurb`: The blurb to display for the branch. To display more detailed blurbs, use the `blurbs.md` file. * `authors`: A list of authors to analyse on the branch. * `author-git-host-id` {{ mandatory }}: Git host username of the author. * `author-display-name`: Display name of the author. diff --git a/docs/ug/report-config.yaml b/docs/ug/report-config.yaml index f9c2e7e454..f0c06093dc 100644 --- a/docs/ug/report-config.yaml +++ b/docs/ug/report-config.yaml @@ -19,4 +19,3 @@ repos: ignore-glob-list: - "**.md" file-size-limit: 2000000 - diff --git a/frontend/cypress/config/report-config.yaml b/frontend/cypress/config/report-config.yaml index 62f2009bf6..bb1f3321e6 100644 --- a/frontend/cypress/config/report-config.yaml +++ b/frontend/cypress/config/report-config.yaml @@ -21,4 +21,4 @@ repos: author-display-name: Francis Hodianto author-git-author-name: Francis Hodianto author-emails: - ignore-authors-list: \ No newline at end of file + ignore-authors-list: diff --git a/src/main/java/reposense/model/CliArguments.java b/src/main/java/reposense/model/CliArguments.java index 80b051d46f..130334a230 100644 --- a/src/main/java/reposense/model/CliArguments.java +++ b/src/main/java/reposense/model/CliArguments.java @@ -192,7 +192,7 @@ public double getOriginalityThreshold() { return originalityThreshold; } - public boolean isReportConfigRepositoriesConfigured() { + public boolean areReportConfigRepositoriesConfigured() { return reportConfiguration != null && !reportConfiguration.getReportRepoConfigurations().isEmpty(); } diff --git a/src/main/java/reposense/model/OneStopConfigRunConfiguration.java b/src/main/java/reposense/model/OneStopConfigRunConfiguration.java index b17838ed7b..98fd7ab1cc 100644 --- a/src/main/java/reposense/model/OneStopConfigRunConfiguration.java +++ b/src/main/java/reposense/model/OneStopConfigRunConfiguration.java @@ -44,13 +44,13 @@ public List getRepoConfigurations() throws InvalidLocationExc logger.info("Parsing " + rbd.getBranch() + "..."); RepoConfiguration.Builder builder = new RepoConfiguration.Builder() - .location(repoLocation); - - builder = builder.branch(rbd.getBranch()) + .location(repoLocation) + .branch(rbd.getBranch()) .ignoreGlobList(rbd.getIgnoreGlobList()) .ignoredAuthorsList(rbd.getIgnoreAuthorList()) .fileSizeLimit(rbd.getFileSizeLimit()) - .isStandaloneConfigIgnored(true); // remove this when we deprecated the standalone config + // Needs to be removed this when we deprecate the standalone config + .isStandaloneConfigIgnored(true); AuthorConfiguration authorConfiguration = new AuthorConfiguration(repoLocation, rbd.getBranch()); for (ReportAuthorDetails rad : rbd.getReportAuthorDetails()) { @@ -62,7 +62,6 @@ public List getRepoConfigurations() throws InvalidLocationExc author.setAuthorAliases(List.of(rad.getAuthorGitAuthorName())); authorConfiguration.addAuthor(author); - } authorConfigs.add(authorConfiguration); repoConfigs.add(builder.build()); diff --git a/src/main/java/reposense/model/RunConfigurationDecider.java b/src/main/java/reposense/model/RunConfigurationDecider.java index af283a5437..6a3cf2ebbc 100644 --- a/src/main/java/reposense/model/RunConfigurationDecider.java +++ b/src/main/java/reposense/model/RunConfigurationDecider.java @@ -9,7 +9,7 @@ public static RunConfiguration getRunConfiguration(CliArguments cliArguments) { return new CliRunConfiguration(cliArguments); } - if (cliArguments.isReportConfigRepositoriesConfigured()) { + if (cliArguments.areReportConfigRepositoriesConfigured()) { return new OneStopConfigRunConfiguration(cliArguments); } diff --git a/src/test/java/reposense/model/RunConfigurationDeciderTest.java b/src/test/java/reposense/model/RunConfigurationDeciderTest.java index 8ea8af1595..f6624a696a 100644 --- a/src/test/java/reposense/model/RunConfigurationDeciderTest.java +++ b/src/test/java/reposense/model/RunConfigurationDeciderTest.java @@ -70,7 +70,7 @@ public void parse_addRepos_returnsCliRunConfiguration() throws Exception { } @Test - public void parse_reposAndConfigNotSpecified_returnsConfigRunConfiguration() throws Exception { + public void parse_reposAndConfigNotSpecified_doesNotReturnCliRunConfiguration() throws Exception { String input = new InputBuilder() .addSinceDate("01/07/2017") .addUntilDate("30/11/2017") From aa1c2e7fe49ad6f4ffe5f33cd160763bb28217de Mon Sep 17 00:00:00 2001 From: LiYuan Date: Sat, 22 Feb 2025 03:30:19 +0800 Subject: [PATCH 18/42] Fix systemtest --- .../java/reposense/report/SummaryJson.java | 2 +- .../ConfigSystemTest/report-config.yaml | 23 +------------------ 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/src/main/java/reposense/report/SummaryJson.java b/src/main/java/reposense/report/SummaryJson.java index 4cd7c32419..324eb30b0a 100644 --- a/src/main/java/reposense/report/SummaryJson.java +++ b/src/main/java/reposense/report/SummaryJson.java @@ -40,7 +40,7 @@ public SummaryJson(List repos, ReportConfiguration reportConf this.repos = repos; this.reportGeneratedTime = reportGeneratedTime; this.reportGenerationTime = reportGenerationTime; - this.reportTitle = reportConfig == null ? "" : reportConfig.getTitle(); + this.reportTitle = reportConfig == null ? ReportConfiguration.DEFAULT_TITLE : reportConfig.getTitle(); this.sinceDate = sinceDate; this.untilDate = untilDate; this.isSinceDateProvided = isSinceDateProvided; diff --git a/src/systemtest/resources/ConfigSystemTest/report-config.yaml b/src/systemtest/resources/ConfigSystemTest/report-config.yaml index f9c2e7e454..271619c6ba 100644 --- a/src/systemtest/resources/ConfigSystemTest/report-config.yaml +++ b/src/systemtest/resources/ConfigSystemTest/report-config.yaml @@ -1,22 +1 @@ -title: Test RepoSense Report -repos: - - repo: https://github.com/reposense/testrepo-Delta.git - groups: - - group-name: code - globs: - - "**.java" - branches: - - branch: master - blurb: "My project" - authors: - - author-git-host-id: FH-30 - author-display-name: Francis Hodianto - author-git-author-name: Francis Hodianto - author-emails: - - fh@gmail.com - ignore-authors-list: - - bot - ignore-glob-list: - - "**.md" - file-size-limit: 2000000 - +title: RepoSense Report Test Title From 697c593bc5b191500c0ff7ec6329d06c495d4311 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Sun, 23 Feb 2025 00:08:35 +0800 Subject: [PATCH 19/42] Fix cypress test and add tests for code coverage --- config/report-config.yaml | 4 + frontend/cypress/config/report-config.yaml | 25 +---- .../OneStopConfigRunConfigurationTest.java | 91 +++++++++++++++++++ .../reportconfig/ReportAuthorDetailsTest.java | 38 ++++++++ .../reportconfig/ReportBranchDataTest.java | 46 ++++++++++ .../reportconfig/ReportConfigurationTest.java | 50 +++++++++- .../ReportGroupNameAndGlobsTest.java | 22 +++++ .../ReportRepoConfigurationTest.java | 49 ++++++++++ .../parser/ReportConfigYamlParserTest.java | 5 + 9 files changed, 305 insertions(+), 25 deletions(-) create mode 100644 src/test/java/reposense/model/OneStopConfigRunConfigurationTest.java diff --git a/config/report-config.yaml b/config/report-config.yaml index bb1f3321e6..b076f109b5 100644 --- a/config/report-config.yaml +++ b/config/report-config.yaml @@ -22,3 +22,7 @@ repos: author-git-author-name: Francis Hodianto author-emails: ignore-authors-list: + - bot + ignore-glob-list: + - "**.md" + file-size-limit: 2000000 diff --git a/frontend/cypress/config/report-config.yaml b/frontend/cypress/config/report-config.yaml index bb1f3321e6..a5d9bb7897 100644 --- a/frontend/cypress/config/report-config.yaml +++ b/frontend/cypress/config/report-config.yaml @@ -1,24 +1 @@ -title: RepoSense Report -repos: - - repo: https://github.com/reposense/testrepo-Delta.git - groups: - - group-name: code - globs: - - "**.java" - - group-name: tests - globs: - - "src/test**" - - group-name: docs - globs: - - "docs**" - - "**.adoc" - - "**.md" - branches: - - branch: master - blurb: "My project" - authors: - - author-git-host-id: FH-30 - author-display-name: Francis Hodianto - author-git-author-name: Francis Hodianto - author-emails: - ignore-authors-list: +title: RepoSense Test Report diff --git a/src/test/java/reposense/model/OneStopConfigRunConfigurationTest.java b/src/test/java/reposense/model/OneStopConfigRunConfigurationTest.java new file mode 100644 index 0000000000..1817250656 --- /dev/null +++ b/src/test/java/reposense/model/OneStopConfigRunConfigurationTest.java @@ -0,0 +1,91 @@ +package reposense.model; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import reposense.model.reportconfig.ReportAuthorDetails; +import reposense.model.reportconfig.ReportBranchData; +import reposense.model.reportconfig.ReportConfiguration; +import reposense.model.reportconfig.ReportGroupNameAndGlobs; +import reposense.model.reportconfig.ReportRepoConfiguration; +import reposense.parser.exceptions.InvalidLocationException; + +class OneStopConfigRunConfigurationTest { + + private static CliArguments testCliArguments; + private static List expectedRepoConfigurations; + + public static void setUpTestCliArguments() { + CliArguments.Builder testCliBuilder = new CliArguments.Builder(); + + List globPatterns = List.of("**.java"); + List groups = List.of(new ReportGroupNameAndGlobs("code", globPatterns)); + + List emailList = List.of("fh@gmail.com"); + ReportAuthorDetails author = new ReportAuthorDetails(emailList, "FH-30", + "Francis Hodianto", "Francis Hodianto"); + + List authorList = List.of(author); + List ignoreGlobList = List.of("**.md"); + List ignoreAuthorList = List.of("bot"); + ReportBranchData branch = new ReportBranchData("master", "My project", authorList, + ignoreGlobList, ignoreAuthorList, 2000000L); + + List branches = List.of(branch); + ReportRepoConfiguration repo = new ReportRepoConfiguration("https://github.com/reposense/testrepo-Delta.git", + groups, branches); + + List repos = List.of(repo); + ReportConfiguration expectedReportConfig = new ReportConfiguration("Test RepoSense Report", repos); + testCliBuilder.reportConfiguration(expectedReportConfig); + testCliArguments = testCliBuilder.build(); + } + + public static void setUpExpectedRepoConfigurations() throws InvalidLocationException { + expectedRepoConfigurations = new ArrayList<>(); + RepoLocation expectedRepoLocation = new RepoLocation("https://github.com/reposense/testrepo-Delta.git"); + List authorConfigs = new ArrayList<>(); + List groupConfigs = new ArrayList<>(); + + RepoConfiguration.Builder builder = new RepoConfiguration.Builder() + .location(expectedRepoLocation) + .branch("master") + .ignoreGlobList(List.of("**.md")) + .ignoredAuthorsList(List.of("bot")) + .fileSizeLimit(2000000L) + // Needs to be removed this when we deprecate the standalone config + .isStandaloneConfigIgnored(true); + + expectedRepoConfigurations.add(builder.build()); + + GroupConfiguration groupConfiguration = new GroupConfiguration(expectedRepoLocation); + groupConfiguration.addGroup(new FileType("code", List.of("**.java"))); + + groupConfigs.add(groupConfiguration); + + Author author = new Author("FH-30"); + author.setEmails(List.of("fh@gmail.com")); + author.setDisplayName("Francis Hodianto"); + author.setAuthorAliases(List.of("Francis Hodianto")); + + AuthorConfiguration authorConfiguration = new AuthorConfiguration(expectedRepoLocation, "master"); + authorConfiguration.addAuthor(author); + authorConfigs.add(authorConfiguration); + + RepoConfiguration.merge(expectedRepoConfigurations, authorConfigs); + RepoConfiguration.setGroupConfigsToRepos(expectedRepoConfigurations, groupConfigs); + } + + @Test + public void getRepoConfigurations_withValidInputs_returnsRepoConfigurations() throws InvalidLocationException { + setUpTestCliArguments(); + setUpExpectedRepoConfigurations(); + + OneStopConfigRunConfiguration config = new OneStopConfigRunConfiguration(testCliArguments); + List actualRepoConfigurations = config.getRepoConfigurations(); + Assertions.assertEquals(expectedRepoConfigurations, actualRepoConfigurations); + } +} diff --git a/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java b/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java index 15c2bb949e..2303fedd7e 100644 --- a/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java @@ -33,4 +33,42 @@ public void constructor_nullGitHostId_throwsIllegalArgumentException() { ); }); } + + @Test + public void equals_sameObject_success() { + ReportAuthorDetails details1 = new ReportAuthorDetails( + List.of("test@example.com"), + "gitHostId", + "Display Name", + "Git Author" + ); + + ReportAuthorDetails details2 = new ReportAuthorDetails( + List.of("test@example.com"), + "gitHostId", + "Display Name", + "Git Author" + ); + + Assertions.assertEquals(details1, details2); + } + + @Test + public void equals_differentObject_failure() { + ReportAuthorDetails details1 = new ReportAuthorDetails( + List.of("test1@example.com"), + "gitHostId", + "Display Name", + "Git Author" + ); + + ReportAuthorDetails details2 = new ReportAuthorDetails( + List.of("test@example.com"), + "gitHostId", + "Display Name", + "Git Author" + ); + + Assertions.assertNotEquals(details1, details2); + } } diff --git a/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java index 8bfcf3037d..5a6bb7deb3 100644 --- a/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java @@ -37,4 +37,50 @@ public void constructor_withNullInputs_shouldUseDefaultValues() { Assertions.assertEquals(new ArrayList<>(), data.getIgnoreAuthorList()); Assertions.assertEquals(ReportBranchData.DEFAULT_FILE_SIZE_LIMIT, data.getFileSizeLimit()); } + + @Test + public void equals_sameObject_success() { + ReportBranchData data1 = new ReportBranchData( + "main", + "Test blurb", + null, + List.of("*.log"), + List.of("bot"), + 2000000L + ); + + ReportBranchData data2 = new ReportBranchData( + "main", + "Test blurb", + null, + List.of("*.log"), + List.of("bot"), + 2000000L + ); + + Assertions.assertEquals(data1, data2); + } + + @Test + public void equals_differentObject_failure() { + ReportBranchData data1 = new ReportBranchData( + "main", + "Test blurb", + null, + List.of("*.log"), + List.of("bot"), + 2000000L + ); + + ReportBranchData data2 = new ReportBranchData( + "master", + "Test blurb", + null, + List.of("*.log"), + List.of("bot"), + 2000000L + ); + + Assertions.assertNotEquals(data1, data2); + } } diff --git a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java index d0304d580e..f93d501c4b 100644 --- a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java @@ -1,11 +1,41 @@ package reposense.model.reportconfig; import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import reposense.model.BlurbMap; + public class ReportConfigurationTest { + + private static ReportConfiguration expectedReportConfig; + + @BeforeAll + public static void setUp() { + List globPatterns = List.of("**.java"); + List groups = List.of(new ReportGroupNameAndGlobs("code", globPatterns)); + + List emailList = List.of("fh@gmail.com"); + ReportAuthorDetails author = new ReportAuthorDetails(emailList, "FH-30", + "Francis Hodianto", "Francis Hodianto"); + + List authorList = List.of(author); + List ignoreGlobList = List.of("**.md"); + List ignoreAuthorList = List.of("bot"); + ReportBranchData branch = new ReportBranchData("master", "My project", authorList, + ignoreGlobList, ignoreAuthorList, 2000000L); + + List branches = List.of(branch); + ReportRepoConfiguration repo = new ReportRepoConfiguration("https://github.com/reposense/testrepo-Delta.git", + groups, branches); + + List repos = List.of(repo); + expectedReportConfig = new ReportConfiguration("Test RepoSense Report", repos); + } + @Test public void constructor_withValidInputs_success() { ReportConfiguration reportConfiguration = new ReportConfiguration("My Report", new ArrayList<>()); @@ -15,6 +45,24 @@ public void constructor_withValidInputs_success() { @Test public void getTitle_equalsDefaultReturnValue_success() { Assertions.assertSame(ReportConfiguration.DEFAULT_TITLE, - new ReportConfiguration(null, new ArrayList<>()).getTitle()); + new ReportConfiguration(null, null).getTitle()); + } + + @Test + public void getBlurbMap_withValidInputs_success() { + BlurbMap expectedBlurbMap = new BlurbMap(); + expectedBlurbMap.withRecord("https://github.com/reposense/testrepo-Delta/tree/master", "My project"); + + Assertions.assertEquals(expectedBlurbMap, expectedReportConfig.getBlurbMap()); + } + + @Test + public void equals_withSameObject_success() { + Assertions.assertEquals(expectedReportConfig, expectedReportConfig); + } + + @Test + public void equals_withDifferentObject_failure() { + Assertions.assertNotEquals(new ReportConfiguration(), expectedReportConfig); } } diff --git a/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java b/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java index 788c3b2921..1880d7a50f 100644 --- a/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java @@ -5,6 +5,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import reposense.model.FileType; + public class ReportGroupNameAndGlobsTest { @Test public void constructor_withValidInputs_success() { @@ -27,4 +29,24 @@ public void constructor_nullGlobs_throwsIllegalArgumentException() { public void constructor_nullGroupNameAndNullGlobs_throwsIllegalArgumentException() { Assertions.assertThrows(IllegalArgumentException.class, () -> new ReportGroupNameAndGlobs(null, null)); } + + @Test + public void toFileType_success() { + ReportGroupNameAndGlobs reportGroupNameAndGlobs = new ReportGroupNameAndGlobs("My Group", List.of("code")); + Assertions.assertEquals(new FileType("My Group", List.of("code")), reportGroupNameAndGlobs.toFileType()); + } + + @Test + public void equals_sameObject_success() { + ReportGroupNameAndGlobs reportGroupNameAndGlobs1 = new ReportGroupNameAndGlobs("My Group", List.of("code")); + ReportGroupNameAndGlobs reportGroupNameAndGlobs2 = new ReportGroupNameAndGlobs("My Group", List.of("code")); + Assertions.assertEquals(reportGroupNameAndGlobs1, reportGroupNameAndGlobs2); + } + + @Test + public void equals_differentObject_failure() { + ReportGroupNameAndGlobs reportGroupNameAndGlobs1 = new ReportGroupNameAndGlobs("My Group", List.of("code")); + ReportGroupNameAndGlobs reportGroupNameAndGlobs2 = new ReportGroupNameAndGlobs("My Group", List.of("test")); + Assertions.assertNotEquals(reportGroupNameAndGlobs1, reportGroupNameAndGlobs2); + } } diff --git a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java index 856f5d5b52..0418553b36 100644 --- a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java @@ -6,6 +6,11 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import reposense.model.FileType; +import reposense.model.GroupConfiguration; +import reposense.model.RepoLocation; +import reposense.parser.exceptions.InvalidLocationException; + public class ReportRepoConfigurationTest { @Test void constructor_nullRepo_throwsIllegalArgumentException() { @@ -54,4 +59,48 @@ void getFullyQualifiedRepoNamesWithBlurbs_invalidUrl_throwsIllegalArgumentExcept Assertions.assertThrows(IllegalArgumentException.class, () -> config.getFullyQualifiedRepoNamesWithBlurbs()); } + + @Test + void getGroupConfiguration_validGroups_returnsCorrectMapping() throws InvalidLocationException { + List groups = List.of( + new ReportGroupNameAndGlobs("group1", List.of("glob1", "glob2")), + new ReportGroupNameAndGlobs("group2", List.of("glob3", "glob4")) + ); + ReportRepoConfiguration config = new ReportRepoConfiguration( + "https://github.com/test/repo.git", groups, null); + + RepoLocation repoLocation = new RepoLocation("https://github.com/test/repo.git"); + GroupConfiguration expectedGroupConfiguration = new GroupConfiguration(repoLocation); + expectedGroupConfiguration.addGroup(new FileType("group1", List.of("glob1", "glob2"))); + expectedGroupConfiguration.addGroup(new FileType("group2", List.of("glob3", "glob4"))); + + Assertions.assertEquals(expectedGroupConfiguration, + config.getGroupConfiguration(new RepoLocation("https://github.com/test/repo.git"))); + } + + @Test + void equals_sameObject_success() { + List groups = List.of( + new ReportGroupNameAndGlobs("group1", List.of("glob1", "glob2")), + new ReportGroupNameAndGlobs("group2", List.of("glob3", "glob4")) + ); + ReportRepoConfiguration config = new ReportRepoConfiguration( + "https://github.com/test/repo.git", groups, null); + + Assertions.assertEquals(config, config); + } + + @Test + void equals_differentObject_failure() { + List groups = List.of( + new ReportGroupNameAndGlobs("group1", List.of("glob1", "glob2")), + new ReportGroupNameAndGlobs("group2", List.of("glob3", "glob4")) + ); + ReportRepoConfiguration config1 = new ReportRepoConfiguration( + "https://github.com/dev/repo.git", groups, null); + ReportRepoConfiguration config2 = new ReportRepoConfiguration( + "https://github.com/test/repo.git", groups, null); + + Assertions.assertNotEquals(config1, config2); + } } diff --git a/src/test/java/reposense/parser/ReportConfigYamlParserTest.java b/src/test/java/reposense/parser/ReportConfigYamlParserTest.java index 7daa0abf4d..dbb7dfaef4 100644 --- a/src/test/java/reposense/parser/ReportConfigYamlParserTest.java +++ b/src/test/java/reposense/parser/ReportConfigYamlParserTest.java @@ -74,4 +74,9 @@ public void reportConfig_parseValidYamlFile_success() throws Exception { ReportConfiguration reportConfig = new ReportConfigYamlParser().parse(VALID_REPORT_CONFIG); Assertions.assertEquals(expectedReportConfig, reportConfig); } + + @Test + public void getType_validType_success() { + Assertions.assertEquals(ReportConfiguration.class, new ReportConfigYamlParser().getType()); + } } From f6c630dfb487c38f565b1760243985b4a0d65187 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Sun, 23 Feb 2025 00:37:38 +0800 Subject: [PATCH 20/42] Increase code coverage --- .../reportconfig/ReportAuthorDetailsTest.java | 28 ++++++++-------- .../reportconfig/ReportBranchDataTest.java | 32 ++++++++----------- .../reportconfig/ReportConfigurationTest.java | 2 +- .../ReportGroupNameAndGlobsTest.java | 18 +++++------ .../ReportRepoConfigurationTest.java | 19 ++++------- 5 files changed, 43 insertions(+), 56 deletions(-) diff --git a/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java b/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java index 2303fedd7e..399ab844cf 100644 --- a/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java @@ -6,6 +6,13 @@ import org.junit.jupiter.api.Test; public class ReportAuthorDetailsTest { + private static final ReportAuthorDetails details1 = new ReportAuthorDetails( + List.of("test@example.com"), + "gitHostId", + "Display Name", + "Git Author" + ); + @Test public void constructor_withValidInputs_success() { List emails = List.of("test@example.com", "test2@example.com"); @@ -36,13 +43,11 @@ public void constructor_nullGitHostId_throwsIllegalArgumentException() { @Test public void equals_sameObject_success() { - ReportAuthorDetails details1 = new ReportAuthorDetails( - List.of("test@example.com"), - "gitHostId", - "Display Name", - "Git Author" - ); + Assertions.assertEquals(details1, details1); + } + @Test + public void equals_equivalentObject_success() { ReportAuthorDetails details2 = new ReportAuthorDetails( List.of("test@example.com"), "gitHostId", @@ -55,20 +60,13 @@ public void equals_sameObject_success() { @Test public void equals_differentObject_failure() { - ReportAuthorDetails details1 = new ReportAuthorDetails( - List.of("test1@example.com"), - "gitHostId", - "Display Name", - "Git Author" - ); - ReportAuthorDetails details2 = new ReportAuthorDetails( - List.of("test@example.com"), + List.of("test1@example.com"), "gitHostId", "Display Name", "Git Author" ); - Assertions.assertNotEquals(details1, details2); + Assertions.assertFalse(details1.equals(details2)); } } diff --git a/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java index 5a6bb7deb3..5bc067fd67 100644 --- a/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java @@ -7,6 +7,15 @@ import org.junit.jupiter.api.Test; public class ReportBranchDataTest { + private static final ReportBranchData data1 = new ReportBranchData( + "main", + "Test blurb", + null, + List.of("*.log"), + List.of("bot"), + 2000000L + ); + @Test public void constructor_withValidInputs_success() { String branch = "main"; @@ -40,15 +49,11 @@ public void constructor_withNullInputs_shouldUseDefaultValues() { @Test public void equals_sameObject_success() { - ReportBranchData data1 = new ReportBranchData( - "main", - "Test blurb", - null, - List.of("*.log"), - List.of("bot"), - 2000000L - ); + Assertions.assertEquals(data1, data1); + } + @Test + public void equals_equivalentObject_success() { ReportBranchData data2 = new ReportBranchData( "main", "Test blurb", @@ -63,15 +68,6 @@ public void equals_sameObject_success() { @Test public void equals_differentObject_failure() { - ReportBranchData data1 = new ReportBranchData( - "main", - "Test blurb", - null, - List.of("*.log"), - List.of("bot"), - 2000000L - ); - ReportBranchData data2 = new ReportBranchData( "master", "Test blurb", @@ -81,6 +77,6 @@ public void equals_differentObject_failure() { 2000000L ); - Assertions.assertNotEquals(data1, data2); + Assertions.assertFalse(data1.equals(data2)); } } diff --git a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java index f93d501c4b..b9f8e0836d 100644 --- a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java @@ -63,6 +63,6 @@ public void equals_withSameObject_success() { @Test public void equals_withDifferentObject_failure() { - Assertions.assertNotEquals(new ReportConfiguration(), expectedReportConfig); + Assertions.assertFalse(expectedReportConfig.equals(new ReportConfiguration(null, null))); } } diff --git a/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java b/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java index 1880d7a50f..298acdbb24 100644 --- a/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java @@ -8,10 +8,14 @@ import reposense.model.FileType; public class ReportGroupNameAndGlobsTest { + private static final ReportGroupNameAndGlobs reportGroupNameAndGlobs1 = new ReportGroupNameAndGlobs("My Group", + List.of("code")); + @Test public void constructor_withValidInputs_success() { - ReportGroupNameAndGlobs reportGroupNameAndGlobs = new ReportGroupNameAndGlobs("My Group", List.of("code")); - Assertions.assertNotNull(reportGroupNameAndGlobs); + Assertions.assertNotNull(reportGroupNameAndGlobs1); + Assertions.assertEquals("My Group", reportGroupNameAndGlobs1.getGroupName()); + Assertions.assertEquals(List.of("code"), reportGroupNameAndGlobs1.getGlobs()); } @Test @@ -32,21 +36,17 @@ public void constructor_nullGroupNameAndNullGlobs_throwsIllegalArgumentException @Test public void toFileType_success() { - ReportGroupNameAndGlobs reportGroupNameAndGlobs = new ReportGroupNameAndGlobs("My Group", List.of("code")); - Assertions.assertEquals(new FileType("My Group", List.of("code")), reportGroupNameAndGlobs.toFileType()); + Assertions.assertEquals(new FileType("My Group", List.of("code")), reportGroupNameAndGlobs1.toFileType()); } @Test public void equals_sameObject_success() { - ReportGroupNameAndGlobs reportGroupNameAndGlobs1 = new ReportGroupNameAndGlobs("My Group", List.of("code")); - ReportGroupNameAndGlobs reportGroupNameAndGlobs2 = new ReportGroupNameAndGlobs("My Group", List.of("code")); - Assertions.assertEquals(reportGroupNameAndGlobs1, reportGroupNameAndGlobs2); + Assertions.assertEquals(reportGroupNameAndGlobs1, reportGroupNameAndGlobs1); } @Test public void equals_differentObject_failure() { - ReportGroupNameAndGlobs reportGroupNameAndGlobs1 = new ReportGroupNameAndGlobs("My Group", List.of("code")); ReportGroupNameAndGlobs reportGroupNameAndGlobs2 = new ReportGroupNameAndGlobs("My Group", List.of("test")); - Assertions.assertNotEquals(reportGroupNameAndGlobs1, reportGroupNameAndGlobs2); + Assertions.assertFalse(reportGroupNameAndGlobs1.equals(reportGroupNameAndGlobs2)); } } diff --git a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java index 0418553b36..3961860406 100644 --- a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java @@ -12,6 +12,11 @@ import reposense.parser.exceptions.InvalidLocationException; public class ReportRepoConfigurationTest { + private static final List groups = List.of( + new ReportGroupNameAndGlobs("group1", List.of("glob1", "glob2")), + new ReportGroupNameAndGlobs("group2", List.of("glob3", "glob4")) + ); + @Test void constructor_nullRepo_throwsIllegalArgumentException() { Assertions.assertThrows(IllegalArgumentException.class, () -> @@ -62,10 +67,6 @@ void getFullyQualifiedRepoNamesWithBlurbs_invalidUrl_throwsIllegalArgumentExcept @Test void getGroupConfiguration_validGroups_returnsCorrectMapping() throws InvalidLocationException { - List groups = List.of( - new ReportGroupNameAndGlobs("group1", List.of("glob1", "glob2")), - new ReportGroupNameAndGlobs("group2", List.of("glob3", "glob4")) - ); ReportRepoConfiguration config = new ReportRepoConfiguration( "https://github.com/test/repo.git", groups, null); @@ -80,10 +81,6 @@ void getGroupConfiguration_validGroups_returnsCorrectMapping() throws InvalidLoc @Test void equals_sameObject_success() { - List groups = List.of( - new ReportGroupNameAndGlobs("group1", List.of("glob1", "glob2")), - new ReportGroupNameAndGlobs("group2", List.of("glob3", "glob4")) - ); ReportRepoConfiguration config = new ReportRepoConfiguration( "https://github.com/test/repo.git", groups, null); @@ -92,15 +89,11 @@ void equals_sameObject_success() { @Test void equals_differentObject_failure() { - List groups = List.of( - new ReportGroupNameAndGlobs("group1", List.of("glob1", "glob2")), - new ReportGroupNameAndGlobs("group2", List.of("glob3", "glob4")) - ); ReportRepoConfiguration config1 = new ReportRepoConfiguration( "https://github.com/dev/repo.git", groups, null); ReportRepoConfiguration config2 = new ReportRepoConfiguration( "https://github.com/test/repo.git", groups, null); - Assertions.assertNotEquals(config1, config2); + Assertions.assertFalse(config1.equals(config2)); } } From 53df74e16f333a1ea76f4a2eafd5bce9beeacdd0 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Sun, 23 Feb 2025 00:58:28 +0800 Subject: [PATCH 21/42] Fix docs --- docs/ug/configFiles.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ug/configFiles.md b/docs/ug/configFiles.md index dbb8c31864..6d3e4b5da6 100644 --- a/docs/ug/configFiles.md +++ b/docs/ug/configFiles.md @@ -109,7 +109,7 @@ e.g.: `example.java` in `example-repo` can either be in the `test` group or the ## `report-config.yaml` -You can also optionally use a `report-config.yaml` file to quickly define the repository information for the repositories you are interested in tracking and generate your very own code portfolio. +You can also optionally use a `report-config.yaml` file to quickly define the repository information for the repositories you are interested in tracking and generating your very own code portfolio. The configurations of this file will override the configurations in the csv files if the file is present and correctly formatted. View this [example](report-config.yaml) for a better understanding of what repository information is required. From 40c60664cb723b9b6a2b77a60f66a84ba7ef08f9 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Thu, 20 Mar 2025 17:43:18 +0800 Subject: [PATCH 22/42] Add tests --- docs/ug/customizingReports.md | 8 ++++++ .../java/reposense/parser/ArgsParser.java | 9 +++---- .../reportconfig/ReportAuthorDetailsTest.java | 7 +++++- .../reportconfig/ReportBranchDataTest.java | 7 +++++- .../reportconfig/ReportConfigurationTest.java | 25 ++++++++++++++++++- .../ReportGroupNameAndGlobsTest.java | 7 +++++- .../ReportRepoConfigurationTest.java | 8 +++++- 7 files changed, 60 insertions(+), 11 deletions(-) diff --git a/docs/ug/customizingReports.md b/docs/ug/customizingReports.md index 741e7995d7..8192d00445 100644 --- a/docs/ug/customizingReports.md +++ b/docs/ug/customizingReports.md @@ -22,6 +22,14 @@ The report can be customized using several ways, as explained below. {{ embed("Appendix: **CLI syntax reference**", "cli.md") }} +### Customize using report-config.yaml + +**Another more user-friendly way to customize the report is by using the `report-config.yaml` file.** In this case you need to use the `--config` flag instead of the `--repo` flag when running RepoSense, as follows: + + {{ embed("Appendix: **CLI syntax reference → `config` flag**", "cli.md#section-config") }} + +Please note that this format will override the configurations in the CSV config files. + ### Customize using CSV config files diff --git a/src/main/java/reposense/parser/ArgsParser.java b/src/main/java/reposense/parser/ArgsParser.java index 2ecfedf435..0af3aefacc 100644 --- a/src/main/java/reposense/parser/ArgsParser.java +++ b/src/main/java/reposense/parser/ArgsParser.java @@ -94,7 +94,6 @@ public class ArgsParser { private static final String MESSAGE_USING_DEFAULT_CONFIG_PATH = "Config path not provided, using the config folder as default."; private static final String MESSAGE_INVALID_CONFIG_PATH = "%s is malformed."; - private static final String MESSAGE_INVALID_CONFIG_JSON = "%s Ignoring the report config provided."; private static final String MESSAGE_INVALID_CONFIG_YAML = "%s Ignoring the report config provided."; private static final String MESSAGE_INVALID_MARKDOWN_BLURBS = "%s Ignoring the blurb file provided."; private static final String MESSAGE_SINCE_D1_WITH_PERIOD = "You may be using --since d1 with the --period flag. " @@ -400,12 +399,10 @@ private static void addReportConfigToBuilder(CliArguments.Builder builder, Names builder.reportConfiguration(reportConfig); } catch (JsonSyntaxException jse) { logger.warning(String.format(MESSAGE_INVALID_CONFIG_PATH, reportConfigFilePath)); - } catch (IllegalArgumentException iae) { - logger.warning(String.format(MESSAGE_INVALID_CONFIG_YAML, iae.getMessage())); } catch (IOException ioe) { - // IOException thrown as report-config.yaml is not found. - // Ignore exception as the file is optional. - logger.log(Level.WARNING, "Error parsing report-config.yaml: " + ioe.getMessage(), ioe); + // IOException thrown as report-config.yaml is not found or fields are invalid. + logger.log(Level.WARNING, "Error parsing report-config.yaml: " + ioe.getMessage() + "\n" + + String.format(MESSAGE_INVALID_CONFIG_YAML, reportConfigFilePath)); } } } diff --git a/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java b/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java index 399ab844cf..c3d94913e9 100644 --- a/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportAuthorDetailsTest.java @@ -67,6 +67,11 @@ public void equals_differentObject_failure() { "Git Author" ); - Assertions.assertFalse(details1.equals(details2)); + Assertions.assertNotEquals(details1, details2); + } + + @Test + public void equals_differentClass_failure() { + Assertions.assertNotEquals(details1, new Object()); } } diff --git a/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java index 5bc067fd67..630a539884 100644 --- a/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportBranchDataTest.java @@ -77,6 +77,11 @@ public void equals_differentObject_failure() { 2000000L ); - Assertions.assertFalse(data1.equals(data2)); + Assertions.assertNotEquals(data1, data2); + } + + @Test + public void equals_differentClass_failure() { + Assertions.assertNotEquals(data1, new Object()); } } diff --git a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java index b9f8e0836d..1d4800fca3 100644 --- a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java @@ -12,6 +12,7 @@ public class ReportConfigurationTest { private static ReportConfiguration expectedReportConfig; + private static ReportConfiguration invalidReportConfig; @BeforeAll public static void setUp() { @@ -34,6 +35,13 @@ public static void setUp() { List repos = List.of(repo); expectedReportConfig = new ReportConfiguration("Test RepoSense Report", repos); + + // Invalid repo with missing .git + ReportRepoConfiguration invalidRepo = new ReportRepoConfiguration("https://github.com/reposense/testrepo-Delta", + groups, branches); + + List invalidRepos = List.of(invalidRepo); + invalidReportConfig = new ReportConfiguration("Test RepoSense Report", invalidRepos); } @Test @@ -56,6 +64,15 @@ public void getBlurbMap_withValidInputs_success() { Assertions.assertEquals(expectedBlurbMap, expectedReportConfig.getBlurbMap()); } + @Test + public void getBlurbMap_withInvalidInputs_returnEmptyBlurbMap() { + BlurbMap expectedBlurbMap = new BlurbMap(); + expectedBlurbMap.withRecord("https://github.com/reposense/testrepo-Delta/tree/master", "My project"); + + Assertions.assertNotEquals(expectedBlurbMap, invalidReportConfig.getBlurbMap()); + Assertions.assertEquals(new BlurbMap(), invalidReportConfig.getBlurbMap()); + } + @Test public void equals_withSameObject_success() { Assertions.assertEquals(expectedReportConfig, expectedReportConfig); @@ -63,6 +80,12 @@ public void equals_withSameObject_success() { @Test public void equals_withDifferentObject_failure() { - Assertions.assertFalse(expectedReportConfig.equals(new ReportConfiguration(null, null))); + Assertions.assertNotEquals(new ReportConfiguration(null, null), expectedReportConfig); + } + + @Test + public void equals_differentClass_failure() { + Assertions.assertNotEquals(new Object(), expectedReportConfig); } + } diff --git a/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java b/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java index 298acdbb24..c73300ba3a 100644 --- a/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java @@ -47,6 +47,11 @@ public void equals_sameObject_success() { @Test public void equals_differentObject_failure() { ReportGroupNameAndGlobs reportGroupNameAndGlobs2 = new ReportGroupNameAndGlobs("My Group", List.of("test")); - Assertions.assertFalse(reportGroupNameAndGlobs1.equals(reportGroupNameAndGlobs2)); + Assertions.assertNotEquals(reportGroupNameAndGlobs1, reportGroupNameAndGlobs2); + } + + @Test + public void equals_differentClass_failure() { + Assertions.assertNotEquals(new Object(), reportGroupNameAndGlobs1); } } diff --git a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java index 3961860406..a4b35c85f7 100644 --- a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java @@ -94,6 +94,12 @@ void equals_differentObject_failure() { ReportRepoConfiguration config2 = new ReportRepoConfiguration( "https://github.com/test/repo.git", groups, null); - Assertions.assertFalse(config1.equals(config2)); + Assertions.assertNotEquals(config1, config2); + } + + @Test + public void equals_differentClass_failure() { + Assertions.assertNotEquals(new Object(), new ReportRepoConfiguration( + "https://github.com/dev/repo.git", groups, null)); } } From 33a214075a11b88ed66c7d9c61421b5300d68bb7 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Fri, 21 Mar 2025 12:09:57 +0800 Subject: [PATCH 23/42] Add tests --- .../reposense/model/reportconfig/ReportConfigurationTest.java | 2 +- .../model/reportconfig/ReportGroupNameAndGlobsTest.java | 2 +- .../model/reportconfig/ReportRepoConfigurationTest.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java index 1d4800fca3..78157fc077 100644 --- a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java @@ -85,7 +85,7 @@ public void equals_withDifferentObject_failure() { @Test public void equals_differentClass_failure() { - Assertions.assertNotEquals(new Object(), expectedReportConfig); + Assertions.assertNotEquals(expectedReportConfig, new Object()); } } diff --git a/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java b/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java index c73300ba3a..f564c66d32 100644 --- a/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportGroupNameAndGlobsTest.java @@ -52,6 +52,6 @@ public void equals_differentObject_failure() { @Test public void equals_differentClass_failure() { - Assertions.assertNotEquals(new Object(), reportGroupNameAndGlobs1); + Assertions.assertNotEquals(reportGroupNameAndGlobs1, new Object()); } } diff --git a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java index a4b35c85f7..f2cfdb3850 100644 --- a/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportRepoConfigurationTest.java @@ -99,7 +99,7 @@ void equals_differentObject_failure() { @Test public void equals_differentClass_failure() { - Assertions.assertNotEquals(new Object(), new ReportRepoConfiguration( - "https://github.com/dev/repo.git", groups, null)); + Assertions.assertNotEquals(new ReportRepoConfiguration( + "https://github.com/dev/repo.git", groups, null), new Object()); } } From a47f3644cc269aa5267773532b014d306dd89fce Mon Sep 17 00:00:00 2001 From: LiYuan Date: Mon, 31 Mar 2025 14:15:07 +0800 Subject: [PATCH 24/42] System test --- .../types/ConfigFolderArgumentType.java | 9 +- .../reposense/ReportConfigSystemTest.java | 147 ++++++++++++++++++ .../authorship.json | 1 + .../commits.json | 1 + .../expected/summary.json | 1 + .../authorship.json | 1 + .../commits.json | 1 + .../30daysFromUntilDate/expected/summary.json | 1 + .../ReportConfigSystemTest/blurbs.md | 2 + .../ReportConfigSystemTest/report-config.yaml | 26 ++++ .../authorship.json | 1 + .../commits.json | 1 + .../expected/summary.json | 1 + .../authorship.json | 1 + .../commits.json | 1 + .../expected/summary.json | 1 + .../authorship.json | 1 + .../commits.json | 1 + .../summary.json | 1 + .../expected/summary.json | 1 + 20 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 src/systemtest/java/reposense/ReportConfigSystemTest.java create mode 100644 src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/authorship.json create mode 100644 src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json create mode 100644 src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/summary.json create mode 100644 src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/reposense_publish-RepoSense_master/authorship.json create mode 100644 src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/reposense_publish-RepoSense_master/commits.json create mode 100644 src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/summary.json create mode 100644 src/systemtest/resources/ReportConfigSystemTest/blurbs.md create mode 100644 src/systemtest/resources/ReportConfigSystemTest/report-config.yaml create mode 100644 src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/reposense_publish-RepoSense_master/authorship.json create mode 100644 src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/reposense_publish-RepoSense_master/commits.json create mode 100644 src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/summary.json create mode 100644 src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/reposense_publish-RepoSense_master/authorship.json create mode 100644 src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/reposense_publish-RepoSense_master/commits.json create mode 100644 src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/summary.json create mode 100644 src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/authorship.json create mode 100644 src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json create mode 100644 src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/summary.json create mode 100644 src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/summary.json diff --git a/src/main/java/reposense/parser/types/ConfigFolderArgumentType.java b/src/main/java/reposense/parser/types/ConfigFolderArgumentType.java index 4b75ba06cc..89c56cf1f9 100644 --- a/src/main/java/reposense/parser/types/ConfigFolderArgumentType.java +++ b/src/main/java/reposense/parser/types/ConfigFolderArgumentType.java @@ -10,13 +10,14 @@ import net.sourceforge.argparse4j.inf.ArgumentParserException; import net.sourceforge.argparse4j.inf.ArgumentType; import reposense.parser.RepoConfigCsvParser; +import reposense.parser.ReportConfigYamlParser; /** * Checks the argument of {@code --config} flag. */ public class ConfigFolderArgumentType implements ArgumentType { private static final String PARSE_EXCEPTION_MESSAGE_MISSING_REQUIRED_CONFIG_FILES = - "The required config file %s is not found in the specified folder."; + "The required config file %s or %s is not found in the specified folder."; @Override public Path convert(ArgumentParser parser, Argument arg, String value) throws ArgumentParserException { @@ -27,7 +28,11 @@ public Path convert(ArgumentParser parser, Argument arg, String value) throws Ar return Paths.get(value); } + if (Files.exists(Paths.get(value).resolve(ReportConfigYamlParser.REPORT_CONFIG_FILENAME))) { + return Paths.get(value); + } + throw new ArgumentParserException(String.format(PARSE_EXCEPTION_MESSAGE_MISSING_REQUIRED_CONFIG_FILES, - RepoConfigCsvParser.REPO_CONFIG_FILENAME), parser); + RepoConfigCsvParser.REPO_CONFIG_FILENAME, ReportConfigYamlParser.REPORT_CONFIG_FILENAME), parser); } } diff --git a/src/systemtest/java/reposense/ReportConfigSystemTest.java b/src/systemtest/java/reposense/ReportConfigSystemTest.java new file mode 100644 index 0000000000..bc7b2aee72 --- /dev/null +++ b/src/systemtest/java/reposense/ReportConfigSystemTest.java @@ -0,0 +1,147 @@ +package reposense; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import reposense.model.SupportedDomainUrlMap; +import reposense.parser.types.SinceDateArgumentType; +import reposense.report.ErrorSummary; +import reposense.util.FileUtil; +import reposense.util.InputBuilder; +import reposense.util.SystemTestUtil; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; + +import static org.apache.tools.ant.types.Commandline.translateCommandline; +import static reposense.util.TestUtil.loadResource; + +@Execution(ExecutionMode.SAME_THREAD) +public class ReportConfigSystemTest { + private static final List TESTING_FILE_FORMATS = Arrays.asList("py", "sh"); + private static final String TEST_TIME_ZONE = "Asia/Singapore"; + + private static final String OUTPUT_DIRECTORY = "rc_temp"; + private static final Path REPORT_DIRECTORY_PATH = Paths.get(OUTPUT_DIRECTORY, "reposense-report"); + + private static boolean didNotCloneRepoNormally = true; + + @BeforeEach + public void setUp() throws Exception { + SupportedDomainUrlMap.clearAccessedSet(); + FileUtil.deleteDirectory(OUTPUT_DIRECTORY); + ErrorSummary.getInstance().clearErrorSet(); + } + + @AfterEach + public void tearDown() throws Exception { + FileUtil.deleteDirectory(OUTPUT_DIRECTORY); + } + + /** + * System test with a specified until date and a {@link SinceDateArgumentType#FIRST_COMMIT_DATE_SHORTHAND} + * since date to capture from the first commit. + */ + @Test + public void testSinceBeginningDateRange() { + InputBuilder inputBuilder = initInputBuilder() + .addSinceDate(SinceDateArgumentType.FIRST_COMMIT_DATE_SHORTHAND) + .addUntilDate("31/12/2024"); + + runTest(inputBuilder, false, + "ReportConfigSystemTest/sinceBeginningDateRange/expected"); + } + + @Test + public void test30DaysFromUntilDate() { + InputBuilder inputBuilder = initInputBuilder().addUntilDate("31/8/2024"); + + runTest(inputBuilder, false, + "ReportConfigSystemTest/30daysFromUntilDate/expected"); + } + + + /** + * System test with a specified until date and a {@link SinceDateArgumentType#FIRST_COMMIT_DATE_SHORTHAND} + * since date to capture from the first commit, using shallow cloning. + */ + @Test + public void testSinceBeginningDateRangeWithShallowCloning() { + InputBuilder inputBuilder = initInputBuilder() + .addSinceDate(SinceDateArgumentType.FIRST_COMMIT_DATE_SHORTHAND) + .addUntilDate("31/8/2024") + .addShallowCloning(); + + runTest(inputBuilder, true, + "ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected"); + } + + @Test + public void test30DaysFromUntilDateWithShallowCloning() { + InputBuilder inputBuilder = initInputBuilder() + .addUntilDate("31/8/2024") + .addShallowCloning(); + + runTest(inputBuilder, true, + "ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected"); + } + + /** + * System test with a specified until date and a {@link SinceDateArgumentType#FIRST_COMMIT_DATE_SHORTHAND} + * since date to capture from the first commit. + */ + @Test + public void testSinceBeginningDateRangeWithPortfolio() { + InputBuilder inputBuilder = initInputBuilder() + .addSinceDate(SinceDateArgumentType.FIRST_COMMIT_DATE_SHORTHAND) + .addUntilDate("31/8/2024") + .addPortfolio(); + + runTest(inputBuilder, false, + "ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected"); + } + + /** + * Returns a {@link InputBuilder} that is initialized with some default values. + *
Config Folder Path: {@code ConfigSystemTest} + *
Formats: {@link ReportConfigSystemTest#TESTING_FILE_FORMATS TESTING_FILE_FORMATS} + *
Timezone: {@link ReportConfigSystemTest#TEST_TIME_ZONE TEST_TIME_ZONE} + *
Output Folder Path: {@link ReportConfigSystemTest#OUTPUT_DIRECTORY OUTPUT_DIRECTORY} + *
Test Mode: {@code Enabled} + */ + private InputBuilder initInputBuilder() { + Path configFolder = loadResource(getClass(), "ReportConfigSystemTest"); + String formats = String.join(" ", TESTING_FILE_FORMATS); + + return new InputBuilder().addConfig(configFolder) + .addFormats(formats) + .addTimezone(TEST_TIME_ZONE) + .addTestMode() + .addOutput(OUTPUT_DIRECTORY); + } + + /** + * Generates the testing report and compares it with the expected report. + * Re-generates a normal report after the testing finished if the first report is shallow-cloned. + * + * @param inputBuilder The input builder that contains the command line input specified by user. + * @param shouldFreshClone Boolean for whether to clone repo again if it has been cloned before. + * @param pathToResource The location at which files generated during the test are stored. + */ + private void runTest(InputBuilder inputBuilder, boolean shouldFreshClone, String pathToResource) { + if (shouldFreshClone || didNotCloneRepoNormally) { + inputBuilder = inputBuilder.addFreshCloning(); + } + + RepoSense.main(translateCommandline(inputBuilder.build())); + + Path actualFiles = loadResource(getClass(), pathToResource); + SystemTestUtil.verifyReportJsonFiles(actualFiles, REPORT_DIRECTORY_PATH); + + didNotCloneRepoNormally = inputBuilder.isShallowCloning(); + } +} diff --git a/src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/authorship.json b/src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/authorship.json new file mode 100644 index 0000000000..6e29968c55 --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/authorship.json @@ -0,0 +1 @@ +[{"path":"get-reposense.py","fileType":"other","lines":[{"lineNumber":1,"author":{"gitId":"-"},"content":"#!/usr/bin/env python","isFullCredit":false},{"lineNumber":2,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":3,"author":{"gitId":"-"},"content":"import argparse","isFullCredit":false},{"lineNumber":4,"author":{"gitId":"-"},"content":"import sys","isFullCredit":false},{"lineNumber":5,"author":{"gitId":"-"},"content":"import os","isFullCredit":false},{"lineNumber":6,"author":{"gitId":"-"},"content":"import shutil","isFullCredit":false},{"lineNumber":7,"author":{"gitId":"-"},"content":"import requests","isFullCredit":false},{"lineNumber":8,"author":{"gitId":"-"},"content":"import subprocess","isFullCredit":false},{"lineNumber":9,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":10,"author":{"gitId":"-"},"content":"JAR_FILENAME \u003d \u0027RepoSense.jar\u0027","isFullCredit":false},{"lineNumber":11,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":12,"author":{"gitId":"-"},"content":"def parse_args():","isFullCredit":false},{"lineNumber":13,"author":{"gitId":"-"},"content":" parser \u003d argparse.ArgumentParser(description\u003d\u0027Downloads a specific version of RepoSense.jar from our repository.\u0027)","isFullCredit":false},{"lineNumber":14,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":15,"author":{"gitId":"-"},"content":" group \u003d parser.add_mutually_exclusive_group()","isFullCredit":false},{"lineNumber":16,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-r\u0027, \u0027--release\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Get RepoSense.jar from the latest release (Stable)\u0027)","isFullCredit":false},{"lineNumber":17,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-m\u0027, \u0027--master\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Get RepoSense.jar from the latest master (Beta)\u0027)","isFullCredit":false},{"lineNumber":18,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-t\u0027, \u0027--tag\u0027, help\u003d\u0027Get RepoSense.jar of a specific release version tag\u0027)","isFullCredit":false},{"lineNumber":19,"author":{"gitId":"jedkohjk"},"content":" group.add_argument(\u0027-l\u0027, \u0027--latest\u0027, help\u003d\u0027Get RepoSense.jar of the latest release of a specific version tag\u0027)","isFullCredit":false},{"lineNumber":20,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-c\u0027, \u0027--commit\u0027, help\u003d\u0027Get RepoSense.jar of a specific commit\u0027)","isFullCredit":false},{"lineNumber":21,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":22,"author":{"gitId":"-"},"content":" parser.add_argument(\u0027-o\u0027, \u0027--overwrite\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Overwrite RepoSense.jar file, if exists. Default: false\u0027)","isFullCredit":false},{"lineNumber":23,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":24,"author":{"gitId":"-"},"content":" return parser.parse_args()","isFullCredit":false},{"lineNumber":25,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":26,"author":{"gitId":"jedkohjk"},"content":"def handle_latest_tag(tag):","isFullCredit":false},{"lineNumber":27,"author":{"gitId":"jedkohjk"},"content":" page \u003d 1","isFullCredit":false},{"lineNumber":28,"author":{"gitId":"jedkohjk"},"content":" major \u003d tag.strip(\u0027. \u0027).split(\u0027.\u0027)","isFullCredit":false},{"lineNumber":29,"author":{"gitId":"jedkohjk"},"content":" while True:","isFullCredit":false},{"lineNumber":30,"author":{"gitId":"jedkohjk"},"content":" response \u003d requests.get(f\u0027https://api.github.com/repos/reposense/RepoSense/releases?per_page\u003d100\u0026page\u003d{page}\u0027)","isFullCredit":false},{"lineNumber":31,"author":{"gitId":"jedkohjk"},"content":" if response.status_code in [403, 500]:","isFullCredit":false},{"lineNumber":32,"author":{"gitId":"jedkohjk"},"content":" print(\u0027GitHub API has exceed the rate limit.\u0027)","isFullCredit":false},{"lineNumber":33,"author":{"gitId":"jedkohjk"},"content":" exit(1)","isFullCredit":false},{"lineNumber":34,"author":{"gitId":"jedkohjk"},"content":" releases \u003d response.json()","isFullCredit":false},{"lineNumber":35,"author":{"gitId":"jedkohjk"},"content":" if not releases:","isFullCredit":false},{"lineNumber":36,"author":{"gitId":"jedkohjk"},"content":" print(\u0027Error, the provided tag does not exist!\u0027)","isFullCredit":false},{"lineNumber":37,"author":{"gitId":"jedkohjk"},"content":" exit(1)","isFullCredit":false},{"lineNumber":38,"author":{"gitId":"jedkohjk"},"content":" for i in releases:","isFullCredit":false},{"lineNumber":39,"author":{"gitId":"jedkohjk"},"content":" release_tag \u003d i[\u0027tag_name\u0027]","isFullCredit":false},{"lineNumber":40,"author":{"gitId":"jedkohjk"},"content":" if release_tag.strip(\u0027. \u0027).split(\u0027.\u0027)[:len(major)] \u003d\u003d major:","isFullCredit":false},{"lineNumber":41,"author":{"gitId":"jedkohjk"},"content":" handle_specific_release(release_tag)","isFullCredit":false},{"lineNumber":42,"author":{"gitId":"jedkohjk"},"content":" exit()","isFullCredit":false},{"lineNumber":43,"author":{"gitId":"jedkohjk"},"content":" page +\u003d 1","isFullCredit":false},{"lineNumber":44,"author":{"gitId":"jedkohjk"},"content":"","isFullCredit":false},{"lineNumber":45,"author":{"gitId":"-"},"content":"def handle_specific_commit(commit):","isFullCredit":false},{"lineNumber":46,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/commits/\u0027 + commit, commit\u003dcommit)","isFullCredit":false},{"lineNumber":47,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":48,"author":{"gitId":"-"},"content":"def handle_specific_release(tag):","isFullCredit":false},{"lineNumber":49,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/releases/tags/\u0027 + tag, tag\u003dtag)","isFullCredit":false},{"lineNumber":50,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":51,"author":{"gitId":"-"},"content":"def handle_latest_release():","isFullCredit":false},{"lineNumber":52,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/releases/latest\u0027)","isFullCredit":false},{"lineNumber":53,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":54,"author":{"gitId":"-"},"content":"def get_reposense_jar(url, tag\u003dNone, commit\u003dNone):","isFullCredit":false},{"lineNumber":55,"author":{"gitId":"-"},"content":" response \u003d requests.get(url)","isFullCredit":false},{"lineNumber":56,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":57,"author":{"gitId":"-"},"content":" if tag and response.status_code \u003d\u003d 404:","isFullCredit":false},{"lineNumber":58,"author":{"gitId":"-"},"content":" print(\u0027Error, the provided tag does not exist!\u0027)","isFullCredit":false},{"lineNumber":59,"author":{"gitId":"-"},"content":" exit(1)","isFullCredit":false},{"lineNumber":60,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":61,"author":{"gitId":"-"},"content":" if commit and response.status_code in [404, 422]:","isFullCredit":false},{"lineNumber":62,"author":{"gitId":"-"},"content":" print(\u0027Error, the provided commit does not exist!\u0027)","isFullCredit":false},{"lineNumber":63,"author":{"gitId":"-"},"content":" exit(1)","isFullCredit":false},{"lineNumber":64,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":65,"author":{"gitId":"-"},"content":" if response.status_code in [403, 500]:","isFullCredit":false},{"lineNumber":66,"author":{"gitId":"-"},"content":" print(\u0027GitHub API has exceed the rate limit. Falling back to alternative method...\u0027)","isFullCredit":false},{"lineNumber":67,"author":{"gitId":"-"},"content":" clone_and_make_reposense(tag\u003dtag, commit\u003dcommit)","isFullCredit":false},{"lineNumber":68,"author":{"gitId":"-"},"content":" return","isFullCredit":false},{"lineNumber":69,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":70,"author":{"gitId":"-"},"content":" if commit:","isFullCredit":false},{"lineNumber":71,"author":{"gitId":"-"},"content":" clone_and_make_reposense(commit\u003dcommit)","isFullCredit":false},{"lineNumber":72,"author":{"gitId":"-"},"content":" return","isFullCredit":false},{"lineNumber":73,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":74,"author":{"gitId":"-"},"content":" url \u003d response.json()[\u0027assets\u0027][0][\u0027browser_download_url\u0027]","isFullCredit":false},{"lineNumber":75,"author":{"gitId":"-"},"content":" download_file(url)","isFullCredit":false},{"lineNumber":76,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":77,"author":{"gitId":"-"},"content":"def clone_and_make_reposense(tag\u003dNone, commit\u003dNone):","isFullCredit":false},{"lineNumber":78,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":79,"author":{"gitId":"-"},"content":" # Cleanup cached RepoSense folder","isFullCredit":false},{"lineNumber":80,"author":{"gitId":"-"},"content":" shutil.rmtree(\u0027RepoSense\u0027, ignore_errors\u003dTrue)","isFullCredit":false},{"lineNumber":81,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":82,"author":{"gitId":"-"},"content":" command \u003d \\","isFullCredit":false},{"lineNumber":83,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":84,"author":{"gitId":"-"},"content":" git clone \u0027https://github.com/reposense/RepoSense.git\u0027 \u0026\u0026","isFullCredit":false},{"lineNumber":85,"author":{"gitId":"-"},"content":" cd RepoSense \u0026\u0026","isFullCredit":false},{"lineNumber":86,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":87,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":88,"author":{"gitId":"-"},"content":" if tag:","isFullCredit":false},{"lineNumber":89,"author":{"gitId":"-"},"content":" command +\u003d \u0027git checkout tags/{} -b deploy \u0026\u0026\u0027.format(tag)","isFullCredit":false},{"lineNumber":90,"author":{"gitId":"-"},"content":" elif commit:","isFullCredit":false},{"lineNumber":91,"author":{"gitId":"-"},"content":" command +\u003d \u0027git checkout {} -b deploy \u0026\u0026\u0027.format(commit)","isFullCredit":false},{"lineNumber":92,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":93,"author":{"gitId":"-"},"content":" command +\u003d \\","isFullCredit":false},{"lineNumber":94,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":95,"author":{"gitId":"-"},"content":" ./gradlew zipreport shadowjar \u0026\u0026","isFullCredit":false},{"lineNumber":96,"author":{"gitId":"-"},"content":" mv build/jar/RepoSense.jar ../","isFullCredit":false},{"lineNumber":97,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":98,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":99,"author":{"gitId":"-"},"content":" subprocess.check_call(command, shell\u003dTrue)","isFullCredit":false},{"lineNumber":100,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":101,"author":{"gitId":"-"},"content":"def download_file(url):","isFullCredit":false},{"lineNumber":102,"author":{"gitId":"-"},"content":" response \u003d requests.get(url, allow_redirects\u003dTrue)","isFullCredit":false},{"lineNumber":103,"author":{"gitId":"-"},"content":" open(JAR_FILENAME, \u0027wb\u0027).write(response.content)","isFullCredit":false},{"lineNumber":104,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":105,"author":{"gitId":"-"},"content":"if __name__ \u003d\u003d \"__main__\":","isFullCredit":false},{"lineNumber":106,"author":{"gitId":"-"},"content":" args \u003d parse_args()","isFullCredit":false},{"lineNumber":107,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":108,"author":{"gitId":"-"},"content":" if os.path.exists(JAR_FILENAME) and args.overwrite is False:","isFullCredit":false},{"lineNumber":109,"author":{"gitId":"-"},"content":" print(JAR_FILENAME + \u0027 already exists. Quitting.\u0027)","isFullCredit":false},{"lineNumber":110,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":111,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":112,"author":{"gitId":"-"},"content":" if args.tag:","isFullCredit":false},{"lineNumber":113,"author":{"gitId":"-"},"content":" handle_specific_release(args.tag)","isFullCredit":false},{"lineNumber":114,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":115,"author":{"gitId":"-"},"content":" ","isFullCredit":false},{"lineNumber":116,"author":{"gitId":"jedkohjk"},"content":" if args.latest:","isFullCredit":false},{"lineNumber":117,"author":{"gitId":"jedkohjk"},"content":" handle_latest_tag(args.latest)","isFullCredit":false},{"lineNumber":118,"author":{"gitId":"jedkohjk"},"content":" exit()","isFullCredit":false},{"lineNumber":119,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":120,"author":{"gitId":"-"},"content":" if args.commit:","isFullCredit":false},{"lineNumber":121,"author":{"gitId":"-"},"content":" handle_specific_commit(args.commit)","isFullCredit":false},{"lineNumber":122,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":123,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":124,"author":{"gitId":"-"},"content":" if args.master:","isFullCredit":false},{"lineNumber":125,"author":{"gitId":"-"},"content":" clone_and_make_reposense()","isFullCredit":false},{"lineNumber":126,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":127,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":128,"author":{"gitId":"-"},"content":" # If no arguments are provided or --release","isFullCredit":false},{"lineNumber":129,"author":{"gitId":"-"},"content":" handle_latest_release()","isFullCredit":false}],"authorContributionMap":{"jedkohjk":23,"-":106}},{"path":"run.sh","fileType":"other","lines":[{"lineNumber":1,"author":{"gitId":"-"},"content":"#!/bin/bash","isFullCredit":false},{"lineNumber":2,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":3,"author":{"gitId":"-"},"content":"# Downloads a specific version of RepoSense.jar of your choice from our repository","isFullCredit":false},{"lineNumber":4,"author":{"gitId":"-"},"content":"## Examples of supported options:","isFullCredit":false},{"lineNumber":5,"author":{"gitId":"-"},"content":"### ./get-reposense.py --release # Gets the latest release (Stable)","isFullCredit":false},{"lineNumber":6,"author":{"gitId":"-"},"content":"### ./get-reposense.py --master # Gets the latest master (Beta)","isFullCredit":false},{"lineNumber":7,"author":{"gitId":"-"},"content":"### ./get-reposense.py --tag v1.6.1 # Gets a specific version","isFullCredit":false},{"lineNumber":8,"author":{"gitId":"jedkohjk"},"content":"### ./get-reposense.py --latest v1.6 # Gets the latest version with the given tag prefix e.g. v1.6.1","isFullCredit":false},{"lineNumber":9,"author":{"gitId":"-"},"content":"### ./get-reposense.py --commit abc123 # Gets a specific commit","isFullCredit":false},{"lineNumber":10,"author":{"gitId":"-"},"content":"### ./get-reposense.py --release --overwrite # Overwrite RepoSense.jar, if exists, with the latest release","isFullCredit":false},{"lineNumber":11,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":12,"author":{"gitId":"-"},"content":"./get-reposense.py --release","isFullCredit":false},{"lineNumber":13,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":14,"author":{"gitId":"-"},"content":"# Executes RepoSense","isFullCredit":false},{"lineNumber":15,"author":{"gitId":"-"},"content":"# Do not change the default output folder name (reposense-report)","isFullCredit":false},{"lineNumber":16,"author":{"gitId":"-"},"content":"## Examples of other valid options; For more, please view the user guide","isFullCredit":false},{"lineNumber":17,"author":{"gitId":"-"},"content":"### java -jar RepoSense.jar --repos https://github.com/reposense/RepoSense.git","isFullCredit":false},{"lineNumber":18,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":19,"author":{"gitId":"-"},"content":"java -jar RepoSense.jar --config ./configs","isFullCredit":false}],"authorContributionMap":{"jedkohjk":1,"-":18}}] diff --git a/src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json b/src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json new file mode 100644 index 0000000000..df2e506560 --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json @@ -0,0 +1 @@ +{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":24}},"authorContributionVariance":{"jedkohjk":41.209164},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} diff --git a/src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/summary.json new file mode 100644 index 0000000000..76b36937d0 --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/summary.json @@ -0,0 +1 @@ +{"reportGeneratedTime":"Mon, 31 Mar 2025 13:34:36 SGT","reportGenerationTime":" 3.40 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"2024-07-31","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-07-31","untilDate":"2024-08-31","isSinceDateProvided":false,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense.git":"Publish branch of Reposense","https://github.com/reposense/publish-RepoSense/tree/master":"My project"}},"isPortfolio":false} diff --git a/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/reposense_publish-RepoSense_master/authorship.json b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/reposense_publish-RepoSense_master/authorship.json new file mode 100644 index 0000000000..6e29968c55 --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/reposense_publish-RepoSense_master/authorship.json @@ -0,0 +1 @@ +[{"path":"get-reposense.py","fileType":"other","lines":[{"lineNumber":1,"author":{"gitId":"-"},"content":"#!/usr/bin/env python","isFullCredit":false},{"lineNumber":2,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":3,"author":{"gitId":"-"},"content":"import argparse","isFullCredit":false},{"lineNumber":4,"author":{"gitId":"-"},"content":"import sys","isFullCredit":false},{"lineNumber":5,"author":{"gitId":"-"},"content":"import os","isFullCredit":false},{"lineNumber":6,"author":{"gitId":"-"},"content":"import shutil","isFullCredit":false},{"lineNumber":7,"author":{"gitId":"-"},"content":"import requests","isFullCredit":false},{"lineNumber":8,"author":{"gitId":"-"},"content":"import subprocess","isFullCredit":false},{"lineNumber":9,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":10,"author":{"gitId":"-"},"content":"JAR_FILENAME \u003d \u0027RepoSense.jar\u0027","isFullCredit":false},{"lineNumber":11,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":12,"author":{"gitId":"-"},"content":"def parse_args():","isFullCredit":false},{"lineNumber":13,"author":{"gitId":"-"},"content":" parser \u003d argparse.ArgumentParser(description\u003d\u0027Downloads a specific version of RepoSense.jar from our repository.\u0027)","isFullCredit":false},{"lineNumber":14,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":15,"author":{"gitId":"-"},"content":" group \u003d parser.add_mutually_exclusive_group()","isFullCredit":false},{"lineNumber":16,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-r\u0027, \u0027--release\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Get RepoSense.jar from the latest release (Stable)\u0027)","isFullCredit":false},{"lineNumber":17,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-m\u0027, \u0027--master\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Get RepoSense.jar from the latest master (Beta)\u0027)","isFullCredit":false},{"lineNumber":18,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-t\u0027, \u0027--tag\u0027, help\u003d\u0027Get RepoSense.jar of a specific release version tag\u0027)","isFullCredit":false},{"lineNumber":19,"author":{"gitId":"jedkohjk"},"content":" group.add_argument(\u0027-l\u0027, \u0027--latest\u0027, help\u003d\u0027Get RepoSense.jar of the latest release of a specific version tag\u0027)","isFullCredit":false},{"lineNumber":20,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-c\u0027, \u0027--commit\u0027, help\u003d\u0027Get RepoSense.jar of a specific commit\u0027)","isFullCredit":false},{"lineNumber":21,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":22,"author":{"gitId":"-"},"content":" parser.add_argument(\u0027-o\u0027, \u0027--overwrite\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Overwrite RepoSense.jar file, if exists. Default: false\u0027)","isFullCredit":false},{"lineNumber":23,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":24,"author":{"gitId":"-"},"content":" return parser.parse_args()","isFullCredit":false},{"lineNumber":25,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":26,"author":{"gitId":"jedkohjk"},"content":"def handle_latest_tag(tag):","isFullCredit":false},{"lineNumber":27,"author":{"gitId":"jedkohjk"},"content":" page \u003d 1","isFullCredit":false},{"lineNumber":28,"author":{"gitId":"jedkohjk"},"content":" major \u003d tag.strip(\u0027. \u0027).split(\u0027.\u0027)","isFullCredit":false},{"lineNumber":29,"author":{"gitId":"jedkohjk"},"content":" while True:","isFullCredit":false},{"lineNumber":30,"author":{"gitId":"jedkohjk"},"content":" response \u003d requests.get(f\u0027https://api.github.com/repos/reposense/RepoSense/releases?per_page\u003d100\u0026page\u003d{page}\u0027)","isFullCredit":false},{"lineNumber":31,"author":{"gitId":"jedkohjk"},"content":" if response.status_code in [403, 500]:","isFullCredit":false},{"lineNumber":32,"author":{"gitId":"jedkohjk"},"content":" print(\u0027GitHub API has exceed the rate limit.\u0027)","isFullCredit":false},{"lineNumber":33,"author":{"gitId":"jedkohjk"},"content":" exit(1)","isFullCredit":false},{"lineNumber":34,"author":{"gitId":"jedkohjk"},"content":" releases \u003d response.json()","isFullCredit":false},{"lineNumber":35,"author":{"gitId":"jedkohjk"},"content":" if not releases:","isFullCredit":false},{"lineNumber":36,"author":{"gitId":"jedkohjk"},"content":" print(\u0027Error, the provided tag does not exist!\u0027)","isFullCredit":false},{"lineNumber":37,"author":{"gitId":"jedkohjk"},"content":" exit(1)","isFullCredit":false},{"lineNumber":38,"author":{"gitId":"jedkohjk"},"content":" for i in releases:","isFullCredit":false},{"lineNumber":39,"author":{"gitId":"jedkohjk"},"content":" release_tag \u003d i[\u0027tag_name\u0027]","isFullCredit":false},{"lineNumber":40,"author":{"gitId":"jedkohjk"},"content":" if release_tag.strip(\u0027. \u0027).split(\u0027.\u0027)[:len(major)] \u003d\u003d major:","isFullCredit":false},{"lineNumber":41,"author":{"gitId":"jedkohjk"},"content":" handle_specific_release(release_tag)","isFullCredit":false},{"lineNumber":42,"author":{"gitId":"jedkohjk"},"content":" exit()","isFullCredit":false},{"lineNumber":43,"author":{"gitId":"jedkohjk"},"content":" page +\u003d 1","isFullCredit":false},{"lineNumber":44,"author":{"gitId":"jedkohjk"},"content":"","isFullCredit":false},{"lineNumber":45,"author":{"gitId":"-"},"content":"def handle_specific_commit(commit):","isFullCredit":false},{"lineNumber":46,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/commits/\u0027 + commit, commit\u003dcommit)","isFullCredit":false},{"lineNumber":47,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":48,"author":{"gitId":"-"},"content":"def handle_specific_release(tag):","isFullCredit":false},{"lineNumber":49,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/releases/tags/\u0027 + tag, tag\u003dtag)","isFullCredit":false},{"lineNumber":50,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":51,"author":{"gitId":"-"},"content":"def handle_latest_release():","isFullCredit":false},{"lineNumber":52,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/releases/latest\u0027)","isFullCredit":false},{"lineNumber":53,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":54,"author":{"gitId":"-"},"content":"def get_reposense_jar(url, tag\u003dNone, commit\u003dNone):","isFullCredit":false},{"lineNumber":55,"author":{"gitId":"-"},"content":" response \u003d requests.get(url)","isFullCredit":false},{"lineNumber":56,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":57,"author":{"gitId":"-"},"content":" if tag and response.status_code \u003d\u003d 404:","isFullCredit":false},{"lineNumber":58,"author":{"gitId":"-"},"content":" print(\u0027Error, the provided tag does not exist!\u0027)","isFullCredit":false},{"lineNumber":59,"author":{"gitId":"-"},"content":" exit(1)","isFullCredit":false},{"lineNumber":60,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":61,"author":{"gitId":"-"},"content":" if commit and response.status_code in [404, 422]:","isFullCredit":false},{"lineNumber":62,"author":{"gitId":"-"},"content":" print(\u0027Error, the provided commit does not exist!\u0027)","isFullCredit":false},{"lineNumber":63,"author":{"gitId":"-"},"content":" exit(1)","isFullCredit":false},{"lineNumber":64,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":65,"author":{"gitId":"-"},"content":" if response.status_code in [403, 500]:","isFullCredit":false},{"lineNumber":66,"author":{"gitId":"-"},"content":" print(\u0027GitHub API has exceed the rate limit. Falling back to alternative method...\u0027)","isFullCredit":false},{"lineNumber":67,"author":{"gitId":"-"},"content":" clone_and_make_reposense(tag\u003dtag, commit\u003dcommit)","isFullCredit":false},{"lineNumber":68,"author":{"gitId":"-"},"content":" return","isFullCredit":false},{"lineNumber":69,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":70,"author":{"gitId":"-"},"content":" if commit:","isFullCredit":false},{"lineNumber":71,"author":{"gitId":"-"},"content":" clone_and_make_reposense(commit\u003dcommit)","isFullCredit":false},{"lineNumber":72,"author":{"gitId":"-"},"content":" return","isFullCredit":false},{"lineNumber":73,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":74,"author":{"gitId":"-"},"content":" url \u003d response.json()[\u0027assets\u0027][0][\u0027browser_download_url\u0027]","isFullCredit":false},{"lineNumber":75,"author":{"gitId":"-"},"content":" download_file(url)","isFullCredit":false},{"lineNumber":76,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":77,"author":{"gitId":"-"},"content":"def clone_and_make_reposense(tag\u003dNone, commit\u003dNone):","isFullCredit":false},{"lineNumber":78,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":79,"author":{"gitId":"-"},"content":" # Cleanup cached RepoSense folder","isFullCredit":false},{"lineNumber":80,"author":{"gitId":"-"},"content":" shutil.rmtree(\u0027RepoSense\u0027, ignore_errors\u003dTrue)","isFullCredit":false},{"lineNumber":81,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":82,"author":{"gitId":"-"},"content":" command \u003d \\","isFullCredit":false},{"lineNumber":83,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":84,"author":{"gitId":"-"},"content":" git clone \u0027https://github.com/reposense/RepoSense.git\u0027 \u0026\u0026","isFullCredit":false},{"lineNumber":85,"author":{"gitId":"-"},"content":" cd RepoSense \u0026\u0026","isFullCredit":false},{"lineNumber":86,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":87,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":88,"author":{"gitId":"-"},"content":" if tag:","isFullCredit":false},{"lineNumber":89,"author":{"gitId":"-"},"content":" command +\u003d \u0027git checkout tags/{} -b deploy \u0026\u0026\u0027.format(tag)","isFullCredit":false},{"lineNumber":90,"author":{"gitId":"-"},"content":" elif commit:","isFullCredit":false},{"lineNumber":91,"author":{"gitId":"-"},"content":" command +\u003d \u0027git checkout {} -b deploy \u0026\u0026\u0027.format(commit)","isFullCredit":false},{"lineNumber":92,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":93,"author":{"gitId":"-"},"content":" command +\u003d \\","isFullCredit":false},{"lineNumber":94,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":95,"author":{"gitId":"-"},"content":" ./gradlew zipreport shadowjar \u0026\u0026","isFullCredit":false},{"lineNumber":96,"author":{"gitId":"-"},"content":" mv build/jar/RepoSense.jar ../","isFullCredit":false},{"lineNumber":97,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":98,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":99,"author":{"gitId":"-"},"content":" subprocess.check_call(command, shell\u003dTrue)","isFullCredit":false},{"lineNumber":100,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":101,"author":{"gitId":"-"},"content":"def download_file(url):","isFullCredit":false},{"lineNumber":102,"author":{"gitId":"-"},"content":" response \u003d requests.get(url, allow_redirects\u003dTrue)","isFullCredit":false},{"lineNumber":103,"author":{"gitId":"-"},"content":" open(JAR_FILENAME, \u0027wb\u0027).write(response.content)","isFullCredit":false},{"lineNumber":104,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":105,"author":{"gitId":"-"},"content":"if __name__ \u003d\u003d \"__main__\":","isFullCredit":false},{"lineNumber":106,"author":{"gitId":"-"},"content":" args \u003d parse_args()","isFullCredit":false},{"lineNumber":107,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":108,"author":{"gitId":"-"},"content":" if os.path.exists(JAR_FILENAME) and args.overwrite is False:","isFullCredit":false},{"lineNumber":109,"author":{"gitId":"-"},"content":" print(JAR_FILENAME + \u0027 already exists. Quitting.\u0027)","isFullCredit":false},{"lineNumber":110,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":111,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":112,"author":{"gitId":"-"},"content":" if args.tag:","isFullCredit":false},{"lineNumber":113,"author":{"gitId":"-"},"content":" handle_specific_release(args.tag)","isFullCredit":false},{"lineNumber":114,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":115,"author":{"gitId":"-"},"content":" ","isFullCredit":false},{"lineNumber":116,"author":{"gitId":"jedkohjk"},"content":" if args.latest:","isFullCredit":false},{"lineNumber":117,"author":{"gitId":"jedkohjk"},"content":" handle_latest_tag(args.latest)","isFullCredit":false},{"lineNumber":118,"author":{"gitId":"jedkohjk"},"content":" exit()","isFullCredit":false},{"lineNumber":119,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":120,"author":{"gitId":"-"},"content":" if args.commit:","isFullCredit":false},{"lineNumber":121,"author":{"gitId":"-"},"content":" handle_specific_commit(args.commit)","isFullCredit":false},{"lineNumber":122,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":123,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":124,"author":{"gitId":"-"},"content":" if args.master:","isFullCredit":false},{"lineNumber":125,"author":{"gitId":"-"},"content":" clone_and_make_reposense()","isFullCredit":false},{"lineNumber":126,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":127,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":128,"author":{"gitId":"-"},"content":" # If no arguments are provided or --release","isFullCredit":false},{"lineNumber":129,"author":{"gitId":"-"},"content":" handle_latest_release()","isFullCredit":false}],"authorContributionMap":{"jedkohjk":23,"-":106}},{"path":"run.sh","fileType":"other","lines":[{"lineNumber":1,"author":{"gitId":"-"},"content":"#!/bin/bash","isFullCredit":false},{"lineNumber":2,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":3,"author":{"gitId":"-"},"content":"# Downloads a specific version of RepoSense.jar of your choice from our repository","isFullCredit":false},{"lineNumber":4,"author":{"gitId":"-"},"content":"## Examples of supported options:","isFullCredit":false},{"lineNumber":5,"author":{"gitId":"-"},"content":"### ./get-reposense.py --release # Gets the latest release (Stable)","isFullCredit":false},{"lineNumber":6,"author":{"gitId":"-"},"content":"### ./get-reposense.py --master # Gets the latest master (Beta)","isFullCredit":false},{"lineNumber":7,"author":{"gitId":"-"},"content":"### ./get-reposense.py --tag v1.6.1 # Gets a specific version","isFullCredit":false},{"lineNumber":8,"author":{"gitId":"jedkohjk"},"content":"### ./get-reposense.py --latest v1.6 # Gets the latest version with the given tag prefix e.g. v1.6.1","isFullCredit":false},{"lineNumber":9,"author":{"gitId":"-"},"content":"### ./get-reposense.py --commit abc123 # Gets a specific commit","isFullCredit":false},{"lineNumber":10,"author":{"gitId":"-"},"content":"### ./get-reposense.py --release --overwrite # Overwrite RepoSense.jar, if exists, with the latest release","isFullCredit":false},{"lineNumber":11,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":12,"author":{"gitId":"-"},"content":"./get-reposense.py --release","isFullCredit":false},{"lineNumber":13,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":14,"author":{"gitId":"-"},"content":"# Executes RepoSense","isFullCredit":false},{"lineNumber":15,"author":{"gitId":"-"},"content":"# Do not change the default output folder name (reposense-report)","isFullCredit":false},{"lineNumber":16,"author":{"gitId":"-"},"content":"## Examples of other valid options; For more, please view the user guide","isFullCredit":false},{"lineNumber":17,"author":{"gitId":"-"},"content":"### java -jar RepoSense.jar --repos https://github.com/reposense/RepoSense.git","isFullCredit":false},{"lineNumber":18,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":19,"author":{"gitId":"-"},"content":"java -jar RepoSense.jar --config ./configs","isFullCredit":false}],"authorContributionMap":{"jedkohjk":1,"-":18}}] diff --git a/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/reposense_publish-RepoSense_master/commits.json b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/reposense_publish-RepoSense_master/commits.json new file mode 100644 index 0000000000..df2e506560 --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/reposense_publish-RepoSense_master/commits.json @@ -0,0 +1 @@ +{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":24}},"authorContributionVariance":{"jedkohjk":41.209164},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} diff --git a/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/summary.json new file mode 100644 index 0000000000..c7e6d1cd91 --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/summary.json @@ -0,0 +1 @@ +{"reportGeneratedTime":"Mon, 31 Mar 2025 13:26:14 SGT","reportGenerationTime":" 6.92 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"2024-07-31","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-07-31","untilDate":"2024-08-31","isSinceDateProvided":false,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense.git":"Publish branch of Reposense","https://github.com/reposense/publish-RepoSense/tree/master":"My project"}},"isPortfolio":false} diff --git a/src/systemtest/resources/ReportConfigSystemTest/blurbs.md b/src/systemtest/resources/ReportConfigSystemTest/blurbs.md new file mode 100644 index 0000000000..1f2fd9248f --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/blurbs.md @@ -0,0 +1,2 @@ +https://github.com/reposense/publish-RepoSense.git +Publish branch of Reposense \ No newline at end of file diff --git a/src/systemtest/resources/ReportConfigSystemTest/report-config.yaml b/src/systemtest/resources/ReportConfigSystemTest/report-config.yaml new file mode 100644 index 0000000000..654f5e6236 --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/report-config.yaml @@ -0,0 +1,26 @@ +title: RepoSense Report +repos: + - repo: https://github.com/reposense/publish-RepoSense.git + groups: + - group-name: code + globs: + - "**.java" + - group-name: tests + globs: + - "src/test**" + - group-name: docs + globs: + - "docs**" + - "**.adoc" + - "**.md" + branches: + - branch: master + blurb: "My project" + authors: + - author-git-host-id: jedkohjk + author-display-name: jedkohjk + author-git-author-name: jedkohjk + author-emails: + ignore-authors-list: + ignore-glob-list: + file-size-limit: 2000000 diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/reposense_publish-RepoSense_master/authorship.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/reposense_publish-RepoSense_master/authorship.json new file mode 100644 index 0000000000..0c971dca68 --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/reposense_publish-RepoSense_master/authorship.json @@ -0,0 +1 @@ +[{"path":"get-reposense.py","fileType":"other","lines":[{"lineNumber":1,"author":{"gitId":"-"},"content":"#!/usr/bin/env python","isFullCredit":false},{"lineNumber":2,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":3,"author":{"gitId":"-"},"content":"import argparse","isFullCredit":false},{"lineNumber":4,"author":{"gitId":"-"},"content":"import sys","isFullCredit":false},{"lineNumber":5,"author":{"gitId":"-"},"content":"import os","isFullCredit":false},{"lineNumber":6,"author":{"gitId":"-"},"content":"import shutil","isFullCredit":false},{"lineNumber":7,"author":{"gitId":"-"},"content":"import requests","isFullCredit":false},{"lineNumber":8,"author":{"gitId":"-"},"content":"import subprocess","isFullCredit":false},{"lineNumber":9,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":10,"author":{"gitId":"-"},"content":"JAR_FILENAME \u003d \u0027RepoSense.jar\u0027","isFullCredit":false},{"lineNumber":11,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":12,"author":{"gitId":"-"},"content":"def parse_args():","isFullCredit":false},{"lineNumber":13,"author":{"gitId":"-"},"content":" parser \u003d argparse.ArgumentParser(description\u003d\u0027Downloads a specific version of RepoSense.jar from our repository.\u0027)","isFullCredit":false},{"lineNumber":14,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":15,"author":{"gitId":"-"},"content":" group \u003d parser.add_mutually_exclusive_group()","isFullCredit":false},{"lineNumber":16,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-r\u0027, \u0027--release\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Get RepoSense.jar from the latest release (Stable)\u0027)","isFullCredit":false},{"lineNumber":17,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-m\u0027, \u0027--master\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Get RepoSense.jar from the latest master (Beta)\u0027)","isFullCredit":false},{"lineNumber":18,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-t\u0027, \u0027--tag\u0027, help\u003d\u0027Get RepoSense.jar of a specific release version tag\u0027)","isFullCredit":false},{"lineNumber":19,"author":{"gitId":"jedkohjk"},"content":" group.add_argument(\u0027-l\u0027, \u0027--latest\u0027, help\u003d\u0027Get RepoSense.jar of the latest release of a specific version tag\u0027)","isFullCredit":false},{"lineNumber":20,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-c\u0027, \u0027--commit\u0027, help\u003d\u0027Get RepoSense.jar of a specific commit\u0027)","isFullCredit":false},{"lineNumber":21,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":22,"author":{"gitId":"-"},"content":" parser.add_argument(\u0027-o\u0027, \u0027--overwrite\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Overwrite RepoSense.jar file, if exists. Default: false\u0027)","isFullCredit":false},{"lineNumber":23,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":24,"author":{"gitId":"-"},"content":" return parser.parse_args()","isFullCredit":false},{"lineNumber":25,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":26,"author":{"gitId":"jedkohjk"},"content":"def handle_latest_tag(tag):","isFullCredit":false},{"lineNumber":27,"author":{"gitId":"jedkohjk"},"content":" page \u003d 1","isFullCredit":false},{"lineNumber":28,"author":{"gitId":"jedkohjk"},"content":" major \u003d tag.strip(\u0027. \u0027).split(\u0027.\u0027)","isFullCredit":false},{"lineNumber":29,"author":{"gitId":"jedkohjk"},"content":" while True:","isFullCredit":false},{"lineNumber":30,"author":{"gitId":"jedkohjk"},"content":" response \u003d requests.get(f\u0027https://api.github.com/repos/reposense/RepoSense/releases?per_page\u003d100\u0026page\u003d{page}\u0027)","isFullCredit":false},{"lineNumber":31,"author":{"gitId":"jedkohjk"},"content":" if response.status_code in [403, 500]:","isFullCredit":false},{"lineNumber":32,"author":{"gitId":"jedkohjk"},"content":" print(\u0027GitHub API has exceed the rate limit.\u0027)","isFullCredit":false},{"lineNumber":33,"author":{"gitId":"jedkohjk"},"content":" exit(1)","isFullCredit":false},{"lineNumber":34,"author":{"gitId":"jedkohjk"},"content":" releases \u003d response.json()","isFullCredit":false},{"lineNumber":35,"author":{"gitId":"jedkohjk"},"content":" if not releases:","isFullCredit":false},{"lineNumber":36,"author":{"gitId":"jedkohjk"},"content":" print(\u0027Error, the provided tag does not exist!\u0027)","isFullCredit":false},{"lineNumber":37,"author":{"gitId":"jedkohjk"},"content":" exit(1)","isFullCredit":false},{"lineNumber":38,"author":{"gitId":"jedkohjk"},"content":" for i in releases:","isFullCredit":false},{"lineNumber":39,"author":{"gitId":"jedkohjk"},"content":" release_tag \u003d i[\u0027tag_name\u0027]","isFullCredit":false},{"lineNumber":40,"author":{"gitId":"jedkohjk"},"content":" if release_tag.strip(\u0027. \u0027).split(\u0027.\u0027)[:len(major)] \u003d\u003d major:","isFullCredit":false},{"lineNumber":41,"author":{"gitId":"jedkohjk"},"content":" handle_specific_release(release_tag)","isFullCredit":false},{"lineNumber":42,"author":{"gitId":"jedkohjk"},"content":" exit()","isFullCredit":false},{"lineNumber":43,"author":{"gitId":"jedkohjk"},"content":" page +\u003d 1","isFullCredit":false},{"lineNumber":44,"author":{"gitId":"jedkohjk"},"content":"","isFullCredit":false},{"lineNumber":45,"author":{"gitId":"-"},"content":"def handle_specific_commit(commit):","isFullCredit":false},{"lineNumber":46,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/commits/\u0027 + commit, commit\u003dcommit)","isFullCredit":false},{"lineNumber":47,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":48,"author":{"gitId":"-"},"content":"def handle_specific_release(tag):","isFullCredit":false},{"lineNumber":49,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/releases/tags/\u0027 + tag, tag\u003dtag)","isFullCredit":false},{"lineNumber":50,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":51,"author":{"gitId":"-"},"content":"def handle_latest_release():","isFullCredit":false},{"lineNumber":52,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/releases/latest\u0027)","isFullCredit":false},{"lineNumber":53,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":54,"author":{"gitId":"-"},"content":"def get_reposense_jar(url, tag\u003dNone, commit\u003dNone):","isFullCredit":false},{"lineNumber":55,"author":{"gitId":"-"},"content":" response \u003d requests.get(url)","isFullCredit":false},{"lineNumber":56,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":57,"author":{"gitId":"-"},"content":" if tag and response.status_code \u003d\u003d 404:","isFullCredit":false},{"lineNumber":58,"author":{"gitId":"-"},"content":" print(\u0027Error, the provided tag does not exist!\u0027)","isFullCredit":false},{"lineNumber":59,"author":{"gitId":"-"},"content":" exit(1)","isFullCredit":false},{"lineNumber":60,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":61,"author":{"gitId":"-"},"content":" if commit and response.status_code in [404, 422]:","isFullCredit":false},{"lineNumber":62,"author":{"gitId":"-"},"content":" print(\u0027Error, the provided commit does not exist!\u0027)","isFullCredit":false},{"lineNumber":63,"author":{"gitId":"-"},"content":" exit(1)","isFullCredit":false},{"lineNumber":64,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":65,"author":{"gitId":"-"},"content":" if response.status_code in [403, 500]:","isFullCredit":false},{"lineNumber":66,"author":{"gitId":"-"},"content":" print(\u0027GitHub API has exceed the rate limit. Falling back to alternative method...\u0027)","isFullCredit":false},{"lineNumber":67,"author":{"gitId":"-"},"content":" clone_and_make_reposense(tag\u003dtag, commit\u003dcommit)","isFullCredit":false},{"lineNumber":68,"author":{"gitId":"-"},"content":" return","isFullCredit":false},{"lineNumber":69,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":70,"author":{"gitId":"-"},"content":" if commit:","isFullCredit":false},{"lineNumber":71,"author":{"gitId":"-"},"content":" clone_and_make_reposense(commit\u003dcommit)","isFullCredit":false},{"lineNumber":72,"author":{"gitId":"-"},"content":" return","isFullCredit":false},{"lineNumber":73,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":74,"author":{"gitId":"-"},"content":" url \u003d response.json()[\u0027assets\u0027][0][\u0027browser_download_url\u0027]","isFullCredit":false},{"lineNumber":75,"author":{"gitId":"-"},"content":" download_file(url)","isFullCredit":false},{"lineNumber":76,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":77,"author":{"gitId":"-"},"content":"def clone_and_make_reposense(tag\u003dNone, commit\u003dNone):","isFullCredit":false},{"lineNumber":78,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":79,"author":{"gitId":"-"},"content":" # Cleanup cached RepoSense folder","isFullCredit":false},{"lineNumber":80,"author":{"gitId":"-"},"content":" shutil.rmtree(\u0027RepoSense\u0027, ignore_errors\u003dTrue)","isFullCredit":false},{"lineNumber":81,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":82,"author":{"gitId":"-"},"content":" command \u003d \\","isFullCredit":false},{"lineNumber":83,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":84,"author":{"gitId":"-"},"content":" git clone \u0027https://github.com/reposense/RepoSense.git\u0027 \u0026\u0026","isFullCredit":false},{"lineNumber":85,"author":{"gitId":"-"},"content":" cd RepoSense \u0026\u0026","isFullCredit":false},{"lineNumber":86,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":87,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":88,"author":{"gitId":"-"},"content":" if tag:","isFullCredit":false},{"lineNumber":89,"author":{"gitId":"-"},"content":" command +\u003d \u0027git checkout tags/{} -b deploy \u0026\u0026\u0027.format(tag)","isFullCredit":false},{"lineNumber":90,"author":{"gitId":"-"},"content":" elif commit:","isFullCredit":false},{"lineNumber":91,"author":{"gitId":"-"},"content":" command +\u003d \u0027git checkout {} -b deploy \u0026\u0026\u0027.format(commit)","isFullCredit":false},{"lineNumber":92,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":93,"author":{"gitId":"-"},"content":" command +\u003d \\","isFullCredit":false},{"lineNumber":94,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":95,"author":{"gitId":"-"},"content":" ./gradlew zipreport shadowjar \u0026\u0026","isFullCredit":false},{"lineNumber":96,"author":{"gitId":"-"},"content":" mv build/jar/RepoSense.jar ../","isFullCredit":false},{"lineNumber":97,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":98,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":99,"author":{"gitId":"-"},"content":" subprocess.check_call(command, shell\u003dTrue)","isFullCredit":false},{"lineNumber":100,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":101,"author":{"gitId":"-"},"content":"def download_file(url):","isFullCredit":false},{"lineNumber":102,"author":{"gitId":"-"},"content":" response \u003d requests.get(url, allow_redirects\u003dTrue)","isFullCredit":false},{"lineNumber":103,"author":{"gitId":"-"},"content":" open(JAR_FILENAME, \u0027wb\u0027).write(response.content)","isFullCredit":false},{"lineNumber":104,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":105,"author":{"gitId":"-"},"content":"if __name__ \u003d\u003d \"__main__\":","isFullCredit":false},{"lineNumber":106,"author":{"gitId":"-"},"content":" args \u003d parse_args()","isFullCredit":false},{"lineNumber":107,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":108,"author":{"gitId":"-"},"content":" if os.path.exists(JAR_FILENAME) and args.overwrite is False:","isFullCredit":false},{"lineNumber":109,"author":{"gitId":"-"},"content":" print(JAR_FILENAME + \u0027 already exists. Quitting.\u0027)","isFullCredit":false},{"lineNumber":110,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":111,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":112,"author":{"gitId":"-"},"content":" if args.tag:","isFullCredit":false},{"lineNumber":113,"author":{"gitId":"-"},"content":" handle_specific_release(args.tag)","isFullCredit":false},{"lineNumber":114,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":115,"author":{"gitId":"-"},"content":" ","isFullCredit":false},{"lineNumber":116,"author":{"gitId":"jedkohjk"},"content":" if args.latest:","isFullCredit":false},{"lineNumber":117,"author":{"gitId":"jedkohjk"},"content":" handle_latest_tag(args.latest)","isFullCredit":false},{"lineNumber":118,"author":{"gitId":"jedkohjk"},"content":" exit()","isFullCredit":false},{"lineNumber":119,"author":{"gitId":"jedkohjk"},"content":"","isFullCredit":false},{"lineNumber":120,"author":{"gitId":"-"},"content":" if args.commit:","isFullCredit":false},{"lineNumber":121,"author":{"gitId":"-"},"content":" handle_specific_commit(args.commit)","isFullCredit":false},{"lineNumber":122,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":123,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":124,"author":{"gitId":"-"},"content":" if args.master:","isFullCredit":false},{"lineNumber":125,"author":{"gitId":"-"},"content":" clone_and_make_reposense()","isFullCredit":false},{"lineNumber":126,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":127,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":128,"author":{"gitId":"-"},"content":" # If no arguments are provided or --release","isFullCredit":false},{"lineNumber":129,"author":{"gitId":"-"},"content":" handle_latest_release()","isFullCredit":false}],"authorContributionMap":{"jedkohjk":24,"-":105}},{"path":"run.sh","fileType":"other","lines":[{"lineNumber":1,"author":{"gitId":"-"},"content":"#!/bin/bash","isFullCredit":false},{"lineNumber":2,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":3,"author":{"gitId":"-"},"content":"# Downloads a specific version of RepoSense.jar of your choice from our repository","isFullCredit":false},{"lineNumber":4,"author":{"gitId":"-"},"content":"## Examples of supported options:","isFullCredit":false},{"lineNumber":5,"author":{"gitId":"-"},"content":"### ./get-reposense.py --release # Gets the latest release (Stable)","isFullCredit":false},{"lineNumber":6,"author":{"gitId":"-"},"content":"### ./get-reposense.py --master # Gets the latest master (Beta)","isFullCredit":false},{"lineNumber":7,"author":{"gitId":"-"},"content":"### ./get-reposense.py --tag v1.6.1 # Gets a specific version","isFullCredit":false},{"lineNumber":8,"author":{"gitId":"jedkohjk"},"content":"### ./get-reposense.py --latest v1.6 # Gets the latest version with the given tag prefix e.g. v1.6.1","isFullCredit":false},{"lineNumber":9,"author":{"gitId":"-"},"content":"### ./get-reposense.py --commit abc123 # Gets a specific commit","isFullCredit":false},{"lineNumber":10,"author":{"gitId":"-"},"content":"### ./get-reposense.py --release --overwrite # Overwrite RepoSense.jar, if exists, with the latest release","isFullCredit":false},{"lineNumber":11,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":12,"author":{"gitId":"-"},"content":"./get-reposense.py --release","isFullCredit":false},{"lineNumber":13,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":14,"author":{"gitId":"-"},"content":"# Executes RepoSense","isFullCredit":false},{"lineNumber":15,"author":{"gitId":"-"},"content":"# Do not change the default output folder name (reposense-report)","isFullCredit":false},{"lineNumber":16,"author":{"gitId":"-"},"content":"## Examples of other valid options; For more, please view the user guide","isFullCredit":false},{"lineNumber":17,"author":{"gitId":"-"},"content":"### java -jar RepoSense.jar --repos https://github.com/reposense/RepoSense.git","isFullCredit":false},{"lineNumber":18,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":19,"author":{"gitId":"-"},"content":"java -jar RepoSense.jar --config ./configs","isFullCredit":false}],"authorContributionMap":{"jedkohjk":1,"-":18}}] diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/reposense_publish-RepoSense_master/commits.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/reposense_publish-RepoSense_master/commits.json new file mode 100644 index 0000000000..a94c9995df --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/reposense_publish-RepoSense_master/commits.json @@ -0,0 +1 @@ +{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":25}},"authorContributionVariance":{"jedkohjk":47.354168},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/summary.json new file mode 100644 index 0000000000..04dbec9ca1 --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/summary.json @@ -0,0 +1 @@ +{"reportGeneratedTime":"Mon, 31 Mar 2025 02:46:49 SGT","reportGenerationTime":" 1.68 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"1970-01-01","untilDate":"2024-12-31"}],"errorSet":[],"sinceDate":"2024-08-02","untilDate":"2024-12-31","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense.git":"Publish branch of Reposense","https://github.com/reposense/publish-RepoSense/tree/master":"My project"}},"isPortfolio":false} diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/reposense_publish-RepoSense_master/authorship.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/reposense_publish-RepoSense_master/authorship.json new file mode 100644 index 0000000000..0c971dca68 --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/reposense_publish-RepoSense_master/authorship.json @@ -0,0 +1 @@ +[{"path":"get-reposense.py","fileType":"other","lines":[{"lineNumber":1,"author":{"gitId":"-"},"content":"#!/usr/bin/env python","isFullCredit":false},{"lineNumber":2,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":3,"author":{"gitId":"-"},"content":"import argparse","isFullCredit":false},{"lineNumber":4,"author":{"gitId":"-"},"content":"import sys","isFullCredit":false},{"lineNumber":5,"author":{"gitId":"-"},"content":"import os","isFullCredit":false},{"lineNumber":6,"author":{"gitId":"-"},"content":"import shutil","isFullCredit":false},{"lineNumber":7,"author":{"gitId":"-"},"content":"import requests","isFullCredit":false},{"lineNumber":8,"author":{"gitId":"-"},"content":"import subprocess","isFullCredit":false},{"lineNumber":9,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":10,"author":{"gitId":"-"},"content":"JAR_FILENAME \u003d \u0027RepoSense.jar\u0027","isFullCredit":false},{"lineNumber":11,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":12,"author":{"gitId":"-"},"content":"def parse_args():","isFullCredit":false},{"lineNumber":13,"author":{"gitId":"-"},"content":" parser \u003d argparse.ArgumentParser(description\u003d\u0027Downloads a specific version of RepoSense.jar from our repository.\u0027)","isFullCredit":false},{"lineNumber":14,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":15,"author":{"gitId":"-"},"content":" group \u003d parser.add_mutually_exclusive_group()","isFullCredit":false},{"lineNumber":16,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-r\u0027, \u0027--release\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Get RepoSense.jar from the latest release (Stable)\u0027)","isFullCredit":false},{"lineNumber":17,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-m\u0027, \u0027--master\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Get RepoSense.jar from the latest master (Beta)\u0027)","isFullCredit":false},{"lineNumber":18,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-t\u0027, \u0027--tag\u0027, help\u003d\u0027Get RepoSense.jar of a specific release version tag\u0027)","isFullCredit":false},{"lineNumber":19,"author":{"gitId":"jedkohjk"},"content":" group.add_argument(\u0027-l\u0027, \u0027--latest\u0027, help\u003d\u0027Get RepoSense.jar of the latest release of a specific version tag\u0027)","isFullCredit":false},{"lineNumber":20,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-c\u0027, \u0027--commit\u0027, help\u003d\u0027Get RepoSense.jar of a specific commit\u0027)","isFullCredit":false},{"lineNumber":21,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":22,"author":{"gitId":"-"},"content":" parser.add_argument(\u0027-o\u0027, \u0027--overwrite\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Overwrite RepoSense.jar file, if exists. Default: false\u0027)","isFullCredit":false},{"lineNumber":23,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":24,"author":{"gitId":"-"},"content":" return parser.parse_args()","isFullCredit":false},{"lineNumber":25,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":26,"author":{"gitId":"jedkohjk"},"content":"def handle_latest_tag(tag):","isFullCredit":false},{"lineNumber":27,"author":{"gitId":"jedkohjk"},"content":" page \u003d 1","isFullCredit":false},{"lineNumber":28,"author":{"gitId":"jedkohjk"},"content":" major \u003d tag.strip(\u0027. \u0027).split(\u0027.\u0027)","isFullCredit":false},{"lineNumber":29,"author":{"gitId":"jedkohjk"},"content":" while True:","isFullCredit":false},{"lineNumber":30,"author":{"gitId":"jedkohjk"},"content":" response \u003d requests.get(f\u0027https://api.github.com/repos/reposense/RepoSense/releases?per_page\u003d100\u0026page\u003d{page}\u0027)","isFullCredit":false},{"lineNumber":31,"author":{"gitId":"jedkohjk"},"content":" if response.status_code in [403, 500]:","isFullCredit":false},{"lineNumber":32,"author":{"gitId":"jedkohjk"},"content":" print(\u0027GitHub API has exceed the rate limit.\u0027)","isFullCredit":false},{"lineNumber":33,"author":{"gitId":"jedkohjk"},"content":" exit(1)","isFullCredit":false},{"lineNumber":34,"author":{"gitId":"jedkohjk"},"content":" releases \u003d response.json()","isFullCredit":false},{"lineNumber":35,"author":{"gitId":"jedkohjk"},"content":" if not releases:","isFullCredit":false},{"lineNumber":36,"author":{"gitId":"jedkohjk"},"content":" print(\u0027Error, the provided tag does not exist!\u0027)","isFullCredit":false},{"lineNumber":37,"author":{"gitId":"jedkohjk"},"content":" exit(1)","isFullCredit":false},{"lineNumber":38,"author":{"gitId":"jedkohjk"},"content":" for i in releases:","isFullCredit":false},{"lineNumber":39,"author":{"gitId":"jedkohjk"},"content":" release_tag \u003d i[\u0027tag_name\u0027]","isFullCredit":false},{"lineNumber":40,"author":{"gitId":"jedkohjk"},"content":" if release_tag.strip(\u0027. \u0027).split(\u0027.\u0027)[:len(major)] \u003d\u003d major:","isFullCredit":false},{"lineNumber":41,"author":{"gitId":"jedkohjk"},"content":" handle_specific_release(release_tag)","isFullCredit":false},{"lineNumber":42,"author":{"gitId":"jedkohjk"},"content":" exit()","isFullCredit":false},{"lineNumber":43,"author":{"gitId":"jedkohjk"},"content":" page +\u003d 1","isFullCredit":false},{"lineNumber":44,"author":{"gitId":"jedkohjk"},"content":"","isFullCredit":false},{"lineNumber":45,"author":{"gitId":"-"},"content":"def handle_specific_commit(commit):","isFullCredit":false},{"lineNumber":46,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/commits/\u0027 + commit, commit\u003dcommit)","isFullCredit":false},{"lineNumber":47,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":48,"author":{"gitId":"-"},"content":"def handle_specific_release(tag):","isFullCredit":false},{"lineNumber":49,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/releases/tags/\u0027 + tag, tag\u003dtag)","isFullCredit":false},{"lineNumber":50,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":51,"author":{"gitId":"-"},"content":"def handle_latest_release():","isFullCredit":false},{"lineNumber":52,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/releases/latest\u0027)","isFullCredit":false},{"lineNumber":53,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":54,"author":{"gitId":"-"},"content":"def get_reposense_jar(url, tag\u003dNone, commit\u003dNone):","isFullCredit":false},{"lineNumber":55,"author":{"gitId":"-"},"content":" response \u003d requests.get(url)","isFullCredit":false},{"lineNumber":56,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":57,"author":{"gitId":"-"},"content":" if tag and response.status_code \u003d\u003d 404:","isFullCredit":false},{"lineNumber":58,"author":{"gitId":"-"},"content":" print(\u0027Error, the provided tag does not exist!\u0027)","isFullCredit":false},{"lineNumber":59,"author":{"gitId":"-"},"content":" exit(1)","isFullCredit":false},{"lineNumber":60,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":61,"author":{"gitId":"-"},"content":" if commit and response.status_code in [404, 422]:","isFullCredit":false},{"lineNumber":62,"author":{"gitId":"-"},"content":" print(\u0027Error, the provided commit does not exist!\u0027)","isFullCredit":false},{"lineNumber":63,"author":{"gitId":"-"},"content":" exit(1)","isFullCredit":false},{"lineNumber":64,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":65,"author":{"gitId":"-"},"content":" if response.status_code in [403, 500]:","isFullCredit":false},{"lineNumber":66,"author":{"gitId":"-"},"content":" print(\u0027GitHub API has exceed the rate limit. Falling back to alternative method...\u0027)","isFullCredit":false},{"lineNumber":67,"author":{"gitId":"-"},"content":" clone_and_make_reposense(tag\u003dtag, commit\u003dcommit)","isFullCredit":false},{"lineNumber":68,"author":{"gitId":"-"},"content":" return","isFullCredit":false},{"lineNumber":69,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":70,"author":{"gitId":"-"},"content":" if commit:","isFullCredit":false},{"lineNumber":71,"author":{"gitId":"-"},"content":" clone_and_make_reposense(commit\u003dcommit)","isFullCredit":false},{"lineNumber":72,"author":{"gitId":"-"},"content":" return","isFullCredit":false},{"lineNumber":73,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":74,"author":{"gitId":"-"},"content":" url \u003d response.json()[\u0027assets\u0027][0][\u0027browser_download_url\u0027]","isFullCredit":false},{"lineNumber":75,"author":{"gitId":"-"},"content":" download_file(url)","isFullCredit":false},{"lineNumber":76,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":77,"author":{"gitId":"-"},"content":"def clone_and_make_reposense(tag\u003dNone, commit\u003dNone):","isFullCredit":false},{"lineNumber":78,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":79,"author":{"gitId":"-"},"content":" # Cleanup cached RepoSense folder","isFullCredit":false},{"lineNumber":80,"author":{"gitId":"-"},"content":" shutil.rmtree(\u0027RepoSense\u0027, ignore_errors\u003dTrue)","isFullCredit":false},{"lineNumber":81,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":82,"author":{"gitId":"-"},"content":" command \u003d \\","isFullCredit":false},{"lineNumber":83,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":84,"author":{"gitId":"-"},"content":" git clone \u0027https://github.com/reposense/RepoSense.git\u0027 \u0026\u0026","isFullCredit":false},{"lineNumber":85,"author":{"gitId":"-"},"content":" cd RepoSense \u0026\u0026","isFullCredit":false},{"lineNumber":86,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":87,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":88,"author":{"gitId":"-"},"content":" if tag:","isFullCredit":false},{"lineNumber":89,"author":{"gitId":"-"},"content":" command +\u003d \u0027git checkout tags/{} -b deploy \u0026\u0026\u0027.format(tag)","isFullCredit":false},{"lineNumber":90,"author":{"gitId":"-"},"content":" elif commit:","isFullCredit":false},{"lineNumber":91,"author":{"gitId":"-"},"content":" command +\u003d \u0027git checkout {} -b deploy \u0026\u0026\u0027.format(commit)","isFullCredit":false},{"lineNumber":92,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":93,"author":{"gitId":"-"},"content":" command +\u003d \\","isFullCredit":false},{"lineNumber":94,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":95,"author":{"gitId":"-"},"content":" ./gradlew zipreport shadowjar \u0026\u0026","isFullCredit":false},{"lineNumber":96,"author":{"gitId":"-"},"content":" mv build/jar/RepoSense.jar ../","isFullCredit":false},{"lineNumber":97,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":98,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":99,"author":{"gitId":"-"},"content":" subprocess.check_call(command, shell\u003dTrue)","isFullCredit":false},{"lineNumber":100,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":101,"author":{"gitId":"-"},"content":"def download_file(url):","isFullCredit":false},{"lineNumber":102,"author":{"gitId":"-"},"content":" response \u003d requests.get(url, allow_redirects\u003dTrue)","isFullCredit":false},{"lineNumber":103,"author":{"gitId":"-"},"content":" open(JAR_FILENAME, \u0027wb\u0027).write(response.content)","isFullCredit":false},{"lineNumber":104,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":105,"author":{"gitId":"-"},"content":"if __name__ \u003d\u003d \"__main__\":","isFullCredit":false},{"lineNumber":106,"author":{"gitId":"-"},"content":" args \u003d parse_args()","isFullCredit":false},{"lineNumber":107,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":108,"author":{"gitId":"-"},"content":" if os.path.exists(JAR_FILENAME) and args.overwrite is False:","isFullCredit":false},{"lineNumber":109,"author":{"gitId":"-"},"content":" print(JAR_FILENAME + \u0027 already exists. Quitting.\u0027)","isFullCredit":false},{"lineNumber":110,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":111,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":112,"author":{"gitId":"-"},"content":" if args.tag:","isFullCredit":false},{"lineNumber":113,"author":{"gitId":"-"},"content":" handle_specific_release(args.tag)","isFullCredit":false},{"lineNumber":114,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":115,"author":{"gitId":"-"},"content":" ","isFullCredit":false},{"lineNumber":116,"author":{"gitId":"jedkohjk"},"content":" if args.latest:","isFullCredit":false},{"lineNumber":117,"author":{"gitId":"jedkohjk"},"content":" handle_latest_tag(args.latest)","isFullCredit":false},{"lineNumber":118,"author":{"gitId":"jedkohjk"},"content":" exit()","isFullCredit":false},{"lineNumber":119,"author":{"gitId":"jedkohjk"},"content":"","isFullCredit":false},{"lineNumber":120,"author":{"gitId":"-"},"content":" if args.commit:","isFullCredit":false},{"lineNumber":121,"author":{"gitId":"-"},"content":" handle_specific_commit(args.commit)","isFullCredit":false},{"lineNumber":122,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":123,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":124,"author":{"gitId":"-"},"content":" if args.master:","isFullCredit":false},{"lineNumber":125,"author":{"gitId":"-"},"content":" clone_and_make_reposense()","isFullCredit":false},{"lineNumber":126,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":127,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":128,"author":{"gitId":"-"},"content":" # If no arguments are provided or --release","isFullCredit":false},{"lineNumber":129,"author":{"gitId":"-"},"content":" handle_latest_release()","isFullCredit":false}],"authorContributionMap":{"jedkohjk":24,"-":105}},{"path":"run.sh","fileType":"other","lines":[{"lineNumber":1,"author":{"gitId":"-"},"content":"#!/bin/bash","isFullCredit":false},{"lineNumber":2,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":3,"author":{"gitId":"-"},"content":"# Downloads a specific version of RepoSense.jar of your choice from our repository","isFullCredit":false},{"lineNumber":4,"author":{"gitId":"-"},"content":"## Examples of supported options:","isFullCredit":false},{"lineNumber":5,"author":{"gitId":"-"},"content":"### ./get-reposense.py --release # Gets the latest release (Stable)","isFullCredit":false},{"lineNumber":6,"author":{"gitId":"-"},"content":"### ./get-reposense.py --master # Gets the latest master (Beta)","isFullCredit":false},{"lineNumber":7,"author":{"gitId":"-"},"content":"### ./get-reposense.py --tag v1.6.1 # Gets a specific version","isFullCredit":false},{"lineNumber":8,"author":{"gitId":"jedkohjk"},"content":"### ./get-reposense.py --latest v1.6 # Gets the latest version with the given tag prefix e.g. v1.6.1","isFullCredit":false},{"lineNumber":9,"author":{"gitId":"-"},"content":"### ./get-reposense.py --commit abc123 # Gets a specific commit","isFullCredit":false},{"lineNumber":10,"author":{"gitId":"-"},"content":"### ./get-reposense.py --release --overwrite # Overwrite RepoSense.jar, if exists, with the latest release","isFullCredit":false},{"lineNumber":11,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":12,"author":{"gitId":"-"},"content":"./get-reposense.py --release","isFullCredit":false},{"lineNumber":13,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":14,"author":{"gitId":"-"},"content":"# Executes RepoSense","isFullCredit":false},{"lineNumber":15,"author":{"gitId":"-"},"content":"# Do not change the default output folder name (reposense-report)","isFullCredit":false},{"lineNumber":16,"author":{"gitId":"-"},"content":"## Examples of other valid options; For more, please view the user guide","isFullCredit":false},{"lineNumber":17,"author":{"gitId":"-"},"content":"### java -jar RepoSense.jar --repos https://github.com/reposense/RepoSense.git","isFullCredit":false},{"lineNumber":18,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":19,"author":{"gitId":"-"},"content":"java -jar RepoSense.jar --config ./configs","isFullCredit":false}],"authorContributionMap":{"jedkohjk":1,"-":18}}] diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/reposense_publish-RepoSense_master/commits.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/reposense_publish-RepoSense_master/commits.json new file mode 100644 index 0000000000..a94c9995df --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/reposense_publish-RepoSense_master/commits.json @@ -0,0 +1 @@ +{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":25}},"authorContributionVariance":{"jedkohjk":47.354168},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/summary.json new file mode 100644 index 0000000000..76d44e39c2 --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/summary.json @@ -0,0 +1 @@ +{"reportGeneratedTime":"Mon, 31 Mar 2025 13:58:30 SGT","reportGenerationTime":" 1.44 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"1970-01-01","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-08-02","untilDate":"2024-08-31","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense.git":"Publish branch of Reposense","https://github.com/reposense/publish-RepoSense/tree/master":"My project"}},"isPortfolio":true} diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/authorship.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/authorship.json new file mode 100644 index 0000000000..0c971dca68 --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/authorship.json @@ -0,0 +1 @@ +[{"path":"get-reposense.py","fileType":"other","lines":[{"lineNumber":1,"author":{"gitId":"-"},"content":"#!/usr/bin/env python","isFullCredit":false},{"lineNumber":2,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":3,"author":{"gitId":"-"},"content":"import argparse","isFullCredit":false},{"lineNumber":4,"author":{"gitId":"-"},"content":"import sys","isFullCredit":false},{"lineNumber":5,"author":{"gitId":"-"},"content":"import os","isFullCredit":false},{"lineNumber":6,"author":{"gitId":"-"},"content":"import shutil","isFullCredit":false},{"lineNumber":7,"author":{"gitId":"-"},"content":"import requests","isFullCredit":false},{"lineNumber":8,"author":{"gitId":"-"},"content":"import subprocess","isFullCredit":false},{"lineNumber":9,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":10,"author":{"gitId":"-"},"content":"JAR_FILENAME \u003d \u0027RepoSense.jar\u0027","isFullCredit":false},{"lineNumber":11,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":12,"author":{"gitId":"-"},"content":"def parse_args():","isFullCredit":false},{"lineNumber":13,"author":{"gitId":"-"},"content":" parser \u003d argparse.ArgumentParser(description\u003d\u0027Downloads a specific version of RepoSense.jar from our repository.\u0027)","isFullCredit":false},{"lineNumber":14,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":15,"author":{"gitId":"-"},"content":" group \u003d parser.add_mutually_exclusive_group()","isFullCredit":false},{"lineNumber":16,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-r\u0027, \u0027--release\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Get RepoSense.jar from the latest release (Stable)\u0027)","isFullCredit":false},{"lineNumber":17,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-m\u0027, \u0027--master\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Get RepoSense.jar from the latest master (Beta)\u0027)","isFullCredit":false},{"lineNumber":18,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-t\u0027, \u0027--tag\u0027, help\u003d\u0027Get RepoSense.jar of a specific release version tag\u0027)","isFullCredit":false},{"lineNumber":19,"author":{"gitId":"jedkohjk"},"content":" group.add_argument(\u0027-l\u0027, \u0027--latest\u0027, help\u003d\u0027Get RepoSense.jar of the latest release of a specific version tag\u0027)","isFullCredit":false},{"lineNumber":20,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-c\u0027, \u0027--commit\u0027, help\u003d\u0027Get RepoSense.jar of a specific commit\u0027)","isFullCredit":false},{"lineNumber":21,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":22,"author":{"gitId":"-"},"content":" parser.add_argument(\u0027-o\u0027, \u0027--overwrite\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Overwrite RepoSense.jar file, if exists. Default: false\u0027)","isFullCredit":false},{"lineNumber":23,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":24,"author":{"gitId":"-"},"content":" return parser.parse_args()","isFullCredit":false},{"lineNumber":25,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":26,"author":{"gitId":"jedkohjk"},"content":"def handle_latest_tag(tag):","isFullCredit":false},{"lineNumber":27,"author":{"gitId":"jedkohjk"},"content":" page \u003d 1","isFullCredit":false},{"lineNumber":28,"author":{"gitId":"jedkohjk"},"content":" major \u003d tag.strip(\u0027. \u0027).split(\u0027.\u0027)","isFullCredit":false},{"lineNumber":29,"author":{"gitId":"jedkohjk"},"content":" while True:","isFullCredit":false},{"lineNumber":30,"author":{"gitId":"jedkohjk"},"content":" response \u003d requests.get(f\u0027https://api.github.com/repos/reposense/RepoSense/releases?per_page\u003d100\u0026page\u003d{page}\u0027)","isFullCredit":false},{"lineNumber":31,"author":{"gitId":"jedkohjk"},"content":" if response.status_code in [403, 500]:","isFullCredit":false},{"lineNumber":32,"author":{"gitId":"jedkohjk"},"content":" print(\u0027GitHub API has exceed the rate limit.\u0027)","isFullCredit":false},{"lineNumber":33,"author":{"gitId":"jedkohjk"},"content":" exit(1)","isFullCredit":false},{"lineNumber":34,"author":{"gitId":"jedkohjk"},"content":" releases \u003d response.json()","isFullCredit":false},{"lineNumber":35,"author":{"gitId":"jedkohjk"},"content":" if not releases:","isFullCredit":false},{"lineNumber":36,"author":{"gitId":"jedkohjk"},"content":" print(\u0027Error, the provided tag does not exist!\u0027)","isFullCredit":false},{"lineNumber":37,"author":{"gitId":"jedkohjk"},"content":" exit(1)","isFullCredit":false},{"lineNumber":38,"author":{"gitId":"jedkohjk"},"content":" for i in releases:","isFullCredit":false},{"lineNumber":39,"author":{"gitId":"jedkohjk"},"content":" release_tag \u003d i[\u0027tag_name\u0027]","isFullCredit":false},{"lineNumber":40,"author":{"gitId":"jedkohjk"},"content":" if release_tag.strip(\u0027. \u0027).split(\u0027.\u0027)[:len(major)] \u003d\u003d major:","isFullCredit":false},{"lineNumber":41,"author":{"gitId":"jedkohjk"},"content":" handle_specific_release(release_tag)","isFullCredit":false},{"lineNumber":42,"author":{"gitId":"jedkohjk"},"content":" exit()","isFullCredit":false},{"lineNumber":43,"author":{"gitId":"jedkohjk"},"content":" page +\u003d 1","isFullCredit":false},{"lineNumber":44,"author":{"gitId":"jedkohjk"},"content":"","isFullCredit":false},{"lineNumber":45,"author":{"gitId":"-"},"content":"def handle_specific_commit(commit):","isFullCredit":false},{"lineNumber":46,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/commits/\u0027 + commit, commit\u003dcommit)","isFullCredit":false},{"lineNumber":47,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":48,"author":{"gitId":"-"},"content":"def handle_specific_release(tag):","isFullCredit":false},{"lineNumber":49,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/releases/tags/\u0027 + tag, tag\u003dtag)","isFullCredit":false},{"lineNumber":50,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":51,"author":{"gitId":"-"},"content":"def handle_latest_release():","isFullCredit":false},{"lineNumber":52,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/releases/latest\u0027)","isFullCredit":false},{"lineNumber":53,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":54,"author":{"gitId":"-"},"content":"def get_reposense_jar(url, tag\u003dNone, commit\u003dNone):","isFullCredit":false},{"lineNumber":55,"author":{"gitId":"-"},"content":" response \u003d requests.get(url)","isFullCredit":false},{"lineNumber":56,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":57,"author":{"gitId":"-"},"content":" if tag and response.status_code \u003d\u003d 404:","isFullCredit":false},{"lineNumber":58,"author":{"gitId":"-"},"content":" print(\u0027Error, the provided tag does not exist!\u0027)","isFullCredit":false},{"lineNumber":59,"author":{"gitId":"-"},"content":" exit(1)","isFullCredit":false},{"lineNumber":60,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":61,"author":{"gitId":"-"},"content":" if commit and response.status_code in [404, 422]:","isFullCredit":false},{"lineNumber":62,"author":{"gitId":"-"},"content":" print(\u0027Error, the provided commit does not exist!\u0027)","isFullCredit":false},{"lineNumber":63,"author":{"gitId":"-"},"content":" exit(1)","isFullCredit":false},{"lineNumber":64,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":65,"author":{"gitId":"-"},"content":" if response.status_code in [403, 500]:","isFullCredit":false},{"lineNumber":66,"author":{"gitId":"-"},"content":" print(\u0027GitHub API has exceed the rate limit. Falling back to alternative method...\u0027)","isFullCredit":false},{"lineNumber":67,"author":{"gitId":"-"},"content":" clone_and_make_reposense(tag\u003dtag, commit\u003dcommit)","isFullCredit":false},{"lineNumber":68,"author":{"gitId":"-"},"content":" return","isFullCredit":false},{"lineNumber":69,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":70,"author":{"gitId":"-"},"content":" if commit:","isFullCredit":false},{"lineNumber":71,"author":{"gitId":"-"},"content":" clone_and_make_reposense(commit\u003dcommit)","isFullCredit":false},{"lineNumber":72,"author":{"gitId":"-"},"content":" return","isFullCredit":false},{"lineNumber":73,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":74,"author":{"gitId":"-"},"content":" url \u003d response.json()[\u0027assets\u0027][0][\u0027browser_download_url\u0027]","isFullCredit":false},{"lineNumber":75,"author":{"gitId":"-"},"content":" download_file(url)","isFullCredit":false},{"lineNumber":76,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":77,"author":{"gitId":"-"},"content":"def clone_and_make_reposense(tag\u003dNone, commit\u003dNone):","isFullCredit":false},{"lineNumber":78,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":79,"author":{"gitId":"-"},"content":" # Cleanup cached RepoSense folder","isFullCredit":false},{"lineNumber":80,"author":{"gitId":"-"},"content":" shutil.rmtree(\u0027RepoSense\u0027, ignore_errors\u003dTrue)","isFullCredit":false},{"lineNumber":81,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":82,"author":{"gitId":"-"},"content":" command \u003d \\","isFullCredit":false},{"lineNumber":83,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":84,"author":{"gitId":"-"},"content":" git clone \u0027https://github.com/reposense/RepoSense.git\u0027 \u0026\u0026","isFullCredit":false},{"lineNumber":85,"author":{"gitId":"-"},"content":" cd RepoSense \u0026\u0026","isFullCredit":false},{"lineNumber":86,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":87,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":88,"author":{"gitId":"-"},"content":" if tag:","isFullCredit":false},{"lineNumber":89,"author":{"gitId":"-"},"content":" command +\u003d \u0027git checkout tags/{} -b deploy \u0026\u0026\u0027.format(tag)","isFullCredit":false},{"lineNumber":90,"author":{"gitId":"-"},"content":" elif commit:","isFullCredit":false},{"lineNumber":91,"author":{"gitId":"-"},"content":" command +\u003d \u0027git checkout {} -b deploy \u0026\u0026\u0027.format(commit)","isFullCredit":false},{"lineNumber":92,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":93,"author":{"gitId":"-"},"content":" command +\u003d \\","isFullCredit":false},{"lineNumber":94,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":95,"author":{"gitId":"-"},"content":" ./gradlew zipreport shadowjar \u0026\u0026","isFullCredit":false},{"lineNumber":96,"author":{"gitId":"-"},"content":" mv build/jar/RepoSense.jar ../","isFullCredit":false},{"lineNumber":97,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","isFullCredit":false},{"lineNumber":98,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":99,"author":{"gitId":"-"},"content":" subprocess.check_call(command, shell\u003dTrue)","isFullCredit":false},{"lineNumber":100,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":101,"author":{"gitId":"-"},"content":"def download_file(url):","isFullCredit":false},{"lineNumber":102,"author":{"gitId":"-"},"content":" response \u003d requests.get(url, allow_redirects\u003dTrue)","isFullCredit":false},{"lineNumber":103,"author":{"gitId":"-"},"content":" open(JAR_FILENAME, \u0027wb\u0027).write(response.content)","isFullCredit":false},{"lineNumber":104,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":105,"author":{"gitId":"-"},"content":"if __name__ \u003d\u003d \"__main__\":","isFullCredit":false},{"lineNumber":106,"author":{"gitId":"-"},"content":" args \u003d parse_args()","isFullCredit":false},{"lineNumber":107,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":108,"author":{"gitId":"-"},"content":" if os.path.exists(JAR_FILENAME) and args.overwrite is False:","isFullCredit":false},{"lineNumber":109,"author":{"gitId":"-"},"content":" print(JAR_FILENAME + \u0027 already exists. Quitting.\u0027)","isFullCredit":false},{"lineNumber":110,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":111,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":112,"author":{"gitId":"-"},"content":" if args.tag:","isFullCredit":false},{"lineNumber":113,"author":{"gitId":"-"},"content":" handle_specific_release(args.tag)","isFullCredit":false},{"lineNumber":114,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":115,"author":{"gitId":"-"},"content":" ","isFullCredit":false},{"lineNumber":116,"author":{"gitId":"jedkohjk"},"content":" if args.latest:","isFullCredit":false},{"lineNumber":117,"author":{"gitId":"jedkohjk"},"content":" handle_latest_tag(args.latest)","isFullCredit":false},{"lineNumber":118,"author":{"gitId":"jedkohjk"},"content":" exit()","isFullCredit":false},{"lineNumber":119,"author":{"gitId":"jedkohjk"},"content":"","isFullCredit":false},{"lineNumber":120,"author":{"gitId":"-"},"content":" if args.commit:","isFullCredit":false},{"lineNumber":121,"author":{"gitId":"-"},"content":" handle_specific_commit(args.commit)","isFullCredit":false},{"lineNumber":122,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":123,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":124,"author":{"gitId":"-"},"content":" if args.master:","isFullCredit":false},{"lineNumber":125,"author":{"gitId":"-"},"content":" clone_and_make_reposense()","isFullCredit":false},{"lineNumber":126,"author":{"gitId":"-"},"content":" exit()","isFullCredit":false},{"lineNumber":127,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":128,"author":{"gitId":"-"},"content":" # If no arguments are provided or --release","isFullCredit":false},{"lineNumber":129,"author":{"gitId":"-"},"content":" handle_latest_release()","isFullCredit":false}],"authorContributionMap":{"jedkohjk":24,"-":105}},{"path":"run.sh","fileType":"other","lines":[{"lineNumber":1,"author":{"gitId":"-"},"content":"#!/bin/bash","isFullCredit":false},{"lineNumber":2,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":3,"author":{"gitId":"-"},"content":"# Downloads a specific version of RepoSense.jar of your choice from our repository","isFullCredit":false},{"lineNumber":4,"author":{"gitId":"-"},"content":"## Examples of supported options:","isFullCredit":false},{"lineNumber":5,"author":{"gitId":"-"},"content":"### ./get-reposense.py --release # Gets the latest release (Stable)","isFullCredit":false},{"lineNumber":6,"author":{"gitId":"-"},"content":"### ./get-reposense.py --master # Gets the latest master (Beta)","isFullCredit":false},{"lineNumber":7,"author":{"gitId":"-"},"content":"### ./get-reposense.py --tag v1.6.1 # Gets a specific version","isFullCredit":false},{"lineNumber":8,"author":{"gitId":"jedkohjk"},"content":"### ./get-reposense.py --latest v1.6 # Gets the latest version with the given tag prefix e.g. v1.6.1","isFullCredit":false},{"lineNumber":9,"author":{"gitId":"-"},"content":"### ./get-reposense.py --commit abc123 # Gets a specific commit","isFullCredit":false},{"lineNumber":10,"author":{"gitId":"-"},"content":"### ./get-reposense.py --release --overwrite # Overwrite RepoSense.jar, if exists, with the latest release","isFullCredit":false},{"lineNumber":11,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":12,"author":{"gitId":"-"},"content":"./get-reposense.py --release","isFullCredit":false},{"lineNumber":13,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":14,"author":{"gitId":"-"},"content":"# Executes RepoSense","isFullCredit":false},{"lineNumber":15,"author":{"gitId":"-"},"content":"# Do not change the default output folder name (reposense-report)","isFullCredit":false},{"lineNumber":16,"author":{"gitId":"-"},"content":"## Examples of other valid options; For more, please view the user guide","isFullCredit":false},{"lineNumber":17,"author":{"gitId":"-"},"content":"### java -jar RepoSense.jar --repos https://github.com/reposense/RepoSense.git","isFullCredit":false},{"lineNumber":18,"author":{"gitId":"-"},"content":"","isFullCredit":false},{"lineNumber":19,"author":{"gitId":"-"},"content":"java -jar RepoSense.jar --config ./configs","isFullCredit":false}],"authorContributionMap":{"jedkohjk":1,"-":18}}] diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json new file mode 100644 index 0000000000..a94c9995df --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json @@ -0,0 +1 @@ +{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":25}},"authorContributionVariance":{"jedkohjk":47.354168},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/summary.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/summary.json new file mode 100644 index 0000000000..92b9e723bc --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/summary.json @@ -0,0 +1 @@ +{"reportGeneratedTime":"Mon, 31 Mar 2025 13:28:49 SGT","reportGenerationTime":" 4.32 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"1970-01-01","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-08-02","untilDate":"2024-08-31","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense.git":"Publish branch of Reposense","https://github.com/reposense/publish-RepoSense/tree/master":"My project"}},"isPortfolio":false} diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/summary.json new file mode 100644 index 0000000000..92b9e723bc --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/summary.json @@ -0,0 +1 @@ +{"reportGeneratedTime":"Mon, 31 Mar 2025 13:28:49 SGT","reportGenerationTime":" 4.32 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"1970-01-01","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-08-02","untilDate":"2024-08-31","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense.git":"Publish branch of Reposense","https://github.com/reposense/publish-RepoSense/tree/master":"My project"}},"isPortfolio":false} From 5e97c3d5b9f9d277f07c89a9d6140cc4a76d4d50 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Mon, 31 Mar 2025 16:19:48 +0800 Subject: [PATCH 25/42] Enhance RunConfigurationDeciderTest --- .../reposense/ReportConfigSystemTest.java | 17 +++---- .../ReportConfigSystemTest/blurbs.md | 2 +- .../model/RunConfigurationDeciderTest.java | 48 +++++++++++++++++++ .../both-configs/repo-config.csv | 4 ++ .../both-configs/report-config.yaml | 28 +++++++++++ .../no-report-config/repo-config.csv | 4 ++ .../only-report-config-title/repo-config.csv | 4 ++ .../report-config.yaml | 1 + 8 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 src/test/resources/RunConfigurationDeciderTest/both-configs/repo-config.csv create mode 100644 src/test/resources/RunConfigurationDeciderTest/both-configs/report-config.yaml create mode 100644 src/test/resources/RunConfigurationDeciderTest/no-report-config/repo-config.csv create mode 100644 src/test/resources/RunConfigurationDeciderTest/only-report-config-title/repo-config.csv create mode 100644 src/test/resources/RunConfigurationDeciderTest/only-report-config-title/report-config.yaml diff --git a/src/systemtest/java/reposense/ReportConfigSystemTest.java b/src/systemtest/java/reposense/ReportConfigSystemTest.java index bc7b2aee72..caad82b934 100644 --- a/src/systemtest/java/reposense/ReportConfigSystemTest.java +++ b/src/systemtest/java/reposense/ReportConfigSystemTest.java @@ -1,10 +1,19 @@ package reposense; +import static org.apache.tools.ant.types.Commandline.translateCommandline; +import static reposense.util.TestUtil.loadResource; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; + import reposense.model.SupportedDomainUrlMap; import reposense.parser.types.SinceDateArgumentType; import reposense.report.ErrorSummary; @@ -12,14 +21,6 @@ import reposense.util.InputBuilder; import reposense.util.SystemTestUtil; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.List; - -import static org.apache.tools.ant.types.Commandline.translateCommandline; -import static reposense.util.TestUtil.loadResource; - @Execution(ExecutionMode.SAME_THREAD) public class ReportConfigSystemTest { private static final List TESTING_FILE_FORMATS = Arrays.asList("py", "sh"); diff --git a/src/systemtest/resources/ReportConfigSystemTest/blurbs.md b/src/systemtest/resources/ReportConfigSystemTest/blurbs.md index 1f2fd9248f..d260bfda9d 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/blurbs.md +++ b/src/systemtest/resources/ReportConfigSystemTest/blurbs.md @@ -1,2 +1,2 @@ https://github.com/reposense/publish-RepoSense.git -Publish branch of Reposense \ No newline at end of file +Publish branch of Reposense diff --git a/src/test/java/reposense/model/RunConfigurationDeciderTest.java b/src/test/java/reposense/model/RunConfigurationDeciderTest.java index f6624a696a..c5a68b3dd8 100644 --- a/src/test/java/reposense/model/RunConfigurationDeciderTest.java +++ b/src/test/java/reposense/model/RunConfigurationDeciderTest.java @@ -25,6 +25,13 @@ public class RunConfigurationDeciderTest { private static final Path PROJECT_DIRECTORY = Paths.get(System.getProperty("user.dir")); private static final Path CONFIG_FOLDER_ABSOLUTE = loadResource(ArgsParserTest.class, "cli_location_test"); + private static final Path ONLY_REPORT_CONFIG_TITLE_FOLDER = loadResource(RunConfigurationDeciderTest.class, + "RunConfigurationDeciderTest/only-report-config-title"); + private static final Path BOTH_CONFIGS_FOLDER = loadResource(RunConfigurationDeciderTest.class, + "RunConfigurationDeciderTest/both-configs"); + private static final Path NO_REPORT_CONFIG_FOLDER = loadResource(RunConfigurationDeciderTest.class, + "RunConfigurationDeciderTest/no-report-config"); + private static final String NONEXISTENT_DIRECTORY = "some_non_existent_dir/"; private static final InputBuilder DEFAULT_INPUT_BUILDER = new InputBuilder(); @@ -33,6 +40,8 @@ public class RunConfigurationDeciderTest { private static final String TEST_REPO_BETA = "https://github.com/reposense/testrepo-Beta.git"; + + @BeforeEach public void before() { DEFAULT_INPUT_BUILDER.reset().addConfig(CONFIG_FOLDER_ABSOLUTE); @@ -80,4 +89,43 @@ public void parse_reposAndConfigNotSpecified_doesNotReturnCliRunConfiguration() RunConfiguration runConfiguration = getRunConfiguration(cliArguments); Assertions.assertFalse(runConfiguration instanceof CliRunConfiguration); } + + @Test + public void parse_bothConfigsPresent_returnsOneStopConfigRunConfiguration() throws Exception { + String input = new InputBuilder() + .addSinceDate("01/07/2017") + .addUntilDate("30/11/2017") + .addConfig(BOTH_CONFIGS_FOLDER) + .addView() + .build(); + CliArguments cliArguments = ArgsParser.parse(translateCommandline(input)); + RunConfiguration runConfiguration = getRunConfiguration(cliArguments); + Assertions.assertTrue(runConfiguration instanceof OneStopConfigRunConfiguration); + } + + @Test + public void parse_reposAndConfigNotSpecifiedOnlyReportConfigTitle_returnsConfigRunConfiguration() throws Exception { + String input = new InputBuilder() + .addSinceDate("01/07/2017") + .addUntilDate("30/11/2017") + .addConfig(ONLY_REPORT_CONFIG_TITLE_FOLDER) + .addView() + .build(); + CliArguments cliArguments = ArgsParser.parse(translateCommandline(input)); + RunConfiguration runConfiguration = getRunConfiguration(cliArguments); + Assertions.assertTrue(runConfiguration instanceof ConfigRunConfiguration); + } + + @Test + public void parse_reposAndConfigNotSpecifiedNoReportConfig_returnsConfigRunConfiguration() throws Exception { + String input = new InputBuilder() + .addSinceDate("01/07/2017") + .addUntilDate("30/11/2017") + .addConfig(NO_REPORT_CONFIG_FOLDER) + .addView() + .build(); + CliArguments cliArguments = ArgsParser.parse(translateCommandline(input)); + RunConfiguration runConfiguration = getRunConfiguration(cliArguments); + Assertions.assertTrue(runConfiguration instanceof ConfigRunConfiguration); + } } diff --git a/src/test/resources/RunConfigurationDeciderTest/both-configs/repo-config.csv b/src/test/resources/RunConfigurationDeciderTest/both-configs/repo-config.csv new file mode 100644 index 0000000000..4254328db4 --- /dev/null +++ b/src/test/resources/RunConfigurationDeciderTest/both-configs/repo-config.csv @@ -0,0 +1,4 @@ +Repository's Location,Branch,File formats,Ignore Glob List,Ignore standalone config,Ignore Commits List,Ignore Authors List,Shallow Cloning,Find Previous Authors,Since Date,Until Date +https://github.com/reposense/testrepo-Alpha.git,master,,,,2fb6b9b2dd9fa40bf0f9815da2cb0ae8731436c7;c5a6dc774e22099cd9ddeb0faff1e75f9cf4f151;cd7f610e0becbdf331d5231887d8010a689f87c7;768015345e70f06add2a8b7d1f901dc07bf70582,,,,, +https://github.com/reposense/testrepo-Beta.git,master,fxml,docs**,yes,,,,,, +https://github.com/reposense/testrepo-Beta.git,add-config-json,fxml,docs**,yes,,,,,, diff --git a/src/test/resources/RunConfigurationDeciderTest/both-configs/report-config.yaml b/src/test/resources/RunConfigurationDeciderTest/both-configs/report-config.yaml new file mode 100644 index 0000000000..100008bfa7 --- /dev/null +++ b/src/test/resources/RunConfigurationDeciderTest/both-configs/report-config.yaml @@ -0,0 +1,28 @@ +title: RepoSense Report +repos: + - repo: https://github.com/reposense/publish-RepoSense.git + groups: + - group-name: code + globs: + - "**.py" + - group-name: tests + globs: + - "src/test**" + - group-name: docs + globs: + - "docs**" + - "**.adoc" + - "**.md" + branches: + - branch: master + blurb: "My project" + authors: + - author-git-host-id: jedkohjk + author-display-name: jedkohjk + author-git-author-name: jedkohjk + author-emails: + ignore-authors-list: + - bot + ignore-glob-list: + - "**.in" + file-size-limit: 2000000 diff --git a/src/test/resources/RunConfigurationDeciderTest/no-report-config/repo-config.csv b/src/test/resources/RunConfigurationDeciderTest/no-report-config/repo-config.csv new file mode 100644 index 0000000000..4254328db4 --- /dev/null +++ b/src/test/resources/RunConfigurationDeciderTest/no-report-config/repo-config.csv @@ -0,0 +1,4 @@ +Repository's Location,Branch,File formats,Ignore Glob List,Ignore standalone config,Ignore Commits List,Ignore Authors List,Shallow Cloning,Find Previous Authors,Since Date,Until Date +https://github.com/reposense/testrepo-Alpha.git,master,,,,2fb6b9b2dd9fa40bf0f9815da2cb0ae8731436c7;c5a6dc774e22099cd9ddeb0faff1e75f9cf4f151;cd7f610e0becbdf331d5231887d8010a689f87c7;768015345e70f06add2a8b7d1f901dc07bf70582,,,,, +https://github.com/reposense/testrepo-Beta.git,master,fxml,docs**,yes,,,,,, +https://github.com/reposense/testrepo-Beta.git,add-config-json,fxml,docs**,yes,,,,,, diff --git a/src/test/resources/RunConfigurationDeciderTest/only-report-config-title/repo-config.csv b/src/test/resources/RunConfigurationDeciderTest/only-report-config-title/repo-config.csv new file mode 100644 index 0000000000..4254328db4 --- /dev/null +++ b/src/test/resources/RunConfigurationDeciderTest/only-report-config-title/repo-config.csv @@ -0,0 +1,4 @@ +Repository's Location,Branch,File formats,Ignore Glob List,Ignore standalone config,Ignore Commits List,Ignore Authors List,Shallow Cloning,Find Previous Authors,Since Date,Until Date +https://github.com/reposense/testrepo-Alpha.git,master,,,,2fb6b9b2dd9fa40bf0f9815da2cb0ae8731436c7;c5a6dc774e22099cd9ddeb0faff1e75f9cf4f151;cd7f610e0becbdf331d5231887d8010a689f87c7;768015345e70f06add2a8b7d1f901dc07bf70582,,,,, +https://github.com/reposense/testrepo-Beta.git,master,fxml,docs**,yes,,,,,, +https://github.com/reposense/testrepo-Beta.git,add-config-json,fxml,docs**,yes,,,,,, diff --git a/src/test/resources/RunConfigurationDeciderTest/only-report-config-title/report-config.yaml b/src/test/resources/RunConfigurationDeciderTest/only-report-config-title/report-config.yaml new file mode 100644 index 0000000000..a530aa7a91 --- /dev/null +++ b/src/test/resources/RunConfigurationDeciderTest/only-report-config-title/report-config.yaml @@ -0,0 +1 @@ +title: RepoSense Report From e92d56f77e128a0144242877676c61c5d9aed94e Mon Sep 17 00:00:00 2001 From: LiYuan Date: Wed, 2 Apr 2025 14:47:27 +0800 Subject: [PATCH 26/42] Add docs --- config/report-config.yaml | 27 ---- docs/_markbind/layouts/ug-sitenav.md | 1 + docs/images/report-config-report.png | Bin 0 -> 352716 bytes docs/ug/cli.md | 9 +- docs/ug/configFiles.md | 18 +-- docs/ug/customizingReports.md | 6 +- docs/ug/report-config.yaml | 23 ++- docs/ug/reportConfig.md | 234 +++++++++++++++++++++++++++ 8 files changed, 270 insertions(+), 48 deletions(-) create mode 100644 docs/images/report-config-report.png create mode 100644 docs/ug/reportConfig.md diff --git a/config/report-config.yaml b/config/report-config.yaml index b076f109b5..a530aa7a91 100644 --- a/config/report-config.yaml +++ b/config/report-config.yaml @@ -1,28 +1 @@ title: RepoSense Report -repos: - - repo: https://github.com/reposense/testrepo-Delta.git - groups: - - group-name: code - globs: - - "**.java" - - group-name: tests - globs: - - "src/test**" - - group-name: docs - globs: - - "docs**" - - "**.adoc" - - "**.md" - branches: - - branch: master - blurb: "My project" - authors: - - author-git-host-id: FH-30 - author-display-name: Francis Hodianto - author-git-author-name: Francis Hodianto - author-emails: - ignore-authors-list: - - bot - ignore-glob-list: - - "**.md" - file-size-limit: 2000000 diff --git a/docs/_markbind/layouts/ug-sitenav.md b/docs/_markbind/layouts/ug-sitenav.md index 489edfb0b1..43c7e62a00 100644 --- a/docs/_markbind/layouts/ug-sitenav.md +++ b/docs/_markbind/layouts/ug-sitenav.md @@ -6,6 +6,7 @@ {name: "Sharing reports", link: "ug/sharingReports.html"}, {name: "Appendices"}, {level: 2, name: "CLI syntax reference", link: "ug/cli.html"}, + {level: 2, name: "Getting started with `report-config.yaml`", link: "ug/reportConfig.html"}, {level: 2, name: "Config files format", link: "ug/configFiles.html"}, {level: 2, name: "Advanced syntax: `author-config.csv`", link: "ug/authorConfigSyntax.html"}, {level: 2, name: "Using `@@author` tags", link: "ug/usingAuthorTags.html"}, diff --git a/docs/images/report-config-report.png b/docs/images/report-config-report.png new file mode 100644 index 0000000000000000000000000000000000000000..dba37fb224dcd72ad3dd0f64cc9ba7f80187bd4b GIT binary patch literal 352716 zcmeFZXIPWl)-{YEs32mYNfRpw(xi7RfT%Q)P7)CT>AePspcF+wrPqM;-g{L*q=eo> zkrG1h0YWI>iu>&IJlFUBIIsKtd$_KUnj4{XLy_$+5H6;@z2?+_c(xZpZ zNJyx{NJuD5PE&watRg8ZNJ!2~n>~1-ru5(eyPCa?iJ7G_3CW}P;o7HkG+OBs^q*Zl zc^dL~Uitjzi*k=o^Ss%^?#E z$y+7_$!FNo^pNy58MYo>z0}P-O2<(-C`&lbHaqhU??Lj3F`$nee;^gGOZG;4>t-)0 zNr3%?y}kFtU_&yAXHo1;RI_3l@3S#tkoPX!9v7#@&X<$QX<;`~qDfvG_(&ChLY@&8 za=+?)f$HH)lKH0rhnt(XDA`rs4xih9#2&F!+>7XLJ%}~gPrMZpDIO5+pw1}%D2%Ta zqkJgazERJ^r!8{6kwOG_$4n>AqqD$T)Zv!G&q9eN>FMo^*8!zhkJvAA9$Zz()GaD_ zm?~@D(S>b@IUyYYIa!lDaav}9C2%f?YTD{-1|oSQ$rrKotWL!SLCrB){^{1z?8k8DSoN0g*9VMcaYH$)XM zA^N8s9$;$7o2>D^8(D>}&rTS8dh99n;Mw`m#EwgRZF9aYoLk=_eI7NOxcBDFtAfvC}6_ zws<+eujOh2*G@@!i_tuMW8li8@$n*!!V}VvtJn7x9oZ~BqHK6AH;)iqGzF8FMP6Og z{AkI3EmY>0(%D-S7a9YuK7@~*-fi?;f8=`h4T{cm3QD%Mx2o!Tf1X|HhQW2mi`L5L z>7F3k>}luU?O)B(OF7n|8ly0;n(?mSd+y&>!Df9j&Q_GsKZwxybM+G@V>3eA7azJa zX|mopu7+1P;N+A?UX&{&xTW~L^&=#Hs2P+URE)ORe^D#`R$8_Nd!XidLQ3&q`8`j= zj-hG6rD5@9{=w5x!vZ;HEEIY#<#Qk0i%Y{4&OWt?7g#7b8q91@ z7?2J%uvWNNHBfa^O?Y>m_W5+4>AT$73xZ8db`K3M&`DB@ybjII1&-;naqlI+(Xse(OnZ1-l(__Zr#+2cr>LQ;_mORYZ`hn<`WE%; z(fTdhGs$P7-?hHGcwgL*YKmlv)+3_o^jg5fuWUUL^JkFFv@b>MnCvdt@z^nJh!wuw z3m8_s_W7(*@D12Aq4$bh?BSmVKRJI2{>&R%_2|k^^H+jJ+~V)z9@B>gwqj;iW|L>z zXVYi9Xq7(*H^*L7zMz)&EJ960Bjz^4ZAe(xbDk`ofyfv+R9a_}mfBKgdWK7;O@Dv? zo&K8smje&YiwC&+owMUL;fXuy5dHCc@yRk;khD4-&+L6Woc^NCYoQowo;MMXxf6NP z26|O`SNmjM^9#iZS1^$V=`v|DQ8PsfLwj#KT1dpoBtReXrLQH!Qg&ZI6S5K#kC8$} z$7sf!61I80VZ8U-Ue>R{x#zSw^wXc3rmIw|2q^L_$ayX21C zR^A@jzklevkK2#LO){SqIL&{aDU50PZiUHu>nAft%P6CYj)I=`gbUWQFK8`Z{0ua+=%3CnSlB73l>u;lxwJ6tR3=wrc%cHrdL6&N}+5T(tRX-mVM!A ziptd}MiVcSYsKLvEgcDhPSKVoyv05vZOg)2Lq_v1}iolsK2@bAVSz3LunnMeXN)-ziU`FXj21h1J zlSlAq<)ZAuYID^R(-G`&erf&e8;J_ZSK`mb4ICe`O0&oZTd+#{UG#IOrlZNd?B1v} z{(j~8{B!JfgKn32Z{=`h4<5sxZJ5~@K`kk`92;2uB(L2hQnIns-=Bt$Z9?jxH2W?0 zWN902@$_iGrwhdjLP4&Wx%pd;`dH___$iq-&pLP@L7PUECR4Fiu{=H8G{&?POC52J z+pjdhX)y2?-Mn!7Qo)d-4b2+qTI~b^FNUApv)pwP>2*%Aot^s};=$wLR>$lqc^Kto z=EZyZMWMjsipC>;RVC?@7A`;6&}_+<)UMP9ty4&@7y1HJTz`< zeB}Sg?9~>hcOSkrjp_DeWZ=F83YgpNug7Uue}PZ9iY zrHy8uzB1YGh|rBFoUQQvJ+eR{Wyd5tl~F!~LK_+zKNe7gD_zD}L7x+bI6s??^` z>C~Y}JRiEuz{0I(Q;}OC_q9%|fo|32O4ga|&hYB+e3&wXM}J)Rf^JXA4|7znFpR61 zD_@gbYb8r052|zMmHT1^<8)$aaVR#2Pyg8L?dwKVP!cnhXw4Q~ug+)2D4eXi!VC!O)%g1p$Gt=(QH23uVpz3Ys@D++` z<7(uZ657GjrfDL=GshE0$lcYx@`8oaN{%T_WLv~U*jXf1$cg2w>~QaLpU1CHTS>Y8 zC>m|n9_gZ4PTYJ=@?pz|U>s{C%bNISw`ukEcM*~tipZK#CiJKm*L43#-_>;01Vf~_ z^Wg6Cl*0t}T!Lx5P0cU6q$E`BO2gV}{gv0duWc&k-3k}J45iKme`ZzWBfw{z=2W>uREuZAkYaP`pbAM2>Wa%&s?{g?Zz_3iLoyywEHMjEtCkxT+U>|kL5 z+X{OGzZuENe|<&;JQIIDfd_G)zn;my_>r6fe_aF**F@64 zKTQ>uNcQ(Og$a0%m zGgo6v-G^pYV9dZdWCTP6#iV}k@PEAZKd1a(yXySUu6M;ng#P!g|Ld*)|E`)2#`X_v ztiVYfW&Y=e{cGp{{pP=Rl;R_P_y08(e+~5SSHVcjP)hOr`>x4QzRN8+2W}*b*+bRm z;2BIa;t!c1_{I6xGuS3IZ!d>jogpESBT;&I@44%Vqk`VsH%7p2^xW#alWk_FPqV)vA^p=|4PDRQ$l<%T8;a3?y6ayj^R^%T^Rp87SQn&YS344vUSAgZ zLDcO7s}a(VZi#)#n$}4uG!FH3(>Ab^@2hgMDBc;hU|wnYqDOI-@x^K-maW=N z+I^c}8|sF+SeSYDa4|dI-m1N*o!3GO;-uMEVAyDYThh4c+rn2hf6bv!Su}NZusi7i zYPEVTVc+v6Y<_I~RjBg8wY<80^Wv>eNrD~qYv#3znRBvMOS$DDE9mmwnX`XBSMRWS z7m;EpSykgDN;~Ok*rbNH!9aIzOI>ouEgHWa*{DJg(32f9gyl=f8l4sfUW3=Gbp&e% zlJ0KX9{^0~v)9 zogCwnxUD7w-?8i~wa|uPv$FH7qGz%lmxp_H81tZLT;xfwy{S{hLr{1$ zwdh#)Q05$o_olgd(VahyYcF~zp9`zyDI=zQ89Lgy^9_2TobSAj@K}z{;v3{`1A_RC zmMb=3x+OGOS@nGN9eMGV`srH=>W&(>tqxP9r%9SB5~{YFTRw4@a-UZzRR*WV=3>>C z7UTw&kn+rrpcxpC`b>!IQ3bvac{E#Y)|;Y~rkSnfC1oYY$GN>ajz;93KfD=f@RCWy zaz1^){f(SkuCK1lpT-(BN$(G280A}uD+P-`is7F)hn_Irit45P?OEs*7ur` zDRTHwRT;B$67z{ZD+VA6bc~oHtp;^Xy*71tlVr95P3LhZZLofqR^i_YBm;FYS+)th*9?XWSinxr~`J;u3@pG}U7p^N1V;tV> z$xK7X5ZhPNdp0{at_84-bNi3a#`?4oMW$&SLlEd{sVX3d<#Rf zT!I|_>+rcj?>`Q~YE^R0-kb3*6C)XstA>q!_)^*9gDsPAO|6X?U9uqk+@i_YjIkLN4&M$ z&yo|?Bm0TBVALXC7);2`YTORmCYFMlX%E~2xkL|j&f;@_x*8HiNHx!dfxEq75(#nM z(0A>TXNZ>G9qS7*rQTj1>3bS|w^?fhgv}03ZHO`a$a(9|ZST#EnU>h=!D8R7BB<|N zai34}@-Sn|2=U%LKH9A}0pVcF-?-t;i8QdRy*!kdIYrfEJoaoIHkug-}S@mn+OU(L~tq)dR>IFr|Y#;PMVg!u*76x-;^IgF5 zjhO^j6Yag3j`SMMPd9FlNK*`9^E%4$&eTYc0qb~B%WSwHX~bpH^T}c>J|A(IeOb~u z2i8nRBlQ9-5LQ`ekar}^YorFuD3kr|1?MZ1@HJ^PS=C@4G5k*XF^?kJdCCn|2U_>S zwV?uwm3kC;}Eo;{n>%(0y$7>cg(?Af6{e^ z8YH2$sKH!){g9m%bgZ)Ic)#O(_f?0>Zgu_bCx;*|@%eg>b@u(QcySKT z@o~$jQNw+Q>CUdqi zn5=up<<+aeZ1sv4q{+TKh@>o!m0Z${V3(BO%2LxpGb!d0hl-FQn_CJ^A_wn{7}qho zmgwXVnbEL|ULkzhU*swyQX}bTGqly&Wj(GosC+!qP$rzZ;?cdmG}Og2S|;FIEDULR z>g}aXT&sPlPa=I7xW-&`krD|23DxU@*jtoE=MlN>;}5-~mcL>c%Nuo@5deh{b+uZh z&|Ba7k7c7c{+*^}Lys=r+g%+$bQM!W8C~R!H2l+*P2yces!0{bomh3agC+mL*sRJ%&ADY0gc1{? zB%Ky#A9PdxJjW&_mTA5ygsYM^?PMW%wgj`HK(GkaD-YxA=XRW|^F*~=LBzpD$fxOk zXxj`ZVbC`9rP41Z!75XNnX0r?hQZu0;R^TWYvffH!>iPj72Hg3XJ{|Uxs|TWcyEHg zFO#Rkz@_>e_Wqnc8NpJHb>8f_Z?a#3|bO_8H5!U!JJp0uZ zqhZJUL&VghaBmC@#?ZmcmE(19XH6}dferlF{k-Z`iR-qdwUEJcAO5Tz=6 z*xwN&-0n5uxjXJPVn*2MeZm(ly)%5>AVqxd*ISfHtd+^2juG`1$m*AI^!eR*5N=WkBrSxlVTB)EgEu3ZZ^ z5X_$j((@d+irE6p%!qrr`oY}}kbv+&iZ;#T?guo&Au{_J@uiSz=efcbw&F&!K@B721zJ`Y+O5yaLd6k4$s>SfvAs2=@|Pr_US@0IY1; zUkulwWt;#RkMjO_dYt_S@#%o;-_6Mz4Fg1Z)VyXtE5-o$^RqLZt=B1Mzye-FguoQ2 zbXBBveecGQh$x7<(9(I=o8Ftmt4hS~&jd?$&Lg=?3%V%7mdFDkF5?5%p`?2on|m`M zv9gB?s$QEZN}*4)wQ>(!M;q3s8d1#*Fw6QuJ2%{bc3;-$g3E+k`bzDDX73Yrd+Xkm z_p!6G#N^Qjur?7M$tv^F3)xJJg;*r&d2i~mg!)h$pZsIkl^86iNrqV*agDVq+u*(| z^!n2Xe61FQpYCKu8bPPU=aNd&;Y4icEXJmyIqU>d&lywUWW^8>kW0&uX|{Mhi(E|0 z4B2A5Zf#gLr=@2Lt4mY%t6&6x)@e^7dFz7Ry=7)T3Ldytl98P{CnP%InvC=W5uAK! z19l*^4sJs%Px)A{>bdc;CJv-WFNp$5ZYPhVN4OxW~0W&Sp2Lh@?Vu8PEm= z={MOKyOy}GtbA*=y9py+gq6ctep?or^jqX@lb*YfVlX52@dNr5?$oMW#im^%VB+Fn z@c@U;Wh4i0&bUzk@T%E;sbX%(o{MLZ)x9n3wzI%uxWTtQ;|Seb4uzD()odf7?kQ|{ zUj?Wq4uyjtD_zK8w_7U{NZwW|#iK31`{<53Gye>$uJLW#)1c;Y=WZRw3-okW9{zFNjlJ745HzJUSrcUwq}T%N7n8Eq%#(G4#7 zmi9AYsxd;XenauuT3-`d2OhZTm>YuFYN4-JYEG9Y;^Yd3ZiesWywn)T7thLDJuj4H zrm!8&U=@Fl)unQpe>Tme)!1kvZ;^k8Z_gX1sl=O?zXrfvpwXXZS3lC!OH&4bOB7(p zzp#O)vd0<7D9-Xze$Jky)~R+I$1=b?3?*DvmlaSK6j=;a#4)t8(9VY4NrcTH#04U2 zF_F**rYzI}U>`i5_8}B#6o-4~B$r}1&~4uk)83mU&?)0gPc7ys5i7ldcgM_eVs>B6069iNxd}I=)$$TBV5P#=_)R(( z0Y+kwO%OR>r^c@6=6BPpGvX3w{(!)l^3Z>Vb->uuc@lPnRi$xZ5PhkqENeIMe`_83 z^u*QItBD8sL2z*ZO@|w+MTwv*)Cu@T8rdk7c!_BI78boxV>s{Q`|vu5b58g2=T5OY zeiA1v7tahlX3}0;Tr?i1!~M!#;nqs<*i=f!Ro``99`-4K54RWl5P&L`njN#Rdq=H~ zC@m$H-N(D1Nz}P4Y8~fdGn5xE4Q-x#j{p;-rJhe@qQp!?@YT;x5@`M3!@`ERY3*MW1ZsLaDPxh5r zp(SKC&Jd|kPob?mb^yeJWd-GfIAw`0ldc4-D(96&?ECsh&$5lCwSWH8@o~U*3ddR& zy@yTVu}B+0s%0hfpvCE6arwi6ga?=1p-c956%*!B%X2#?v-g6ia1Db1{Ebb*F{2U? zrRClIt|xr8uVO@PR$HE2J`Fzt+64*)(%G}NWv4~rj4(-$ci!ytr)D;x_2Jb@09nx| zp#w0|zfiV5k4TX$ZO}wCtZR{5 z+QDRz^I2=4zs*aDCTtR86q}jP;G2nhM%6L>%?o9_C4gugFV>VV%cqIf` zj5*n6;q>%C;qD(NIrdMSYN2wKgN{JyiUq86IIo}l1ITFOJ`*JaTXw@w%DF&@*7e?G zwyEhk%OcjD_u%-ltXB;+UG*75SBSSQxp_MeF`QDTRO9JBz`XF7@z<$Y>@qvS+%+h91LO_MnQpIi68n_6Ra9{y$kUnxDRF7bZ(m&gKmHmWm^fm{XT zu@d>4DmFlM{${6?6UHyV#;HxnRLBhX6ehy}5M5$6Uggv?PTe>`q>A}K{#}&XD02W; z9zA(5Cz^>HXPB~kq^AfV0EZeBOr;%f)jy|Oue@W(+x{IsOI1L2_{b_bc=qz$CKU~S z%~~r_^ZuVhJ#tcUF?5^=kd#qth-0`cg0bxgAVE}wzV$-AMpuX7c{Jj)gWT0ceVf{W zp*o;Gpnx1Y)$)W@r@}S~KalT*F=gg|@n&JNz8>7;3NW`&?>Q3Y>%DeHOqJUscn&Le zbP0JyJqpaxb~8WlYrPa7$lDqde$n;TZ?vaD2CQ&I z{x(TAdk*(TLe2Pg4f4{hr=0Wm4g#(R^8<+fcw*$zk}tY$DX%()JFnteAyjN*I*?C< z@S1TGjM+=P<0b|;|!IQSwXHLgC z8N+;R{lTLb>GNAh!d$ai$8+}hMVt6Mrr9=(IE|<(O_cmaSolgPgke_ zQBKILw~57byJyb(6k9>N4G^l;RVUi-6hDo}VdB^SS5hMw%k0l56Xo@v)d3PXT6V2Z z!{IwH)S8M&$i;igy~cOBN>N7$?@y?}VRh1N9r_zFN48O(w&&D#wNCmOhHdXTlIb za54dh+6{b0Lon3efSn}UNH_|Gp3DRjkiO-{FdfC8mzge}GzSE6UFX}Xt9sW6U+wl6 zUFQ+w8CWD4pi^PfSL%Gmrg+17eOAo4A8T# zbtR@vZ+oSD(pp6t_gr=@@!s2x3HaIyATnK2TOWz?;Nk4iAtCkz8O5F*bral#kDloW za1|6$ebMj+H>u^ZY8RX2gkJ75^9+`bZI5`!Fr@|gH`HW+l$LCSX#>2#;+)to z5>WsV9w!=5sl(5}qxc3dRk@?CfFuG#^aipIP=*anN9|FY{_(1kV^>d1^WG%Z_NYaM z6FWZ;c2+Fl^!Z!zLRSG>%Wws3O)1Mf-4ifMljI-d$2z?-SzxR~Gj%gL!QDE&#s$t%N8qyH`(i zmDT`c7=3(zm7N1R=(co%MOunnWm*e7WCGkj%SW)<6jM6fhuVY9cB0DtO7qD~w)>n` zq3#Q*3escMMWCt}ORQjY7Jft)Yr{NCM}RUgZn@*IvpkY!F_h;u3Z!n$?(5Zc8O;J4 z+^_!O-LTJ9Ke14~T9y3*v-sW>NK8CYa|+W_{V%=kQR(OLeX6o6^y*lRd+T&sI!T?| z_P5Rwi7V`xx>z~jA_|7yZ> zVC`Wb&jz`NsBjV4(q(b`SMm9@M}yA3Cw5#@OXMoQV-;DQp4U|M$kr z?`+3yL&W>M4mK2tWd;tvhOIlQ(ho&}ETVk~G^>sVZ!#1p*)68iF%J_H4XA(_#|W8y z(a}s=y&fXfOHp}=`_G5QF2pM4f&F{rbCuuq?lNu9(&ie>a&ec)DK^&^@e{4Wh`&X!FD}O!{(lNi4-ClL zD8m4rn3w~>qu9iAf4i`jQkzOmTHj@{zm-p=)_biCZS5sx_KR4HtX?j9(yKl9QLvNW zpT+*sv`hHY=QLfEI;wGEJx`G`wew#AIey(wwk?j03uQd1&9}P03*cfZZ}tQKb0)-U z4SaVs(`k%>ih)+0kGq z2X`t~%XrqXm`Ip4Z}swBeF)#R3Xw>cqZ(Y6u`F2r%vH`yo_I{8GN_G=z0T_bpd^|o zyO%QHL3Y1fD>u~!vfRo>cSQ}%6Q{YWIhPKVP~+R7ByixgRwL3Uu((a1QER={OPt4` z1%<|`wms(bv4SoM4pey%g# z0zF2g^xN0IgMqO|KLY6Q(h^_W`zLJzx?zNlsy{BT$=_C6BgG)_?J!H#$@`XoxUbyv zQ1>a~{o}Puyc;jfE@Wr}TR_ZsrUr)~;>crZ5(se)pz3sor^RRG&(R~%jzXU)S4$aI z4`>Y#C1lvvfqE^9wknUm@(Rmu-L_j%4Xyc!lL4Mmu z4iMh%bewBF$^Ow2PFdsC{{pdx$I`~9|9F=EzQ#nk#EFw_vLNCY4`}J*56UEmX z$PUHG{#;%Ey@xL;?v~n77viWd&c5xo*V>PeD>kF$Hft@H|Jd{M*DT|t%@F-v!HJHw zu4-K!2hxlAYQ;?OcWL9Z|1V#fYUjg+e>D3@yuSkbS>Q$W6aVP+8A!1A64uT*|4&!Z z@6-MFZ>oet5On)#*4cEGxt6$D*b?;0m0+>)GOM$uX z%p|XF=px$5%&Z7jq(`d%mV^{1-vAkk^nqaW+*SA|^B5BZM- zu>XGItQ^tQ)P##UZi<0DZn^7uQqA~ar3X}qPxIY497VQ>Jo3IAE+5^=f`a}kkLeems(D>ffBE( z18^vXmvFlMl*bS~FG)3D=sy*2e$`X$L?!9@HNcKG85}^_>;mU;>!TCGzNMP9XS)`o zHBU(TR?o;AYRALE!}$9>{+4+8I(-k?Dk=7SpS#I@+Yxt|8-C+iN{S&?W};;gbZ%xT`YjEhFb9T z``yNACx#K83;0$#U}Ka;Mw+y$XTLo8T_?P=3QcA>-zB*wCF`kvcDC}!)Qa$R_y>|8 z;p%lzS^h@8{Maz@04NJ(eCj+^r9&plZ%@&PTvzz`=m0@gs%y0QY|FqR6#I(nKi-i7 zSMrEGIkyD1#{7zDNtIZp1;b|#cJD{&VLobIQN=j?Lq8E+baB6%#Z7^H~- z2HY~~Q}ly-^@VHo#}hd~&U1dXQaU6;$zcPmbIu0R+@|C`p%Dzgv+EmUae>ptd(Hgh z_DmgZwK?q+RI~8vvk#EuwbwNdh=)(g>-XcV8@~=R5 zkzS&C{?}WY{n}nC`o+%%wV~XWN5I3sSMPN#n^p$1T9P=@;T zVj%m|FXt~M9RNd`;QrQ{uDrJx`P?*ysMFTQ%5MI=PPDXda2<8QtKX=Kd)xck7|<+% zid8AhBtBeI%uqcy{X6Q_&Z9SSYKDS8nfqgfEh1<>aepiXbxJzkjPHdxYFk@fvNmva zm{sY(Q(z?IHqdY~fiaO`{+ebIingsfZ9hL>KZH%F>FCwEIIoV4OlH>|?OSYpr{aqu zS~GW``MGR=mBNm{6p{%Ke^`&{2gMo(P`%a*-OD1%fywZEcbxX=$Y%)h?;@XZ(;93i zN||fl6xdGPFaAcs90jbz<6UzJqLKY_Zi8in_!o&LU=#;GMv%9hTr^5N0G=?R{>`XPOM3*x$_^zCUedL9WsnA0Yi!*dP<2wr8q0iOBDDX9D0>FM%Ds--x@Te z$H}=ZQmi+3X@PN?O6mgR1O$TxARNTwiN(Qq24%`fy?q_6jLPlQ0k7ax0kwp*qLPvH zm6l!+2%mvfF-oK?-3r&>2^5f2ie$R)i=Yw@^!Z?Qnyt`V%2|8)q<1o*V9}I)bGJf5 z#Rxz+u1vA!LYD@d+?+_~8fV$9sZ610;~tlt%YXU?+tPLrC0Q@$c{U6?Nf;dj7CwDg zMDTygT&0(EUXo#mX2O*V+e8af()zQ@Sr<69*S>HOOH!HHsyX?#utq=XOZC}v4{`h> zpv+><+sn({wS&#nz)?TZc_EHg@08MWs)-L{I-NDJ!R1nEuSUpB8E2QH_ovh-nbjcg}VtEIm^uKJiT4HW@J7Q2U9PRvDG!+Mw)i#!2P5i~OK^o%ZF zX=Zr7>R~M&-hlvaml(w>p8cG!WbLLOmjC=9+in#}m%o=z{<8_<5*neqxHIq-6;VY! z8e%_pW>>b$3#?J!_8=ODE$zb+Ik!{-<77n2#^bzWrS?9dk*w#N9nsjm;qa5knPri| z@ny+lrrY-A9A2#4IJ^LRreKt5>}G#v@h>K2&UDt-8q)mKG8X2u>Z}@n8HJos&o!>yHm@ z;LuFg4y5cPvH8ieX5+`)uu+IGXrz0r_l`PY%ylM6K^ouw^2f)^WeKDxN~bFx>))K_ zsL}5`HCC2D=N}4yhu7uzX3WV(yLKnwUC1Z;mW96fWGR^0S7HO;N>wpp?3RK`sTAY@HWsh7XS&py(@0BINSS?C# z(p4_hNP%iJRqzPWYYTs=rSF9G+Y!Dq>QB^2i8ZD%0BCo{BHssR%Zz(Sm`EVqcUK`C zH8IW*z80B@GUHT>w|S^e1hHWc;6&t#ujCkmXwFR9i;~eD$+a4zt~0p0T((T(91AR{ z-I_Y8v^}8EZ7vOscOGr!eL#>jZlj&7L+n;h5`EN-%bt!1D<~u{Ica5eC?83zIv;pU$=$XZL@~tV z15_~wOh@HQ{laoLmyVVrE*wi8tT&=S5#Mr6Qv%V(gE8=DbNzK9*_4zHawU}XSJk=2 ziS)aU^J;xLx`L8%*edvs%cjFs`ys27zz8Ths&d#TeQ(!voMC|8F@LD|kxoB!AhdS0 zbWYuKyv51VX^dIeoK}lZq9$V%Wk~3kmM0``O^l<5^lhZ!R{es;OM|(G&e-n26}A^*?{#1+G7&Csd7D{d0a91hK&3vFflD2KN{{81P6K3>0zqR>rmv zVf~ujsvB(RHKZj(a#al2zx2!9375-zoZw>>c1Qbpk0k~HXe!xF-P>@W%AUpd@HBe* zp`WjqIzuLL2Dx`%cPILoQkHvpF6QgU!B;Dtzl?neM(9jJ`fzc%OXZ{Kd7|zVy@$G6 zM9+Rf7vq%2lx^njm+spu(E|`kZZnI({jrQ$w_K2yUD+b{SaAtnY8$&n3x>&oVe28Guc?qeZ#)s_4^fp|0eLwndD?`c+8!VgtI zl#*dP)n7lQl2Yr+e^c{)4XEOXkGVP7qI{8#dW9$(=a2|j$Jh0zdhcy4^rV$((DGW+ z@ut8QUY3+!knsIEyxdJ+ z(m5X`m(j(fq9SQIk^F`}R$Y_97(wuChum`iI#EV>-QXy&c6L73M-TP1_3=TNPr6Uw~OY@T6I;4LhYLA z$X6bmP^o47%3wAYc+I+URb$tvmOTyrdC9XrYzZ+~#{Ci4flNo8X9gp;Gpz#Z<1|(` zdN#8UrE(c=(@LPYs{Ca@!x?#3N0Rbj&Av*@;&z{EzDDjBY>Na{;H|JCS^*5YWkQ6eD`{`r}eA{Gt&3Wg6RF;LU7TIq#jo-o4J5YdW)ux{UX>xYCssT_6t}Ju^fH?H^Zq+mojOlaTwH=X_<0-_-71!}+Cug*G%M zd}-`9V*H}zx(iDDWm}x{X>1&&2Dq`TgiRUz?4>Q>M?ph_`rKnm1dq3oB{laXcSd>V%2y|C_PIE|^l|Apyj=*u|u<6oC^}0iB<|von$rDnyuz`Ga1uG8`N^-y( zUGSZUGexaRd)$jIoCvn1IyprBpb#L=iaA~l*mii+oU*HXV3JN$MoKR}|YK-G_ zE|%jJ?mAkr>LgFosBB3n3I8?a(XiluCP-GBvy9Z}5bfNZqa(Awz?98rd`NzHV z`;}Go6<^oOpgwk2jYOqa1TDL=G}@$ZoV5b}sSumk>eVc)`?w;G1=FbksE1U(Q7TEb zd+EU8<6X+3>tL3HjxLozH^m2Sx719iWvyaWY z%r({ifl_F$Qgz`nN0+DSkzs9)T;*sXG_ijxtxRL(UfF8J!qLULpPJT*HNV4(f7gKd zb&0wvw(QykV+P+~h~1Q$C=`U2g8{DuEw~pzWXle*t2V1L=}Q4iBhaORqsn@gq>XNc znZ*Sd_N_a9$bNf_;RqKGjQ7nG4$rkyd$BFY`j4j&Gw2k!N@>Cf?rBo}aPm@BCkMnL z^n=f&gnx8Sy@GA z(Bp(5Kw|kY;_P13wk>kIP7qPtSfWGW#dcsDsgyjPIUT6ShPBXukx<&0tVqs=dhv|-!rLC}}LxK|(Sj2s zQgW7ch||$9o$y$e`eD-Z`j0sd8=zBS6ZiF(m4|?H%@-qvvT&l-@&dogbe{YHRhnXL z)V|e+??4YXduXCw(KOT}(4p~_y(pf8l6J6CqzXUoq=t3X`Kne3gVDbDeOc?1h%1Mh zZ+zCkzc6UY93JgPrT=w33du%(aNsC< z9w)ZYwZUnyc#GlCapmDqe#+L(lK0Suj}4%-CcEe~k+sKSHngNJp;R)E>h~~xN-py2 zdDNLfb3Ez+z1GN46P>Zbf$fwnoumCu2#w2`W*&KPD6!zhanD^ z>W}hN2Il4I>XiX?)rw!6x%Az^OMNW1ap$t{otfC;b>Ov!We_USEv~*&yO}j${c`?< z7H|zE^o^R+X5czOKc4-!nw}?2JCf3{RJu~wN^(NqO!H-9$Z=m8WT<*R_2e1@IeaD% zma!Bun2S(%E%^m6w}xDQEG+{TH1BZ1@S_T(9kzQ87?pxicto;#;wbYx{_34@DOtbv zGyaWMSXM7-E)=L!5Gl!b3!_Xm%lS3($^?@WBja*64n z{ixosNy(VJn|0I5^vsll{DB*fyJ52=M||jpv6+-q$P-Th9IA}6P5GqJzFp74^%M|> zKUXylE|-iU>r14oBFtk;J5oG5?c1sbBl-rv3^oh>N1sVz4C!J|S;{miW;OC#sz<~{ zPM7)`Rv8ahB{YN9IWo;=uM^APsWL4dlnwvDWW7eF;)efN*X&*@2Zn|m&ic%<-_;#JkNLcRDC~=ZKtp+^*0tD zdwugER|@2fqGvPp?b)JbaDV=jC-)Xaf)`J`9%WJ^gyo{zw)tM9jjf_Tn`3l#ScD7~0Y z4wobNji#r=O2-qYZfWCxX0@J7Mif7 zn6E}(t)6gEZdN%n)u&Z1$!q<*X%;u7YvzIg}lV<~oz-Uvz% z8s@G?th_%Jf^}7F z;rk;xtuy0+mL5gU#Va3cG2m=3@5tFP<8@*mkx!lqWFBvdiCXD!_Z;8iJNJ)_pX!5F z-vJBwaoO;pn2d_cB{q%Bd~b_7y+}=#shWtkA+J^p{)qHZj7WgAq&w|4=JQU(?(-l`6FTKug5L7k>EreX`&xM;WwgWcigywY}B(Qm%m7&^1ZhTAU_6 zskrvOrH=Gr!PVq#Fx@2&MDOY@an!Rp{R0xQnPguZZ)Q^uL^ifo^P_uO5q+de|BvP|Ii9Ai`TZ-HFQ$3T&fbVRE3y8;H?LJO`GwF&V^Ht^iqI60GK5akGtSQ zb93u~f>P`eX(z~IYk571gLzXiFj=pMx3mhdaBNyc70anzz@MAe_GrMkz$j~a&5}4& z4kCW5=G0e;JK1Tz--&+{5eFCPP?cT~n9GV`3`-CDY0#_szE_g{@DdZGP5@!$rPiHx z-*P!@&J%LnT>j&SClAuRwH8tFmD7L=FS05_Lt101x~DG$kxw+~xX}xl#xNVbTllM! zTRB-wK0=>9<_e$y6po;)9p>btevonsqbRY^;G5Dt=Lil7*2!zGFdtI2_^^-`rY`KW zmW&KIposYp>lM3@9-d^Z5bZFIuM>zSuz9IuQP5ll#^8M)jnC`HL#ok*BQ;`S;b42p z3;2K3}+llm{4hyqFMP!UQldBcdiWYQH(%Y zSC7n{__UeP$@vi8*l_50>gHBb+bx=8VX16SKd{N)!1Sj8i)` zympe?J`~V>w(F#xJ-c(bC6LVMQY5TPIjyVkkzZe~ldm22k41}negie{Dy{turP0i1 z!W-L*{LOrX4R1%vy*^I58*Saop+`+IrCInisH-M!OFB-}%!5&@Z&uD`#-QAH#X1)$ zuW|jOd;I$nwe2sdY0h7XEpg0CO%s1zhy?N?DJL{!o3c$Ru}AV(Vv45TTpA0`+HUgU z-4Pb4ELkY%ah6r(Lq}6k@eZcKy-GrHPuJ#V%ABpw2-Q|o^-Ry$p4=G;BzLc_|5)t{ zlbh2;91Y~H6`d6g1oB|sXyolOGvllA)guQ%OMB{L$sD`*`PemMI2XMhu`Q ztPZh0#DQ}ed)8!*@~5-dBkG9Z`#uI(Fxbdit+-lO0a}7VB%_L;@Z)HNSa$%r zBV?@e4TTf{NL*UB8ngu$%!gwV{AhQ$ypi;{f#0}tN#cY+8M*$>l4dC^jRqul%1z{B zsUM_+bbLFBZd*2KaJwt~Bl5~kWkY3Aj8w+fk5qvjzO<}WnIq7L24Swm&EZ)1xMFSr zEqd2lc5hQegi(k2u-Y@g-s()4v-*))a%k>G5kUu$T@?=sg-e8&+NxFEQd63k0VxyI z@`+qnSHS+){TJ!h%_9N^tPL7y-`D60p=vhFB=g}$YD1z$OBfu6@1@z08q)u90k&^a zki7w$6KZnu=|BW^3={|E+PyGd<8`P#5Ee83Ie7jX&o!(rI!kryj{NW+3Cre4s5v^xW7vWkbzF3-@D7T;X+TI?7Y;Pg=t}8FUycp zgM9}}rMHiH6G1lr?!fdb9i!**ev4vA`KDR)a!h$65IV)jB!XU`%L~8b0?UH773@+9 zb1tDZ^|S_L0sWR`pcQBn&R*Giz;VlGZ(8^@TA%q%mA$Z;C0h(SKzuv-c2BXf?`Tzs z#uKy<;<6l%Gnh;!4UQWw+aLWtxLrT>2zITn^DUua+C4*xhqD3Jzj98dVXl7u@bCtJ zqQF?k6kTQb9ELw_8gPy;DP@cAqqJ;UM?7vm79$^gZ(}_663NxF3hj+5IfyEr*B+^v zq!-yR>KAgsD!{!>r9`0d@O=PY7{W-wDiK{kM@~PYq?KND*UIUZP?@EMzf%P`lTNvs z^bfx)d^TK-r4s$K z)aY@W+PQW789U>_#iD6ORXm(jS+5aP#lHwMs^nhNYh~{vl=nIO*hdahBP1-s>lQW1 zcV-krg1dPV@I7aOO8Q!6y){}PK?m-RbheF4NqquM44Tu|Iq@omm04Mex_n)Y?C|88C6}%lXZkyn0xbWVxj%LW8Mz^Dt5Pco?z!&AVP8n z5)4p*-&3E1fZ6c>378tLK*&-qssM_J70gkb3yz}eXrpa|j!K}l!y3?*ZFyG0PqfpOq?#A4B&KZbv~uTKfE-dhx4ChRMFWpi6jlV9*ymy@&p=7X%4M~ zqCm%?b6?)xlJ>@aZxiQ>e}pGD0&c-}9hP>RRM9#E$WzHV@{)GP2YDa4oGlE#rwqzY zMoh%e`1qq!H4f$^S4R3HE%iravzc<}dw zWUoB9S;{fxB;1Z1m&Y}+p=mJWhqfEMZ$2`rYT9%9Hv9cdp4sm*U9;S`oOXTLeoH$Q zZpVthzWWwf6QfVHPCa6moV{+V3YSDi87oAcY?okgn+%QzQgkhHHD_m_@(+vd_{+CX z9QO8<{O@DG+ag;HY7T6|~N z?4UP-s6dg$XZhrsS0Kw^96E$RE%($~>)WGuiu;SB{=@{?o7?2tjoZ!eRF=HvbrXVx#cXI0h#T3x)*g(F{i6ADQ`wkSz zr__m{R)~9gpOA7qu{QmHgwV7}}*?U87^3XYF+@>eB- zlkOWww`n)-_ug1kfPl3EDe&H@`4@J~G9j;Q%k`qLJD5SBGVoNuC7#0FVtz=1*fFwl zZuu10osIDem30n(MmL@SoDQ@PwZG_^8+U8(Vwyy6Jgn}sI4ZaWGtXX)|L3d`rA$Qj zZoo(1fJ0__lwAE`sM3w>@^Qz^J=ZSMx%3>)wT<{|JS|_o)`$mYktfp;Qdy%4Ve-w> z058Al&qLNQ@A98RHg|b^k@kAK<&XHJ)aMq%xLd?vx8&s~a79DIW_E zWxG;r3OGADSsR|;-#>0V*?{KkBi*<(0_w+Vm6B7Zj6FRUk#Yar%nTd>I=)HwP`+1= z#`ExBMSXU*8ru)V0{XVo`l+1NR+2$l?3KJj!s-hvka=G#Wxs@dmLC$Ns!cw-GAkze zjdodklQVqu&}vK(baCl$Eh^aYVz%m*B88{*jl`WKH>l7)^KvB_fFX&>Y3g4vs*79q zMFlPbK*I*zqIocdErS4ogcRvTDiE{$k46#tjE9jezPBv+BS3t!fr{+BaxO%4aw}s+ zoCcuqt)wTb?XWZs^$$;4g3H1+pPlTXao9QK+Kpim28hKLSz>h6eZ!esPwp_h*z?FHGmuLd0Ek^0xPxCdW1(*jQj1gWT8AmCs2>LR|Ln+7_8UbW59 zFAjf`&esG5@OJQY{Wc)n4Ky}xwI&97wW@m*m$9Pc{p;PQs)jVDo&HcBM(IBqXdJ0| zL%_hQ$10P6!B1;jOch3d3AE zQyIgBOQUNQ)cVzIL5U#v@?|qCJ1f&RWx9H*poDxNi`dXxqcsb^M4P(0;*F^hYT~+s z488nl-B*&(H?Ay4-zvVAivv^Lb#3fo)x=aI(M=gO1ks=?MacFmUt2Q`I@!+)$v;eN ztOVm2?c?*~L#37dA-6;dHJQ))2agczZ_hAnK?EZliec4d% z<_mZ+*Kd~4;Bn3-Eq5(fw%!DhJLLDCKMz^?%gZ6|qL5rL7PT~ft;UDWUl9Xx8)+eV zU{j_ATiaS*$nHYcpO!V5L&jM$&F2~UhgVGIN#6j<=8)Tfzqz=G>;05g=cZR&9V3mY z$F&2P$9K8{Fur=1IHmiZC>oQ>%EMm6X11qR&(SrFY!iASg1URvE1`q8nStc|8X>Uh zniArDSR~u9vBGivQiLeGzmVmaYEYHv`Z3!Ya!r)@f<3yIX8_@gTbe-LzJprSpRzD) zKznKhf5d-kj`*VK;O&yAhz~9JNpmmMZ)5V*kbt+VMRdSJrPM>FRtW4C!zNcVl>0oC z0-#EaHf32pIx0UA`LR2I1tG64Y*{cNQcm+M1NE#RAQ_PkYt-i^o9hd22$RZh5(B$Qu~L%sz6$g3wEqHPy;+ zx9aSb$0?UI-v9*^uJH2l5BjLj*OXf%c}nUWddGlXLSx%f@V=t2t5OOxEs{1UBqjOn z10$XX5uk_W6~ys4NCg7{7*>N|i^(%X=6O}lGC@Z_@{#!g)mgYXp0f!E3-C5=ObgRW zN1N;k<(WDn68sNe@I*?;oc0l%hEjQmPe0&sZXt~gW65K@%>a{UFju!$JSh!mp~5YpK|(ib(fI6 z^RS<#@L+j-LfJ0TVEOB(4K=0ZI8xd4ZLJ(dsqqGL2XMFh5&obNk>v}KGH3Q~mA&GO z>3GXeu2~f=x;Nq-(IO;b2c(Gx-kSYbXCru+xgxnE%6$XfjF~cJcfCupc_31zjw8<# z-PTR7poESnMOaOKk6SJpy}M$`dmnWA7N0cj9#PNZLnq@)W#F3qa(DVwmi?bpe%V_5 z{F^9jP*$}Q*RNYY7tK~w+JAQjT1so26J&GuPg*edwW1flMTip5R9gqx4-}S>Z7@C7 zIi1Lio-p}NAks3fY}t8K3t7eD-QOTi4W9W*(=Kwd99}dJ(+Zi%9$=(Dsj((<+4Hx~ zc#pm!TnqCFBx3@!OYd@aT&V=5!YC7Jp+!VNG#9UWQs<%@RuhgZzxigD0=Hr-Gw1S-dCVRE&^H^J zrF8%6NW^90#gh+#?pVY)z>3u@07YZj;OAGFQ~Ie-i}N5tNPv=ZxHJS68fh>H3zxAn zQAl*ldb8M7=C-Gnj{NdY%a&@B4+L5KD9u1A!j)W>6#?|!cn7Yx($L614j9z0C2AVm zKR6?gWdHtWc$`yN3zoZpJ`TvhXBpdWE#v?71V+Mcu?NzA|=O5i{ zg+$6@CQ@Dp2O!%HmpA$sJ)Wy{M$vhv8H8eMfJDP!!3%Ng-bx&U{ihRS*-ry>lc8CbY<%o`COyJla;O3 zJC7guit((tn!g>(FBMnnvAGH4S)bMvSv}@xbWJi+c{^|y<9ty5_QH@?`MbS={)Z1L zfGto2?#bf^gJL{Szs{)l0M#Vv*8-e&hRX)52kyt8H^pC(6iv`O{EPv+u&KD{3L5JR zwm`Ru5hUx{R{0Tu}0g@i2u#PeyYZ^>_Ndt9tcZ!t1Ua zZlUEBhNmE!{_SmCOF+yMo>zos>ux^29JtK{SKTsx@5|p)9wV#y z#D~V&XwEoc)t&=4@HJTyah{Lh+krqj%9y1epVLETvrps{c^>?!S|hJR18cwrqkilQ zOqR;Ql~4~AfLpA>@D@BGn=V{y`2=Vao@|KXjuCWscP?xmfPT=bJkiH9#8cSd@~dZV z2ynvUVi-rNOJ5fn_r1C$kQ8Xozov=_`&!fL=~-uA0fZ+X<{V}X><$33RWT02Mpz)1 z%vt@&j$03)fsBW z@KyQBsH9o^5&%9avoX`}mVZ0f&`s0Mt9e$)M!~-BR`&W@p4*^N!zmze{l0y|17-nG zQrITzle|P2uWxv>buY*((Q9r^yWU2YhtC$P=Xp4Mi9K8shLvZ`L3*{WcGdy;@wlFc znxZ^AS%5~~Z6Q0=kVQE*3HH2-_SnGcP81N(zD<7m!2g=6G>?3i@}O1TJv?Yt1L=y} zhR3|`_Ioyb!JA(r7q|_TAIAPq3a%S{H7+sh_41o{4h9KS312>g<;*N{vzbA+gBu+E zQEVL14SChC%v*0&UWRz~XCdEJze$`(DwOrS!O(=zhMzu|uje~C%GNtaCslaa*@sax ztaO5a(45?L;&Drd#JJo%oC>u#rC{;L0^DeFvjy+mycBLQFa71YL zLG7JMzh2F$n(7a5?>kGlBlux2W9^N*^qMb>ARs|8>pK`RjL0^>F(#_TRfni7`;u9E z$Y4#;qE~%5$mWK{;~fC!d-8)*gxQse=ROn=|C+j0%i zKa_6shr$PGjl{whGLIkV!hfigdb6D!tbwA61;AIom*Q!X!ih9~9hZ8OBlVfBS5SXz zRnbe_U0x(gQR0_E#`#g>*3WDeJJ%8AF@U;*9d0m^ni9d5fZ_PwSNJ&$IJtjdo!zf?duZvRk*9zN7t|8|pFENtHjM-^YyqGSTNbB}Q!ufQT3130cc z4_G!;v^bxH78hV+k@IlQMX1`jS~Rj-h|3Fe285ZA3aDAkRU2sy$Bk3>aa5sQ@r2P;jo7tTORN3j`bY; zChupQdTN8}$5;{p$SP1BGXfRpLY%W; zk6Rsg4ek?2NFLV`ZR7_X=LfJaVpUQFd84E!!-2*PTR=suaU)Q3)APvnwLSJ@EiW06 zH7oi<6mRc|C-ys)HTJZ=O}Ol50>|R_o?}{MWq_~!M*Gh`DG!xG?=-t5FGy`D79uR4 z5Mm7+!Sb{9Mv-5{s&0j75AuO`8D=@d$V>e-6-+`)-2z9*@Dr^s(NC8RP4p#?NDyN$ zA-A9EVC&$?j4c6m7p+Q*-oYbn^|#v@HlS6oChP)8jtK4hZOU6R#NOztzOslv!L}a( zf~vBI5ayk=5}>;k4xP2J^|n1{GS~l)f^?BK`;t#cU_M;$UvrUKILf-^C2Ye4Wu+m| ziHBJkj*ps#WOi4WzSfO=%$}>PCybDA>;>WhYKEjh~^QcXU?g zI7b`aHlJFb$oym2f98AYtIId}u6VbfPE1ZdYoxz)p~&!>>b2MH_cLy;nL77=5*0iX zeO9TxI_?wRB+8_B%!d`ST2eWN9}DOSCm;x;n0dxVHUK5AHiQ#x2*_1z1kE}q?Z&AT zsK3B9B*Q3OSugg)IRRO^E@Jl3^i2r`W<*hbsz-d}<1#bkQ%Id!o57$sIeEj-9L&FN zWNwJ3KW}!gQSLk{|6T(-#osXRs!ABb3z26Tb$4~9rXQ)PHTlPVJg0SWdOd{Hhf*nG zp=Shex5_mNtTBKHf>S#}*18(x(+B7kcL40~O+n8e5y)|f{3kl&{F$otf&-m?DSjlT z6jPBF4p987vqp^}83By>!1-r@KL+nJTR$5sbJ11Me_Ql0DvQ%}Nhwm(+%a9>`&s+` zGm_klfkN%BcJ=A>TlgerHL+sgaugL`e_Voq$ZGa`@U=+MKh$u#?&NZMP7(|?Ql%#J z+w*5(HR5fXcWL>0v=`aVh|C+QS)LH@{(I<$mA3QkxU@}Cy}^F|dXdTFY3Ojc<*Iv) zBqgZemc1!i4)1*={Zm6%wN~DeUevigrvvzw$>Max zB%CBu9p2-YV5pG zYj}pwRVL%rLL zREn7XvKjW|e0=r{F14%ut%?BU>lBnHv)&|$qMZgH`;QdZil zmHH^@Mwq#c6Q8|>YtvMpHv}5TSFcv!#B_iEWf#YueFsnf>_3|C?AwNP*>P4)G8@4dZ zHlj9Uv=LFA|GZ`!6c zbpWWz7Q#=l5SF#4qwwiq+zqR5o(@bK1R-R!&+8pPk&%`M$2G*S9bF; zx9r=iATPUe0)^;gwq70t>9VIo+(<=9fX`Xu8`zb~1EAeqH6R=+Tv&BGe7*Hl>NTJ~ zcW=04-(Mdk_P2EX8t|#Pv)ZqTLjl3vTa4hP1{TB9YfMRK#i|qZ{4XP|gV}#`J${L7YXCNN&a8duO8EcgC866E)Idi2PM#_lfM+)_y(wGk~-_}_%kzn z07L)sFS`}Sz~fIn;FY^qFbpgvGNI?V(x2Yi*OkN(0lsQ|(j45$ zU=0I(0pv*OOUs2iVYyT6t2+hkFg%b+P~bMemHFtg>vhMY_em9gcPX&5nOv1N>X8{&hIs#qn>k`qu&Z-(vNz!|}oY^p4gU zr+@s97QnxKC@0axNSm&?9@h|_sjdk`b4FKUlTW_FkR{_|?sfECRd3Sf~)xQgmKdVI2rN9Wa zC^5S^RC}Wxk_s#@ig4crF%rT6Ot+^%f#+|Y{ilCa00GnEO)s~MO5F{a^Z(n4uEIkw zLJs)VAvaX+H`~KM{RKaJi2qci6Fgw!VW0#*2Nu0K`D$Q);dn5TEu^uv7O@kA08rmf zV-5NpU_$s-&(--jXn?xXn}Lh9c@LlwEU{PGv%uy<^r{f%mIwkuUP-~SX4}j&UQlg2VA){&LdX2B|vsZAoJtR0LyOx*&+ojyR5B& zAZ!OP`B4fI@^&|qTc|{{O16N%m<$0qSlIzEbQ)6(AHwI2k-?)m#3mNqCyM|FL19OooI&)fI`_`!wu z;@WSuY#8da1)$-p;$WyX=a=9a13q7bG=PG7R+g+P9Yd}3Ah&=9t7NQ9#Z7Af_RRXd z1rR3349N$Qb>~@ zDUCP9&%rUmj6vX-p0rfz6Xw}fzV&t#NlQt?O^O=%z*oA9F^vt`z)0KZ2ljyH4%96N z-XRTLd{O~6l0kbTfFG0m#;ti*AjcfmN50wk+0a*$mI&9c2aDUzh;2%Qirz}k`-q6Q zzz{E$jWXOQ-rE6$O$S^>W^QBb581G7q8!*Dw^c_Bd;jS8WDn`x?#Ly{%gR}8CC#U{ z7alDEVZpkgZT_$ySDZ;EfVE#=KCb);ENXBU`(V1| zo+Tx0tX#uuFZ>FSL_Cf`w>J350SP?I_+Q9+KgmkEdt%O-pd43zy>a5p&^h2`^&6r$ z33X?Q^L7gvD7@PuJ}RPak1oWh-v78U7t~}MzVPsrQiQn7vr+ucdYewF=Cryf(OQTl{hgXszDc z>-H5ylI;S@vN~#veQ_0(Iq2UabvOFcGdrCvbw7Eyu7>TuxG$Ly*OZ@nQHBzZ@=AfX zo4o*(lKP&qj56H~uo`y&YMZIk0i-_k_Hxe>?x;xQ=7xQ4Xz*%#c1im%U-Ftp)1&;x zEP*xzoV0vmr)h`T)IFVV40Vj;oE|W$wg>S}G;)z+id7~Ef~-52HQ&zyY``@~367~1 z9WQ|bV|l-snLO;D_ncR@%0%(fyT#?Gj0*7hsesZ@j>w5v65U}(k?DCLLYnuAvN)6y z%AGR$K_0{xvG?2YLAX3cB*Z=b(V8MQZNt?7b1#dyvqjjkV`Ctut0UN<-;oN|nuR^R+57fccXwR? z=TUJkbu)saHGB?YnY*>K2Y1}!KkvnA?epZIr}y3tX6aBlF^Dxt_GQn!(s8=1@Rtt! zQ>ekS^=otXzR4nP?rW1pdDsS|*Qa)OxjnyjWM(MV4z=g`Ms-@&3IJ|w_jhSP(mo+aKw_8zAbfA6Xt#hYRejaQ zHN+|?cWW^rB2*ERepkeLO(xHtofHq{#c2S!|JpGsu56-;h-DNA?J3YZ-F6pA=}xlq zH=ep)yJ_arOoMtAw6Cj0_>0Jb!_W=EOsF%uHZmD!+u-^=AHXZEwlnNDf@Y( zr8RKs&LVEO1yT&$UfpmoEP%eDSci-r3j_&Pb;UHKu@x$xFBk7X#r3xh1r>#ust#uD zCzJcaM*w<@m4e7`!*z+%2j-kl&*$H@#_N~5F`(pHVqHvkNwWl+S)WR=5}rKjc`8rM z(jV22l!7H@YrMUc^UQ<*gf#~>C8boUgNg1JVOZH`Ss8t#TiN4>7Lz|E71Nu14CD{DKo^|uhHBGB_Y$}jRbGqt z(luZBM}s+)*_mhZ1hzSMqKHPD^r@|HTv-eEsPEa-2OQNL)I}1rLK_S??$m#A1(+g`?q56-A?+cJt$$QifwR(8PqFd_lj@I2sB-2{ z-kt{FDZ5H2;_L@-5s7?0I}A`M;t&t6hFD>U8^0|+yK=yN)CRCzH4<+5BmkvlnhK=p zbz~Yq`IT@BzCSw;PrHNzz%uW>AuU&HM+PUPq8Oia(}%$>ag`)2R6{Gcw7glVV{6VEnSQX0vSk`5C zMF)WNs_c?tdVwO%XVtyMm#tK1rX6`(dre@W^QpKb zxbzOzAUU`FLMb2#7+-pEER|r-T~K4dJlYLcb85DM3=lln*;hgDZA5Nm z;@S4(S(}?cUY)9{uc-Or6B~-wXRJ?_{+I?ux-*8<3+RNDq9C9o{A&98+}yE3xppH0 z_vOJQ>=>&obUE)7YuqaiMRrWQu*;;ircP-3~#CvGpq87gHCFzMnG0GGw3C4E=@I| zEdl6_E;rW;Z^$Vuq2k$A8>-r~jl!^nyi=^#jRU|&kpcux(=HI-?rdUqJmNcr@IS6i zfjsNvHoSQ@hjWr~6}2}_B}^92G4(cm7U^c&=6R(b=7}Byd|ea!6jYNU!A_%3Wl8OY z_?K5)#swnx=83~6ssN%=sk0bv;(ee4((+@w~T;q_FSfXSE zG52nhNC!KOsycLtDX``CS2LrX*abqeF4AOkn4(P#RYP$L^ctrtIK>U8n;B3^QTRj4 z#?KlC!3{Yd_f<~$J`^qYd~L}k?14D9P(G#gxJ|c_Rz{F4Xn;a8*ETsjXi3LOkxPNW z6+Pk({h|x$pDpYMMM7r3*a=ZD;lpcpPpz=LCj2{PEe~bCA=|XNA9KSMLeF<)I2!2h zuUC#(f7GnBud4N3MpzGac|FrvBeGzR6nZhvEqOTC(xBzrOsT3UEDvccR+8>++Thdc z`Dke=T#K-T1cWyC7P$MBff<3jC8|z$v$L5jWYx)6&<;&n_i42qvR}PRp;l zvNNHxorgGXR3q-neWzgVsSC0;j`944h?>|i+K-%XnUg+oTI*=xe!DeuEZ*8DZYGGq zCdvHSHjCw=Hm$+UMT`$)#!*5|>mQ~#<%8cHodl|@*lfR#$GXd*FZyf}rm_*8wxhjq zmb4;dcMp84(_!1cPr#2Am$vyfb&s>aq4|L$H%HPBhG?Ifwllo-b*mZ^2Ft88$BfrbqLtqEvbB0S;zY4VN!IV) z-d=RK^_=<8LbkTM08g^pu_XKXRkbn9{@b;sVd0d3TdAs8kech+ospVlh}KC0B?|c} zZYQK-MK`3WX7|*{iY~QgcCCLmoe=L6Mzd|xeD?y6si>)cE3x85a!07^ZX4$+iMCz! zt}BAWTc*|KP1^>9t^HY_;8)M+I{LB`bP_B}^dPF{-Zr`ek*${tTIVR6FtoV1S>V=?46Yy}Kb-!80lZZ_3eY1uD>_x=UFg;IYVrJ)L0insP%R4u z0>;6kS5M>Z!oLfk?}-`xQamDj7g=U#ayFmIyJF*@?4!L2LGrO$Qf+^-`0MBL)1JQ7BL+@EKk~~U%pi^6XHX#lT&dQgSa}i@ zEf0uGvE z@Y+813hIHzX25GA0N)BmO^?zIfF20JF9S$FZl?Vr4Q&!GgEa^lgEv|3l(V7TD?dK#*hJOzX=eZ2X4`ilH_8|vpc>`x{x3fLL_QWSQee{BhCkA7wU{Ip|B zRdrP>y;}C!8nvEyAlo$0=4JGuVyCZhQ(KUQGkz&J+NgUy9TqGqUR3Y9}NX;)D9*9 z!Moy4B&n%T0*_5*X23y4v>%(R2|YPV3(|rvK}mDcL?F^2zY3u zT{yk-tEbj8LcE&lp?yBY-PR-Wq2s*WNj|1QQ^!+;V^=RvBRHo? zV(ZDepMm?iP4Dhtj$M-6;e~Bgp}*ba)$KG14HEv%fPU4xnE#mg3SAl}DS zlt!X>b0nw{J=XC(Azx|v#}6AlqXQ~6Jt~X330V%>f3eCZ+Y6F4ajxx_{{Ge)p9fw0 z_bUI6@;g-4v458&+pnGwHiDZ$9}`R$>S5WOf}?lSk+09zk`L66ORrcyXkH;@FM4Px zi-V-R@2wm*Y-9}3T+~&Si2)d_mMPhA1XiTpv^k`V)~f|WA@w@S{PDY_yGcfw3V|W7 zYRp3LG^ERL)F5M{E=HWTyi)L-J+*Kr4ven^mFk#LC}OS2An*jur!|zYqw|( zMMKyXD=__*cIVSLB;Wqak~ z#*mO#`@i^*KYG%tXuN0v>T`v!^X-GX=`2#2fTz}H%z+L8>!rJ?Y)W*IVr5PJ8F^wx za#cvc=(2SE1}4z%3$4hO?p&a~|HT-NPh8h=mE$2)hpOyb0jne(0F)h8_@dm;=0=Q~ z=FglR*m{ObK;wO5w(?Nsjf)XKf-h}@h9(o3gL;I zId}>9mt{dc6z7*OlIJ$6iA!=|x}PZjQD864oa=6;4S$u-lcVx+Ni(F#jP_*{?VI?1 z?I323Czw7AIQ};4pJ&XGu;R{pOy!H|6(1JI6#jA{tZ8n+jDA~w*Sr2NBYORh%hDDL zEFGbj*2rzsWMCCyJdar!zL~4q=HUJNr@kU~wYs;Ol~H_BY2FfIRPbmie5D;wss@)G z$p^~8In*%JfMylVVZ&hstIq>kxW$#U$g4czwPcGV?hV)NYF0|~{cNnkeh%^3op=1< z<%p05jw!k@FSX*$?IW7cJ#E)EiA}T%U*t`oK6UQ%t(uZ_u-5Q&{Sqcy961e)6>`rf zs|dI}^K=~yz)e())lBYVn&Rvsb*=Y=IZL z@IZYi{m-U|5rphC4R39=qHK|G)_G!waX{+>a zdQG|rq=jL*2k;Y<$nt4KRHg$H%-Z_Zs)t;N3E-b3Bc5~L)EbO>I@w*StKH7QbmK@( z{1h<6I$8M#5JkfS`lnPk1WFVyzx#?*bJK^(eSK95m|v!KrdUF)fst785@cr?(k(F& z?RT;F>SYUjs_K6ZXY{)mJhVgL;2}+Y=G)YMN#If8Q90jR?29T>gciYnYtr2~m!R2O zlt9Aghl8DpCo+wGz_?#nxsMj&EoBm8#y+d(Y|+OD<}Osk1o;QpQZZ4XOZ8G{j49QK ze*|)E2Aor%U^Qx|@_hAEFsz^fMTdE)l(gwk&=KM62}37)`roU8WbRD>U%;QEarXlK z5D%c_-~%P9zc`ubO=viIbu40sWuSc}C@m0gcV(bYP{K#PSo?xgMOzU2$QHxFTV!%O zS98%d&&a}Bl2>;GU&$Gf)!?gO9YkFM9a*iMYuUDUh>?q}w(#?Vf$_!J&TKAVfPE9# zb$#@5z;@hbJq&2=)NdD>fZ54%!Q4HED<9*&@pJ4EI}ch} z=g&3Puq5h>(977io9uD^|9xz`@;I*iDgp6&giMMf?X3f(a|uZfzKx`bUa=DfZ5Xe0(%RTv)C7Ffs`^ht&q>+oCtQf>av9i zosyn}XI{I%_Fe@VX8zCuxQhjtVODJjBt=_|D6`#DtOsKc_jb@KO!;*vYJ{0;_3rY( zgmuOL>vt|GJ}=ipqmZJvrbU@Is}Q-4kf1_Lo?M*kmP&fX;kr!V4T z#D~LxE143$064Bbd1?;-Q43vR1riqMg&+IZ7#dFB^WxRt9J>#k=F;p8K)b*EwGr%F ztHs@sZ;hE3@=uD))jCk$(^0>Zcg?h)7=uGcnrIbF&JHrYepO|lW(*9SZ2X#GWH+7NX_UOtlwwmc#z-RYZ0N+ZJ1MfNQZ@Rr$?iz9 zSEW%qB&g`*UQOUHBQGDZ(W&9H1{4Y_Mg`ojENdi)^$xn9veXv8A2_BfFJ|`t25SOk ztbdU%esQw>D0|$O1uug;HSPaY|9pol`zB(b7-~KU*5i-kJWg=zLGA-}?a}$;UokMS z!*muudi5u}xn^f$_Na!Nn7qom*-(`EQ11XA`J~c7o~;{CCP`GE?qNe zv~mEe?kruyMk{^!XW%Bl#FNc-%q~q(40^%!tJz4b)96b2UAOVQAp|uM_G=2FIz+r< z%sLIMY0LWIH*=+TwIzcKV4I6`TH*(TTA63!S|`BnTdOtJ`VI!w_pun$D{V_{3X~rm zDISsjoW5X;)SeZxJ8|A*w9)+sdr;ih=d;$(&#rAjC^&xB^U+jgTvUO7_iHF!M2GyM zyFsRj+pt#mvMcw45uhk0swFLUH6syF&gb`BAgI$y{DG-dDsTv{oIz9*)dx=7GsT`f zu8I^pR6(uA@nw1aSwC=g+$e(T=OmiAtlGiH=0f=5ZrL2 zVgz$S2{tyC|n%5ma)7pZG&Ec@h z22DT!cP$K#{RzA@5&`}5a02sHFcA-`)7a#$@d9&SBTNHg4^npHH%$l4X@k$X7qY~Q)IIMgp8jl%??>&pF&K<6fvUGfW<{#c8?pL4tMFaamfKO@Io&;(Zc zo_mQqpUa%y&-r=sO9BLqUc2)*L9lc*Ownyo+*s0ZdOloL2}F5S9lQ%w=K*`$EA>PF zrWE`(_0^nd$P;~5wh1E0J>p#iXna%IWaMM{dp|N)1}(;Xh)FCC5qJio_bP{o-?P`+`@io$MLnFk?<>yp zS0_F4JjJM&L_(txoif1>F=4T!5n{|`qAk8x8Rt^=}YqCL0F=zb*z_Q#C)17H=JQN6|khW%N!Ij`h;ot6Cl8COcv=rMo zIkfFT9-iurngPcyijQ*^rni6o5%dF{@T!44Z~I_4;XB%Rw6LvU(2K7$tX=tH{Qy(iF+sW|@=%rEzg=t^pa zqQ$W}at>pRMUsCDmeH*x5?|@y@!GLygOWvkuMb24hsL3-jNDUPXOr#ZBR(H<3me<* zCtaGaSb0K=HQ=cTL-dZ7>+L)wcl+tpLT~l-H<}`!HCINx?T&E8cYIY+&uRv|3YQeC zah&c9uCe>!bVEqmTQEYMw~T_LcnE62WGg&!H@1G2UC+>HvPy>Nv*nx|f6PWSOKnxh z9L{piQ#fZ|O)>_cCk0Qyw6nhBI4t{Dk(Vi^HS>jmd{dIG@bJT%i{_Iq3B|@wSr_*o zM;Ac&K$8VfGF8AIiQD*DN-QmRyS%T6HzeTRyBU)Xu+%=~_{7S(!@21$SYJnZXs=0I7IM+Q~s6F?p{RknFti`<=Y zYLmqsGs`sI$*+|55UZfyw>m5=0CLhm+=AnBNr(wY* zgin+Lxxhdvz9uXDXE}yQVjYf=s9wI(hd_S*`K(NN0NnmcT z*2b!yeK2;2 zU&b1>Pyij17NYKL<6ycq5ei@khR4J4jU)b?rvX92H+}Y=i?|I^vowMIoJ6C%4LaYP zmF_(?y{bppP6Ac@sN793Hwc17IS!|t2CC-=EU$uQ?4d8tPZDvJH^p9_mToOA5Moi5 zjWhoi@v>dnjYnVvh5t8x92l3UYW^X7)-Q1aGiA=NJAYcal*!nWnyFtmmp4}D! z=d+VK!yu(USnv5&L$o`gS;O7!&7ct$Vn(@W>OK^bI zzie|OPgY>UJR0I;BNCO@Cq@z<)sU~!(;mxVYY#f7&gRp<+)dEwKmVN*w7Pfqoal0g z2n9MzyZR}C`to$qs9YJ;#}^GBe9zbOKnGd_;yBW+m!~$y)8xFkqkSNWT~AR*d5$7b#s=fq-Nq9i~cxrSz#JOTfk{9bo5 z<52?;ng}1sN}Q>EnlVcww>^WZSy7bN++khnSe`z2dY|P<+HpKZ(M0(ns*BswuqZK<|9~ zS2dA>Et{(RSPp<)YnL^ua~=Tf&UqK?KP}pj(Jt1>5N&P3#1_Q>#aO1$wo@yjo3$vV zS*!biQ%bMJZ4Q}fwPf)Iw zskap!Zph0CHN=l$z;-Ez#&PctF#Xi=*d6$}ml78>OJfVc!ipb~D?&-z^A=Iz74q9{eiS5+s|~c zcEOIPy#V&CE5DZJ>{gdMDx5EpU=(yJ_VOxrxF(&`T?$pPB-pQcZ<5hI;Gkw`BPZ5- z|HY|H(xiA-Ome1N(+f`p5(4(~np7r#A>4B9NBfPaS|r&=&p*2D2(c;%kjD z%kIG?=C?@efB&7T=FqhNS~a=&2r!ouFFCkHSG*>b+~4Xnc$?P^$Gw(lW) z^%y-7)(8GnWb?>fI8(S2!_8O@`Kl}ilAUiBF02&_7K-?kD6*uhz8c-{nFB=iELR&dr)FCFKzqf=Ub z`LIHt5V8S=y#OoOpc3dieXpU=xo^z9g_!OP+xl2`{>REKr?YgWQ7$i7bdow)_-*kY z;6&H;ZYQTct_~{1#QPM|9=0l=nv)Y@O}E+5%$u~YRr(CK1vsglxK2)5k81cSq}1S% z0D_u?`u_^8Ra`bbMrW4mV2&RlJ3ZUCAUY!btdZ~Ue>%WhS%IRi{^v+hXRM@c-xxEfe=|f`sG98P;eMZFr7SQ9SzCydFmpg022!>3Ebv z!*T^S5NS+c7563`-g)u5Nb!8wA&&gq>t|Woe*SV#gXc#HBO6>IcvxNc;~z0Nwzj`3 zJyIb07(r<@3;2(Qv&T5gaZg*k*1F2PE=DX?M7t4JAwW#;!(03}X&`bo^|whX)0Y+q zHt9p)tQVgZnxN~1wE*3dU zOwPiTrzWyTH&|MFb=dYWFVPkOY{aT}`PU=KX`F3@L#N~6rjU)i=jQH|j<7YkR#X%JBXjR9VgO5%88->PkMKW=^_x_!A9tl)Dju8@f1eM<~ ziYYS&e|GK?Ub4!>evf#zeec6YiJTvvQq5DTAR(eHfYIfwAgP*!nA=4HNpAJHH-U89 zIEy&FoHex1W9=;{L#qErN{3!NluEcQbgo+zuVIx#s9+c;%z4UYIl)ZAmRC2quM3}7 z7OMqS;l49$KxBXR4{>_zvEAS*AJNfmjTefkk*ErADuiGRW$HvH2K&IhRf%3yki?lO ziT?nBZ4DQ*vvl1wkfT7jw0-}~R<3$^NAk=@x!>@0#rMM4bVJJge&)qxnfD{q_|dtA zYdWtJoX+==Fb6Qd^Rcsb98aCC^s>O}t|5{UBp%n5%WF1SHeg>QZ=SjgY&f48HJL$r z(t7ueYqUo{Luq*sUVWjOle;z&OKx%ojg?)CiTu)39UeE-IC_j)OJT#7a&N>sH2V7_ zyQht|^B12@0sQt>sq7^zNvHt#0;b~E1WMRkf$XjBk|d`rW9leJjoch>C9mjR4P+RS ziPjUXB>%hQ5vLY1;^XSqf|%qxPSm|Zsh5lE&ODRPmMs9T^$+N6XcEw`e?xO^D1ul& zdAyM{%CF%P-)!PU}5Y z$PUEJp=$g84MP9NPb`_ikuE~L4V~z1e>Vb+AQ0Su8oK~9iEOM~85{XMz>~HciU|~` z|FR;;BlKqcDhlM7*&l;FKJ}T(`)EFTUCWJ$nl1TfG2XX$OK;T_Fi9PiU)KK`&ZJ6& z0v4fGx*g#1D4hmvmc>e{D?1d4HxT=M(GGlc3vh333iT@y=A*mS31U{d#Mkb5{+lN` zR?^__Nh2*x)Mg%o8dV6L)pZy`xjGr}ezW58un(+e)fJ`ZRx zanIkRF9)Yt60WqK#Kfo*#y}(o3asy^i>bK>OKW00hS0lGqU`TOou+UKRIvRMHudkT z*yKBiP|MXHIfI&~XIF{Dk4)rM;~+DSRRPUmbq8M&EqwJ$e{rrD+6JWzgKIL|&AOZ6 z8Qn6-I18o^X94K=cF!N52)Iyg`F_@Vz^Q+A{cGZkF=WQ+Lfq3J6kg3J1>T?j%^P~c zSFTd~LPP|W*WOTS6ZRpx(j}R#b=bol&PChK-R+A>zdTz(Z5Us|NmhM8#H8jj=q$*Y z|F2${ruMq!e`EptmzQ@(E+P&DEvg>_50M^l0i-Q6+g9%lE5<|TUOvc0G=sC#7FKXD z&VPrPv?ryV50zoHJT=}#qB~E8AQD##+|AV8Koidu{ljbmM@IW;1Fuuv7Et`!!G%=h zoIwwlSaYw$96;AM{+wwL4F`HnCxj(1JDn!&rtM`j^NCr)%7}1RMCaGcC)i z{{WN!133AgwU{s6JrGScOl?;2-)!lB{3Fg8;GO#)KGi>dU@{GG71GUcY5zAX^Pj)P z|Bt_<2@PpAh;-~ZUP(8000h{hiD4~my{~q)YdIF^3S?T;aUwnlJ!QM%JZxs`Gc`F+ z`bLHDQamXhxwD>3`>4a~a{leCRzwVRDrlDGK@PKt%?WJ0SEXLD=tdq^wM(?m|ECfN zSsd7-DTZ*f|2S{|@fVyeSFKVUWrToGJjdcDRK1ph{#M0$)+*i0MF`Aa?Up!em570i zBqT|e2{OXK{L%wx1w_6_rKw5}6uAlpC58Vvq(uQqmx!1kKFVh<1?Z$a}0Vu8!e z>yJ@!W5XSwTCqNthdvy@oDpJ zRoMZL>M#Kcgr0Dbt#R_LUn6+86jOQm49wWiaR62ixms#~Qg3CgwkW-;n2>hV2?K}* zWk_Gt1NNdi1X!7(DvTEK7?duOA|&>#KYOMS<9ePBy>vfbzY>xB;|m5#J-0f2^;0>a zvJ2Y!Z^6TjX9mm{Do@pGy`0Tzko>SNe2D@BDw8jwz?GU7S&7P+;<<;w|C8YR-#%z? z3;K3sJcUzsQZI@a16-GSI*I1=8p8{e@x|8^_lfQm$b0_ zTjn@L6i_Yjv3VX=VJk&dN>*f0`RH3Owa=nQAK4a;+CPUZeUEIT2m0)j5El3` z19=&1|5QT%*NWq(*^A$RH-98B{YF}z489m9kfsrkezwF6W)ncB3=6Ozsa`~Q1DXjj}R$#Vi+LS7EwWkt_?)=P~-TRU{ z$2lr59l{%tuiNw><{-v$kysC2fsF=Gm9c>sZlmMv%Zu&2l>NbeTR&IP z@#ze~RMv#|7mT!e=29UEpS(DqbSyI^+i5reuhnwilVlJb|76N|YB}pZOZs}UsFg4I z!j-Ei>3XMpivK*6|2MxNv-HI|(Fhk@2!6Evw~&}%BQ|y0ZrKN<-10YB9jX!(tbRGW zWCag!%Q`XX5EK$~@8CX{VAbfy8M;hx@mv5YvvFZPrn?mI#e5BiNP*M8O*c+8cTYcH^4m*?Gp@$&V`XW4ic=25H_1!zL7J;wke=cHc(gsa%E!VV`Sb9>hG^pP&6tRL z_gQE+xTt*?-^3URXx>RmvR)%KNm|__A>S^cVWABawt0BkCFpk(Fw+3I6G#nz#Oy>( zYkrIn^6|w;9(3*~^W5{r;B-Yy9Xh6liEQ--^;q?vDXwhSCnWn~xNfF3OSIpN8TkEO z*?Q=_ke(#TjfPr2!;@yFg#UlM!IY|gzPnyix)q)11I%sEo`x*V?tr`RfoxNy>)(IG z0Xw(J{U|^tqs7kTghh%$Lq2iD^?6mV~|0)l%R7+^3lkh>$cL!!|q zA_k0DRO4}IZvqELl0|k^D`T8)rr6Z-{(mNKLs|i7eu~+w4`fJ(>`j)sIfj4-@2K|y zwKKo(GMHY|B)TTA`r6wCn7!}#L0np@QTY^!#f$^EzLu>3X~}9^SYxSY8gOqii?@hp zAhZtjeX$*4TbC^_%0kDjegb=XX9ox%c)tlO?j5^DT&4hYI~9OW%SS7e%M$hVGnr`6k9 z0~SrWvz^BYQNnZ*LOEEY{#3i3KZ*VgIj`rI>ra3me(J1^uE!e~fhWqg)?8@_?y#iy zx*UoWnZ-G)G$7c?*FNHZ7Ei?C|&r}qMMeG`>E~k*gj2dQX%`Zr1w!pmXnRp=?O&^sd zxK=JX?DBB3s-dz9S^UzB$4jBmyB_Ph6_Kq8|GJa8Bnc~w-r z(V?8xpc{}=ZTju5Z+h>mxq5MqB2#NigbK^@fpVojd-eiA(0t4CPbyN?khzs1>w9Q) z&u+3XYlreAGbB^(pb8Okz<5vQ*BoTeAb$`RVEeZ}Rw7RgWnX((LHLzZgKd#(T(omP zpt%xU2LAx`UW(zqc0KPZ{Z80DKfpk;!|iVC|Do0b$h)qu*CXA%W36sz$>{#?RGnsa*J6F27f zymiBbF-UjUh$cv;0Cs&V$z;m|_1O*f-8|nxsP;1pk3Xme{@eX09lBl;OtiC{M17wr$hNcS8OuK=CftD3WjrtPiXM7-yGTXMt z+-WVY>qYoEHtn$fEzm6R3p`uW-PU?b(`KLQJoX6xK~6oWF=1n@8=v1S0gnZj<#4aS zimt=!RrZNX$Rt&Z8xP^BdUJ540qzzCT&0};gr)rCA!eRSDzjtA%0GnD<^vr3p?c=w zA!NqRqgsroY|TwBUB?Mn0f8^?;;B??)H6JiaL^j0V_0^Ca(Nmcd=cHiYAM_!3Jg4` zMo_?S-%9)Bl?-PRyI@WjH`Lhn0_0c%sy#uUBL|o=WTOdUR9)(9spHP0f9}dNr;cnk zr4XT=yTYD6^qGL)P-1z2 zH_w>MX86?70-(dFPg!_|_4CG`F$-@sW$TnIFIV)FD48`N7Bp@jY1}0CBd1Wgj*fA~ z^}{B5*jv9HAQBT;%lA-d#Ab0TA-b!*zJE7n@$nUmPQ$vER}oC8Ut?fJ*EZg68vxKo zuj}&8jSe>;v3X67vZe960W)r&0h^4oFb@51o7l_}0Sv*g^zpO9=4b1h`b5rE9Iw4N zsgbFb$Bn032K|DIp$8dUMVeMkN543>MpP>dVAR1PvVF5YWh5}J{HvlAIC$V}YT|~a z%K&%#-&9=xE;*MSeY>vLF!gXH9)npM9_0AdImkYjgu_3W)b)Nh^wYN=%GT)W{l%PG9gXnqKBZVWp*(oOhrR6r5qJk0cB9~|R#Yo=Uc-Z{*__V%U;Pe$MGUz%-; z{@#DweI~KS;QRTuu*Su^H?iSzn&$T&Wxkig)q5)WwP1_=1A)4!&TX8(@r!w%w>;KL z!V3=8xGVYh-;ZttU!}{AN;|ytT*0LDdTa^7ngpuO(gs#$on~#UmIb_)1qP~fxut}> zU$(weAY3`UJ^d0w#*0B)_*{cfpDL9h;iMT2h@a?4rw3G$gwhBGGND8)C>w5qjq7}i z0#g$$gwGlaIw`@YH)M}4K&Ke3FMEf|-?RfnN|Gw5H)6J!vY#Eg7K+LDUQ^ou)L#yjSX; zW(otmg??fWAWXp&J_v4I8IunQIC{yR@1H>!v%sDCA@8kYcW{CS0QXLx#3&m7_!RV& z!AZMf5{PQwU!aeh4`oRo3*zPEohGpwhIDMQtQ=Wegi>rM+`e9LC>dvR&4I61_nmi1 zsuOeV))L>Oto)DYHoR*ZFt&oKi$3^())~#RL|@{JdC=?<1NtvA8=S%2oPrPXXmHVi!TXD)fEF6u!DaC zD293o3DuaoSP5ULT|}y&*eq_^cb3On_vQ9IBY6^}_xa3}qPKfpoyQ-eJ)Bx;YJ##z z7jz`^m*A9jDykydW46ALTj(|$odcXF59$~*XuhPlz=7Lvkw?oP<0(Y{5|h}qS{ZrpE5P&X48!b*@<}`x@yGQN9A{hEC@UW4oY)9n67RA#73}1qOsc<<;g6=* zjF+iDyXkO><}Q@WC7d$wtBOlq2yH z6-eXkpEpU1F#Z-jJtPt|5hR7fk&KLT%QXhIiN!B;$~DW6_eiC#jzL5N+PZ|Eq(kTqK5^l#m$>j+ouQ zaBy!AJL)EhyAlyo-r-*?6qTnGh@tK_)!+U`x+Yj1y9?KoSXfMrQ8T&=n^c+9n!LY# zlPrr%Tk)%=mj#7oM1w&<9NrbW2RQ!kZ{NNu^%U>_=r6n<>#pARV|4|NM9;m=KFgD#>01GF+aUW6~j&`^*9 ztBZ(KY(u=}bi(pRZ$F;Xy_VJ#{`bO*Qq=qe$f5r7<~m2l0_G#u*DIZxn=FSmV!1TL z5}Qi&_V<}M8X~J~lAVEgRmZC-pT?ciw(a!s;d2Rn`6{F>=X^x1c@_DK&?iY=AK(Gh zx`^~_shSR|Q$>yj-ZF0PSG=v$4qlzT=K%-zL@tIg3#d>xvM*JSFE=?AmOFS!J=E@r z=#un(#6{)EZT^;ybVOM8jU?t=6CmpIpR=nKD{5W?@`{4>qRuQ}hTvXE*e=rV@)?jl z`C0Ear!<>;o>zwzPTJO-7WbO`lGP%Z>(D(bX4ux+xsX^^&W4-&!n&z`I_F%b%sn!2 zV)HaE-K%W4d+wJjJImP%J>7uto3}PllJxeK@qK4UEj{YYr>P$p%w<9)yNnk!8E9zh zMjw``AgqG=9=o`)lx1D+Vw9>~Rtvt1wG|<8>xds>I~0M!t<<}%Lu4&MH0%%(Pk*BB z{dOX5vs=1lrb4wRW2|>ClHbRl|GcN}|6;%V*;br##MXhGqc@)Q`EO_6lT&z(0e!95 z9kB-+wb@Iz>oOZPo@MxD5K1yIF4p*T#;Hv67tA`m#I7-{h}t|+HS&Go1p4C+nDkwx zkH~rvn?H=<{Gq7Pqc784!=F_`aIWSAi8!y1tTHf?Cc6wjhN;*&OMG_M;8Jejss}P) zRvvbG$(n0;ku*kdmY-;ZRgzE6u|UPA?d;~(w^YM>OCMowZ!`BHalgYnia=PNC3n;P zrKVJ+PxQ5$4(WGvX2dV^WmyhO|DxmO_>4Wi&(Iz7GKC{6Pa?QKdnKJjwYpz}Xgk>jzKVV$#O@`OceJTyG%gMdEbWH3jH2keU9Yi-sD!IX(yocWmI zWkLtgQIlCJa4T)d>xrynKgEc1s0=H%wkg9<1@WweY}ryqYARR;{f+`+DRcSv;C=nHoITs2{$ePwwp@{ggVl89qYcxUB?9f9%5h4g zdUCHA#es%a-nNg8E24PVySE8-&a<0wHjgW^y&~%Ud;o5~{7}Ln?v5Gan#=5#nodYt z$BjoqRTK3rH>%XjxUaggVa?c649G+ou-rZYG&laW?p*XHHa~jjKEO|mtoqWHEB9$3 zIw#^geWhq>1WI#QfW&0RD(e2idPy2{sdnDu#43aK&n?m2#D~10k>J=Yf4#7j& zUuD_;KRt;5eO_N@z1nxXM!)%6;!bM0h75~rk7?R_jR-2Kl++Zw$TGQ?PJ1BFM919F zB2Y#XY2r30iK{h%>SDDnbjIj@|3apE`^_}r2c5{f#N80wx5}aV^WEhmb+J)~b@O6i z&l*eBwt4wXlYY0$R^4?|Bh#H6zx&z+z(W(ivW@Dm9jx*6H9eFPauC~k6xsK&{#g2| z^||xQ5IL2#D)()MFQVrz3JWStYi{?6`!!wbk@PVnfw}jnG#x*TLbm1Xc6A%QGI{k;NMqIX%}nQ^&_nN@}9`pU@*Jc|-{8kE1w?mb7iMP{&3*@1~zrM(ny zg)DEceDZI-K8{_%H6Mm+XcsovorBp====Fdy%wdf0+_gJk=?yB?0~8SEuSVyuBXSc zjg7zBmP4|9)uQhlx8nzx9@W|JTU#&TpVZRs#GV_Jj~c09^!sL%bJM`-rQJB8bkvtq zXJpg_G~?vqHa?>Y7fqlE)YDxnptoLTE)qOjw3mWsNek1+eO$KC9i!)oKlZC8tU)3U zKy;s_O6u=qn(>||=wEyaxj|A%eIdN{mUBILzdyJ4Y*I$1y=y$M{ZyXVkMKF)`_?287?B}YC06Wp06wnqvgAKt{S?6eXQ zji&;ob#r-(Y?I77(Ir;mk_0=;`@?eH5QMSJ-!wjw(gmbM3xpc>)^7$J(E^jP>E?Jz zb9se}lXjeTcLnjztH!9N;Gkb3ZloV=O2aKyK_ntRXmeE~qkDS^wQ)rLx-AZrz&Ij% zpA%0*s2%ViLjO#DR>SBvB9syuKYc_^zY_=y-KzhY&4qrSv_UGw~yMCD!f1tzoMz`=~VrRi1XY!pVOZ$PT#2 znlR>6kHh->3T;R((nzb`}rpk6PweI9pdwjNpE90-~Y2uc!hX|No zNN4|6>RLBa+-dlF;1o7MlPd7|%#eeK!ltaJ+p^&xBi1{YlNd%mtj%ep+!300nt3#$ zm~L~@KdCITNiqhWDsSggJhzHi4-q7xFa> z$3K+)inzXe!Zlw>u6b$wmuBGF%_|0WvR4#aKfWIGYQ56)1yny?SFi9Ms}F?v536~; z3Gz6T|I%JOXwwI@t&Sl0k3b5Oh4(aUg6F)D*!KI` zmmx&(2lXGE6r%X8c&1vPtNGhJk4Rs>k&FeUzRi@U+5&JgBgG0Ppc3C-rI`~gU?1$4|zf)a&mi)m> zpQekoBYzgL@l0Q}n>R)^iTBY>Qsv(<_Vh=xeV7Ysk-^V09AoxXKlbzEt6YM=yj}Rn zY&pmLgtu>4J2MiDBwvQzeNf~$6&?s0Uc({+MLy+R0kQ1gUV?=`X+A(pKUr7ReT_&|t)vw+NOkk_Y2;jjPE-Ag#^0l}vl8DSdu%aX zj@XOuqi4(xGWR3q$-83H@0EY>IL>_nQc*RBXm~bni{RYr!wC34>-pfbUv37C^sbv| zMaZ~6?Wv}A=oz-f4JaL~@%lA0tjZ}P8Nyo?{pQqviYrIqYhu0JIp1j-QYt)W_k~8^ zqPxiSmjY2*nW1goUj6NTOhzPsUB2CzEs_2jxZVOvB`@xzKYa8_wPEk;;?GR}n8v=I zyZZz-&hDM>kpi`;v$7ua{vJn6Fg`%{OL+H-k ziqYI8l;Jg(?-o>5!M3wc5NZxf)8F{wOEF+L?1K}>v!P)$>XH7^MHr<3JK5S}tI0EH zJ1k2ReSr$K#|X78U4)7!m>s)zpPKiMUl#Sd;Ny?i=(Rc|CX=Aioik_I*Zo7{Hp)ieHtb#Y#GFZMIpy^brgL_9wM*ukvp-qVoiKT#Wd7Ip_zfQWFkK-6FnSJhW) zNx#M(k=fnwan~G*?vlm8dncSHqYv|(%Vg-?)$dwnS>^>|3dSfYn5YH5C+qNjrnhDr zP+XUQ#b0a@QFI%uxeoI#&v}6NgyyC=B<5IIbFJC$=pdQZ&T%do+7&h(_FjU8HrHF% zBjk+Ns}>+<9Ug>(N&cb?9h{ysSj;#Ya=EDJeikqBuRXI$i}xle9%1m@$NN=@Pj>dM zt~k(U-%Eb4<^C-|I=1!7!D40z`={I*1merJCm$8U=}eG+wUac1`KtjttIu)j3rcKz zl1?7ynhllyOjj08!mxHo{UnBDV`b6El%JKX3)9~hRb%(VrN93b@U#byGe;I1%Jx}n zvv&Q4zQpyc@=10Anf#dLgWFH*K1LERc-A(2hIa^1_-qOf&K{5db$TPo-|$CqL4_>3 zr9c$lubAN)>6IznqUz=9#x#X(j#Iek>Fcw>%`pX$&xg{2e~yBmCQmasY-pU>g7b2A zc!>tT_rmqRbJY>s`xUyM0v1BSkJZbyAkuRK&?xK6{dk#NG+<*Ip7J<4^+m`;4U|XT zc|bvFC}L(5QtneM2hRE{{k9Y?5|3zPINvQE?d=R|Z|Ua`MXkST>mp<3y+L0{K&_mT zCn%mbHegp+baIICV3KLNoUCeqFAMOEBZncR=pup?&N(_JQsAL^iCGqP~u>w zUS!UBuR5;r$*bfSU{lL25hsHyr_fz+RBa25YcG`*H5Lti;2$9K8WZ6XgfYXFd~Cf_ zDb1jTj_|3{bl_nAJ-7MvS(d> znFsk`m&mk7%#VG7Q8Yu|FFnysPW9KL!ye!oskoyS<1moRy*bW&z|Z@qiC#DW6{pp_ zqetMQ_)kp;{?XGy$m1FHTAG6bcK2&MC3`TB*SknJkpVW+=LXw93zN=S22xyFG6w`@EeY zLk>;YAiUGHRsRZkk&Ug#_B&;5)>e9+oD19xa>3+B5n^}-&X2xYe?~+^XMKx*V50xv zRbo-a1KkC!c1FD7pS{llo^L$Nz^*W~7q)ZY{lsG>q+*g0Hqd?Yd-mg(#$kLM zGF2|iSHwwu*ZB7-W;#`y23ok0Ki?|bvbN;h+7A=fRutsy+|O?tEgx>*57pla+0?yB zS4ef==6SW$*wpng%}@RW?1h3%;ZLl~$oj9`Th|ifVv{UMH7&d5(-teNqdLCoztqB4 zW#_h-KDU2XNrHqsJ}%wHNt~9?_GerZy-F3AQLmCFTGJxqHq;Kk__2pOqKas{-$UaZ z;}UT2_1^r`VwjTSc(#&$0SC=2)&(u(oNu@QA390!@w2KsDvx3w@#3$hTSmkU-uJaA za7iL~xAW~#hc%RUCalpTb*rsx2-a;VboJ#@W9P-ab3Lti20SGXCv_B7KqbNl)=w&k z&kIMJ;+))3@0Dk@@W+5`n}^}4%4M#B3I#PeA}2uRefPYCW-b_#8%&TjSofm+2jTv2 z8=w2EEuP1Wv|M~kE^|?1Q27hWEaA58U|}4!iaYpVKqMg1WWqrpBA1gdi%GlFuknG1 z;TaLL#C2ABNq13ziMeL7IGQRYeu{oVzQUzVXIO3KQ=z`b@P(?LnrGPoePfBGMe$?s zb5ppGq$9DJbp-Iu?*uvY+oHv|*f0DCt&H0yu3`=MWNH4m#n2;l6 zfBI=*_VJf#A^KzST+UaPM*t?avIzBKM=L;^byy>23+`?x!vbTx6mw7+PAP02JZKXe^S!L z8hfsM2<%$&%u@f)E!p9ox(Q0To*rR^+yB~<9SOasLSs?bsBi&IDfhBP=N7gNSy98a z@j9b={GwpB*}TT69S`YeEZYPz`vl-ZN|Jg{lH{_lhXQfl--~@UWUjyWKa5U4W2kKa z-F(tXyUE+l=xf_{wqnioO*fOb(`4=kzf5XwF3fR)onPqjcCHBKyH%X zL_@{kmCmR8fm*huKNp0&(UXNzO>5@*{urrXbKPsc+}t&XUSaH*6Xpj_)yCbAf0 z7OgFX*9X?a4J(Y6@^i>3-Dn~nhS|AUX3gOz)5!Xi?nXK6jhv7rKW!w|lv(|Tq7|X_ zlM3&;xw~^RzpI$;Ny)94nEfoFUV{G};<*QrUzmxi=6c7Ryj5gQg3IFi9PD5Mzr~tD zrfe?$^exW#Q-zJPc)GN`fZia-Xwtc-3NyLew>v55zUI!Kh(?wg^18I!v+gsVUth9` zVBW|oP?$JL2tpEy7cAl10}l#9d+%Aa#ZVwml4i*CN?*4eMcT4ux=pIXVZ#Kh$IBI` z(A(O@;&5B((fR3WHj3bgygT$cxe8*Xoc671TghSM z7^%lK-^OQmorV?C)Knz=9zNWb)=(a;SWXJ3^|0Qm3SL^-I>RSM&ddQTivv{$yEQJ8 z_c)KXY6p+SsqpO&jsv=FL>eVQ>syIUPYWGQUq-$@RKiE)-QR@NNm?Ey{`303AkKt{ z-9zgBLz+-#LC+qM?9CVTbFx|72haU0xbMUSMMpi}c;u&m<&bDMBV5~^m#@yt&h|XG z=d~5{I#y|hw81q>P2~tmb#4ngD%F_{ueLPkmMx&v6e~*5JgJWN>{)#NvcC5#4r?zy zEu-ayI-AvpCDEl7%Q-N# zQyJ@S``yI%fE-;Jt_fG`IBoP4%1W0j`RZLacrm22bS#*U74%Jq@o6-Q_Lf?K{rm@o zj3@3k==0&+6Q6^zJfQs+#J=XsCJ`Dr;Nfbpw?`?1hlJ(G%?MQmHj1M~bZ=US4fL3# zJxvMcAUho#Q%i3arQSHIO6t3Li0m#4Rtwi-({L{{$5`T_{-Z&QtH2F6U==W{p37VT zWDRvsTNiWsOLN0NotnB)eF~p63(Tcn#lPo-c-wV&SEa#?#EAGyy`~rA{9)j&dHPB+ zrav9j9yyNPgS&Pnw06JmX@jX35ZxP8V%DMkYv6iCGZSZXaMO?{(3KIud`_o+Os~q4 zUWH^eem2H8DpyJD?xZ(MVPQ$X{fJdR*!SWj-;xn2HU_98kLx-5mkJ~91^>8-p9#fa z>1zD^Q@yLWh|s$r%b4SD%D>v)S|MF5yic(`vOrR6;x}YMncvKmelGOrlLrBg-EErC zr90um>)+l0E6V7bM?YkUtAVBPgEG7YM1c_7aZmRpv$0yNW_iVam3qXY%JSL4Pu%ph~Aka>d|`sIu4?K@e9tc-M}&rci`xTuNmeMA~N> z7{m~mwTI?ma!ZZ=4)VtHHIn_w>*+5|D8%6H3iP>zmAlj+akGKbZoJr9hPpy|>BuU? zc4qxek!&@oxb6?bNg(jMhV6n-P1`9rox;g8T;gJak0wYyE4o6EvTX{8?wdKj5@fy) zlFGz?>e|mZBm1Vma_m__)>xjy@d+kt&dgWq`CB4p4bntnytQVH)gVtr=eUHR$^_@o zj$OIl&QE+iP=)i3)Nnt(Tywk@!Pkg9iZ|WC}eqg2j4XTsQDwG1=_5?5$mKIl*_$M z_R%b%R6@h4zeG6+KAJAHef0HgBf`R_bh|Ovdv%LU&>de>tKnn?qCvOFhd)(mNa(-i z2*`PK^G&&OUbV_9jTif`qzATwcwS%aGisxcrY)207vMxFTOf17hdS|M<-wigxihAu z#o*(#M|qtmqu>9!;SC|tCEin#Q6|Z?GcU{ajfdY4bAGd0?1)e;p!DW2pvC^B|0{P? zv_yHy#Q)oo`=rnqjSa1CG>`pjnu2O?d5_p`C{o#ka<_y4K!qrcAKzUkmjE!D*PWkA zn`%co!c-njV(8s;xsyDfB)iOecd#j+U-WFCu}5IZ#YSWjHax;^$kM|SK#qneUWKRYnQQ7W?yO|hBVh?L_exd56s>d<0} zMN{*JY8P9aZzgTiPaJwn$Xid&k{qu(M3#DSBW_T-fcI|qMAEntq>Npsug9T3`&EMP z5ZXq~KWUw1-9J9v)T5ik#pM)Nzhh5-*jC~+(HKsAwvhJ7EW_(yf;mAdnoLR9wfTLq zE~|-mih_6j_NisHsa+%38Q0%nYDkwu2O#Lph)4d^S-{-xIt_ciDMpNR*gOd5+{$wn zh{zoa1109Rm8sv4g83J;VM@}r2WhpthrE{wK9C8!8Lx`igO+Duj1Y}W-&0q$Su^K- z`gh$Z(`jEwWch+W$C0tcy-5$xJ{?~E~LAeSD(%1 zcvyK(5xTay^iB=(ikJV2W>UTpI?=NpIp7|ul#1^9YPvL+tn8}Spwi07k?7O;*IP;$Sp#O<{1_|gG^lHSe6ype`Q;~mqTRRcB9`Ukz0%gv_0-volUFi0 zJ&)d`{4Jt(R|uyb(XZwm`~xe)dAY9BlvEJ0Tc>4{(_fAWvuuHZs|S;GVHvH=|8Yp= z^oKS!{k^AGZ^?u0;eF{|8y@RLL+hfv^svqkf8V@nyS7vnaYYW-D}=f_#4MXcRg~*` zfY=7JOfXvey5nECAH^I=+r`&(Tl(CGw-87-kvTh~Y*W6|#?h7Tm1u(k0bV6xhZkC( zo4i~ckJ_GNzxPrc?@pD;3M@Ps>G6}9+$=3!18SDj)KY)0g8n>kG%Bj2s^xcnnD<%?GVIgmtMMDp}sIu=_kj#+aDx)4eS5%#sblIr?7sUjU0 z)z8!6clL|afAfX?T8@ISq-~)87&1c{*YL>)!Q((T{twqeg2Xx5AKu!l*yF#Go++t9 ze-Rf*De~NqPtu;5P$>ox5bDyhPUQWZW!sZ|ls*P|l8J|^7+iD6t(~Keen`Tv_Tc5$ zx4@Sx%M)z8_K-Ma$DLbuox`h0mPK`Dc}@!|JY9M4zHJEv#=*$ADoyi~9G>vrZ~6Up zloTKJc%v^?lK=Nau$5m}z-kQd*3BhNGQMu%OXdv5O|UY^VjAZU!SQx+VtIT?PZ8PRwD{!B7s zjC6T+g9%BLA4?TbZ3pAUia!<8ASE<^XY*tbox3+UV18~(ok8#wl`N6CHq{Pe?| z57tZq->9XYIqeeU0iR%dSm_D`Ynmj5FTe;LX?)Ih6Agbj_Fc8!{Tkh-p7{V{9YP+7 zjibq5$iTz-q@GpFfAk>bDa`@b+gck%&JnNRXDWBF8F@D$(~}H0PhYfeMF$)M^uHEM z^av@MF0(uk)D{xdnm{|uXOFvh`VbRdSFoR7A-;U^=h(jD8f;J1--Q$?&g!!$BQTAg z#3s(=j}BQHfoUZ3fLfH0X?RGZiua)423jJMpRQ$aOoBP4sC;wq&BPay?=4AuFh{qo z{pa&h#{Gmo$+aw#Udv);tu&Xz4Mg8D~!p;k=)1Z zSbwu{A*OIyM9FIfLes4)#6@yIGhFcoWH6a$kJnSS1U=ZyCJi>)vv7ZP?^y>#)zMU& zi$;w1J&(M#1<;X(79ES7FA=e`)p`@reupFY%QoyW&0W#Vrvq}U{jC2F zYi}79*Y>4*CqR(kA-IPmkl^m_5+EUg;1E2xJA~j4!L4uz32uc}1$TEX3U_yXcb&ec z-@e`Fe{bJ0`dc!9s=fAJYwfw_eCF@5WfUlnKx55K*;oy{szR{rp0QiW7S^vSb&UUmbzF5=8O zR)ORZtn8))rzy*}FRQ!yu^f-Vl8^lss-M4B`bh=j@Vy4~+C&Mab=sg}{M_3|KVC?X zi)nvz;|~{odOz}-vy4jE17jw^=W&iPm2J~laJkC~(9!m5uBn0MvOxx@=r{Ng@|h#_ zAiAt&a9;aASUo`LZ&eg71k_$Xo#@xGfMj;@k8vbwF#{awml`xn=(oNIv%bn-0l|uy zIb;rzJ*aTjkUp@jP)y2bY8wp1RG>NlEfHpD_eK823v3&OcXE9xj7#P`tU^>JLYDZw z7_2I9%QhfPIFZkKkMtRF88jj)=5Q5+e>eLF~@1YL4x4LD^k}^V?eae?Ih=k>y z0PA;z3XzfwUsV+i@Q%C)glhTBiypZ~7E{QP@YnD$Iju{gcS zNs<)et#l<2wvZIt81^IMtX6}xC%l>c@XlA!gj#EIygs3VnA*}LjlMi&_TkiA*OG|{ zNx{l``pTL=#c_O#a69Vqz$$g*nhH^y>IZHLOhm3E#sEsr`OBTyE+7xb_uRn7UkICTSc2-4j>Ta@j!)UtW{N30-JK}4 z94};#LS61vNoEZH=-i)q-Bc=`Pt!W~@;y(PBC3AkXcCq*4Ll=8ALD)y`**T5j>9vW<*^@PTZS9L+pMynx%?^wIvgnFnxAG74+jn)6cyk&DHaX=$@{FyyZXG2h z89!Kh7snocg^#JZe=kr+=aTawnCIprw2-rYDO}Q!Eh4F9&zxEk(3Z+$IlouST;k=K zb!9JcwcUq{nM9>kT<@E0--8Q1j%2eQTrMAMH-xt$hcBGHvI0_3iuKub)#2ab(!;uc z>;*+Wn6?%!m}JI^PBwaDr@31doTaM1_^Zj?>I1Yq?(zkM$j@Ij+`wzy*`NMBdr8(CvcBOwD6qos< z-HHK$>f=518;4cKo6qALInFiCo8O=R%>n?9k7XT~3H1Uf4YwlHXu{su0o>o5qSNgJ zW5LyB#quA{-|So+E0fD!^-zENnNaHCvIi)kE~m$2D)!}Xq{!dhd}Q^t3#Z;@w#*-o zov%^o1EdcjX5Jd0&D)sVP6Litj?7Bh~%j!L*r<}HvV}Q{5(35TNDWB0Gw#iLn!BMM*RMIkDau>mU z!4ZwxlRKGPAV5S{aG*m85G$h-E9_Y!8Q|h7z`LFn`Z6kk)-Dw!PTYHNTKv(*{d^#l0{t zhB{&6N6Pcpk0u7jHV~eSZciw+aE4uC6zKT!rs^v~yRt}*u!Uc{rXR1Ia*_vHxm@Ku zOH)KJTZIBse5PQe&n35&Lp`Dh(~@rqts!yyw}g%X@;dw@U8^jtz;ADf>W^oG#KH`a zKu^QDVoYZ6_83QT;6WJ5DM6u-9}rN8Y-8Ss<@P^R$ApEUJD~(#w+*(v9CeNgDGvRs z9Kz!xRtp8zewQnS>L*9pEcVL+!k-ki*TQ9)ZnF%9^hM1jhgVg94=t5H!!$8z%@Ofd zy%rX=20cR$yS*Wd9+%vHBjK*X%bsfOXheg&nz`+BLONr!pi%=N@}}G)s{XA9{x}(~ z`a`o3h-{PSx?32a)=PhPOw_NWV>{*};2#Q-p@N1=eL<>vD38UT@f|hqA58YWO8m}#y5y%zpu*0N+CFugV$iJZWGpEXWie`fYA(#01MPN9zwzCFabN-o{IB*|CzOzUh`I~X^!>yFk> z#n2BpHXZ)8SBo#fPpfm%o&Ieuwk;@tJbF*LPSZU78*u>(7Hd-lgA#=xS24HY=g&T; z!=uqdnyMJS+J z!eF{K1}4#`R(y?Sd?L+sps#19d|;NYa;Yi93{@1-h!5l z!~44~-KT=3!oA(@eIJ84Z4F;F@$_|5a}o#hnzG`o$lzj-mvH2yHL5T3?G1FmH5CO( zzU`5%1P2cFz93aI}L2yJSgD1%FbIP$O{67hkSRV17QH z>f-^l=&Q)O2(_s``-A}~Xw@@%cOvU&F>Bat-n!!}UB?r0f-s3A?wDKn)eG=li&a+M z9L5@+$Bf{%WsbniltbRhk)PS0Yr|g^LJx#6MO$@tX_0hwA6^NdQZwI_2~1S?hihh( zYb2x>@fU$QsuX})g+c0D+^E%PZ_RPqHZVSp}j*9 zHc6;f)5{=PvB5xl(-t=1scc&LK6D1XlFzWK$1ubf?+u>*c&O;E z{(B#T&WlmC^?e@;p^@*8@v{lDi}MRjwJ~x|j~8Wai|6j1 zE(Cde6$)5d3qMX0E&gUI*{jazJU5pMWUSj0F7qY41Dj;rkGbqR@B;if;IC0}db2v( z@5Hw4;aOM#NvEF5mHB)10Lp9G!m12fq)%i|Ly!Pu&@CK=^^oC34KOd3TJ4LGM{>}| z?#S5>iZ!ERRz_Is&Wa|@S3*CcuMxYLk(B=S)TaG#qP<&g>lx6E;qB_A?AwjRtkqQi zedgw;ko>}p^8>0k3Aq%-=0IGZy<{?;20wa#IyU?;FH%xQYz#)c}6oPueuP4RykJTbgffN78HK1`7?6|LpFi-uFnhCI=Yyupk)n;0(cSq8bsQ#EIs16E| zLI<8z2}cPDgZh#A>~@jr?QN5Ni>ZFlZ$tKXjW@B!R9O>dyF4CM`%9Nldx~0Df{ScY6U$}7H zT8BvGN4C^R_7{Og){jcgnA!^IPXczro0-KNO+E1D=fZCXp4W^HV+i8+3i^H7Xf=;l z^|{F02z1;B5sG@9*b}rJ2Ivc2fgM{Da3M3yAJU-KQkO#)tmN|jMQeoX=Uq@vY_NjucDHJV1D;ITWvV{Venc(v^d9r>zXnD_*Lf;$H&}CmW7k~G^Vc1r`zUT#+TW>2nOE?e1cYQ-xT6O zsH@A%V&W&qtiQ2w3w_OX2*uN#V8_ArhE0?dZgZE}@aTuls2jt%C=4`f&W5*gT#G8# zYrBVWqJt$Itk?Fl`+b3wla!<-sc+Dv4bRM~0N*)=+X=4E9N^PAAAtm_bE(gsCe}O| z$WCv)0_jl1Xg&#a2%w|+qUDxHw}9XXK*^u$53!eI9|Tt>bL<2y3V4d#ximSwx&#;T zg)8^rVWy)opIwjIGW+A|*ndI$#N@$cZRejP>bH%3%if%=9C#-7!+Q>1962-q&C4=t zkECrpdaZUIlIK>y!4*-5X||J~uCtHV&r9((UjloR>$7)(uYh2x3x8>rAOtV{Y4G#z zGXcDa83xn1C-0oy-x*RyONru^1DjL3JUWq5@`q8LR9_nPO2f`h2tn_+fUzs(mnuDQ z5%hq|$M*gNx1|dak<`^sY2Un$Z$ri+qm44~1zaDDF}cm($tdz_;e~d!;fm@=yM+7A zh*D=gljR{hS>_Xa9f*?o6c(80Um){RMM(}wLd5ZK&EuPi21dHaipkKF#5kN7lXN}( zfjiZj9SG~=OueZ45jJphOSs)mjolN#YcZN9*YM_=#}x9@pZdP8pqm$1V8J}DFUuBK z$yhpkOYm%HXi9+ejUxM`;=!2S{m3uWt$fYwcL=MJTwbnb)dO8~Vwy8mq7$-{X0Lkf z?E`@5PQ?utSFoxZqI8sjE$WI)>b;yJI@dC<)w& zt|}JNCn;|`w=_vbePB3Lm*fMAY!SK|zj4M%^8)zQ;U}!PoWe9RnNRKKL(5}WuW)Qg z$_mj4y_+zvD43lgaH+by&?A;sYmt6QlA(Hpx|#Gn%|iRH2^Rj8va(2P;dgI$qNI+- za3wC8RXa*0mL`eOU-jF1DQm6Q5=vYF$eyIwLSF!^@?4L8*ZDgKc16BYT}!aXpY&)B zUB&eB)J)#VEg&0LU*uW_wi-viO+flXWI%Za|ry9eEDoTR?;o< zd*`t$R9e-g>eT(t$gwpa*C}@8s1cNJ7I7tO$xGHq%S{JtV(FO!PoW+*J}RR8OS47M9r9Rx^A-+#{}$WoF-TzVZJ^l^4Is?QzmUt8=bU| z_XUPGBZ>{^YmB6R^A#^Cf2S2(C`K9&`uSn{hzJ%Woc=%s<+X$!Q+srA!0R%S@K)@7 zZwM6S;O$4&iJ5tnqYcy~myAm%9wLzIN|UO!*UETJuW)oIC_9=Q2=kXOXSS2EHx;maleBOZmtlyHHW ziV3zg74&%8rC<%sT(Q1wgZR@wY~YbfklITTk^Q~^hZ=clehpZE*CGYh7wF;!e{~DR zPx>L<&?h9EzNF|Jc%yqe<=zRZr2_nTUlsPa*!0Z~9%T!)lG?peFW$<<`?IH3s=B8t zXy$pmcGAWQ+DuJzW*Og(aq+n+x7&~PKM~X7es(`j=KI|6E1kVt{JX^rz;aD3Om#(WZ9BlpP)Y zu(W2htzUG9dAmGx$kfLphK9Ox^g2?#q-k=evJT6XgFpE)$o_Z0@y9+A%+n$nl~yyg zw1K@-ROJ3hw4f)n0cs=ZQKhdG{Z$tg(zF?|?Qd~pU1O~f_6Z3FUhWJz9`2Wg_f zb5y?lG%23_nJgoX0w^;aWznn^E;?TT(UA9kw_h-T$1P;5)EnBi7(4Qxj))7dJ}&eF zOE44`J8s4ul6Kt~?sLt19EiwvQRcbQ2NyaHd1!UfoUu`(v5GQ$IM=md&21K6p*~u- z6UzmgBy`m5Y7ZpeP-NB?Qf__Z+iN$>x_o!@TkDPn+V_PXPpb`mA;IGC?LHnS>Y+>wOoanpzR~Ad6LABsp zmL2>LGH%4d{m>lAgYK#Et{=x!8Lp_Ml!Q z{8kt<RIp5xBfV0) z=2?)3z0h39_Jt5P>sNfyi`>58P$O&<0xmD#wMkO=2r)5G?8d?n<#PxOQMtdl7km|O zgqqlBU8ywf?jIYp5t8cDvUmy-r3V$wpzwZv;85y|Oj>3^mb#YIcIe81WV^mpkxoqqVKnCl50BJF!QJssq zae$1Yi|7OH7!FQ6Uw8$+LD{?@#Uv~V$0BmNx;u4!`#SSp%TmrAtat(j&T=EN zEjoJkgkD(dw?%fF-`j|hrBNjsc1qgL1Dif%>|)jrJK7sq2i&*V;;$(VZYa#*;RLB| zq+2aIvj4)lQekDr3HJ8XOYi$M=d@1j81Y+$f{@2ud&=u4XZG9YwB=5Cc-dZCOQmH% zr@}A_5|+>~9>S+wT3^TgX^RUWu3*aO=E1-H=DM8*SGpfj&geye1m%Y9IIA1^E6A$= zYTD~r1dTx7g|NJF?F!)M@E|0laG+@_!Z&#$t@v`Cz+V;zlSV}TcfN2>*=NZeel3O% z^1Ps4Wl2GtOUV`Ow^!oKX-fbJ7>$EonE|cMO|guqCko%RYrN{nFrsLdK3K z5)A~~p_ul+UY!v+)9l+Zy9+aatAUKfxkLg&UcnTK+qqW>4uo$okGp|JGosE&a$?vW z48f598H;S~Z%*k{qyf?sw|#bEF#chLi)< zSRwu`i#xa$B@?&>Cfs{dVQdnHkJ0jrc#8|mQ<__!D#`^*`!tnFNKyx^y^voSR`7-! z1P;AF$vXp%xm(lOR^G)7_crRdnV{~t)6Qp|WE46lb2mbIBAo_Q6#ykT5O6bPDAmqt zrW}1?87W71C*d@`B#Pflj`Axh9jHx9%0Q;M6mm5w_`lw;8$y{ILLlZ$vX1;(ovi%E zUSJZyV7&iuXJbPadrW_>(ej;D^npuk=$KuDyAqa&^Ir8gp5ta%y%L{G;n#N=F>#{j zM2~Mt=&BFm@Md>nYvNRl%c5_seJ+nxOgmvPeuJx@*1}cQNkA6kQn-(+;9)uC$}sm+ z5SkpkXgq91hwjsX<*Mh)37h@N2?F<|laBdkvoe ziEK>G{@LB6Wi-0}f~9Fnp}CBa~M{GZ4=sJtxr@VKd^!0_{@gDh$=|p zZAezJ<(~nuD`ZDv(TGK_P46jLUlm?UX7lKcpqN|Q1xP@{wy}P1B9%`x=k^Iu`s#UFPY#xBhWlEbX+PPh0j^1D-Np<(zxE$?SgZ8~@if8{hA zw0!T6#-Ds!YcaVuRE;9TTYHp#)xiwA0Sp|IQu0}OaIjmJj`nz|F#(ue~ zpusoR01Q^xy>$h>ZNi<)Lv42~024Hca7*FIW3nlznJZ!BS&Cd=`lu)ujlauSu-5b? zwT-cE$Y=1jst`j)w^1`qM<#S_Un zoe1|wR9B+f@%xzXb++x-s)>`KtFMm?m)hZ>S+S7?0zXmb$$wFQD^1CfNh_tHe zX0wYn3nqBLhsen7s?Ja!8N^Ad6`)zfC&>P-{&iKM%W8n47|#9a+b)bwOPWrElD=oT z=9Ew`%FXeC>OuF-zzui;6l}y$ro+fJ7;3^;l#^e%A+I{6 zKVvwU#)g%(V?W2kEpy@H4rYu8G8aayMZ$y583k-{jhx{skb1^f~oX zv&yuihOAe}Lcf>6iK{T zfs(frj=6tPApW8kBXa%CVywX3=@)4AA^wfpul`YLGmyJ!qKdR*>bNy0kcXzL=UvH9 z)4p}hvECv&q~WT&&YnqFfO5XNr*!EUSGc|ykuNiIspCItut<2)Q@KTKn?kk6dS!CK z5BTIhH1g)&z1Lu!aRg$HQa0QxSW!ZuzNRYi5w8CD+C7++N%Q8NaE6PT1_eu#t@>A; znTur>U#ORhUV?iGHLnN{ePwz zu3HJM9|3W!wO$ApzrbR3W9*N+FKbvhuIACkxo!0LyU%bl9DQ{P^ZE;g=L=dC%K2`FGl{H&7c|&wwW;)gJQn!3!GIZCj@r1Yjd3GWj`@peuag0C2oe7Y zAG*E{C-0PU;cS=RGJwI3B5UFM)PFWDq-&M?8pLr9s{z#-5=JCbWQ>nVB!8WjiNj0H zsv9K2mZH90LTdwn0iQ45Oa%=VnN?#pA~X$;OYFJ>6VjO4gyHJ-x9s^sMG^OYdb%t zQKlx<-IEmMX9Hpp-!ravWQM$lQ{CJ{RO_Q|_dshsu6c-@3806-)d_uRp)bi#GAfca zfyr!b?jY6ph)ijuG-zEcz+*eL|4R3-#jPt@Z~e;*OnoR!T$+9hpsa|`0eZWEG00p; zQM>5o#3i6QXRA7~ZoLNZ_IUC2OE;kY5uWqs`h* z+|BZTl^U=EKv*63Z+)>%tTPp@)U3l?lHW#SwkX&(2Lt7J;z@H$_GhlQ zM}S2%k5V#QcD9mO8`{dun2Kog$Fz9Zb2~sxCHpQDMW;+(6gZf!q-QH3S6DJY03vxS zfC464(XwsoJF7DnVjW{pnCpzBkMP~nkJZ}y(FF&I8j@%2?2DRqC4VA#FO_4zrCGiS zRVQY#c`GLQYKP;rgHASz>1^z)clJQGzD(G*RxVjDxh^%Mf>`dQ6NgGNu_HO{Rv|WR zMr4%pv4ug&0Q$wqB>)(RChI@3(Yj*BUMtC(bo9}YbZ=&*sGubKlY~JR%sgV{J&#)$ z2z{sf{NAJnZn)#zFts9j>n(n{hl!b!2^6auY7Bl@2ttpJ>Ss2BQoe6vYfw=w)^~=K zpUu*>%`r121%~gcHyt=zaKY~O`T{<6ezL6U%!o~FLu5MUQekDgHR+z%HPyamQ4Xx7 z_QDfCGX#Ly>b1Sw!LA$KTupN*;=R9IZhrI(CapDZ*?~U~aqEj9`O^i{Z3Zo6>)j2f z9g&;c$ohMxmh+OZeJ>ZBQC?EGakYltk_|rOU?+)Y5H$GK}5KAN=FVvwICbw;} zXWO$mdMX2p>6+(-rP0P$X<``iyowhE4!TDkrpuP8>DyGle#&BPvjUllRXm#y+5O^g zlbnK7MF1?39S{}BAZg(jOpDVf=IdR3t+5fL6?_$)53^2C-OqC$`$!77)2ZZ(yLcX0 zOSu2GwgHFWvLOS@nRJ#KA&9Iw`B1DLICOmTY!QvZPVAc-+GCvW zHnDtL%&&YWcc%EKEqG-+jvlRiZ({QaqL{?Wz6F>hHZLm^@q6E;*_WsNKxN9-F==ji z?(XB*o#d{NVrKvc2o<&=@$*>OLa$gjZA-YPmdFfoL<<~iaxPO%Pwgf?X(Ujk%H08a z0~L&)15{a;005M*>(0v)5MD+&+GjQv9KjcMxY4@#<@8>bL9V~PTsG2>Sv_rtA}@^< zT@ImhD`6aHudy+3rf>>FUo5iQmU317?8se7t-7=nM6J_J4{|W$C61qzZG6M>_7Otv znX9oq3-EALa0?E>)T3_&nbJMJ)<7;F6Sgkng)!0|mhVkQP7{{`6qBR!zTs@iDu3<$ zAhX;Zqay0Xw3b3O_tu0d7xK?d*R$1evK&{4Ch};lHt3j3w~c6sWdOEkDO@$+m~4sZ z2_t=cw6>|-5Vx+u1;X{as@4L=rX8y=SJq;qr7g+?mdl+NFcsLN)*3;NF+6jrhVmTJ?UQ z(KHZ|edyUU^Ufo~6w#4kfUd{#Kc>*7>0#ylYEB|*DwhmYX$$(bC{ka=KkN>jw1W3R zYvPk$KL1OUNtJzzNFtOi3RH!OiMXudDYmK7#RMNtO+_x)j$&n}vq9}nA|6qJDR|ix z+m7N2gUt$=BG9mLU^Y&lee>auM{g)q1~G9ToPC@wGO0iFG`ae}KWff!i1Gf&`tR%R zKhp3Ld62v{+#9~UMhW<1Ap31cDN;V!-fzWOG}>dSg!XnZ^Rk_O zxuy)EWi@K!-K>yIpd@95fQc>HoLX|}*0f^6KA)kCJLIc0oTq2U(v z{iU7Ffa%{t-JvEf+J$i{#fqy4&K2=B@ihpI~rc*XQ75F>42lM`qVfoW32~y49 z+e0`3##hO+Gp4p`Y@{P}L6TP+vz}x3y6oQ-I&h?)2dVw_UcO>+?hIKAggM_l+zvhT z?{48Xff5pM$4aRd;wOTc*gV+>c1NsdYL;stvQ44kl&bRn{fm(_$sGxdy>8#qz*-JaGqnse!T40wzmcmf|Lq=)-NpeJ9+ z>w85Q8Bg}}MfyU95N2SMZH)lNlHVMXzsc-pbER0H7zW{HuSL z#t>^PWZ~1gwCL`kp;KBEd-%MeX)t4s_I9epF0)hTM7Q4ScXY1VY2z{MZ{h5wCG;+i zs!ocR#Ti_0UCjBzF95GS?TWOeuO2nzRXLu5o0{8$-D#T1(ng^O$zT=zVx&Tqy*1Z7 zhik12?pS$$gW-vC;MgG=TDj!p&z}Uh0Dj&i|IL|m39i=?^1{5T=%$en#hB^hz`K;k zJ2CswwS^#{TcxS&#FqQGzbilGtYX}@>eA_j3PNK%Js7Lpi4$3Doo~I`TS`tYU2RHB(so`5U&Gm?Is|n1IaUEj*j)woq+ze%W$B zYy;A(U%PNrc-Z%D@UWgoP8Bh;U(4Ru_*Cj;=dw0!+0go;i512{Cy>%;V2?#zrRc-o z$mIpO3x=|4l6Rg!h+R8wl7@()y@hbLPUbH9BVaeDH47Dg77&5>5$%4rC1+CdT~~DB zC(y0z8HvHMEiA$(>fYyq&9dW2ye-h_L<3WMR}ZA_+0Ws*_>AJ=+M{C5pCye9>fHUH zTr&UA-_+YG&F+2#)7R6D=`gu(Ep;-ozi+O};lI*<3H3JgsfO*ezWFp+Hl+0tE(n6$ zp6q~K*PxzX3n|=4{yjMV-{_W~XVI&n3Xp2GtOLx)v}^|WY$dCN-61huwg9w8ZZ`wc?t1!s?l;8KQi{-!hU^I;?$Xs@hhMFIZhYp<~42ZXy3|{Cco;b zHywYD%{@kzq$#=BNzwW^&8(&!sx4F8Ri=AH&Rc~Zhb>GTO`Ui1B89_5fk<@BZDK$^ z%)Sv&?lysj%5124{5{&U?xh&$y<>uo+p}|=aE&?9*8n4h2T%fk8M*WjfihpVA;X7O z%wSToSdX>79UObh0-HH*7`u0X{!1Vaj$mw=CZ`m%vkkaJ0K{UlMz7VcdXyn|O?AyUlfoAvM6<$9A`@fdE#G+_9%PN6nkjl7oP(`<=E2 z_>!wiWF4g4QJ5h_3l$QsHj>^8>nckr1fNygX8;5|!BEp1!-*I11lN(#13e*706)&H zc`aI&NwvoU@m0UUufU@*mShQP-$x=Pt&+XhdPm}VQ&|{SbKw3fxnFPfAUN777D}r@ zERUZ}(b_e`H zPp@P_SFb<+>JeOsFmq&jg(`OJa?C_7SyUiU%|M`+LbP;x6NN0L(yMVDeEy>PW5whB zqecKB!Fiw@#!Q#^gXaP*<=Z=cVlL+FPYZi$m3pM=`(Qaqeix8_J6ef#O}jj|30X=; z%m_7C{FK6mK~*luqxrBh!MCy}eR9Kt1U@OxK2A{m0}8N22pa_&=JuXYsW%@_d|mJX zu;!HlZH<`rc_IQw0uxl4;ipfyn>n?JBe#7>I!7DYgu0ga8d4`Hgtd;4{QIpNI4u15^riNfzACI?EfS7sX;t(iFF zl-)_8H&AEegkc^6bi0K~HVcMN${fXo+4CFHCg8E>-d3Y|@|rsQb-ODf)KvoRo&Zdq`FL@l_`QU? zfV#~`0@l6nU1$-Zj7Ln@igZDO7hThdwViX%Cr#5oD}dOPRXS{MTX6PHq+zX08KNuD ziMj%q&04dX)FJR9Q6NLj$^3Z21x9BRb#_0WF=NCVXdcK?YEV_y%(NHmHOn=LVf&Ed zkqg$VR?`<+lvbhtHT5+!Rg-#!d~a#&E66TpqKj<4^p8+8lS)$tZD}#19ZA&fAay{B zh_^?ewvtfhkm!R~lamr4k|v3##P~^-O@VY2%(^{#?!6s3k@}PNv1EbBZP*timnwWI zHGX(LSqW}cIlH8%8>dA2q3hVn0>-EbeEx2uegyYkFL^ejLsyCcx}&0a+r;l1dT#R| z!z|%m!k6WgYYRXN4FS&? z4B&{n%W1Kxh<^n@$w{qF7?TT@?MCJ431#+xmpf){59QwO>{U77Qq+Dy%4r&)>$Wn`Vf%jW*u`+biPH5IfZ1cpu(;CCjCC&?KHOd48g)fQ8rDCtNB9cdnpNuqA?g=U@_KJM z;MR>g_K|o`6fhz}VOvehV`QzZpuU^~tUUZ<8IC8f7lQHGmZjMwoPqmC-q7KIIQU%g z1Fg$8E;U%Yb~5Gu)Pc$jjcAlks%*!aB%G#7PCzUP|2fkTNj;m4NZ(j;nk&jgk`YF% zMmfJ8$EAaI!D`*90p265h;uxYQ2jIQEzq%5b}Z}8>)=i#Gt&q3B_gPW)N`V(bAYj- zKczWFtsmv?WJHY55YBu6;0h;*xsC8EnT+~QZVg&s!Q%nol6HV-2u1KE;qL6tCg{2z zu-faqP@kRO&GjISM;~@gmU*xne=qywI$OO1C#&5d()U`wQ|FXkMk?~IZVK2L)bSHV zX@{wht|0Lcll%nrrx8ygXh3I4E(V;FhLFT4?`CRF2c1uum$}q;3{m95F2DwX=;WKgbF`80a zNq~FUF`sB3mZF}LAdp5svllU29hK17sv8OkweQ#7WHalYjuc6DPs3bM-}b{i8^Xcv ztSCuK5H5;~IFK)+v}183YJ!<6^-b5}A2p1J%z05l5xww~qrkhO0GJOf)9|hSi2Bzn z$F!0`#-@v}jZwMvNc3%iL&%7#YWoUzeu58;4*xs<&E^-mw zctR6bqe#$$%aO$^6@*igL9pua2@w+sirD*)*r zLwJsV!?MGk?Rvm(s_pCZ4OeZJ-D)2rPeOp5ae8?AyB6oX79YMtO{GDWIlg>06W(U1 z(S#q#qigMUIUC#=Xd>g(aWXSS6r_F6#(R6Ja*}gbBU^$ucDO1^6f~p@3v9t?jKHRz zul`-iq(#xHI7Srso63vx_WJ6|usV?QYLl?_Fk2DwnKddv@zUT7cEdz%uescK1m@k) zXq9D6Art<_cv;YDdJcl^%@#g_sfm>9Bu1AF>QMOHpElVA<|E6WJbcd_!U2o2^IFwR zhS&ueCM3z~>)=6v(=%KDs8^po0jFj845D}4_x-Eg+Mb_>*{&=E_Eh{ z&{PxDJxKcql0-kfoQcgkMn>-M@C8pQ1JN*#Edr6er>Am%=FJ<-u^ET{JhoT+Ra~0P zt-;hpm(wT?UabXQc(BZ^Dn?>V4~;aP9z==Gh-qV_d`X|vd9GQlmbR>Lm3Ei!+vAZ0 zo;Q(-3M?mVqx5NgcmYb7o?mrNG8%l>Q_N7&mx~U`M;qg;?7QG8A88&&K>5?|o(VRK zfcoN=cW%7^Hh!}tnWYxsFy=$MkHJ5lq<(>YTDm*%SCy?uCfR`3L^n^J@>Vs@0P$LSO|LA2QCHhy5#^{ zg8R7(=vK3jV451(DLyHaJba*5I~KyB8LnDab#hB-+q$ao`jpj!i<6$K^vHs@RKEOO zv&#=Y1<%j5L8Bk@R{12q`NY(Od!^*4ndAl5n4! zaQ(x(ZmDAqb+i{=@%K(c8)15qM=HU;<4ch)4gAs4BQhY{pzxv#be?FBw1Y+=b&#i8 z`(cf^B;cK=0k2TI!)%!EmF9Obx&R0C9yA>uZZVWa`{r2qI4(=l6T4RDu{KVbS{WLg zvdZ~#w${tuOU?41JMP`Jxo!8_E+v}2+p`w%vNF~%nt{;8+DfJ9NdwxEa(6zSC z-!5;IQrN-IaCm?4t{S0?9)0b@#l~c2IU!PS^}0!GqzSOy7i6(2cSxe1&qpyvpXrOH zA~V(cjAkP&uxpL7&QX6z4AjRX_@ZJ!7)C)syo)+Igrd9Ys*vGPw@pGdt3dC17&rt#hP;eAT~oaPooq`CbXAa8&)_d66Sz+m?fIC=+VO9rDhB60ulav#DZN4FXl zXOWeJ0qKtf*Blt7)T0t>yusm0Owx4hG{D%Xf}DL`el#YDRvCdU{d%lxT{H1Tq`tOP z>ph*VzWZ829ez7%jiuz^jW``$>gb5nyhcFe9bgan<|~B^U5+KyYaau-^p>*sAH)^j6aXxuXfW#7b687Kd~qL!R0Jk)?CtSA4wfK zxyOax*eYN{Him-$M$cFi7LhNiN$_jU4S$yLmJ%er*P8ecL{tG3US2F#f15O&tz1je z7kRj9g)N2E@*Rg<7DKML7iRkE5Bu~t%2}l zIFXumm<-8WYW?ul9g2~CXM9b!+WdYy38+EXur)wV=prpsD_s2HumjF%^9KHigl`0m z_m^(1Z=T&@`;Q~KcG*tHcsS@8&IbnqGBieQ!fnzsf@MLYPMParb%=ERI@@OB;#n!x zjf`d!$&|!qz~&HqtFqoBR4Sol6n2@dlv56+a#=8%Zhw)Q-ZlwVXg$ zYF@LqHE>ja@+tJS8ISkN9tX%ec8L z3)ranU3#BG0O_f^rUXN#aM?46<*Hq^Wg$|^?Q}+Gfy}3am~qytwKO=-SW`x?8(kjM zwJnwuRaC$2%L8Am>0`;KT>|( zNm40n#D>3u+8+lpLB?$qx-!@0Rb!G0#3 zS=kAVdDhEsRSn3`c=t6~fK0-FzL3AI2856bcxk>?2b~H!T*k=N$~5u@tiDJwBC9PN z)T5~Jl?1l-`fS6ddRCVOLZsC*py}*s)w`Q3hMUgm_mZtrW7F704N8|mma`ypEwBb9 zFB8^5sZn8?>+;2MwRFJ~p#bhq+(@#)Mt%h>xd%~Grm)l=dEV6CB>CmQ#6zgIn=R+E zj|e2P9sj|%s+yG}VKtz!*uqsd+>OpcK1cK7>ZsY1=@F~>I)Mp&e7l?O$?_P~4@NW`@VEto2bIO6} z#^PAIPDk7vgDg54i7T-o!+cws7k0q8Ub9JttEduty7a(cSr)HT|J6(YE|??) z{q}g~3Qq3YA2HiyD^i=RdjH4qq;1MJlQb?m4c>e`zN? zNFL+&dXgACVMXyk_d_7(=K(dur|jGcuk+oR6jG=y;JAsv`gx z{(WCIq3BIq*6%OdaSJ+7b}QjO4{hh7F$lU`ENJxW_yT10lc^!@Q^jnaKCg;_m0!0S zb9BdB5mx_CAL)kzTpIz~>087-#zWepv+H992LR^ff>uCP9wFbg5)}&so`6^x4V&3u zx*dRg2?oev%!q?*^lbMPTWarLsQ{oL5ri=OHS8eN*iE?Px`}l^Sq}u_&xf!@2w>0` zj6v|4i#%W+h?f6)p8sM2wifXYpaW&>tf!%dop1Dw100Bb^baeJ3^PJMsO%?i0x!`2 zvUu;vfic{;-oH8j4^wZG9p!C;0>aiIIcp_{F`$9yRRVk&0 z?<*aB6kRYj24e)}v&H89?(&26T+V?tZ|3{3=zqMC>VVp{iEw;X_Makv|8mWLUn%Mn zBtpgfkG5E0#p*Bq>s9&_0YgKxo@9jo{RjVb`2YQ%V7VaXtmV)+`J7 zzf8V=zOw%`VcDjEehIJgq(}Ms{}aO&4@V4}XD7k#|F|yxTbD+If+`4X;<)F}vQGa; zhH8!&>ac0^GeDm4FQ@Q-_a{Jg8Hj>g^pN6x{zrz&j2P-{u}15ET{i#kGl*mtc)f^B zD}EdOC)bGo*U$Tth0wK#r=2O({nK9YpJvqmX+Qky=K512b^%We)$9L}p;n_aIZIp# zU(9owxBPYYHc3$cVcNx+(w{q~k2Pg?Ol{8>qUhL$XOSkhqKNUI@QLB8E>LHY{x-(& z696BtiO0EP$Gi^b83Z$u_Wxt=J)@dj zyS33JqF@72DN+@df)o)^dJ~0MDT*L90wTTFP!|d!H7Zr9(gi|92t7nmKtmIdA|)sx zgisO)B|w0Dch=r(ob`_J`flCl{5j)W{~Q9z^W0^YYhLr3slU3JKREcGuP`H!-(T## z_Wywy01y)JDlZ#F>{4?7+ZF%6e9XT$u>Rl;{>O9ow?~+UFvBnS*=vS>3XK2Q@c!ga zd=7yltr}g}e2C?5-}1loL?9d#V{QiIc|q}CY}NnuNB#K%D$&3yXI{Exz2{f&>c4%P z4K8MEo!4>G=%+650^@4~r-w4ui}M@w5c?H}M?CprKyfZfv^Pj=M*;7t)Iy(vJ-J4X zEHOT(bz+X}&{<%4aBMI86U_00dj=l!}{^vj5-sw9af3X#F{tNU8)cMxJ3+>oyT4>&vysf!K(l1gb zW>iu`b8dV{qk#EP*jn30FsJJZ;KmC%Ts$xQbFZj!9K6_`-%J1c8vYXotZ&+nQ_Vs+ zKjf?eT||>Ti6(I6R#msr+L^Do!+(c_f3(cU&b(#5_1d1ZYX9{RcRkpB6fRrMI$*$f zvz4G-3#-kOEoqV1eS45UwJa|nU@c%>$za6oXE+O(sj-KZH44N70X zzija>53X?)jC;8sxYVgQ;^WsL*OuRJ0!4QQ%d_t{3h#=jP~{JbK#ASaAZB>`RXM#+ zfv`6f=ZAuZf|?n{*29Q9R?=~DJTe2M_C+50%GDp)_`jCw&$FOK+*SBOYkl%lV&B># zeipm`=-J;p0e{~y3pRD-{i6e=4Oa590W+_Pdclp($_m?FUj~90h2P&t6#?>7Az-GS z=MF~#s-0A~^Az4?a%dd1D)@jg0z`vZAit0ys#bO*lLbQ`qx9l}D3(j#@VH)n*@f>> zB856Urup~ViZZ;en)#3UI-izLe5-ZX0gPk}zCK%4sfUcJQswuqvk8poLN<`=sRzw( zqz<~G-U|HO!Pp(4mCcH^PqThh)vZCFJVO@pAAQPXxtEIAb4kut?!&+(;jPv2i7{|yXQR6uk0e7k!Xea zCOUcDL$-0DxeD|s>B~fsl}cH$;eXCS)qKoV+M$Gxd$?NvSj(SPCof)DT_v9Xyre#@ zKYMKs%*dXVYDhn0tqn%tsl&#;{7MVShE)VT7(O@{v4BfVcXDU!U4N5@!y!TvvR!Y12u7m9m`~q? z>#Zh0&`oz7t@TVca1i|PVBhJq`_JuCEM5kKo+%P&p}BlmT(6_81{{cd=HNPgdSWlL z7oH3z@mlOz1yl2cs`_U$XA^dT7sqU`AnQuP+RV?>_>t&UGc%`G1=I`sBYvYXw#FF+ zw~b8;Oj;{HolMVP(|M4~RC09$>>VlG>epGo2+wL(4?UlY+_@Ei9b_RCWrqfu0sg94yfe@yJJlQmXsV9& zKz2m3-3@f~2%aqOaCj5V6gnl1dUto`S%FjB7tSV}HkB1bR)EOaVUw75W$NwnTxB=z z{`wasJm_4z?uyixX+M2S?4j*;4RBqjPH9}+s9d?9vnijweqE#LM5@JE zOe-1#I7`(kqq#P8pe+zIAO2u1@-a|%Q^*82(Gk#?I_Pki;;6?(m6b2I=qq%ICh-_8 z7?>u#+^1he38ghS0_)&T_+sK-@_w=^4CnMZGk6IK#4s~0`ka1p84I+Ktk}}hXOPH65C=@+`Psv%fRZYO z0F+waOnntnz7Lq7F+~9VitgzK)?)|QzCCacT}mgtV-vX!Y#y{8T#P=@Vx;&RMEOkA z5$+iV9m7!NB!ek7AKx{FE8onU*#~^?I}r)!y{9@<;=T32z`2UH-aXF!O+LO!1_tec3IgJZW0H2SJLRR~8>ECi;QU!M-I zWzsfi+bxXbF)MBi#N1%oFl7rIG@Ke}O=Zcic`%^E$b=!dv1C#$m?f+^d6hueqUm04 z7#M1&&yjuH<4_H@6+%B31~9lB0&+HK7)HoOt74Pk*NYKNK=>o`$oES2WW|d)o0$-Yivm9v<#E0*he5zEHq1!SP6kcK%tC$Qm!1_R3+JQ zuyD6n^96bWy&5YR39XjSaq3^~{j1}h?}9sh#T~QdSB?z^W3j&Q8>hnvQR2hWF3)>1 zE0X9W@~XA)po0mrl1xU55Bdta`Zrus4F}iPj0 z(r0R=yzm=#9B~nmw*q?AN37pR2?t831I>R&FoV+(G$HCkw`NS1FhC_r<$#Dr;~UpB z-f${kpKR!2EcQ*g-lc$r-fOa*Z35guwixTfwh8!G<-x+U*+3Zzrl`68NuOE)@g0MR zgjAg+*XmV|dbL+g#7I>i)nk3gY^Yx@g>FfTxU;h7wS*F1IG0zwW!aa%i`U`|6q=_S z?}XX{CqA`HgFfd2mLMO)*rF&eA01r&0P)fowrRalbgWLh$nch0<1N< zY*5rXwdKzjz1P-d!`83WgONxuJ*Jh z2&Jdov5B@IU|bHhpS0!N`(Cq}VMnT$b3o%c9o!G@zb?(6{h_^nYY@QjOvMpO? zRkb2`{^&s<(OKvva-y$87K{ZO~7(^fQg|@SGE~$_8xkQT;N$ zWTe-?Njw+bK44%BtoL?2No>m&D!39cYZ0pBB! zaQ>EL9`*?99Ek~1$-=R}N=3(z0>i0Pd#wsL#Gn!{>cw6H%BVx^Z@1)!H|aBj^VDZj#eb*5K(PwN}AB$bGLh5{0VnGwG7I3x3YdN!4h<&!SGDc?4taEi~ zfKaK#-UJ7^qdE}%I=IIM%52(=shMxe@@zfoh(HZEq4YZFB>k{qCo6;^mj#$X=VNEc zst1Ne(1JUXpWi;r6&m-OcaSR>#QEvvH2d!i@Vid8SL&5gE9PFm=V({(L>!q1;mwt$ z4tf31k#s*U)`ZtVtRe05K$fU`2boPI__bO?zmEkcm!EWrKJUJUS>YYCXJoXoI$qRbnQ@N-22QuNxazt$(jAn zAA9qJPp%GgK?I*x4?C3_mtIoBWO-KDsNMAf+G(i0-=cg+ZRbNmKdeT2vko|`PaA>* zhtFtzzf)(Hgc9c5ZQ=25*LrwdZZ~t%FbT}vy}#(EwF_%&z$F1+;`>gchVW(gFM50e z;dYkDBc;hF7pcl5x13E7nO_GZpU1r}tCQ*Mn<630J?r0}f&P8vbafC&tgDI(Q=_|a zN}7aKV08#lvJ>=gor>evV`ZxER9k{}P;6-dL>Tsa%a$PL@`pf>wteEv{y=MI%9oke zXtEIIBR|Dbn95gKk7}jZ#v-H5@Q*u#_>0T!Klg=}aw3ZGNbX?w4~r)7zM!9r=lR7x zFKEHg*5_~K1Y?s}4dg09;A9UvONmy_(GJrb~nZs2$nsgbSr)?QJ=jk(A zFry6kti#Jl9&B=nSaHy(XIo2QU2>@wgfPc`nU{7+&^7{(fB6VFJ#mvII9+nPW$Tdj zP_8dcnCz6JmY^u91o^B~9#vajBJ%99-Qv-u=iR|48!laLs)gFp*||7{RI2y_t#~MT znBpm+LBB?DybU`s#Sy}s`^rFve)9Z8D(A$7Y|wgf>9Nx476=w6KYNT`TRf)C>Mr(H zsL4~mRar)ZV$*;OnRiasu*K-)U<8oY~N4`W6R)P`t(QBqAPq;(CyBhCZ4ok5-jC-azZ@Dz_VRc#?f6V?1VL`ag-DfeZs z^Prc6TK$rl^Jb5}l3!uV&i$y8Bt?}q-1CY%Q9m@Ue>cE?B4W;bIt8P}YC`jDnrjyd zdi^36Mfrq~TmcC=cFJ?>%xsz|eGsK81_pLe91EYHq}^i0isdusn@^P>Vec@EOLtCd zPxbiw-kvLVY5)oxg!9g~WfHb@Z<9sO@Q2SRPo;?=MXcYllXx!l*<^FwO!&i}HC*hN zaa`pct48JpI3?-UmL{rY#c`um1^D*sWLms;ni=D0ru{YBve=i41_F3TFf)kywp3S%tM6RBxwd}@lk$ceF+GN-!2(~9Mo*4pv=U|SEl_* z1*u(}4=(YA0uc;Xzw=L62#bHbXG(LcO)>3?YpYq7A!~Dk`OC(kCKUtW?+XE!k9Sd? z9KU!o19?Dk^bFfuu}Kk5Ass1UiN{g;6T@*)ffLq|AmUV(g;V0z+$1IE+KAd^ydk== zl(c}4$4XxEB4l?GEcYbx;!C+7liLko?qrPSPC}%+KXeziLCRkBr_3%|>X^Z{`*?Ao zU^u=QYi=|duftWJ4nm$KGp^!yC9 zJG|6>{Cjb)z?Nrpy%mKCNqjvK^kowzR;|` zIwZ#C)85~d*X&?)Hi75eA{g6NRn;$^c=+V?kNr$R6OirgOp&wXNJa2 zTC13SJw5;gLatX_X+Q#3?NQz<*zY993YGdj44^ubG31r5i>wPEaQlkAxD)h$+N6i@ z;P=(AuX0_PqsM2*4|Sm)V4t9 zHZf*-v2er6tW$Zgs%_GW7%~@X$f*cn+S3J!BZf0opWt4D9hphgkZw8wVb??&=muNE7Y-+tiyUzzO%j zH5spL+JrGfR_evwhR3&o0#{ch^J%za-)?ZI&OPB!na*jP2N3X!ZtuqEk~OZl18}PB z?n&;_?d3^fw@W!{k8VZy)ePCh9^oD-9Rb~M>7Z@7oA@$!y474f3ls;gW5;nCX037J@o3J8Z-0@5nYHC@bf9bz$wJif$H&{)TfD5c1fi zuLxMKH>8-SXZqRd=Cyr|KRy)kql5G>czrlO%PrTO#z#7K#u3w^pVj>4hOmukN+5c_ z$@)ypz{<$f4W-?l4wBQChYrbT2Uh6WYk%a_-1LZvsqD#p=ECZj#~nT#$eptUhP}qz z^;Zrsmo8o553garn+=kPWDl5foX~M9I8_mm5V){qY5LX2je<|+uje#coKh{B(PdW{ zmhV)+HYd=CW|5yo?&EW)FaP zoGbBz%qGx5aCmdi7-VHOH#da*dRbh%<2!#p9^g5vjro<~)LQWg+~X(#fRWWUx<&-z z-^H_c^2#JAcZH1BcxHZ>eas~m)3S~dr+MVD5L$fs!^es{tV+3w)tgUrE3DM6djMtp zLf|o_)Il1)E-y|dcuBEgZ9_dRlLq%>*QPd0jyjEvgQSJlg0hNh!0aT5gsxcw7l)>* zAAa{O;Ez1qMS+$yJZgawyO;R|7@mXK_KVKED;k`!a;-E|wR?w}7XP>!tdKHNQd<5Z zZ2g!pPQnbz3V@;_{Bh)(An9r;cR~VmZ}D~l4)QcPhm z{~`hPE^@+qzn{Y*t9a5RcM`Y?WlF+$1wnl7a^iO@4qe`Myf1J7R|cVBAu!{HKFb41 zsXNg+d}sU)+Q!dJ0GtrUMC514mBqoMRtBhnR`;g$`y6i|X|I9Ko=I6b%5tUgjk!Gw zfvZcJC5CHx49r@TveZ4X!p;dPMi|E)^I;W<-Ak;{<7!5rLJ_e3aH^N@dO%K&6?>(8 zI2BhNi$B#xD@=ELAw_I!WN*-QKnac}toO^5yoQ`#orzhu^p!Q)i^M??G=itEf`Rab z6|k*hCadc6Reh3Ut>CQADdEgy$_YwcRNwx@pj!5s)}~x+n2|ua8`u#)jyi}^4ep|N z%f)?!D8&-`De^k?JxfK`74gg@J+;LTGyO|+CEP?RPsvU~qPV^uv-%PmkU!NbBaaW{ zL^Au?jY;6R%a*U$Xl)&pfOnVfYsiXRLu@6b`um&ZG&Xou{~Qw0z;eqjXS1xb3mg8{ zX*lxiFRmZ%m6_%ULRb4Mxf5x5K428u+PM>uzk0!Q#wsw5+|G~I+z ze*qlw5KjbeTMt0N;hNh@3@4pnjY}vp&}CO=0+f8lQAlt=!a!+|x&jSd$bY{h)fnir zb5b9wf~wJVW*YedxOHkT4rIdw`dSM-cjfo%R?CJ$&)hC$>x=`>0FVkaIRcB%O52>Z_B2{cU*d+cM zaIb=R;U2jt?^~#Smz@!H{zgNwlBHVN!Q|oC5S`mJaPMP}Iiih?wyXe@(hJHDM(!x= zW*}LP5ewb4v63ajU(dQY8|5?xhTST>euXK-38L9{EU^+;@h~Erqx{?@~{Wy33{u{gkT4B`%*C2Xyj<*d0^btmcZi zkZ8F9-MDKaKedyEexSb7%YE?W9BRN!N_}g&59Y>IcG@`FDGws(c_~!Dx>WRziL^Tf zC4YP}o3TkG0g0NfF@Q-3DLe4Ei%tq#)7UXOI%KAQ{n~$6U+w7t(5g|>lyrB0^Xa7P zfTMdWz_U|9)oFZEa?+5XJj|5j5X~2w2hx$Bl3`2fdYdGK7Hn=~i#CpoaHIi8chy5l zvbil(%t0u8v%HF#uaUt0>qx)ULIALLyRRbU&#^u z$Y+00V|Dfx^vr~Sd7lCPCxW5f=;Ea9F-mC7&5*`UJ?D;T@O>4fpvWryUo%&pF*W7X z8vbjqYk-h}570Z9#23?EYx%PfdcOZ<%tvX!w%+1OJFQvk@fX&GytUmoG8hPhZUG4J z>erT?DvPi?v#I>X7M?BV05(K5Fuy^w&r#cblXU{GdJk0H^u0UeAlriRO9GscABYY{ z7H~EntI&1AIe>~69ktJp({Dp+f1p{ac^sx`^=yb5Sk09_hqm9w;=L8kBCzcUtH{N! zi27Mqb23J?FY~Lv{|2`NCErM&q7$?~a?P0o#b)o1irQg@h~S~u)^#JrF~Z?*9k@1xRsCGDPBWs9 z1+w<$SpwBr60>>%^K9q?<}2XD{yHFOTEuPwz=ta)pvEgG>0Xo9+lGY5g3(q8QcBXj8HC-{Pa?B4AMr2g647$nikNEM#r zx~vYSd(7N$ZlZK*!T5CEDhgLAT;UJ>OABBPxQ-X6o7N`O9s#SqB_sj=Okqag!GbHo z4yVfTUbk0YtlgFe(d`1W%ulFWgq;Cky3v4yFyCVX-_4B+zO$Xq=)gY@LKN48l@qch zWB{k4lcOd{l&v1JnfFbOfyO?Day*6}e9S{50gyYJvJOh163p7$Iz4w_=W);Y4=5_I zNut201;Xecem%xuX{jS-rGtqYA3}lv$V2>XohcFgf)oFN7#3#@O0KV5%z_7eRxUid z%69A1CEqiuEd6q_W=2rBF~vH97aACg>tF(J=pr|$HPp0@5Wuq zxG^9>A(S*;g`xrRqA2>X_|(-ZX2q>Q)Kxj3KV8V!%Wxn#bu4PI#BpiF$>|z|q?q2nBHwZ5qOiMBZv55hHX(ltjyG)^TuK&p z$+vCJ*4T_Lo@|ZV_**~64C0W~L!b!Z%H^5aty zSk!AwAsAs>BRJd-Ogbn4JQPuVh!T?h?xR~qBk|wXr+e~lWi){L74j^um?|vXBOLH8 zJlI9f(p6-s#N7HUV9iYJ9qMn7rL_buHZ}NhiN<^LAD?HB)BqKYx@XHwZ{feRf(%37 z>~9)!=aNjIMG1Oc=8)3WnNWg!=dtc9))SVIB((Y-K{SvJ)iq0|A+a+HTx#*3UE-9> zpL)Q<4EHFPZ15&;1s#%rJCQJ3^X^L_NBWZh2(cP$$%IL<$t(DJ3{d-|riCu&i4=ee zm4-fTQkYBum7xecv}%DFZYzF|Kg!a5kH#>m?MuRv7<1vX(LqV(@byb|dujdm;u) zi=lC1Rza!;O<>~q)U4zxH+KWx+x`ryRz&=I+7n9qnnhd?CcxvBQdccxqEfVJCq76< zLlw&z4fHexVEt=zk3C*%!H%0S&X-CtdZ0a`h zO1MYlYW$RuI|m0wF5cT0%%u90MlP`y5295;J1-Q`*7P|9HRZ$w=I*QEE6 zgmqM*JCNzTx@#m0z?hq6MnXtbwhazYxo#=zw=}Z1>gv!EhMB2H0>FA3&5Y8S8qZ5U z%6Bc1@CggQBj9x`(Q+R)**hUE8z3gB>UT1%2nGXf9L^&L(gTi(YJbw1Ef0z?0{pcl zL1WpC5*q*Dq9SI!v$0kj6cOL=Tv_E_Vm|X!36AjD#h>zhnGRt(s?_1+*2iUZ88kNce;=8*7-d>`^wbb}- zZq4$t5EzS&?j?efa5n73ab#})M*mYAQ?wS${n01eagf*^UHB=&+ufOiX-+Hzrb{UQ zB3N`@azXd)8>@6stTbaf7H;Cq-2j&*5XMf;Od6UtTA6!&I&q8DAl5kINi3}&$;FZ2 z;cg_HFf)!C26A^#+pLVv$|4UZ*TF&JXrW%Kra1+Q-IjF^JPniU8`TPVP47LeVr5;Nws3Yf~OHz*zwun3rL zfA;HP5Zx?c(#qWo>%g2BxSJxY$bjed9%f*3q|n@@LPv%AQLMDVd8vsV;eSK|exSkD zgacpmbsb}02DoM<@HSoCL}$cd0PlA|R!B2Ex6_z@<4E#aJ3<^>balxwD0t8086u&{ zR;BB9nLLOT8YN1OK_t!&17+W(s&~5u0KBc)a*?ZI3}}U>@Ir2n1H?!nyJlu3mbUo$ zEx_DgNICN^#SD^1nH5gR9gU5IKSD->P-bd5T*M+{-*uabLND8}%H#x}PMxd2VW zHFTnc1}6X$B*Ug5;MmCr-+N-%u2+MIM{o^EJ9_ZK%Gyf!+Gwep;YtXI$*+Sdg!xLY znrulHWVQ=swVg3d(OQod&q^z;=4CsI2|k#8U^t9*LS9waC5Dc6xF&-#DutpXrh;5$ z4rZG!w(mii83iH-opxcxo>&DZ42+Z)E$zYvNjm&o4YKpZXZvt!*6U!PFsA;saj-fB za6Wp0bZ|10kRTNU2Mj+Z(=RGGibH7F3Dlv~9%OTkW-58w#C!{6U7iAX{W)NYYs@fc zMXph163S#drPjoHnAWj)qg|P%3>#`YCzrzhP!9~QT2XL3ui3f*c!&9^&`@QHYDsZh zEz{Q4A73*T@>#r*L1PzUhA$*w+S}|Cpm9x1qi|oEIr$qE@!f)(-2*PC_r#8=cojV$h6TvNuI-2vStlG9} zLv=Mt!ufh*NQX!xvC@@?Z!r8=)+;8M*y9B$vUpD#S{B^4BE=%Uw!@3Ihf zPQ8&k_*0dP5K%xB7VkKfypw<`(WUjAnJe7GQdng$6M)aNnu1}WAU z=byLAix&ZLXkL9E=g+ah4jefs;ePCXkma7Fnj%|MQ;^7($$1pEhf`+^TV%N%;&~rC z5o4v_Twq`vKA_tF zPzzR+R`vxfjK&$+hs=8ZSO7lWb+_XC{qp%hDf%T~9tWOY0RFikQD39(K4cbAW3|y? zuW`%!`EyqGbGpa02|ZSUqyk}IC15awUEOVeYAaX+O~7mVhe)CKC7DQE_c;c3X2+A3 zpGpw_^0x;LgLUJxqYHs`qu7$dSXr?QTkBuq{^$4umUqx#O)s4p|4od4d@9xlvOUY^ z?hmw3Z;L)r{$&Y6 zK(!ZFgIm@8m;e2rA3S`A7_cFC^&-Q6syO(!CCqULcaT0Xg8P3C|IdBInY-X_v30yU z^K;h!Z||^-0GDe#a75r27l(Cs9H43cq0GJeSI=E|6F2DhF?SF8DLDIYC-dzeT+@Cz zn;X;QNo_IDF?4rf$u52axv<_ z{H*7Q$geK;3$t%|oSIifu0MZW>Ri1l+ea$1fjAz5Dy%U~tHs&wnlWVQ6*|o5EJ{)8@$# zznHnlzxwf~55A<a3$5QjTKRO`U&TO!g!m-$ibm^ONI5$LnC^}GyY0pQ zb~=CR^rxAzf@Yen0Hx#W7mpevX{|dWG0zLD@^QDtDTzxqzxu(~?O1e@ zsg92h+^CGpdZnMx1UmXsp&@8Ht&6>J@_OUqJ1c+LmtA*dNj!-j^ZF=Kqs&#o}6gm(HCEsjyCSN;&=;FN_qOCoMA1aPYX z7JV|G7aAZJ&!;Tfsf54o2t>(mfXv<7ooqb*pPd!oL2yMveE741snN?*0`3E2BMYR$ zYoN}AWSCl-ILw$XfC5W?tZco6$igsw;0j<%(Y|;upgr=V1UKvkAD=w@$f?}=2a zfqg-r?by{o5s);cIhOS3K>A77rWPK@`jqY}2j<9bv(P%brRC=gz<~2CNTx1pZFslM z*AziSgtDj7Y!jB_R|Z5bUlcfEtNe|PUQ~+QurvexjeRg)iy_&zN$(ZSIm}ikHwnVF zb*8(P*u>TgQb0!KYk1KBy|tbcmK}W7wQ}B%^v6!kqqpp%3@GwNl_RNxbl|oN#{5T4 zbZ;!l)DXHKP^n}+{VX+Q_0{P-*iuXC9Z_b1yutDY_Xx^An`aQ4pgNziinAYo)tndn zq_q6a%|KEMZ{O(#a?0wEHDT!yxl5XYCj=#vH<--NyAJRP?VC`_Y5 zZX6KewKfi?R@>9T&sl%>D+HQF%~ z;5OPwbtp={?GEePi6vS!2G_HcIrB1E%ab#7dkK2kWOEg3+`ch$V_ju#zPq2jrD=6| zcKvf}+|fSjla{w79Z-%W#(a50Uz~jB7n7owouM&L79I(>W`uf88oH*`DCXhp^@2ve zm=Anx!qWLPx1UxmJ{reX>Z#&8s<4u!DZHF9BwZ`Y{8&*7>diWjdErA&cx7F0 zpiyUbSop49KxKTzmK-n;;k>;9VaEiae(AUfL6RI{@DQpF94zRl= znlH3qVZ{vlY_4j{@6OOp`Owe_*_)dW3=qZ^Qo;tM1@fjEwtKGEwp`oxxsdPsVohYOkg5SnuS%)9 zn7+Pp-y-aNyd!RXsWRbz2oBlwM;tnEp=o;NbggP)cUw)1a+)7lDR#=LHcTSz+rxcO}+>b(tSj z_K$r-(?%2hsVG7!Qf>b&4t9<&TI)UZSPN_5r|G= zJFC?nAS)N|?nxS?cf9f*n(yL~a8E57i;Qo?jWp=TQl!GEW4*#b(G7jM) zzX2DE^NxX3k~C>mtpZ8fv)iD}()S{?KY;(Mh|V(}=ju_1XDSPNOR& zyTkDj=;NqJALk375T5JSg4rhId!x^ty*pB;?&pelcSwRVY*>Q$_}vshU@h+wD#d5uCFO)U-g_mibG(cJ>`3GWt)ht!>~ zr3C@z0zv^a_|f~^8pzMA(v{@8F_VL-1)$yY8e-t9H+-x&K5WxAtESJtDZScdeI*Cb zYbI%i;hP=udiq5f!8b!iFex5mB5n?-Y`|Jj>;%1=Ffi^Ts%D1Hv_qOI(@`XMf80}I z+s@g=Z-I4|>m%d&4?_d)#xyZJw#4xgvdAo96(5HcY07^z8hosOI`l)T0uu1=8GbXT zk{Q;UQ3Bg(Q|a#8^?7mG-xG{231#HnxA*1p-}B#&@^Ilrl#a9p;Grfpu_3nP{u}hH2+WCgUXFXBC5-=! znIYNd1G+f^x1f?S-tXzN8tCBOHvgP^WMwi?cH{BVSP#u|(bP&vf>Ls=qPx6NIH;qC zw7C@e{9%gX=3-&F%P`q@&3P~P`!3(0(_`x61iTxP5!eoH zFNMrk-(ZQhC6PC?f=~O?8o6!fKl%234Pj76y?Z@1)fFhz?~CyBbC^>xwvS&2M6~7! z!ItGn=HtV!?<6tXNUK_%N+)T~Wp(#eNRzju+Odbl1;Z>A8hO51)>{3>g!EEkD=&m!H1R2ja!AvfrP!`qZH?gu!mxk*Ka!rlI zrE0Duo-Ej+jfi)HI%Z~HDYEHVdDBeEx$T}sQUOqe+3|eHZ)CpO`IZb~R6pVovyiwv znaD_w$?mdZnoTCfiexU}mc(oYpw1+dn2yuZ=X)dx{OVOe0qaX%H{2l;nh?~K;^b!T zx-!4~Jd&qGIyM?OSJ_NjyZ0?4iw0U&gLnUGB#)I+2$)xRZN#O)vU5wwbIn?YacjG- zYS_s+Ux(NHoTcYFfyC@ih*PYFg06~6rjrO^TysXMpGGudC0(o0l%}9QLaJp-D_kU-#=9lk^x>H!D9k%oY$4A9%Z&P3 zI1o3p#Lck9jMmSZlEn(gnUM&69GYnr{5?|eUK-E^sg^1swuCC;0{W`9cMJINEtin# z!0TNhW_jzi*ENW%4^R)2b+Q-x=D*kWoJ^lwGQgEL44N!+oy0iRwF(1BYkA=!=ad_M zQcVAxeE^P^UmGxQtnG_I;Y?|>UF~?Y=B7wBo(y@XVSmgk8kyY zK}pYw+QYE4Yr)%*CROEvc)Qdsm9fpD^xib(RzR`P%pDEMO0a$WTQ{T9|D*@>#<~dO zSzNL7Luu>H_=+g)Q--UTs>@CU`R(OOzIW=4-T2j)M%CEf7*V@ZCqF6J3N*aDf280s zV=JSruFE?+D6E^ZzLizbK@V5~2PtTPW=ck9b*~F|*3CH2bn}ll%=LcpOV}0~dHL`J z;=&_BZ)52>&|p+0z-djdb`8oA@?DmL!7FHa+b{j-k}n!CajoU8i0o3M!op5J6u7V_ zrVbhxe%{VK7xPK+hJl95kU@~#3>&lXbky+Ho%?%2L}2&wm+Bqgc2;90eMezB#CP|C zB9=-bocfCNV_WAg8VYoKcI()QX3ZinO+k8FfS6cO*`?7o^X1g1ac(^6SyhNPWX{OgGzg>0jYi*4ea;E5X?`M=hnSCzQ zb>n*me(6?2g0MRF!}NPj5H^3ws5D(}dg+xhT$eASePKP*f6Ugs3)WYNRyvpZCA<;#|*3Jw8f#1JvtP5i@dA!;KhR@)kXE z*gndEoO+j$W(${*&&K(UAfB##Q*@0jK5bc_B{Fzit%p7WvQm6jP~i5bctN?*!t2lH zaAI3Q*Qr~SAzRgYYAt(@z8vL9w@&x*_;5gUZqo>0grw%TzTGY0;&2NZfU`6)GALKR zKlJ&TzlSC!Iu(Z@e1Q;lk12jw@$~1~%J6L^t+pdEcN+Yn?xgSJSilt|m_U8)miKRb zG1D0ZT$1TEtD&O6d`yO7c~k5BgQO4UI$@?liFce!ix>S=ei zndd>{IpK!T&=!r2g`GeSg7*4=!q73Zh~-!r(>K36Ax{8;IbzgXtz66FY}mHH-=fag z98lS*8NA=+b|%O&GmX$m3ghA?;DHMMbwM!LY}@%ce;tIP9}UQt5b-2`#3nk`w}%9p$ns zb9xC&Qpg!+W`6?Al-MW|)0&7^vbe4>8O}Ba(psCq6rrK&$)lz2?Pc~}W)|xxH&jDV z+ZFoB-pcKibhcXiT3n6C!9RXuJ@(rUcCFu7e)P|`jFUT93H@Kl++0c5JUqL;+Sflj zu}fI?MmD)D{mSF#Q@=a$d9))6BA#!&4~(@;FWe=I7r*dfhwDfRHC0Jh`*lyAuq+|F zar$ei|IrJNB2Yr`Ez5h);_yiK27K2|7QDXL;-$f*uK38)8WsU_f!mQ4d>!;N!Y&p6x2^w(x!jnr*2Lu%Mqi=P; zUww92?|~a-FM))QYBNYWxFO!%txQu(TsRIb^3m5sdrQaY#qw5L%57b9boRVNi)|>d zyM>j#p=RiyDV6Lop>4B2`m9&oXXkf#N9W`l z*+j=0+GfRl@Hf9BQT2r-HR=%u>QiEN>_zAZLq)ZC>|H_gjmOfpZbzcCC!4ngAy}m= zUB?fVi}>CP;td>H;CdxW0^LRfe&dJ_6BE9QqkQ`ik~iB7ep{WWO1+w_x9?CFW%vZK zF=zeKK$O5ahl6d1%jmwTB&C-hqA$VGva`<@WP+ZbyewITkyx>z-Z-2D7gTQESZ?az z4sQNn?~>fLMgXG=40gD9RhZ>lygVo0(WC#H77^zE6=R}SG_ zI%B7$38{@whKBc{Vvf>V{>y0j^u_y8++(la@65V&fAP{spU2=YU37{AK|u1YM|BcZH`SLI<|AI>ARB- zC1-P?6V*gyKI|D8CC0Q(iH(Tx$nL$7q6$O! z^P$`G>kL>k{Gg$Mko3jj@iA9uQQ2#prgByzBUdMOjynBS;sPusao4PXo8VoP^I+HB ziOpD8mt?qhlXC2O^}-x>R(-`o5>erHKsWwcgCvjI{1by=ygI z@N#LOyVPqi_lzbO_xvAkInxY;+a>JLD;tw&SKnis_3?SB^!FKfy;rxI;)yrc zYr|EcXTqB1zXrnZ9P@O%w}LcWZkvSWF|5MoQ32_0(MPi4Ct$HdFP}j6T#@c0f4k@s zi*dC^nUo8ePKBK2iM@72?))L6@;6w?)B0O+ftzx(o{rJCZ*D!scoX|v6EQ9}v0)Z- ztm8XHE1vsX{hsS=y3{9vw%cV%e*Jz#)p_cwZBMh#>NsP2(DXyfor^ptPGFbG5qfPc zEPSSSodI%ZjI>g>ms=XXTsyBbZkrX1RTyu)w47VJ4WCmnvE+!ZK0F?n?&^|>8my2N z{Z7vG9~Dj|Hoc>nSAGnv^&fs@G4izi6P9rLM!NEa`EE^y%aYjYn`!CTw?Jmji^WXD zeX8T=z$;re|5x?CB(Emdjj(iGPaY-DKcp?NSBfbzd@As`?AK`R3%BLe`rbV2*D>XS zdUvAALbn2|&F;bKt;PbYZ_X#rfu^ari1(bWAqchOc4?s>4YfD&;K~F1ZbP4E%FNoD zpbO}brFq421X^cKo1RKdN}f^|a}L5~@|zaB37VclU;?*_;T_%sYv3A@r7srl!S0*4 z*T76JW`3Mue2)@76y&c~o>;kwkP|;1wK4B+2%Q;PtW}ER9l>6-Q8c9wre)LTEfPPr z&_q@co_O&TGgd=~u+$u**qh{pOSzJX(wxrw18bUNq=w%;eOqF+wNVf;vE8KUl=0DN zp2ytHRMJ9%yq{h<@u{SyHD9^~JG}1PGkX5s!m;dor{tXE9#CVx^u(q;%Thy=h;raqA81^8;@RFg_5|N{z(`J@V$e zmz*srMej9U5nKFvR6Bv18uz@#o9K|06%#kl1n?Xhl^1&)m536DO{dTsSSgFpKwbk? zmyIxnJxlnS74Ir#6YJ0laBiURRHZn*PS;7PsMtmrqRGBvjNlL~V$ zpAqYqJ}w?!m6cKH$#@Pg?P3Og!FBg!LP4u$G%Myn9<7QolqQ$wx>4mwUVZ*FdAl+% z%!`{u`M&1;^r47)`uQdl{TknC!J@swtK6L@-n?kisj`jFN)-uRemTZvdYl$36U8B5 z<+Q{2hl4dgi97ZDT(?Ehi!&Eui&`?m3t&O)WgXhYevrV!aeo1J7C1`$!VY-6XCgph1y zUlQ4O##qXd7);2%FJmxfhOrwnp3ih$*M0x)`*)wubzaZ&dj7lq?N!ZpKJ)#&x8ryp z2T#}H$E+Pge+{NTFp`nY#3TJ0lQOOi+Bb~q;bHLXA)!Q#^+HlJ^W^BGD8+Muldopg zrZIJKb}7|k{>*aHKE$8eXGgT`roQ@G2``Rl-6nD%U>Rs=Hgw^-t;8hc60*d+J%DwB z3C-khOP2TfuCxWTZb%y9txWutkD3^T3r-3Y6CXFOX)i;+#jKF2^QO+>X-eu~lQ%+t zKQ%VaPf#(CJRWF$Sa`NLVao85v_*sF-G3AgGs#==iS;2 z1~m5wd{0?0Q|jp_pvY_CtR92=c6R3!7LJ+{^|;;TmKGirY+?MzL@7OWt0(xl5t5fa zN2S5#H)3OWN{1g*b5It3E-Nc!oWBzupOpm=2t|PPyS&vGS5f4S-_CP2z;BH;XB~d3(3oeesaiVa5 zb0I!CBR1eM{og;WWp;f^ulo=X-`Q+Iu5(w|rXDGL(&zr-Ju{a{!vl+ks{IX{-DQgE z>sxf$4PxrO?*lU^<5=P>w(QP_({QQbCjvP4J%)iY3?Unm`f6Im#davI8PS@c+?LTZ z6WkE(VJr(}h^y2^SkYs|UMvWt-S9c3fB%>C=3)-MiK({slNin)VR~V@uV0T+ZJ*#a z8!U*{Nr8krHu=~SV$!7+`8CfF;a2YgAxYNBq@v;lH-vYF+4i2UJUTOT?UA?lQb3U( zrRE-E_Id$XWF$fb6Fnro*d(hnu*%nRg3=;6iB%usoUZf1iC#wr-{~+#9;g_jY#kPA zC(ZeC5UyUu1hYE=im)ha<%d#>|FWf`&ErPH1Am{5mVs`u#r=*=fy?4PBanr;ceHn#Rkw`>|s^8>`n9@d*z=;q2By-BaDP0I?P#@MYP z+R<_u&c@=QlA{2gHT{sib+b|bIdhB-=vUKbE~l_+%e0SNcy_ZQ*HJ6&fq4+gT4^O( zbzou!(P)&LKGwGNws(oxe0oiLy7kl3mu+_<;7me(wbUBZaI^7hWpFJRe3S5H<>6vI z8(-~C6zbU4ALZxlxrb_I}Y*YZxsRx7MrtbVDNb!Rler)?D$gPwRM{}`{$5{7AB z-3HU7;UX=KrEysk>!EDiPWDp47Nve`q_6Y=q6kUd3O388;}@InO0q>tInVo5qF#wi z=O9Y7^(~Uk)nf}*G~z`{RYy2Z2O0&>s%35%B8l@YIH9D8kS3y3mt)%n|Jc!@h|^+zeM4Df17;U}A&S!M500+l}*qO5P&#DOyjCH#qCo$&d>@CxrXp?R!G5 zg#n1I)WVtD;@2$F43>O*s`YEa)eLtnCu#h$j>-qc?g3z>73a5fnc!UqNbKwbhXv>}EmSPPnYFL=%BgRTQ>*xL#zfLz40u;9g`F zreI4Lv|&_$rMf*><@*&)FHXkdrj=mQvA&`o-o^yK)}pLC3Qh`Qi-a<@u$W$w*yq5@ z^#jo!uF&GR4Q%PGz$Y6^^UlQ6vFv^5``5zc>!*Z28X0;)^OpP9?t98&or`IB%(Bbl_ly~Ef+tid_i_vd{QCrj>0oPKV@&{01BLU$WtqE+xjd8d`#qg){E z%xvD7u!sIB9>+w*LuPsHFNCo#}#au7dl)d-uszWcmB)YYKUd4lAy>w%D(6$TCbcQkINcjlQ=@OgJMz?-Byt(oB z0?BaD{&mx4R{4<)MBY{>mM@iWCpaqlUZl3&cuhQ*8fD=XSHZvB*VQi&C7)UEE6$iU ztbX0To1u_2FWk_jj!Lj-W6#l!p!XJ#EB%c!67H*Qyh1gU=rOr`iZGIn5e{vC94;CN=_QRC(`#{5)@xg;fBFj6!+gd z?2&Ib`0s)ysU*BMPGnBgM0N9tQDr)eZbw2#k%L$?@;Wy{Q{Q++lQcm4=V{AbxT zo$a<67+uPh${0C1cd`byN4qNO$HL@5- zSB*O%T}2a926kvkmg;z-101cweccA$Pb#NMj1oapZ&1mA;6^#?{B#xH(3B&3(qUTVgNb*j;#UPqeOIi&I!^ zaX>m&xlY)o_!p&kY!0_yv?(Ii&B9}Xtr5BpVvjAbJ1qC)#f^IpDy-sF$Vi>9ZD=Fi zRE@e=z5`5TxA=4zMs{ur-c9FJgS)B4)Sk@4NlqJ=$U^9c+F*Y@zf zuA<{eIDDy{udePe>54d--;bZ|2J_>}Hr3|W!v;p1F2hS7`=nSlohw$qTS^G|rAB?N z0vWZl?n}pMZow^x-UgMl)H(PFRYhzf4sg%^0 z6PP{$@d(J}h)9TVFxsINo#+O1Z#{5c%ROS@-&8qEK6j}XFp}l_BB4)$EtM#9tfS78 z^(jWK)o;KhGzWR~Mp~9oKD*02OKeDdU<}=Nzg^#fZ6@fVyphbLkt;(7h5PhI-`Qe{ zg7yRxUsAQ*Dd-aC1#*Z$LllaRIQN6je?dO0qLoDvmO(r{?Q2~W>;>XP2;_4gWI1=t zKvWRjpA*}NO*{rJ;GU3)W{qt{uCCaHsGSx0Mtk=2N=^<-GwcZuJ9KOqJKElIIwrU` z{=l$A3g91qGK+Xmxh~QzN33%U;qrO2=JRmj%IK3E7W-w(4=Z)=unyY+W?&q8g*fN! zJqdwQE`8J<@Q@%nsF&x_W2S>H;7QkbN4^QdbhA8jIjSeS4TJ~DHN{(C;#GRyxn{yl zQO-N8u#x%))o@8-`OuQ7Nx{1+x3Dw;wmV39yS8{B>(qkhmhdE0OFx;p)#t=R-G^F> z4&4)_PyMsO=zk=*lh41ps{Sslhf?+|@E_z>f34)r7Lh9U#jwj3m}Qvhd%@rZuhI9S&!% zSSTadtuGTsiBujt}c_Q}- z_#{bNJY$9rYUeOdhM;_brun?JF)8Z`eWo!-(>h((t4L;hDzXsOCZB~!t6gPs| zmn*t+WQynN-s=RS&jng?-axZ;Z^glaNWR?p=tJB%wCgmtsCX>MOWxqFT27)Wuass6 zxlR@#CdA`ymAz|xHDoxOQI!FkrR{k%eGW{#>riQ_P3K4N7ni9^n8{~OS|;Dc ztvNgUIm=W6^()F%Ao?3>3e`#Y+I(?%_ai|_GN~#2OZ?5hUmCAVL9U|4L**NsMHAC> z43mMB3$8Ty44|Sz6XfMb)nh(LCa(A`jFGJ|Wj9|gkTTaNP#OL*wF|V6a;Ilr`&-Zb zI5m*7Wh1tC4XssrT>6J&y}`lqUf0mQO;*NhYi5!U-lWSZ=|SAey+i5{(K4PK=mX8z7c0s!%LaDW47(hpYVJr+`DhWLOyFw zo-Mxqr)OA6HN~A!?9pYh8)2g=x0fUv|S>DSO_6pO(n8zq(UmTGi`|G?jNW z`f^6w1yZI+3~e!i%<7^y!)!<7bks{~_65k!18!9xeuIH!yQ(HBy?bHaw)#GCqJqqg z5v>6WCDUJshdM^lTnKoJo{X%2g8f7SQ(9;@&vwrV?vcXgKosUtkK&tD?{nepwN8*ehcLEu;|GbH-`MFWc!%sc2{YW$${FmO{kZOH&`Ky`sujXeymny;c zUTwomDdTNhRzGE?r+B$66M}4mK_x22gb{GR(cQ&Vc1!^PwPp+|dVT5S*F!~JNk{mE zWeY(ULF5U!;A3p9u$Oq%+m~6~iV;gFnaDUW*tU9C{709OE7pXUCn-9WU&Q5|+ZBn3MdN7i zD`}hCz(6Gv+n3biwxktfpc87Rx-~Y3b*$eQWUjxDUOP1exwaYp@w&m33Oy!l@J8a~ zx`;d&&S@f57S0wmhzqRN+?soIC#p)L)?#jN!fQ=5-wX+_7+U7`wd%AR6f(DYVyH_ePzO>qesKht@}`xVPx;J4ZAXW($>(F| zw)$)gbc<3Xd52CkUJDTSX#Ysx5T&3v0)3=Cp@F2`OO$xMwLpqj9g6>gPC(JG2+HV% zcZP582MDCOhB=mZRb}t>iV5u|A1B|w-ADuUe%Oks=cjG0J^J*{Q#xDkV-V@t5n<8$ zXBGXdySI)wnZvX5p-PDy4}Yu z|5sAo^6xw9_?~~@MEL-G@JO}XPwT^mgkbiZ(UItqLF9c?ij?*c7b$YJ1+4Xk2Jg+4KpOY>(PuVEt}d z&D8C=)ucrAowl;WS8LLp>SC6zH+MZAAUarYues!3?k;g`Hk{u>Z8pR#Q!*6 zH|V{E{M^Xl_v4b~yI*U%59Jnz+pB=TM?8a3=aBg0i3iGzY_Yzl>ezQen5NBrY+ln$ zexeUgi8~Yz6vF(uF6_PobLAZ4HxsKh+VCe)sf5)1RX5!tTR132itF~uQoK|I5`smV zq@8+OY!kdY6C0b3_VWoA1NdhtL25tPgVq-JPp)wUBARv@-fIB(WqR5-aj7TYmf3f? z=k9%WscoO#nkU?9XhA%f^w8iNTV}4E630gW_AzYV#$EAv5Q8T6N9-MbOp#&hvd)e6 z!bLTiIzo!Co2EZa{Sv>pNSk_7-fPUtOos+KiQ7Xu48AkWwm}EQu)(b!*u+N3sC=#` z-5rC$K_q*!MFK(wu!*d>j;*z`#QI9Wi)t)y?4%m4%bIpx&mpn~`cL>mE#AnGYtY$R*l5RH`E&EMV> z7W)MZOAZ^ipizM5SU}P^+&vM2F z-D}6{iQe(2zEjHZ@r~=T3=qaDH|POoo=5pzy7xgi~!pzch zl{i-0TQe(>>|nRWrDIk$-ta_SZFGAHxoLOl)&glU1`IRyXSw!ER5K6zxGMOV0bJ7k zSh=PG8r-9=|Med638Hfvv2xVE*NBb=FF^8OLdqlJIHGyx;`ihG+uJ4GfV9;rkcQ9S?Za8BBd;*MSEjl~e+SU@#P zU=+_^NKPwl7oVrsC64qkX@Thy--U5GP;hM+Ss?D$vp^$?j2Z)AD$d!)K@jMbV*MO# zKk-2|u@{k1p8M}EMd#+;@?v2$fq*GDk~lCH3N>9G=p}H-{J0w$HFDov#RTCOT)`-; z{0#YWsrt%~(cYsp#m@Wj z?)7J`76;8Ajn(nZzKx#w2r&#W-%CDizOw>4LyO;2HHXltjcr!#ig2XGOpJyCWVEsF zgIZ{#44%(cLGfcWI~(6&OzV2NH79j9Hb$pnvF|eih0W0}2kszklj;huRmMexaTzjv zm$}8N8q@wh@UfxKu(YJzDZjtpDL&15%sk6ClQaCM1#ChmRB^*kK>bO(LFc@P3jCKc zVeyzDIbzK?nD4gS-_GsP8G@L`i~Kr@GlK?^bn7@7iDm}_$zMR5T?+cL4yMw`P0#Ns7-J!9VX(FqXWIIPMGyLg+Q zR^J0k*B%QV(`m4s%Nq^h@_TrsO7zsLNteO#&+r&Kd_g%nf||l{cVku!KfbfxSBOIJ zaqnS2*FD7D4!@?7fWBY|K*DXwVHe+oh>j~)S&#yrQ%6zhYXv*P$BfK^NJ{~cyPF-A z7q29w%coIOiQ6dJ)Ivmgr}N{r0{ylVMeXk`UmxRipcLb=1RGJ(5AbbThgm)i7MfmP zK1ZknZP>HO_C%gIWsvY<__Zq5x-Xwi82NUact$NjL$}DxyQ0Rnav^Cp?rd?kiOd%9 zA?+G5WtOMLsX=yNWXZ}y5pZ5Lx1F~+;6v@N*(O46wR`*?{93g)eX8&iS~BXvI%G^H zxBe~f)c?*qt&APEjjONAj}=u&rYTDLy>hHu<*yYPq^zawm%L=Y@iVw@U-+lfEgHA@ ziiiV;!Z9Pa0QYw=Oex zWia-AYS;ek3BT|c?o^2%3vIT6kB>@+-S*y|j)w2p)Wg1XH%_UVKd$%PaE-5Jd${uM zNWqix$6^H?*B-CinF1F!$Ct;YODYGUax9;JRT8P>Nx{8QY0D-rT6qB*A|$|`rbH@z zOF`9|0yN=4UQ*5Ccbu8x?ra_P9x+Aarn&>K08?*HO2F&#I|APj zBexw%vr*v=)V;%{jZDd_tW`#Ww?t!Y???|^V&T}MYz&S$LL9m2aqD7xmwlrL1hWt?*$k7)R z3>a-9iHA6)pmTAoZeG%N zBW)(^i-cbrM>QhY;0aUgZ)kG1@7_t#(t>&LBTar)u&pr0g`<_KtFt(6(6$Y|QvJ4j z+8lv5z=q7VsOhmx?@EhC`?0lKR#&=8^NY=`JpP8WKy%Y!#A8~XPBZg7qUrC49yo!C z$v}-7t7lPKiGsmCHr4-o1in2wU4HRzMSw7~_=y1oq#$LmGT*VR-Qt3;LP&*hc=Fy{ z*}S;tT6NkI3t(frN!nAHaH$&Li2lDbSk)5>zkJr~Iu(xm;`fQHll1p-7zPN7f>V$q zaPYg~K}dUHtRKEmRJXL0i@zS3syca_SV|nNDSecJ2{=43mbjen6SXjckxwujToZpg zVHD+w{sP*W@HHq0Oik*>YNNx_FLsCZ1wALdH*^ae4+rP@!fo9-8Z4gV z9Q2Q}iymv-r&QJhk#lLEUxG=SGY<}X4AgG;ZHc8RW|u&xNoS*c8Yi5R3Ssf>t#Hyh zonK|}EeudI9O;qMawO|lAb*~-FPNSu`-$8|9t$831D4T+;@8owi!WXh+^j5J43%_u zU7;Skq70rtSYid*8VYo{p4~Jxy}sKGmDmM)%+4;cliVN-TuE(gswnSn_D0lU3wj0D zlgd9hhx-lL^y^St>&h<&fT?PI2&ezAQ>*0KGtOX{UR}Ds-{ecY)YA(8%i%jFe%ZvM zvKysN>}C$8cP39(W)a)L?h;zm{8zWJ>#kT|b&{t~qfAkA>FQR^e6(~AfetZdXzFbC z)MfYhpam9EW=|ON%8D3YbM5ap^q4I6X{!@9S;04+uUrd*=q;HJ$~8`L!cJ!(f*nx7Z)sWF2K{%mKvyVXno z(ZX*vn8Ukex>S%)_&7r+p=OP+9W|R#!!I1UXA_DqZMz%CB0T9ndmK(w@Wh7A6F4TQ zM+J_N4%ScQo>WFuS1j~(+|c%cm$8bH$t=8_ZtKw)lHRs8EbWfhu~FgEnULmdnv-cE zGq%J8TrTxPl+B*5S6rOA-D34mYIX%*ckI_T(+zZo4pmJVvB!wF4NID@IFLb^4r!%Z zt++z+T`TmQp^E8!7I_hmedn%OT($@;_qjz!@f&AIi7W>$ zk=dFn&+fhfwT#5tW57)`;6Gir(^0=VZ|d~zT_Y9v8#^YXoFi*^cKYyR`vjOy#RdZP z2e>u4*~-P@OVb|w6yfcAsledX^#CxDde5QXOJ87V94NMG0iY(21h!I0l1^hm0oJZ) z2R+D>kPfr#z!9Od=p14ct*M@Ae#}uRqZfWUMhx~fx{lpEhoPfyw<3zwBwq|r(?|Mv4Ar`nXT*rnmHEwaG_cGlCvU*w91h zo++|tuyd|m1(sFQr#`A&retg_{ou|Mndk_nCa+9!C56rx9`W5@5mc_3aRPXHMzvgm#~Scnkj~LHr88x4 zte2w=x3FEq{Ptn~boQathRCfk|AgZ;i+8Xao5=k)&M7_}?b0}6v8}7)L}qZgV#8WD zvc+Yu9oQoQWpDFyrVbm_X}#n-ihFjKC49Ul)5PBt_P$duy36*CUx#s-mS{2_nLQfH3T2ARmUC7UcNtZ=luS^odC)_} z^qpphX2i3N{S5U4YNkAT*!=GI9)g_5n%R2MP225mP=@6|ywv8e7#-`)>`!mzBCbD1 zNQmdJsBI-QI-G;f6U;hXbD8xu1C7ot9TLJ(RR19J&StUOh8T(pX!;C5hocT1763u| zV_?M@OlG+FWg9)|dpF85sqeawNn~)h@k5B~piS(yExm7tY=Bz^X}jfVRGT(yQ5Lz2;M= z+nxC5z>Ry(_)Mx_ppbt0S5>r9eK@QKOS+2IQ4oiNDrKmQd`LB97>LkbXpz4D+IM5w z68PWgT@Hlm#J9SJdfFURX#cwPrNA~%N8~qrln_iZG7lKmmd}Vm-685OjrwptJ(2?e zb$pm>E7~G$<4&F3*pUJ4iz8<-86mH~PnuIxcVkntY*Rhpmr+x-G{^yVD>KE8fFKj> z67r&!`*ajb11Y%K`?I&a?TW^f%6y7AepT2T_g)gzVVOSn7ov#(h$bjor0k{KR~|r~ z_e;5Zoq9#E?|A>x$PHYP{(MwlM!G)4f{#1;fc2udMxoPqZS+@!rGqF!{sPOhIxa_C zz561%n`P%tPyCR2mb?JU%Z;aJqi(eZ3@sLMu@m@_#}3 z$_~_+R8;Pn7&cOuFyBRapH~5AS;X4>9n%i+vdV$oa$yiAHafJl{S-r~C&j;Yq*2!e z1|j)_#m@G3J&so&7Qde1N_V>X+Q*7p^uX3aKo2S6F~BxxR;_&5GGaudsZP9Mdkj8S zTG2mjZPJdT4G?J_0A=RLGF&-W%6eU%YlMX3t#v)k?Dp?lV>SRBKRnYKUTE1Ik*Qb( zcMw&QDt~2bvvtjdywc1J^M;6<&wOtO=Bf5p?=ZiLOrDoo6|!YX8~2MU>MG?u6@at! zeIeS*U$-ks{Zas?wN}~Ih0>=iLJv3YPy4prn)L$ZwcD&c5EK`@&b4uTBOA0F#plWA zZwFO@_5I2*FU6ZSzx0^yDpJ;2uRA}XLd99Oe&rdUZ1oE7_TQyzzsqAsVOBvILrmIHt@`9(vf)20YfkezmAh&Ra@ZDK$ zv>-*eB0}oOg7+8fH+A9u$lcvUf_Kt@^WVVYOK7Df#P))}np?ancz-dKDRFxFhqn@L zUBq{5+%wa5m~JcelP3Xm73s0T0T$IN6PG2K7yWTyY1@tX4$MXiVm-}D`-=VmStqU# zaYvF_YOJKQI!N4eL{Dzjk5$2WoiqP%`V*_(3NNW?c9WRFpoH^I=XB>yRUNZ}p;>no z2w^gQbImpdqF5m^ZIh5IaJ`i2e&EKgp3+E(`iBJyjBfI9^vQ;9A&-LYJP(*6m>n2S z_)8aZvVq;OhFzP6$z;XGyuGRw?^sV*{KRmFu>FX?#4pc?`Ml#K}Ne1$vXMxSpx zsCY0CD~FQl*92tV&x$uk{z^5f1gcSSB}ap2YO2cKb{{LJ=tKWr#pYVmvFJ!mSo$VB z1xIfw?!vvlp~ci6^eVV{oxAaEG&z)COHWrPl#g&u5ildSd8J6M%k6G(Stt3h z6Mx|eZl<3*dQq_Wj~`Mghl5CoP6tULzgdo?qnb&ZnJGRbHL;O>6_5G^V+3&+`T!C{Pzd)}$ zK%W;6z?eJ?Qu9-T`{Moc*EgT^L5QQol9#kW9O@QAkp6Fjvzzqu=>6{0w5zS(hO!Lu z_s_ma%fF)Zw}<&cU-1Fn`zdr2jIu{Sv3FkK(L8b;tkyo_B0hdSfcYG^?$xz`KPCxeus7wPAywA{^?)&r!@QYc?R%*>`156|E0(N zpZUg5Ux6=k?wj`K|C|5g-v{zvL(RY6kpK2J{rf=v-yDcYa}a6&)=k3_pAEzOQ&;|P zqRCX0cKR({YJMSdGqL}EKU+WqOi{LrjP@gYFN#-<nh4Lc24DGa-{gM|D}VB* z?*v|0aMy1#GqDe=N1OW=O5w0)^J^Y@Gj*{0HU5N9YFZ4<5ml5$9bxta;nKeV+5c(u zOh5giH9TSHJ)aUZ#lW!2$HPd}qc~5D1c?hi3(* zS{uyor5eH*N)kLRcYsier@#jz3a=elD4_J^g<0^i9z_VmeCBw6tLF9p9FG3+Nz#t-_W2Vpk1$8(SBNmq@hvBid7#?poWM&$QAj?4KZ~4y z_Az<14n*jidL}Rvjf=P-+bca{$8#QdKOM7P?9C?B85Ww9atWaL4^|qW;zb?51sxu! z_w~g6%xEry>Z%)L)2l>#D4qpde4#t^rt?j_!Ck1tbicjr)p^mDoVc#=F8>cI2aQ7F zyyN;4+`zAt9VWC|liWyj&g-7I+{vZ(fQV~7^)JTZn_8!~n?cbc2D1Z#PF1s`xq3XN z-k7*GD+=(H8vzUu&6!$YQYL$?`#qnA!{+iZApp>|9fAF9=P{gf9q^#u1iH0D>cWNA z8TYy;EheJYu;?h9N3WUy6rHMI7N@wkqDpg79p3@A^A14313Hnv8zR2{B`5VMkVP)Q z4Nf;bSMqE9lFsw)G!*;N#=DX~r2tpP`&K7j3xCq&r4BMp$22?mOUI^gadL*PdFGnS z?DuT8o}FOJYlmD8{fl4t06mFY7ZXH6xM4?d#f=2Uz8aTDWI%;UHu@$c{4-hBtpG5= z#SkwJ9T`dWC44mMCF85pXk3{Jo=BOdqWaB|P8p!hU4D5Pke}z8G*fQ$OOHKk?py&= z-jDbOGJ!z-oaFQr7+zN>B~}u^>|#u;ZbilDF7)zN(4MZYB3Pxf2S>;+;3RUM9*9~$ z0-xs*934DI^Ug%w`6hU~@?>tVhbvp2(BXCi=we9`%7jA&HakdrTF5+HQ-S-gD7#U{h*6@6vOd{bNBIY8*)8m9`Q(kD{iJL;2FK z+Ju2sv!~Mb0|mxcqSF0#>^p%Zr_#VDHCCgQYiRjv=7PFVsm0OiVwzj4i|g@ol2;=7 z5}z%ljeJW7t|(H`{@wCOPa-F=BK`RYLq^~}Rd)8l_bw8BJ=FU_+(lX8M#(^E4V8ov z9~Z?-Mx*!&Ed_$R!f<`U;)SrC(>~%*#cbAi34B0?ckm)4@s3HI-z{RsJK9N+{tr%y z#G}gxPKu8>j;vPG<*dc1=_u#pc@suO^L7Oit9HMk=HGo z=tuq5x=^oTTHP4>F}%J0z>T@%6?8(OVV~-2|0_2-|22d;x2s-Nm!KwMaTmI1G&w+^ zyY{#<_g7Pz&(fDou-|awMMcXPODsT0#E0qwaU*s*Y$LRt&}=XX+eV(^|}SHe@0_Mh_w!v0~`XVcc$jHT;7v_{u$rYV+{hklf3VHOzK|^k zY1hMz8z@Zrf(95Mvb6CQ{s@;}2?ubo2E>Y_R%&|;fGECN_nWG0TPtb4-WXIh;6^I#FXztz6UNW@#(nBQk(pe= zBo|b-h%wczTNY?l=pbup_742QWfN+ep4-zQNK}>M=m_||mYfZCT|iPX%w@BMzKl1u z3}#~#GTNt7fGj@2)uk=sye-WIG`X8}QP7AIxPwUu+F@%}{G;-13N2to04+D)`K_gc?>mjo+_0t;4<0;7|Te1%Lp; z18g%jbl*^8>}VttSA}42J$8vnz$~pMx4KX!WrqtkOJsH@$i$bgz zY~iA?5_*Cy{YRRJvbSNGNewsuj5`Qjt+QIwHgS8vso*MuQGdWpX37D;YqyQd_)N*y z*G+x=5m~ou%TR_2H(I7hesfi{QTvW!JEG@3T^KvJtlvxb$}f3-Q8FZ5K7ujeUoFUl z?xgnu%-qmw4&3x^ZCT7|T4`gZ7H_1y10;oQS#EK;w48ziFm+zbAU^uYi3dzoS@@q&GEXqcL7k9p;o` zi-iO@SQHx_$<@nJXK3zg@@@oLIZ3y2SuSdHp05=Fw5H|M0*f<7@i~0!vG0sCH-MjH z?|6KTp|*-uD6%j4)f`J07l>JKU~Ju{%*z-!6Dlx+t6&XXwD6ifYzpk_XGeiUb|7sB ze$}IhwbD6?Q(@*J@aulVO0ccm0xy?63lyamE0z)1s{l-v=WoZ-+z(idjSOlPiQv(? zOPzr|hltnKR+y|IkfW_DLd>5-g0oo;6D|_098zVH0T3moAlef*(A9O@urUCR2+E;CPZ)Q9yUCnhfwA&;jhgp6_zN3_}g6yg$UVI2LU7G zo+EDI=OlGQ{(Z9l4r&(Pob57L?>9?!m-&~c9yg(MaYBOPNp(;` zAM;d^Sl@3RfPu|&UgL_;bUFJYluwmNHmWOGwr>EWlQRZ@n+J`I@At?C41fQ8)9RxB zd?Pd<-t!4)#@#f-yoz>Bra(r2|G^X-MMdjT$x=pGJUihxg{LE0*QduP>-0%m z7JBU??Ag2t78YL(oNcnpYqj)cv-`SY;fqH&+}{C5md9cop>KI6>xLkj>&xWP3B z%u9|Bijfv38ykV_l^J0|iMq->Xz22F;nocHndOUHT|!0+*8S%DbJlDWoxolFxNO{7 zT&2fkh}36Qi3(=@AEkhKXUlsK0jFEhrs&wj)|e6iFVh_|&t^_3mCtE7E%wT5=}QjS zW=B^BP>sz2KhjC3Mo3h11yQ~9lQruXnacgh1%gN zN7KGDF=XH|Y!EqWd73h4nqyUV_95qeUc22;Ks#WWaJGis$1$7*_eU z&I!#;eS0}uHg6gt>EdP(-q9@Iw}i#wL^LK`_)xN|$WyVb1x?j`BWj_!VmmSsF%`9! z1koX!mVc5y{xM_u|7D#y{^#lPFQeFDG@<6XKaQU}ZQ2A#8I|QX#rA=+`4Aw!NTz%* zI-~5JeD8ZGd&|-$Pn0~1DRiK+IIX}_V+xjUDC*s&J#$N(n^`od?cjZF>laYSFveOL zwyKf!+d+&uda($kImHCqEqp&Mr#G{=%#k^)WyqqgK+5)-xV{^0$}a< zcm88FF5P7ny|LS-L2ihjbO;fcMhn*s=jm~B@QgQ6poj$-0_n^bGE2h(j)f!36iV%k z(5V2M8nB;(MW;D0mAhfPJyh_31tC>_#^ACbZ8^T+b~vQlwPrVL+fJed^mU8h;~BGq z53-b_fSwgQZ0h?5xIztZfgh)1hOQ)dZ2_setJyivK++8Oa{CVx$@kV~tU<2*0vaa^ zd<{fD8*)AoE7W{qg{!C`^^IkfE}7k)c}?fc)tJ}P^j@p&VX&pQJU*9@Vz3rik*&~1Oos3OzUa_xL@WX0n@?kBOh&3 z7lZB{6}?n51|pp$haB7;yUVbX+bCE-5Ki_4UePg*fDcF@f&SPHQsbd#u(<0--o)D< zIR!Riu!+_aF8;9%x4(MY?=hWhf3)cj)xe1hyapt4>5V8ALf_#{hY8d$|4r0y<* za>AtJDZ)uJReo5KV6$O9K&%v0Hrxfjrqd{}{#MUUg6ruAkxE2Zj%av^2;gv1nD^Y6 zu&|7nRn=&?w}*OI4_gEGDoR7CE{)%uxs}-N^U8%uwwN&*Zj;b6m@m|^)HnZk0>yuY z|Nb|3XeuxmOgrAjci(-Ma;o3kS0bM|hEKjoNEuPUPot!EcT{!!Yzzz(XxhNE=L7)K zk?RZry!KbCwDGe!9L+a0PK&vCEvS5}D5ydCAU#1)tY^)@-Ke-(goAx zYdm+}J_|p{Td*a@rHp8*dfMMXt4swr2|ZaMQ3Mf(d`Ig_m`Wh=^$CurV7l-Tr9TK_ zw8pH*k>JY9makLv44dRUmSV3JIOU1ATh;W&uR*Bf4V{5<2(5r3)Y;0{!5j{k~ zG77lf!>blQCyfBFu<^?MovD}1v>8HoP~(X4K)X#gEc|Gs?ED#{KU&Q|A?hD{zs*Kj z*F5j0lvgzWQqh}S`OCk$jgph&i?7U`Z_NDBOJX{Vd9o#(2;^};<0wM2K>Fya;QK%R zMF`9^JpAflrw)uV+-#?}@zs-6fZ5nf ziCK_x2TNpZSkOoa@#LQ9JC3%IHGzD>fEP9pR3>;Z7J|DF5m?UOLmfnbbv@KD8N;vn zVT!($z`c9fP*-f40^0o9k-L!b#_bs<0lKY4oL~mXa;~ATv7H|ufexd_46L5Gyl08f zkWbYd+zJMvw3k8O25;=t`$>(~v?10&3-{A!i-7O8?Mk!(U79C$_e-98+$N~Q9rOpWpZUw<3_%>*(YdK>DpsFNz9?~5(0|gVrBo{4|D>7=I5ek=l$L1qy zel!K*`lGfUE_ak8i1h9?G*MU9LJzSU=efi9rMzX-z$RrZLA=1$5Nc3mWo7_PW=2^! zoNeAV;G1Dyo4W9?Ho#mdv^xS3Gt@1zmUCZtC|xjc1!c zP>}qJ-J%BiZK3_4E23X{&QPlu=@q>wFq1!|nQq{Lt2K<-0PB#+k=tHgNzJQfxg^tC zXn6gSnrF}QT^vYq$6@2}f!H8^mzti;EjI*LE;Gr5k6mptJyH8qOJOAAiWgfoYHBQtrg@B~T|ljDTGLy$A}3*z+vuQG(R6OTLcZ^} zE?MQcsTnG_utQgw1UfJ6buA1Xo)1 zkS56uO8qP|ZfHL3ze+d*9{Li4nsb_E2$Kq@tWxBYz|&3pClwTL;nMfkOgcYzGHls- z+1c>fDLOpJW&aYZ_j|rVF0C~UDp0R}Ip{xfFanx~ONechFB9tTd3kyPQDO@?%RDf^ z^>NAT*U?VGi-=p90&t_cPzRVYI0^FDU!%uOJ^v@qO@~wms6;GX1ge|2>Fv=1+%F~@WT3sVrV~qWL zqv1m|z9u1Fkb&YYRfHDETSV_yA7u@ZDmPtMceZzC(_4kQ>uJ?AisTpL5gk5V4@CT5kD+k&A(mwYFEE;vrzWIh!VXJiv3 zNg2ay)!Th)L5lKbkwjy_D3v-31Fxl~Io}$DK94H9h5h|AjkKwO>uCDHfcB|b;;w@Y z$uqXv2U6xCJ62P=xugNy%0l+0PQA_vi2+Pn%5jqq`Y^KYDFB5(zauML8n1@{V$W*D z)l-_Y-33`$xRi5EkWl#M2+Srr(!ku%^N^fD8*|XPfOwn3OGh^*g0M5&lu|tT_T_?4 zft@#1KKt6#6ux!)*8jm~(RNRRd-U1jWyPXlvvYk_t2#m_ShjU6oG14E6E6C{ABFw} z)UuPq!-sLUf&)k}gq+_X6)2G0{rLovfm+4Vu7mPT+{I)rc@gY<-uSMN7|ZHKdvWrT z?Qpe%A##$CMra2kFfyp=G9KrMIg=2S`9ug9%{3falqKU53v0?Ox^f+X#`{I#C>O-q znQQDzwg+*NU!v?0z}bJ7%p4-lIZ;9#vB!_NkTiG@OzG@&>~Tm@!TgKLrl>5Rho%`YmlxhxLT*q&X_=a*b_*eF$TxYaD&0v+v;+h zGU{WD38fn$WgC?*;Ne~UPK@V7){zQXp~axddq*v@pJhpv0d1)(B=If4wbb*MHZ^Ya zYbldqn(sFY&x-oDSX}0p&JU;ZU{3K1VfJWXt`luny6BJL$P@HwZ$6}wy=6L(T;v%S zOd#@Wc4P-(jR4wEOpcSS{NQ!Te3vhmcPk%Jee8Fd|3wjihlrdBw}Xg{Vb_(+XAe1T zP7}79{?7ZyBnh%n^t7H}VzZ!a?!yu8+Xk`X0sfnt*Fil4E)9}^^=S(~;0O-~r7SZy zBcnCZ#p#7=Sw>4=u8>!@70s3w?K^z_V;tpHxEt;>u=E))=erO2@tRjstL}x&O_Y&I zjHXJg>%!Q;Odq7nT0xi2&m{p5@8Lye(pF+Jq@@x5&EGd})Sx(bBy}q zJ|#IK3Tj%SkHj58T12Nw1Ik%U+&-PNA&5HLNO4VCjETt$-HMix2t5@Yu+q)J$Uc2}Tb%QKOjEH1}z<@o>BP#NzO2@tn1{k=y08rPkFsM!PU!6l)f5)|Q{{ zp|L*eMqhEIsv?b?Je-`N{M(K-lWFfhosTwd43f%2$;ADVdh-fl8Nznpw2w}vILsck z)XuZ<_pNB-sY(g`+mUL2nQHugdylo`aNUnD$a;h+)FXLmnT9Y2O$N4jmqc zh+G=#$8}UnR$gdWeohrv?!=&+&lJUM41nkKG za&!*mcew4SNy^$5)<;|V?ScL0(;)`au?=FmhqfWv5W?*j?Nl3=t^Dx*Nr`Rp?Qi69 zlGp}?%B4#bo5hn_vzXLtEX#FP9(Dy6+& zjlLV(a}3+hw9@tlRTX|*`$#i%X}KGwwLS9JwQhglPUOD*&0EC4;za&2f{}LCduvha z-J?zCs*VLlFl?1ItyQf(G{mYD%*<`1`*5%-T7enC#-9!Vu*to+v}82o+qcO zKU^vV>G<(y;`E>X_H_nplVNT)mdn)P3i1Z3RNi*Zk@Y0xqHVo{|sA8zjs=OuIqn4NBQR% z^|K1P3d9(38PDNYZ&T?$w2Be7ot&k2iKk*orDX{aI5AaX@cDTRuPWaWdKNAqh-%87 zNPO$F0dJUsNEh$EIxBWGj>M*HggfnXB!k{qTgi+AHsj(!mbAStAyyk*^$^j#P=1;! zNV`t)RcNLEo_AELi{@b#BqN!b)bE!c!td)dLh+9CgdBYXbW^$#az0u8+%l z@}H2<;riJ7#6vQ;Q(rSDX-w6hufgMtoOWS?_Tr*K*yk0s)uCrEJueVqbyhK|p<-R{ zp5kuq*5qru-BMcBX_Yl|l-?^~RLQ?+EZy;fXy>(n41=C0886nBu6yA%WgY$b#Fs@h z4&zhj$A0^MZ{*t71f!TS67ja=ej#&9X2*-sbxEQZO6ua zN}1~N5s|gVfXo|3!PJ;25KnfmW9YsC4I-KC@u;mcLQv5sOEJZ%+*t}~(@fGSiZ%b$ zmSjhv7cgh?469|;y!K*}YFrIx(L2FxF+wc7N=Y_2*V|#1Kh~QZA1azDWOE-r$#9(< z^7fC`W%$S6EI>wAIC%q3H{c<_zS-v8A}b%LL{1zky>C($3$h~3| z)wt&|nAt0wThq~F9^geD)j9%dx?Z+Y$)zAG+we^s_)%ecf}@Ij+1C#cj?^fROOCq5 z+k|gYa04J<*4g7x@7d$crD186pv{a;FgOgLP%G4#ypZR;99dYBrTOTU%QTcWH~G z?F&t#5XfY*Oo-& zThprfCVl^9+y7p^uaS`i(Q+h}AqDCcAW$uuz0! zr)hyP9d&(d(<+7dv1f3KT-ee+SP(E`Tqy*gn(6=AXP!)FrF_mQ$=jd$QTi<8pSrW> z9B!QcM$!Nd0wAPL^GNd01mT>JuSIcRj?AjCa#UOvo~QNlCQQwxG%I1MG(l2Q_U(G! zhIX4@6NH0%7vg!PeVoHi5&Z4!=>Kc~XcZ8PJoWB*G3tDH=5JOrjtI_y*F5b`2-3w% z#CQJ6$KW^P;B%wAcH)7=0Hi}_ppfNjS67%eSZJPA(?aII)7F0x+q2FvZkc(uGmP(- zwnRV}sELYpd+tdf6@{m6{CW83&uolx!p?2IwSPOg_+JI+;QfzPp@Jww#GcmEp>Ugyp?FU^XW&dBw0IyYDpy#E?Z)`=>&zbQ1n*E#i#DgI}Pod-LVz3j;j0O3}sp z#^^if3DB6Od{@h3I`1fmy&~lF(LM(bojU^?3q(jN$~_K`Z#>< z;meD7Ni-^YCm0ZoEEYm+ip^3G4;hJ4t}}sPTcQTUk|(t`(BxJYb+Kt;p}Y@G+m7T#$^x~z{FP|C-+4W=a=hRe+d zQ!u2Wj$c6fMeT>gbVMt$Sj}{j1o5`1&eAcSb->=eXjpmI{1hRUE}iiq3bdIBv{`KW zGJg6;#r`GF<@$t1&ZSA^=wjV&y`DWmij76-?s!@@vYY9dobox^B8=SBvOQ+BiGZb<0byh*8XL^(UQa?91&WPui&SB;-}zF9w;Y7fO+h=o~*}C zM3SxWemyO?GZN2}dj<=;cDr>}GP<}lAemA#+ISlGwTm}Fkc=8rL*)og$re_}siWJB zmznue`9o%~hm+*A<4rLBC}+i+4xMKR6;(MU6jj(+$i<_JrS!aapgF-FPU47pgz%DY ztsgRzFL(uwJAX~l5lmQlQnL)jfwxmZ!Dsgq5~*{56S!^DfY(Y<(JTaL-0M?ECt*G3 zm{XYE{;(3x3#>sQXQn`15CzmKeIGB5*Twh}|7jyI7nEX(r5c0i(75>YiEKsWy15O& zs$IEs48B^4e6=_~UJS3V)*#m{WbhFAX;KkT@o**Y|Lt}A_lBTW$Hdg7(!W1$krk0fntHeP;NiV)3xZ>Oo=_QvrG%J2p8f!&1I2Z0mmibO=L0*0rb z+}i<&6^u6^R=hrVfaL}t4wlcK)+t!5x^L9%VftZhj>oF}?h;E|Bo+FSDz6wNiLM^- zFDEC$kTJLDC6JIPIbv=c+D$;1LIbWjCw~;bI8`nPFpd0kzvYv1z4^f}D5awFtSl$g zip;CuDzh+@4}f>!qz2h;dce(rmuv^3bm0WEjjVO6rI0VkuYf-uc*8$Mg&-2YLq4cg zl7q~ki->dn!?EHaJ7=#GA)<3D9(dzWvqNekf;U8sMFnnPPs(PtpZi_<}-hP-Lm@2d-cZv= zsz0GlGXDz1Epm&+f$*KNU2u+;H?P^@caD5N{8o~T$H+{TlF+QdIY0%KmnP!weMc&G z7YVW4rka3A$pGdR>QYFf{{@!(8+-Kr6!ptId&fFpI&XV(X8113`S=t5@lybyWYqWR zA4f$8gCk7&y2Os3>iAHBc~H2ZqeF;YEzX{?{^7bpW5L@p`H=yTvW9){DfM~?`2K$m zUvv+Wy*qqNf^2=XjYj@-MS_O>KY8lZZ)p>DL!+#BoucAu%B{04@V0@D+h46|KcA#Gqf$@EQOhbea zt8Ztg>P-r+Ypnbb4FRDv5>@hN?EVYd{2S~01&$xb^ns^61?7BCE>wCiy0^3-(ReyD z^qv+3N8D)z@WbfG?ERa(&r}=Q@rt*kEPoiFP?8HyBJm^Kw4b;jFXM4V$o?r39ZmR) zMHh1h_`(h8A~#fg=b;Qi{V7E3 zPtDK?0zyhaRaSR7!}Tcf(#fZ5DqPpylWE$555Na5%=i*whnT)z{@)@N{aNv7w<*V& zsk2Cssd1nJl+X~AkRx`95kDm)r2!>8WWN^zwg7@Uj)*f~x_EgP-)X5hSx+wW$ILj; zg2OFhEOh4-#Ty5BepeQLFhY-QCdm9~_Z=-YGWw0lyP?q6m4~a4+H>PDsYRvFv2~^Y(cr z7?pl^kw5NUI5j6$)S2Vet$MN`+M%~0|J&>jSs`S97$thLRdqXGtL@u|dVu?$hteek zhbtXjd0q)WiGPS>^<1lMQg%{f%sx}uX-5|y5--UHKb%=d);|XUw=|$Niv@bAzqyN_ z|MdP6!04(yH@{31Vu>$L#Jm51L4R4{T@qN~(+B6J@LQo6$)A2K;F8YteNt?Tl8*qh zoOXCzWap_LNcv$Ur(ExWKrs-}1Bi%b%00yIS@k^$`p@X;WWA~Y#1PVP`H<|YRGUbY z21T;CyL^+7sFM@W)zay+JMdfZCl>GyV);znOme+MN?N%pf5uPmV!V@ndzC*h@Mv-b zSmV$eHlyG%v%V^RPqwst3na!IjcSGpAX>W+YY>CJ!>^Sl>}dfE$Va>7OJr1m2Dwb6 zRDuR^>{`AAcP<8ZuK%JOh2Nc5gDZ-peD}GI_l4wyK`G1!j? z!^Z8?2Y8`ZDS+ePfw$%q*;SBUw1NH8<(=Af^{3PM8+Ah6gZ=Ll%}IZSawf|VO1ktT ztm>vql8X$h-A6=X54c0eo8!>{o*_p1d&os(1Ei~%mfWYz!gc-PsA6<6U#g2m=J&VB zAZjvOk!#U{yswVJ$s&m94=9SydZYDzxTAe}T-*@v|K;OeC_^w-0BWj`6P;>0bO^r_ zjU^!o3LyL%Uqgrmch)|)heXN0p`iy{r6#QHagn1Ke$h%MJf%C0@Pb@kXsdiHM)7B8 zel)J7x(2|p-tcu}knOW*Fmg;^ZXJdfL3~ixl^;(L55vudbpan=Y?2B)M+!@(sJED= zM)2{=UEoidcz7uozg$KxKl{)_QG5>53&n|cmNXu{t#cbp@pF@U<0`Nx7o z3PlwWxkPSswWfNza^knGzid5Q7LLq)DH1;b^;4G7xo`@3!F?6@dGeou*B4<3f|oj6 z&;RODJ6R!nr{Y15-=pErD?qJ7Jkw=7`}r@s#!Hz@k|FlhSQQHYxoiB^JPnV>R#7F) z_=TH4Z!$Us%z*`V*%~j|{L6O!U%JGz@u*cGVQBWDW^00KpYRtP8ZV|rR)+&kp}g9( zP0LBT!L@h3Dd>!%SVTKg1h%%embtyPQY8{!$J51*G=7ao-`jsf)_p?Jtalr#&`+mm z7M$nMDJ|aUD|DD_;^{IEbOhFcY^eB7kml{lp4dEB4`br9CUR~wM{Q0SCqVyeVQ1XA z2(zm2s*7K?i%g+JZ5+LuUIHlw%-U{E(}{|gjf!-H&KUdm1iRlP2x89^6zXIdNvlIy zn`47rne{ELUAt0ow}J( zznbQ`Y35_I4MoI~dUW#v$8cX06`N@^%%3?|U79p}R>l3zCyZ@#KU6@5u8u>!x&YKv zdM4#(Em;m{oFwnTaHvLa09lCybA-Ku&x5yl*ZNn26lJbAxU^;^@%VmQ&S}#Qqv6!N zx#$uuxbdx`ISlHjE^d%R2^XgX9J5v3`k?6jjsWPAdKWOTZ>VOuce&JQWjwU1u>mS~ z)#hDdX*>Bw#i}k9_q>0PDja+5tKg>6RlW-4>r&8kW}Q=Wvc4`#M5G7WKC!b6UA;@W zNU@fm$@i2zexL+0q|L<0k=aALs6T%#bb4__0Idu6&U#>TxS^_PbIs>sVdFsD!`A0IS4nGJnNh>c>L|PrLvA>*?4!u-6q~-Z4n$g^ZDBUYRig!W zee-uJjy*==&-Do@bmoDc@dc>xHrfu}RRE1vabAzPY_K$BtB+;fZjszr$O6g$qn3KM ziV1Ie7ZVrgywG-bUGk>&^vbvv&F3L>pp5B}$B^o-;4W{GjR7j^Pi%Q6l)g=4Dp#Y6 zd+aRjQ-h}mRXGtUuQ8)*yTvioc3M87)Yg~b@W(XP+ZJ;*VO$<_z9&6MAe$mt>x{qj z!r3Bqf(gJNlO|YOT`xRsDqG{P3U#8pcJHofJE7zOtz~{J*=|s>mjL28iFM4NbibM} zm{}0VaXt$u?-QDVOh<=5*xjGE%@xRu-auupnM731ejmu(8Q04f2>BcjLC8PegrD{& z?Uz4OdfwM3Hc^os&_|GD;?rk{`a4?N;TI^8nT1){JozCBh8R63B~+l)js* zeB1q6X}d*LM~l-PX@^ZfYOI5$o1DtldoH>zO-*%!83-r-G<1RWS=EW{LE5rIxXA|% zZX*}Y+{g>bg_>J-38<~Mw!_kVn1)6#zRgk3En`RiHNS5zSQ^9Cp&AMerM_OHFbh_$ zvVsng9XnXru3x>Z6wuC~ecb)(ySxB7vnH&Sq_JL8^0wD2e%Oej-2`hM?raA7l^FXGJ5D7+mEw-ITkaEu>jug zaiB5bP^AxG9dey1FfO=$|5M-Qj8n+YFFnv;7ro3DgmdtUh9*S%kGB>O~C)!45FadkJqR+-{bFMod z$eLXH)_XBb`n_o?!Nr+bE@&J}?=d^1ksx$G4DZ5pCCvo172Hm6kQqsp(@sd6YMN+C zZr&~Y4Q=juM^u<_D1&9r*Ow$#$72<%<}g3tz6InfT;3RR{;}wVRHqWNJAt89bIEt_ zcxS$WhcwU6E*w0UshAE^1c;r_Fd7yqFgzgsvpN!aJtvM}ka$n2TL3!S6THvbHa_)BhIg^f8`lAv6~5v=Ekvk!w4#Wsz{ zyE~SL&&zOnyt0A*5W~C(Xo?+&QhARS&OuuGIMQzw_qHkCY>$bHgq{eeH@gF^X>W{$ zCe;#g^k`htJDFbqu39JL)P|j)PI{cf%30Fch=M90+SVNAi5e!U*a)Q7MF{Zr*w%1l zggvC)Q7_|kw+gO!4K2kPscY{YKB?Wse>O#54 z?=fuXsz?j_aC?upYFG&wSllMYL;=O$LcLSzg`?%b+z(9$zMgM1X!mBxtUH%G$TP-&=VC#FyX9)NK?yQ!!g7Fqamr5PFwmMAQ4N4yQ0^m7FjB!4qjrhM(L#<@`v>nQuC>#wE}n~3!> zTOH=}1czwwYPe#*z&ke$!}c+cz#xAz;CMV~8QG#r)%{OF^%R8zS8wK73dxT`u85NTkLCJU8@TB>MSu z%NMU6Znz^>hZ#Rf&x3iaQn&i)sFl?GNL`KCd}c@qJJK7#U}&IU)-4e&Xo~@2FQcWW zW@#lnYzxJ}0Z_X-SH&QUz*j}8xm491E>kDXr|Ly_Vr7L_4PFdxM{Tc0O^webKLFap zWWndjsfDbY&}ku~7%z1l*+9)`@^md=C3h^F&S%bR)KjA(+p8_1ewUWP#hl#i-z`#I zUk0W2(nufP!|c_q<+7;K)jBI99UL^N2_#1gz4_3$|CZbCW7a4aFc#rCM?HiZY?y2L z8t%kejAPRQk-i}bY3mw{BFG1^&t!^Kz7PP7HH#Ou@JqOTzmURf{a`F5flfzE<&40H zJ#=q}*d+!ZA9|}!-%>MaAwR&+ss;R)OXC~!4QXxJZWh^^Ru|ccTXYs))5bx6yoeC4 z`XqK;Qb0a>CB@3!tU|4~$-P3vH*)?8xzNR73UZMR>otJHFK1v|Qyb7h(+q4XPZ5N5 zeA-9Q)}; zl-m5idSI8Ptvm%b*UPqDv5BGUb2UDaoCR+-sx-3nBFG}nj`CL|yCg2Kg^(cg5@*vo z=blMQGoY`8Luz*vx*ZieZX0&|=@D>wqhcpvHyL7H#4mfu%_ob|; zI)4O_BM#?9nsV1;!VhdJ@2YJxLbP2>bC^!n`@NX-J9^_)BL#G=Q3RL94ldu!dp)XY zHT8_aUuq7bhjXn1JoY*#p>Hi;JiVlGTAyM4rpw5Ms1B2P(N1a_mhF}_kKj$)6JpeQ zGYCnW1)z*8yVTasi^~ak7|L6%!-<4FD2T1o=*7LbXg5;pQkNz29CS$_MW;;Ij8-{! z8K}P~mX&s4kfy?TpqV(!R>gpjb6WU}eiGq2fL+%YK;jM17@Zkr!bbV}vff;9o-giA z!I28;r*4cAX|I&i#xEC-3N6NmD!T?G&1O`FxPt>r>4Mjy?+@R2Mf+e`P_!i7_RZ1= zy00-hs9Y(sm?#^EbR-$)S`U=lLpQHMm6dB(R}&a4l~NT8W>)B6n)Ku8WI>28T0-~o zwMnEpK`?p7N4P2%8blIkq1#K18V8`B(^W=k!~gvP0*&8E=iF*|R^)eMY%$kZWW6IX zFOQd6XcUmtA=%;kCuNTnV4G<4wIJ5)IVXZWBl|rLyOH;G(;Q^Fp$OPRvpgaJP$xim zdRk>-IWOH0sh^+Qx;H32xw?4k6ZM9NKY29cP9EWo5uXyni|wyc-D3OnlBd2jK!N zV5AMju>h{CZmY=_b7cM`cdK}cLtT`#bPDZFbGk62GJP_8&rM2p8X+*Jk964 z7e0`Cob5_mEIVqMf4m2njXSV+GFr}a^l(Aq>l=PgVc$)=eeC-mVBY-pv*agdot0`+ zyIIM&$_DmXNpQK3OR{kVv~Pd$77juvQ?p^hecW3&Z7Uc~chr4jG0jstStSMIBhf~B zRtLyX#=-a1((@IEu9?KlN{}dWrzSWYd8fgZ%G99byNRvW07evS;|#fhs{g3)+Q&JMt^|GcToWz3$fgRTN(Xx$-W2s zk?>)mVSA%NIVx(@V8d<)bdNhZN^U$q>Cp(mh;|v$5xJ?p?W<#U%>K*e4y?>eyWVpl zTlq+8!5;D#Eult^TRPS;f!mBQ?eeoK((8$EB3GR zZ49LJT{~*DUuXjWAh$X{ywWr4;pDSp5lC!8srBe>GX@!DQ}YIM3yU^4H?eX{Ku!_~ zB$z9<>rTZ(blAN%tHbFL2v9|O#mHQF_3JC>L--l-wmw^AzqLA=@b(~sMx3kT zYs~hE4F~;J|SjIvcMuN8Y*B;usegck9DHqO7Lz<5A9bC08s{?GChL#kSji zkJ}JHhDnJ%Ykx)@rfirqmk(!fBZsFf9Dc!ArVva(*3coVCjz;L%o1Ikk;8Lwb!`TM zWOXd2TJhR0j3*Kzkn~m2#B7qx=G{JO2ZX^mteW9BmJjqA_n#t0xo~I;z;fW$bl(<- zncdoWLIQ#^xVv<0Kc7}JT{l63v3S$w_xlTCL(5I7s z$!h)&r~CdYB?3>HxZ5-X_X(6Ee3O<1td2Paf{;wa9#}OwTub63?wLUcmS7z6J4YL_ zH*HH!@+aEdTAXMZ-^s9oa?EL+H|ypfwr{kfZ(2>ihgrM>8J9>H6!}Gu!-tRkyxbOm zfs0ze(c1A|tVWIY!rFFU%}YXTfFMqSe>B_<6W0%@Z5YWCZft9tO#z<{i~EdSR88s( z0vX+BU3FSsn6;%!%TEjD-d4^W3fV8uT}>tUz#w>(UbE0LL93> z#L6ZqPJoql8>B>Xt+6t*KzJ;JBotdYec0OyM?pkxfM{y8&b$efxd9PM|I^Z{&`G+c z>k;>ml+w7Q&YD8&30&c`F1ksTui@gZ9)=Ha=afE!B$9~n?mntF1FXvD zzI%z)0rW^dZ&C=CTIQ)D`m#fhboNsfz?t?*_N}$%WpXYj`t4!0=`6{jVh!fVEDIQ`8+S zaU8V6CW=iq%F>=2ZH&wWG)i?Gr_j0zTsRVtY+5zR^BZ@ON8=y`9A7>7iKZ_W$1$jr^88cj6%)cvKrbN+CuUPn_ro>f<^WiBE&w9p zLRb{e+`u6W4pQ$I~{tJH1|}XjIeKg!?<-{LXdZ z=pHD|^LfkpJ`z(&NAPoB>laLV8B)62bt*%Aty)}IAuA0-Mu-hQvV{NJ5&ALffmhWm z{Ti&<8(iBsE>mFEP34QI))ijwXyDxaVE69b&EpACktf*cHM-ZKb)R2o=x~Ds#c(Oy zRWpR8nlmEj^U~a~Y&Dw!ylqGI7QR!DKo;C(32A5#fsJQnBMDjMWEDfcCM3&?okJ+1 z#~~4ZM#Pg{U?^f9d9!dTosVEYdTg~Hk}~{V0fN?dx;?iy&wM(j+@?smluxkNn+69%4gj0peRw%lqnY4RY1q8%X;IC^db0jeEoOgEHMCHI}M zKtNDDiGNiZ@k1-*0b?~}e?^+I01QF+BXB6aXj~t@0GSY;eS7yBQ0GdPO|bB;bjfTG z_9O2P&^j-P9k-dyGppC9TM3jEex)1;zotvP`0ZdDGx9p2Qcw&ToB?sA(qbaaQKROk z|CDF=+t-P)vuGeP=78as(gk7;2!%iOdp0Yd+rtPAhXPTt_o({G_nV<@#Z173n}vH} zUUoi^Z}65MB^)8V0I+afF%3mj1OU>_%WNSJ`-ckxYsB7k2v0XXXk%* z4OkwZtE6hadVR+>k$)q7rdfD>DiG@(d*sfAJP6&%XxJrSMnuD>W5IpNn|E~Q>v&}$ z%r}2gXEAceY%zjOPCGepE*v2nQSkKiTmy=O=fM$ux06#oHEYed4v}+VI@8p$n8?x* zB09A_tKD?i{FolQ`g31xn;j-w4Y{7c4AeJ(#Dv*$bV^OU@joF(lx{a6#f1MC8*_O^ zKEj@q?!nb($$rISkWBvaym;)+$g$O-Z$4fw!*;+5HaS(sV%?Ldalb#7(diOGHUkf2 znK(CrPd!U0X91?^SNH>nKiBy5IjX=4&$i&7A7@I0HrG>Hcbf4lhW(kN@tfm z|H=RUwx0+1kHh=_@=2m9(74z%4})5DJDCon+U3G$|JXLZe+WGZ^%BQaf4QC{dI)Nz z1Uw$^{RK1kM_RUy0_q<8leO`8>wnZ!e3^r~2fDJWyYTA4{fi`^N1%F$w@Nwfr(XW8 zHU3BTc%IOe6pE2T^uVOcFMfq>4ZA{$suN#mJTVM?EI^g=dmKwuRz^ z#cFCE`Gr@)?>`vr3TrJg=BUH3qVq4cEd}s{b~dKJToW_{0x%nybCL2_PiGj~=Nl^H z&i!Q#cwK3sI8-mWwl+!q^DX_)D)Lkk6)i{`f|M8jLn!e1!x%71;AYe$FZ>F8LVm@6 zFY!Mf?|(1xKMUc1HSs^`@_#k)KPnW-f6eKCazOv}iGP7l|ChJ@uTT6>b{PF%VDq08 z>c7C|KLg1Bmx0ZS=Tzr$7s_v)5)=)mh*-`p~z5&xQSymHh3^>B3QNZ$CcvA>zjf zsi_|4Zl$5^syW#QK{RSNg0N;#$JwZjc}^2&kZ9g0maTr`8Hd0%778`d*TFP#Yq_)( zqB?FIIzg{t^dgpbmaT20KHPN#NO9LAiwbAk=1fLv`PvHV9U@0-3pVG3qs}Zi8JuS# zI)K_i@XP-}6Ee#^LOG)aMvt8(Bw_U0@vHvqJF|oBhU?;gDFO34w)T?D1$i#qS&%0U=g%=kFYr7m9!y3bIkMg5G|=eA39C53r0L z`K=Wbk-pu5#u2&<@|pj#j383wh2m@btrauMp@@NE_X}l%-pOB=F8B!|Ou??-6m3>W z8hAzC*?;uc^#Xo^QLdi*w^mF-B?l5hyztSk{kMc<2ii9Ew^odb_96x5h-l`>3bEf( z3{d5L|5auDF75xHs{DL^5G|{cNdm>@=5n8aMc>q+-;&hoG|=n$$yeC?-&1XXC_%;q zA8-GbEn_SLEon$IelaBaTUwF{rXiB}K=N<-4)g|CCBd>tHGi_-J2_xfai&4N`#lB6 zC4(2(a53WJ{JoPS1H-1aBjd>TrK5Y)`k_F;u|g-~*0qJRm=G>2q-u6yKJ`+p4N}X& z@90;cSzxOYcOzKKCjZHvU)XTF1$d3w+ysC0A70~t&U^2Sd&hBiIGkFZF2PGU)xJRQ zs+4oQx4*I!zihxyW&d`N1m{z!0|LpHS>LAUejkaT+^ngU##o#dUkc+RUY2q*Nqdy& zB-*T9x4^m7dh`|lif=3`LZSBrZG!^ly01I3DZPD4&8d0ENyO@jC-WXCXY7Q*HdIct z72f1|lroo&&+ya4b4Jfj!^A0lD0-~<@);TP2GfyM9%9*1(b0Q`#a;@UwC39k3GOj5 z=AO}Qv42p{EFDU9%&68a-J+IM_?KLQ0jr!YPyvbgW%ic13Os=_7xlTp$_o$lu&Eqp zt@}~5j!I)0_Tx>?Jnamg8;#I2MC;@UV|W~c-G@>Q^e}{7xfWTm0mA$G0rR5(F8>ww+kg zM$2B^8vN`{Iujdr9=h3>F3i< z&%ILPpue@K#A&*-#bs&gFx_WNFxxbYV6r9O3l&35g@SoTchNY<=}xtr+bx=HNp5}7 zmv?qSgMh7>YLS!(sXd!S^7VM<&9e_nx1s3&vCG)8Zm9d5v9sKb;VK+E(FLQD?RB8& zAr+bE*VwRX2;+fDZQ5=>AfA<^bhw~#bO_OJUjl%BkMM$)zpB+UHxjyb!#2BUot0; zxSl4XZHa}*`G%jaxULx|mdD)NO^c7eOsaY>k~6(;*g57yyw1^OXu(7CE~bLb=Vlcb+(+Yq>jLk#7u&=RH+m zNN}F1WxyyjpM*>&?z;?SX5Cb+f+pAZYulm zq|8m|2r8IEw1MAY!T=fe3*`$OHRs~9K0p=K0O4?F6%3#rbj-QHKuGHSc{5{{H3ny0 zIitHnLtmLhgS^v(RW&u-b(u>&)$oJQ@JRSgsPEPlcMqY3{! zGN{ZSlcOf=%Qg+P=+19cigAPP-p3zQ97bt%+|C)Py$0G;%Fk5ASklqfxAGNKUMVp1 zw*_t&i4u zgF;BRB`McToAtj(4~z&*XKl&UhH}>^clI7GV|~UVHFLS?aPHE2^^}k{qd#h+)Q9DhL*-_J0CK;dXO)Z1Y()6gwmdq6@lr^jaKQ%7P# zRu}P%vMhdFw+ODn_L0z%?{Fv)`{l`4}Ywz#Gy2xEYFEzzU7Nt8!kIO{#yh7vN zl@HT1B@kHl$+FJQmG=#hVmqdJHDffpcrClzVS-@vlw+`=D|pR=jlTjmswvT$`+|(Q z2Zffqo8eGKS=TN5N`>?c`>aKXPs?!ziasDM@YK6!Ak0P;`c~cceAn!Li_SbNE9h^r zrk-8wznhxa9~6ixD66;Me^46Av15~EQWgV5w(_~nTG{2B_tvzOZGqpoRKZ_{YC{=K zx)*&bTC1yf!dOMsatDGYpO5!of85(5{wpS=~bZijTN$;XPR&GwdPQ9dA$3}!VX#q>f|mt_akgj*mJ*1I0uErizGEEud= za3h6Q`%a8Ilv%aHJl?h~82)8*{Oh+*lWl2v^g=HAmB4eLmCav70|iV+9t{ki{E#$hC4gRKRwcpp@bZcYPQXa+tzq?iG^P$CZ3;IRUKiu10vdYGVs zOg7@p+7HQZ-tri-X?4ZoaR}R{}f7j*PWu#3BtWmajZt3AyHyWwS~p>c8+MtPfSXc5^h< zb$Nz{E2zME-Ut~cDBZU^ViwF=(X@TfF(~Su2~QTyEhUHY>KyG7mjX;DwAYrJC74}k zw|ac=v}Ss5%Est5z3!MR-kmV9^)~b^I!d;Tf-^VKDzwkYG_2Vbrt!_y76zOak@oP% zpHYl&e{(&sd$iPjT`aHT71}nU3K>GKb@HUR{}cZ+mtxnI!C{A|l*$g6sU6qV@-5^Y zD}6m5J$YRIX$j2zo)Ji$%r>eEZPYq_;f2Z27pY5fsdnyw$24Gg=Jab{PTQ2KCx`XP zXgTc(R$g5&E}TzX&&e$relE#=$8)Yn_T||RUzfmP+m@khph>X^I5)M6^Q{K!m%9t> zq_>5)R`l7R>9fevK??@aeL||#O}Zi|SQ-GCCfiq$iH=^ZrV|=FWjyG2Ic(&jRK(bd zU8hxX1!nx*N3ycfhnI^pGWY%h)~;?Kbf#z72B$IQwR699JSiqg)K*2YkPzFMC+Y=0 z+WX9@Eo1^B9R|kB(WG3RW+P!Efk{o0ZCg-r`(%&rx{UohxG*P%O-GN2@|;d9BSpU> z+?wrtHzU*rY>1@-Up?G=_!f9>dui%U<$Oh?Pa>F22&~(4k0KI>^8Z~Bd2S&rH#PF4}nCc}Bg0-szHVlla-#k~o4e+?H z**j@d4U+E~-6WVCpcKk}z5CYPzDEf;OCN#Ip%}9fxI};F zbmp|3*vI8xlcZ)UhG(KraLtu}aX!o{JGwPQoHKOzj;~VR5Z%4C=lbWH!Gvg4u2^^3 z-|wqf@4xU1dDy2>M7WD`sY(5$8n+eBX22k->uT|Aa;Xt)nklG!(Wgn8+HWMD?bTN} z+vQ!SOnnEiie0dS-9Z-Bs0U=Uj(Ud5&MLv$CCN8(S=HBlnv;yCl5Nd&2T%o~qx+6r zOh*PpQ5{l=Gh-lBW>gP;yZI*lZIS`olcQX6?!Z8CZ8@2=L%+&@F-)vax#XsDg7TS; zS!S(GF{dN1Itm?<$%>O+I~LDPUg9-x?=Q#Pj*Xr$4AG*Q7H#F%Dcn_op0%szt5SAI zlZ&}aCeOAp%jU$^pB$YQrLZTcMru@)`X%+vmIBpa3z5Xo!cUIfHdO^X@(Ejzc94rf zLi(Tf0s%sWn;q@a7O)n)128oc+IVifQ@#)2ze_fznDyN{=|<;Gy&h;6DDY5<(H_-6 z_am-c*-ty?c5pJxBj5Jp?Hv=uCPmMJ9~jAP<{_nDzrMamoze#0zWlwgRH}QTr@QB3 zi9E5nG{3f1!qz&&-k^1l&7!*X=QEUB$7wvbo6>f?Wl&o)9s1bBZS4WlK3P!B=cA#x%!zKjKI3;ZRe*BFPX%Z@O7X~I|A53|<31=Fb z#Kn9e?tX|XBRQ_3kJxeeR_wLIt?>dPZtiWMQk?*_DU?8_cBe;OO2O0)j zGxWuIqgs4fQOT?&^b&G?Tl`-fQ`B>ORQ%xfmbKCalDD!wfY$(tMGN9zDUW!JyfU8p zu$#et8c>%368p|?RjLlQj&5zPYe3&}H!Fkx(zHS9jzu6WptajvT@+xGo7uVAQ{=?E z**x4}nEt$2#ED;+&iGE#JqB?)hoJd3XSqs8A=J|`7X|B^qcY3KXTzp$mai{Gfef?> zjlxjGg|I?z@`Y&Bx0r*p z$0LBYU5rCn6@T_F&#_#Z>Trbi)_kLmJwe8;p3r!$$^AW$Qa(l=a5+Z*fujRwc^fL< zVr3pG@^A4+ks3`NnG4Kov{f$QgoKPv@VM(YfrF(?caJ977MJ((^XN1R=4)5I(alJm z)V;*NLf5?L470~i36@P*853a~$obc&vb5xMT3V1^T_=+{LhROh$XIbA7jgFH;vD$D z_V?(Wl)EsCHG60^t?sBC zjf`|WN5XiT=Z4%)i>L*3Z5bFyclsVR@?!D`6dyV7SN1Gc1~Rk_96=K(t-f_-YFG{yw6q7!YHvg%&hU#HwY7zo8^{@7HQsLN z%>yr3ylxe2FI5csX)SV17#3J)@<617b=G1oDh0{%(Q7k}3jDx@a=c@NM=#Yif;L1K z;7(jcfiBQf&~Ltyb*YTe=5a}&0iLRGUFbtF z<_uOA<_jKG?-KmjQ+|VfP;r?uK-kV;xAV{H<}|{`IbS-}8SHjQ3(Mq9CrIk>Qx)iX z()F!rwQ95Z()8Rskt_3j=r7-TvbgXpj-Uyj{o;{=DbT^F<|$*;;;?I zfN)L5?qh0JV0K@8hp@PjF~LaR&^Z5I{H6Ah^NqE~5a0Rdvtsr=~ z8QI7A&ftMJ$SKj~$lFHvnJv(CT(q6>Se*NwxzFq4IV|XfkCiS2d3`BS{1#@cGujN8 z@KBIY{oMP(1sjF51j#Z@;w5J_0l_SC9&iHl;sJ_6E4etm@t{rV3di*_Q?*A`J7)oi znfp>004|1w4hBX)Eo0^k&MB#Ylerx)-{Xt0e;8K=a)A-(pq=XQJjjrL?3{*$f5sEc;qQysm_jj&};`@Q}>@isdJ(>xad5k;1 zntH$2@>F{|L`!$doKMl(?)*Ya#zqrGqok!oB7e4GgtpQjiLxG*jB<=eLpW$Fs(N46 zfN-1peSoJv1IkG8q$aTbk%IQxXYNrslLnaU2>$X~ZUp)A4-wj7x2#2<-HxX`(UMJTV0gVTfOV6(%7j61kY z?PJ-QB7-+SJP6jR*1KRWG-pIf@gChHbj8)mo1bnn;t45Nh|wsi$w|#kckO<{p{5mf zct5}O_Tc(^-F0&34ram`nUg@}CPElC_$=K$^>|6o`ki`c5Yn=vn+C)L&94|Sc#J}M zsiao2VXBI;fW-+v1FN3xeHML3C^0Ogdq-9M?`Yd+M=y~LpI6Q_5N<||!+llqICg_x}A(n1n~ z@zW`th6)qlHM+G40tI)vrmDZ}v(X=MMH|CNRjax;CwjoZQFx|V2jMWcWs0H(B< zbVm#9*0#9=OI101%%wXsChJAcGl{wM)`S$_nR9v<+H@-aL1~o1I{~y~HR~ho<&Ft} zDqS!@^?Yih0dc8EpqV{g*n2pD>i+hmX7=rTEKNsbU5phG$l;^D6$}hy`GZ%Pa|x2dU@z$q#HKOBzm1U)VIJO2F3WiU}bB z1wh`ps!ANM9JtYZmZID6pfJdID@u!LpaxI?@bl4Efx^lz?U#U@ajnogha1h-kmF+KwR(6-d84IqC9UKIB zT%lfuwpXJRw)oN7t1$pVdI&TJ1uSg40XEKK`;MVMXF&}7ECGtmZPSvXI*=S2CmgD_ zrcok+RtM1UheBmFiM7msIF)e?A=oLiK&i(=RA9!i&Qx!{EHta7bl2Nh>kS<_T{TcJ z0*ZsPgJ>{k7Ydp_8#ke8G!q~h!NV>KIxJyiL`#w)^y&PgU@KrYa#V9mfJ)7fHV|dP z-s02vl84Ca1lM23<2={M%%cMqFdW|+$o&y{H^YyXykc#v>?kxT^~#wxI=Y;Cvlu>q zb-#hH`efXbIZ)@wRUr9ydd8a+KEt=(c(O1XL@N;%=S#V6uw?Yz^27!}+Xp}zL!@gV z*;zn5G6BsOOa($`vm($nP};c{Q?(+wvyzrjlPDOlnA%9aBAWdWty^-I$-}MvQX))@ ze@eT+Id@B+9My-EfCGd>Gu-*tqwUn=+~dBfW!?*;(wC>Q@-j1_AW3nvP6EHRhyScG zok}EL0l3vz)i*W!90%ey7^b*gG%0yiWcuYF-vV1l&OJyS3_aa#`KZDBPjo02qlTWT z2k}l3sN$8LCTIRs#WWcJsvfyHI*o=z*j1PE%fK4m|{O z_xs+lN7N*nG8+a!&f{)CkiGoke4X~N*+45won)}yz1hMW&3EL|4-PlELk3Ys2J7?D zcT&ya;>9$~*f#12B(^4Ss{vMUU6ONu!L!C{8Lo4V9X^ZQwbjm}aNdx&piJ_>aXa*~ zZMOlEmRs2%8K>qW#ZTfcg~}?0Sbb(?@`*r%tUyRL_v5Rr zOdL(}J0k{Q{(NpVA?imlV(Z3Zo54 zdsEalm-0Y?S$XD5+F-_wkqJ;m(Tr3#ZZ@s-ESL8{S-Re;svh~jhEuWC&0 z3oCnh1kj_jmFo2kREMGsQq9>Mg69N$4N?`1kHGCX8z6oZIl+fL&%QR`_zXECWxF*> zcFf>JE|-HGpq&<^#?W{cD{V0c#M!z&?u|n81k(f(dSE2T-6Kas4$V0PxT#*g=(}vM za3?tZwSeTIqt8~=paVtC212n~mk9Ie_Y=(zmv7S)@~rFUC{BX*Id|r`E=m3a?PsQf z&oRT|?ofO$*a1BRPyC@__SH;>3Y*45u+un^6{47K%KA%#V3@%d!nous$@6GNEK;ZI zdTy0ZaX3w~Oa-gPFWxG3d*gRfC$~RqVF1w_1l6l63MV;s7{nM>&fbU$46JzP@KN40 zMG$y~TBFk#^O8Jnk8SloQzsB7X)hNK1|ltSUX+=h=`kXVM9DlTd5-~bwCzf^xj4Z{ zh<8_m90r+iFmA5vYZUhg0^~1f90L>{AL*zp{9%UCNDZt|GY0k2F8gAO?f|$qNsdL< zWHW@6m>Pu0;5QeC9gLou$Gnj{_a+;0FvW;tad=!6NGTrcM9A?RzVN*T2wPg^Pn8w5 z<{$(z4ZcG@P{HcXvrZ6L% zKIM8RV^-7z1^QO@G4{b^f#}svU}DYc!Tcn-=cD1&U{_V+RnXyz1p_I@o!Vkn^o zq>;E3Irw}fr?k26cG~{Luoo7oy6Q?u5;^M){@J3=sQaT`Q{1C7ccNVfrZovKH@a5R zGrL2Dz2eTTm4ehudH6ZId8yTL3r1I=(btD(1kV%XzWN2Srp@h1tgEjqq;y48r|Rz6 z^6BmzxQO!GQOQW7f$%l<)~3kbnAwzc&I6nJpYI5$CyuB-NL3*|a^v|!!zZM(a>uX7 z)*4nByT=_;%|{kMoOlL)oXdiMwoq{+BE$X2BHisWAn z}%9C!urg~*tWAjLa=E#EU8|8MjZX@Wb zGIa2Y-e>RoT$v1q1?bhYGMQmSuvf)W2me3=yzR2C2=;;1himQIUf^qPSw6t0ZL>~R zWJe`Kft7S5Rg`awWUhirpG`?05NEOp7gC6LnlYC{mt1|#9Qi=Z;-*ZVMa^aFp4A79G$t4DT3h| zsyr7DBB6n8f2eO^$#?%pqQ5bOv0eTillum8)I{VP4H{wtRPiXl4u%O0_7u8<(rjW2 zaekzlcy>ET2GzroOnPR2h3|?3$)Jg`U+dT-;D*C3JlZucHm-~Ss00RYikB6uR+3T% zI#0w!VEp=l{cysRubMWk2n^n;&6Zco#X*TmFxAXvz|nD#GAr6E-+BRaXF4+uJq@zo zrg_&t%qqEeaOfU?@I?i{z-3%fv&(>#`dsi_XxOjkETx|`(7PHsJ^v^Hb+49Q{b!&! zI0@zF2ojQaikqI~dI(MmLE6I%=+Wuz#(|h79-0=@ugx|AP=odfxnppYL!OO)z{oGCVgyOzKt?31av{^eZ|F$$AP5!-e3}+p$>Sw3;sZ`DWM= zS@Z9{0O+Rr>t`EZtXAAjRRPmGa^bkim1JXp;RD`EO}H7M1;L=$X}&u6lKQar)545t zw(f^(9wlgH=M1ZoB#-9pGzrScMm*B!UI7&lGeT@pCp*t}-D#C#cew&Y_X$-bt_eXY z)2=)3&rh%Ff!`vX{|wtPaqio<2W$s+a~AS*Gz77E5Kpb7z7_eSKI>fZ)?j}|#D_aE z5+&Wj%-`W=acpx3lR7uSsTlyu^rJog005WXEtGNOwU=H1h=%iD%`WiD92Wo6wPZ59 zGR0Xy$FLDOt{S`Zpth0bD#)XXMOM_1EPn-^;6?Lm>52a5Qmsk?N8^KkL_LktPI6sS z&0yR)n9}+b?-cp#+r4|-4loO!wLk&VItQd#z5y;!|BL;nMhmRkspsFA1uW)PPPM05 z1e507)X4WV1iFD(;Z$+*KJm!Ls%tyFB-w%+_lm_m18LJdWfZln2;Shzv0+Mz%07L8u=yZ{H+ki!u1Xblh&-3>ztibo=?b}j| zg)ckygk{b@voU`08-MbjJ9kn$%ixDfjqk-HK9C zvdw(NJ{nxsEIt||YjJ*8c zW9E7x!6_=;QXJp?>uJ&dv9aTzMolg%B+)~m_c}R2m<6m3xd#59}5wL1WHaa}) zzr)1&YnwVG!N!+YEdLJa_OCab%L9CJSf5_?_rIR_8=nG}w<*CGneyME;r?|dH($SgamDa`^8prsJMXXTO8z9s}}RsDO-N*Q*Nd-Rn&LkFgdu{H_>78%om7UgD-!9**xXcp2ub#66 zQo$+7C%yib5nH#=_^_xh4|NMILB_u`1ndbE9~pG3(CBOo7|wddVs}E!@h#0~h@3 zMFMu59nOlTJ@}MMF-q4@IR@_tSTw-14js`wA!*`m z;Qo7N7XplaYiveL(BBI-@(zJEpu+u9|Js4S_W2h+1^Ax>{tl92$jkK$-~O*(sWSzk ze9z5-zwG$$d|LbrY`(vbsiOGbTRF$=HXxz)9sl*ce&H?ZVz+z0CZ0~D{=Jp^-%bC8 zMgDKT^1qw@|IbYix=1SjmlnYPRk!|~U~dH+h_=&7+rDmG;p2q1KyQ(A;($8YDI;Z< z*Dr_S8Wo@;vVvbbEzs|-+J<;_N~AK&8+5uDhfI_*0xzbK@<8pp8C2K~X8HC%EoF>c zKmfaQ{KNN6&|>vkMhy!%+WkJw31Uvzteh`;rRg~)<^4EM8i1z;Y>b3N#`f$fQ+-rM z^zRIZPKVxAD=MeAY-|a8^%Sm8i=noAMx3|USFvEAjf%%<{~Lcr*8jt&0E??Y+*A7s zHcBJWMgylob#6wdXacmzqGyQ2jak1 zg~h0=A*eV5ASzZs#&U2KK<*Zg@9o{eCX?$$F=dQJst>~Mka9NVzveB1M-U&yfTl2r z>aC?=cKX2UYDg|~l|-6sO2!W)DT#{H+P+1t;2vq|`e2Xvi|~0E6(d$sdz|4=PpuTE zRDU~*M4lN!evJB-Q)~hB36okT8QFTM^w)E(0Bexjils$2I$L@p;#4==f{yq)w=eD} zrLj0KohKOxmMJ(Vc$Ooy&J4uoPZqxd4GTEQT@B2*{(|x@C_W8R_9%aEK=Rq)Ae4ZF zVF4_o0^3>h7A*|aVryo)GDmQt7v`TSV;_upP$XIzD`db@Na-Z^@xp>uB?-hz{d2fOWFv%Sg(<`6pNqayv z5-)0I2LYaa(YDkvRb~BD6Y)JzXe2}p{j#gP6$)0&tMCMg3)(x+!EOj?^cqq^fflcymP_+4+JzMb)dlf6<^ zB>CbedZL>u+z!;;@43*va9Xy)y8rZydd!3gMMjK}RTI@Dmq-LphM&!ATbL=0?IWe@ zGju$a)qApAg8L`NH=66$0tX_FFy9k<$c74Ye6jvcel&|F(QD`mv9Y(^yztb`kFRCE zl~OCjvL;Q2@cDDc6M5Xm4(FHLP59+3KDTju(Z}=_+k*Id=LJ(>emo)H6D}(e02{?D z%OV4<${5Ub3ZDsAxY-{zO)~+Fm!}>rXU(k;EDN`Z3mrP@&zSnEl*?Hr`R_gFy82cD zKP9dL&)Se=(3|2{0kKspkRmw;hVvDm5Sx{(wK2W{&_j{!8=lZpah)^k;Y8Pz7q{T~ z+>vS&mWF2xg_}rrL22c$Zw~0^_$GKx_142H$kbA&^+gouW!&gVcOI}8QqOmrxFfVW zm=6$sRG*fpwAX7LwTXjS=Ib|RyVj3xcgn%_=oJX2Z|Qov(4c=PZ}J>yT*wDBnSNku z!RsJg8$FxuC#Ki|W5UmGS~lB)c>Ba+B*KJ;K7d3j1HLIyo;4AHfSIl@axx;13BlnoW;vcrCC#M>udoJgB=ygYRC3PZ)sA6)hl$5R6vJS_#1 zXs6gmTidNWd-!Wg5rAY$ZF#xbg*_`g0C=8G&4Bqk*;^p(EmcrD;~*qZA`b@FSwWC1 zG0GXW`bs_FU(M(uhDA;$&IghomK^#hq-E<{v)tqtFL z?pMlOC!>l$iUnwQD>;eP!mPm^j;PqnpK9cVqIStDoDV9jVnviQqe z3i!f5FUb-5x0x$L!HhCc5vC_I=d1ZB)jvgxDQd<0nP z@=r&V<&_R70jYAAH2719*5+(oUHQkr5hq4oN%oh7npZKW^Q*EI zmS>(OD!Al@@U#}b7c6jV&dbLDCWk34(IpQ+_`RciYOVvb3FjfK;|dtT@P)TpoHx`R z&x+Asc=&5Q*(JC!j1QV8Mim0w(3se{iiO_6w*<&J%eI~+R-zAD%9lY9o?7-o6d;8jk| z3cFg<*&b)9P1W6bv%Om73n6eY1cs`d?)YW#rI;p*+Uh~?ev+^0%m>< zlJ>!A1?3{E9i+(q!r2?Jn3Co4tbG7Q58vC9g6Hsh%D;jO!C z&ST|b?#?s)w|h8uDM`VJF$bZ1z^ejWT7Egt##ZO9-d%hB z2b&oV!-564HYx=PY4TbkH|IkwFz#1N;smz*b#q;Y`GX!`gL6p}&?QgKFBI*O*88$C zIiy^bXG#rnu9P2H{|~2A^*RlepN|Ya3eQ#VziL0Z@-}VmQayxd3)|`NH;Zs}AWfVW{ zJX@Mi5|-Ab5<*F>YUE>n%ijVn^4++Hq^Xv&aE{gYW&L};y_YbNIQqD`?RcI>W%si& zF=N1?dt$h?Z%+|@Zb<5YKJ&b-&;0dPONE;vJ-rL_e?X`hpDo4q(rXo`w91b#<(?y8 zgH(|nx4JAFUGQTq#eBAF&IdOuRufv}{d(}skHs`Dw@6`yAp>^w1IlF>)$~SHIi0wr zF}Uh`_@hDNA!zar5*M2t4C1RXIww0@OCia0!C1_nqU(|GseU&kNbOU@qfKkHRCKAL zJ6X%@6T`PzduzipY+hA5U&9B7=foXaIeyDl53fvfW23ny3ZCS%S3TyVE7|@jRACq2 zkr|PX^Um)IA2P1$s5#pu!;1zk$IL3kEIhT_<{_gAu$Lh<#D5$pJI&bIt5_~y$Vk-+ z9I-Z@+oFa@MrSN4kU>LW!BS!}d38U{C6%=PBvS2htEzGrJPAJ!2PLNhzp3M;Tlg`? znDDP=-2t1@0At{5b-fVg<%k-XTa-kHQQ_1<#h!1>ZfbrNA~PakZv6V=3TC`rvNLF_ zD*~j+(+`jMm#D6ErP>FN4q=yH>N`1;6LD<`645j>TZp8|$4@ch52{vQZZc#a#`tu6 zdMWSKWKS^s(5P^-(&+vcbHfoB9_Xk3Rl)X^%$p+Fr&*X&kzpa<;l=XD#36WSa&q5r znEmb&9nt{hQyqd8u}L2p{kb}Soe#%67q>pN96y=W6v9r-ypiKNi$56?D{zz98oTu^ zAX0UvsJpR>E@%ei(}?7Dm-6XV`n^wig%a%!16JM3*MhkR*5Bs3U+@s6OrhfESpp$l ztjH^)yCn~<>j+e8l`Xn8+4??D)lODI&lEX{>7+R6oz-!3t~z09Pzgu$UXawY#ozi~ z@<~}#Nx^TfB(ynRw$?IyIpO1Zx72`o5$zI>3RoxtGfe6m3 zL;nMd)Ob`koI!b6%ZWpecNI}Sd+Dw(qOURBTY16_^9&2gpfB|Yt)uFLuCo~}Og8)4 zxG@0(5mfAXCJ$?wv-fK*um61d)?)ALCb=pZvlJPL$r;Z1E=O+h#*P^MpX=O)t^nU6uwwREhkH3O#@bTy}dIf zrYWIE13kV)h-zO3AjhA?Uz?8tBO8`ai>qZI@{`Jy&+^ud83p^`H;f4MTy%y6Vw~Huwd7?-4Bo?0rb=_mR3A+HC z4HSYIp#@R!j!Jx{91Yaal~)G8ZFrudj?R5Ft=)Hu|(< zYkv`xxKSr_=-y?6O~NOJd+rRp_YVD}fd&C{&S{{cS@lOMWTWj9=eL_>xHQz9CH`v! z4G=tv(!+%`MFZR6n;;$4{7(xJ(w)FB}ig?8<&_yzi~Rigf?AT{>a6>i3jwmp%zZS9S!KCEV7VnP*L z7a*PjA%(dw4~Z4mwMs{765ol$^Ied5vEK6O9Eh&^;+&DJbvwsHU%hkoWn6FT9w6R&M5;Rb$S*j!T^e!^KLtGW)G3vlWl5&Xz7#qFqz{d)-_6!CHQKsOq&j z;MH_-OSH}t-jejl;$+NwIgaWdl^QUN5cG0zUv+2nxhM;pr&9Fp=pg6Kxc(bejaX8< zOtD1c`Y_7{SAwYfv00HlRpfaa`KKrxV^2F1j+Z8?)yLb8Zu7&i$Vy*ENSQkpUL@X1uQ_sq+!M(kDm)bdf z%Vyu#1)Ue}hY0Y*=p?Fjg6P=_-+zFkp>thzw)Qmvt~E^ymqiQ z*Q}P8LcD%zBPA?+Gw%lI{b1TsRo5Ct$4H1;Fe%#}t70!VT1d#bGkzj!zH4j9?<3!? z`zXam-d8F+$1mcq%uNagO7_a&G+CAu0?E`OP$lubFjl07#jOwSjm{X|FPnQs z6uijT<#3beDVG)t*y?Mw^SKum|8Z}PEq9n%sD%;A*2@sy65ZeFlf?IORWRe+>tD`| z>tNf+^L+G>_jR4bwH9URVMV`|vb}5q9J)Nw0%$5hbwXCPp9~G41`Z}<3-YU_1=_NW?M~&5M3UPZo zaN5{exmT-IglM7ZWpG?X8S3v5lUd-Hd{%Yu2MOB9xnk0}xBu|}dCOa0@4~^O<|2a# zjngkSoKC;Ve9Ee#=YYOtpcYYK)GC5bcy^@>-b#e^IbtF%80evtoP96XiOdbJR$%+g zTq4{^(^lTS5J=^$d~x!m3_d%Dn{J4C&}n&?^|;9aDN_C9)zj$VAQHAHd68PMmmcq( z$ibesP$5%EGk1kcFgjR4jK=f2DJ_0Ca208+!C-vQbcJQqw7PdIVroQd4zy6E#a{oY z;iH7-%+!FC_eX>}_gi$U9Y~@IgJbyM%+hvfq_&F&16_b_d};PSRp~=^InsjigyfNA}4gd!7>W+ymrCJ_YNJs|Vj|H>VXi`!bh9ry~_@ZzV$7Cu$j8R>i6-Dfu z1_~p%Slh{w+UP0OZVjLLA<@mP$YIaTfy_6nY1JQ zKg`~Ue?RP`?ALRCcJOPES+vvlI|9yP+Wumu2JppFJqi$Ec?9a&SN#c>9? z#om)Q!O>n+Z_e%e<7Vah7(S>DmSg=`&@S*&;HxXh(NiN>@x>GuoqO959ICHGC%msKv@7e+UaS z=@E@C1q^zSPIasZuaKo`#XD#?^7pR25l1_4FIGuR9xL8HxFzhp28# zhq9*Z`ZI0V{Ri|$R!sxlrl`6<~}LV^!nhr1Nmx0p-roT7}Z~Ixvq2Bwt!P% z+a1AP^_n?@L%UR1eN53K{tb#sUxy>B@^|#8f3~+KZBItuIHo8-E9Cm4en6GiRcs%=My27GikCM zud6#VQ2vi&&C8znTo9)WNI@zFZ$=^--@_Mzl||Jtn|<5(lTj%h?0IDpJ(6h*vh^==58UkVOr=o;9hMxpOuX~9j1EnK!ahNnf5}ufFz*$eg|jUbFv2HA8;%KVV@xp#5^0}>|rjKj;X;q`iR+fPy!}+D@zE3 z)+#7Je2G{#bHV+r{Eov{T;h&aU!6JyK)LI(`U*yNEal`osVZyVpNoP~c*9+KS*#_R zewO#t2ZX^g{GUNTY$uuA0& zBAP07z1!~aojK!sp=N8xwF|i^Vci=mvpE~n%9-y%Y5Dc$c*P?qJkomg!d*+w2HB$IrX2jcE1$sMq^g%A-~mJC$(92H7rrA?QDO8UF(bMMDh;9;axD zc}=z1ISoIF#FbmX;0gm^lH#Q~AI2)4GUP{fYZN$Z^hG89+=J&Ai=A_ix?IR+0U?bd zpwH7=d-wg$#M)%O&`9@(A2!t+--|7Az%oE-1{$~Rj8lFy+z!<`mCy@XnlieRc|Gaql(2%Vy|C_`b+hv4%cw$tQh#y-ch)h|tAh#Ko4^&1#t z=PXuJgRI)zgRoT6ffS0CQhKG1x{c&1gZE?id5$JcXKw5YnyBPFCi2fpRG0J7z4kTS zbs2dW>m4Q26z3metf5H_Nmk_oiPlR%atoVzOhWRSw4O#z8;!tPGG(iJ4W`M^Yc@8^qc}|I5Cd^hBrN#pEO6 zq%xnVWZ#B*>Mamc;@4?N3IMt+60K4PjnF->K!vVSKe+@Dsf)JQIJXfgC!p(Pm~~SQ zYXSPv`2=N(8l|1biuiUlfV1&s)RB=jpczC(FSWg{YIg}2aRlADVd#?2^k#X)pfTMf zpd&kdRamKakC?t!^SeE5EhvT7@Vi2%xuPR|$9QI8S`dQW1zlbtwe zpkje$SIJpcK(OocOU2AKyO7@RY>_3JpmESBgX=j$*vJ0S({aN7_tMNRKzUBHpMuPK zI)@)yv0WD7-y6Z1wE@~y4>-El|Lp&9fQh^2MUT06T!Bb`k>sx>d^0fi27L@5xb@4J z6O8A@h}#-?@nJD!InWzI_~0ryolPNgBK8eX&7c+$BWVt?tB;B0`||NPp_fz+x#GdjZ&hH@Ca2t_ZqA$OSoK zQ$MFJ_Ne=8cuOIzg3-fqdQ4S+9vnKdZ>mZLAuufcUW&D zO&?izl_SU9+@p4ypR4&c=$PdDmqp&0_5y!pNKO=bWUty;u4qolrBJNS1eiA4NZF3F z%SzmdlX^qRwv`eSYQq>tG=4P&F>~9`t&Jbe4Ai7)i8#Da4SiYjwyz>EU~pLG>`ulV zy6QZO;|+CM{T2oj>LymC-s+zd*2Vr1SYt>iCyjE}RSlc{$$ z=^U;9QK+zp>icZl|8ut?{$Kv1@Kqd|U&`6wzM)_Ti0;&_Ed3I-wb#KiXZ`WrC$H|8eqaTA zTfm*@O-A-)sJc(;8_lFlPcys8`DO+g+L-1-e(V+CZ1E^wYt~6KV4wYlp)JjHrl1Mxkpu2L7gf;WX%uT+#?p;O@<0S#XIC? z29CdHHNC6gOLW4;D^u6_H6eb1!QP^~`R$RflZXZ5*gSNTU z;;F7Ya|`+9UTz-O!983#{JPuy%&P^@%uwHdbou;vYJyq60Lg#u)@+U*D$dTi6C1L!JH9M|Zfs7j=_c z+F@KO?`^l?;a>k<9X{9qlu>ByN!dK|z%%Tu^3l__Ocf}7w$O>WvEyyUE$h368a}19 zSG%7tx`xc?=_nJY$=dCoUl`;+Up$E+DKWv2^1$u8+!j64%2_1 z>ZfvZ>=vhmzV;p8A6m*SbA^Omu1iCDf%aqj{>*0G@{T(Y4|0-1xq1E$6SgqJo0TpJ zFU@6rLf9NynM@jD)pdd~M2ERC=1NflPqPy_qSe>OA%pS|Vgp%qOu??C+t951f%IS< z7rOAd3rF+NO0A!}MRvWoomM^UwXhls`<$COm9dc^fL~rA(bCvD)}}ue32+x9*`d)ngR2B@Y}l6$5c} zM)C{-vA8TFWbEoW7@Ez@CiutUO*Gbh$SGqHtiA_oj3BkhpF^Q&EoD=Apo(9m>47UmhIZ1dHCdn9p_6)XrDeQ)8Vh|k%YDav46fsZ(@ zPZ-TZZaC^U?fPX-wZ%yd4Ig#$x)p+1ZTV)$U1(u9!PPElJ#p|5kXkzoYw7D;dg}F< zLr>o9{PPFvXxxUW!qyJ%;&|JH`g5riAo}d3t~PEZdhfFmuj-r_)o}n$G!opUFVVL7 zBTCx~vR0L+&DPnvH6}_eL8_3UCF^HlG*$ZArn=JFK0rh2w39ci0xHApyXBdSO0zf= zbIx9Yam>AVTd%M_N~{z}ad*Zxa-qusDMY9ef?98~Bc2EqnK@+U^i08R{FZ2w0#Gra z1L-etKj4Js^>2#AFAq9rB@eXagh(Bh6VOGh=^#h96I&9Vl;jyqV_p7T|CG32Gz zt?I8~fw?lJa}SZ`vU^35RWHMjI8h2O(5Kp|k@P`9c|tF%^HgN9a1n}iw0NVRRC4CM z@yA-9k7iVE6x}DY?2vw2;I4^`)W!BR^7ON$`sPcs+0-YZj5sr^01o2G*XKK?LglS1 z)@>6nTcQ$TkYnKr&z8cEjD_$D&nzEZ%O%TQc(8R=+ZDC;LriGm_0Hg)J>q;2l2Qgr z7@m##?1wIyTx7C9i9MNhf9{Cgb>9u8^o-Ub>+(rTaSu3jE)~-^v8_XX>vP~9;~6yy z_~{Tn(V->HAi6JlWj-?S?Gb)PA9v*C0)vzxN#M5IF8zsVTU^rNV8egY|Fjal|lf==hA3p1Q?t zp9ZvP18ORq|HyZ=;~+?AP=u7JZM##7^1iNItQ8^44{S=Zbl*OQ+BhB3-Bv8cxvUl> zn1+7)?1!Z@#$7}i6cZyZX5OngJ(p-wYQ6n(%3Ra`&G`8 zR)p7dO$pF_q$S&iwVECD@UBgU$>j4(uF+DZ#MVKviuXhD48hxjr+_0hVT5n!QD zU2nY!{2*{@tvLl%NLvC=qg-OU+4?JU&I#pdGTv3lDfffCpH#*s4Ks-G4UDYT#AKBE zv0i{u)+o7rCOkq35Rrs{gL*%y1M1V>|JCggoO zN>I?k?d#TJ#OJ5U0=noYlw+KpI~pJ}>mT1Y%yJ*_Y0SZrMh=gaHdZqg|F_>L>jJFsc!wGi{2r5 zB?9R`5QXKfdCqY2E9qva?r8AapZ}{6KwI#Rozd9!kCx!>Q8^iuS+jMDC!us}1M}0( zY%jYoY{B z^=oRU4!XBQye=8@wcQ-Dj+)M|URCUk(*4AoKWfVnE!pcDl+rg_m;`U}p}v;XK|M8q zE7OzQeK+W`$n%>tS=4&=fOY46Vj#EllwCqk6Wx>r90#yIB{#tb_^X zq~h^n&$om!Yv7AfuR*Lv2)B(z0#RvS3$qnUh>!~iWj5_@5<$y+m9X3VRcfDI%Btrl z7B~J=>KKgV89Ok|Ag!3VFqp|hr=^Wfr*2{imIM4{>+F!r0&zYRAczQ*OVXdVW!A*n z8exB0E)6&_g={N z>yk$WEnO?rm#0uD*ZihScaw5j5y=GG6s_JkmRrz}-H`v%eb`kUQK@0Z8o#!ikW1d} zQ;x~>V_BzHI1UqKoJuAP-+%sw?i690;JGRP--nt1l}(J(fBt+?x;XccE;_X&5n9n5 zE~?&gKj}jI{dlFHQh9YJpV>%WtMmCc6w-%4RBj{n&9L85=kXhIfqHqTpT%woRMRiuFnN>!G|k`VdL( zbE)%NESe@zhShgbB>6w_<#^O+!RCjPZj>%HZ%mM8nCiYsX->~MVy$y!+8W8M;yt(a z5Yy?1u4oX0_)ptVmrz^R;(of!CV`%LA39X$_{Iwpt>XQsw2Dh66SBypL3YG_4_=wF z`Le`IKCcoze`Ko?tmPGamj{Duj8ObiajD;%fR<7w`R-m-5mmOeqkP=wbFDDn;tgVU zXvih9ic|U$J@2o$TMhY4K678$$zLlQo-wmF3qg69xfFg@Uv7jXkM@|rHXx?ACUQD~ zdnli=^k#8DC|?w{R{Sqooj^8^dF`69yD=E4P2^*5SeZBj-)}dU_=MP`8c=ie`cjLs z%ZJ$%7=ryG@8ZU%F<+jF-X+1S%N&O|j9UAbmY%dcNE=j9B?}|pX5NBpE**pe?}|d4 ztyo-?q;5~4NOWmBy!G=)xt7=B0fb4z##h1^kv>QumtU=ItRNdF8xWX8%WlNOU)H`2 zU>!W3TBcq&&;?>+R&C02sY5$A^o}P{=o=~VwSqvM!Fs86#edo=|GvU%Z9?;T)cJdl z@!WsNeo>!qI|YqUoSdSowOKt2xRAOzY34$QoRzRlNZMVXJq)l9uUQPk2J&G?J;4N~ zifu%f;&l8+qjDVoNchzwzE30d^R@wxcDIr^mFjOb_|y~k{fr=F9I+8qz1gBlSACOY zVakc}*?5YbIF!inuSL1O8}N?0zbU`vnf|?Kz=^_I{4oaXBWAQkjXc;gce>nRI~+*0 zN@L%7Ie+7xZ1ddK=q*lIDczEu?1F4W4Sxt_ z{(w7|V(5x}Kqa8=Pm7r;iJ7lOm$Giszv>`yI%)@$u3P;pJ4#j@{w`YLf|xZ+ZolaJ z_8dqa&hl{n`T_yF;I?zOcq`k{y`g)=MfVzg^2WVfA^SAN9d z5ip2_S?SHST*AF`$ahu>26QlsV0leFWid}}w;_vlKZd?a1&(clj`FktJzK!vGe|;_X-(KoJ zSN7l5>A!v@;D5LMf4BXAI3xe{EKO+y@bZcyvqBwTBtD7gQ+g9Fed2#8c>eb}aS+7H zS7TiZTH!M}6B~78@j>guPoEcD%E!|0B~RK4DOT|a%NN|*zxeAO@ZWyt;K8^aehicK z$4jWUP8n!I370U=v@EJ(o1j%FRNmjTdY1@-&cXBfP39PbJHOl>^Y4G^@`1Ja>f6wc zPxa6r*|MdIwxWHr#-(0doY6YTfYazVYeC|(J?BE0NVWVsH#}}Xhm)6EGGnS_(zQB- zXI3x-y#7qHA5F)D|14q(z(gJVD{5In{jxDi+`kM3{u_Z`!;_68N@sJJ9R$Ey(4x2b z8^~@)NuxY{jgm|RBbiPNRqg!Z7{9$#_o_qrjj3{Le@_uXTsl=_u^xo?x3?l%Q$CBS z`L#UA7r>-)DBk6a&ien@d+(^G^R0bUP(;BI8z3U!45M^GsnQ(|HV}{!I>;b3grbBV ziaJ;juu!C<2oXX;2?-FYq97%plmrMJBajeEAhb|^pEzgcyyw2Z_uiSa?pk;K*1G@9 zaxK>=e)nhZ{p@F#hqx*gF}8!tMHKxjH2^}1!es(%YdVX1_RsTcZxd!saMR@hJ{D1VnR`c&1 zT>jVhtuO&UJjvGl=zsLHE4>9@PQo%ZuJb=T`++_RGB!M=_b<2U|Lv6=kOg0kw~zW7 z`yYKe{r}(W|L2=sf0b_&6#aqVn&!)JFwDPs^=xS)w{OSGbG`#^sf@=j@Jl_d5##*M z{$STJ$~w~;ne&fn3>i{chtV9)+5YPuuRp~Pio$jBLvhYicT~8^)gW{wTho^3roc)g z6KpE<@^;WSkM-7$$BGu1Bv%OW-TxMVbGT=7AMw|-&C44AMCRNm(VpVSuLnl!W1~l_ z!bM>fUvjhifB;9AZ>Bkt=abx9XCrlDy3<#jp;_qU>DIsAdD_>}2x_mcM&X7mWaNj# zm#d5v>v}mv&|IvjucwNt%_DdIKW+hlOX&?dKK-SaPKaj7qzKET~s)<*H-w#vz_&v9hv9oN#=)l#H?dxkv-SdFJy@6_9^nKRcc!Wuw7$AS$!Mp z{jP~DTG8DWTz`Pp+dFu}P=d_ckt8|Tt@w@Del9NXR?H-z1DOR;Sxf|e3d z`$P+-YJyKaax(2?SQ8;&bY?(;8T9D8`LIIcp~#v>`A3unPSIT<-~;=b}bWP)(DNuf5QHYjxK zz;3EwU?k`&6%eL--q)t3Y5Y5U`BzN--~B4^nLcHATT8s?)b=t`FSMMRV&54ru0wEM z-)5#ZQ~|xRp-O-YJP3+W>+Qb{j^4cY!_esVih3ncjZy{l9L``nfaYr^Q+H=`Nf*pf zk1_!D{;FdWT+J=F2by?Wi-y9Ey_`F&7>YZ+o>qh!0=GKnyKZ2U?DZo7@slyS*^I7R z8B8-dIej1o@LD7W=;_oer+*At{3tyCB;iXO=8&|OR<#Ks>tZY8A7HcdMdy#1Swo$A z2i|t__u|hRk{Nt&1P~U8{asgE+P|6hT|M!4%&x!<`$3xgq?z}me{@c~{mq7rzLFtu zOLV9t$VGo$#3OSY$Zlo*!VvyJ+S(1Yn^6=mWL88S#kU5n1?G~JDQn{bQPV2y>J)Gn zweFm2uzSqK5Pj@(j`V^re!%?>2q89-H?yOe?}E>b0wSOisGG18<_UC6ydqx(%@`~Z zNC$%xh3ZA3%I;5kG#Gd)N7Z5jx_{$H>*rSnTD0Y}wF|!>6(jp!Z*z{8`hNZNETtC@ zcQW&XG%@(Db$Tl%V{9>SN8Gfct&07GNZI6=dopf=N%JO|B)bNTT3+sN=v|a$d|*M) zl2({2pTm!Ta8`>dy59ZY3&#Im4Ez(K#rPiaG}5njKh3$koWD+QKqakiWa(354oWBPe!jGEbZXQS zG(k6)x<+rdYK?sAFNiPQQhKTFV?svF9m&2kOnA?9*=brPP(sN#->sy-JRlD$GEKu= zDi?LJ`0Mzig9Xvd(osXaU^HjsV!fE}gdVHcf_;nvq}wv@5^)Zp)Z}XH;7Z}mLqB`E z-v-?iD(D9XgKH0nJo26-b?Lo*&0s(K4l>Gad*Ojzm7$RB8@H{>=-Y@G*SYE`6B5d{ zn{#+xonDOH80Y}E$x}Mi!NpA=Fw#;DR_&@5tq&h-B!DZvHFc{al&A|o%fB6)0b-i{ zyrK3RKyhGJZ8YLezYvkC(RXHL=P#xwj)wP1jRJ0NoYXGJJ;4Xp3cO|%#)QJib)fkyIW|BxKL zxlfwBZknNnE_W7V@3njY8$zrO&w+SVFmHuw@qZT0ywjya-ufz~w|H?tZ+kKEg(krl z=;Pf3y8-MWA1v`()Le%u*+F-gQAF8j2tls!J+W^I^70*s$-BX@Dgx^yzE?L z-v~H*{t5*_|JdWADGfuLFBToV*|szUdO;z??WAsL#4#qbC% zkSni@K&Gzpob@11i{Zugc6v}vZ4X(B%ewm^F-Adi6PE#(>1$~6*MY+Eq3<90&>Xpb zMRgUZqE!Jpwj;~`{eIt>{M4W*ASW;k+-^SEPV9-@eJ8HW^Z=XoKVsxhA2YeFokOXD zm7ve6&TYEHCwmHx#*vr<5L-9F;PKyt)gEuIM(tGQINI(9Z|?#7gOu)gqSrSMo|Ln= zUUOd%1pU4WsGqI=dW6H4Ziu9)(km5qKsp`*5s~guZ{_S9HgkG(3J4^x%L5^^*9~Xv zeXHGab_HlPut@e@ya@37`SlV&Zni=Z1(imDCrjj=w#x{G60cx#ofP=K{;->Z7tn%NHj+!^e$E=~x!_BfDJcAP4%|>)Qqz=h2>NJl&%fGT5m#KMSt2&x6^G6RW)z zrt7)4^4pzCv~a?Er@-Tw^PTwc$Ry@9kj%uDuh?-AgLCQ;R=SOEH8o6(+zA|;0nX|; z=U@iLs3c4nT8GCGdLul&ug1#A35WoESJy?-Uk_}t2*wIPI4w8F%0cgriqoZplQUSv z+W3Q!Z{TP`Ft1qPQ+8)?{bUbJ+j|qR#_sX5oGaW>#A8VqO&~n1rq`>0uM*rGhjpKT25N5 z4NAa!Z_ITlcP+20W-Ok77LUId$)1dM;Hi>rj9~Ry2Tx@MJC%zOQ;G@4z80q?!}h}z z6mX=D#dIAVhuO{d6}@=wKrqfhbBY-+j7Y%)=%LB;NOspch-VeI8alyLR@(675eQ1b8<}$I2zsk9AUQR7saMDM|vB?d<4Su zsXabqd@Xo(zU$P~%Haa2qchKWN;zun`pzJ$2?957M_cXT?T=PN2otlcdn;?w^0ayVm_zhPF4o9t%XE-9@%Yz4n+2)Tz z2kB<-X86n#Q?r5wTcctOocDbZHt>-d6mFNmD40>W+gSP0VBpGQ8~!HeoSjOic*q#n zPU?XU;L>}kLRlG0uWOK*-vKZ73`IToJ8)ddhR5!NS@tjc?q&1#9H0vM zJF4Y6ylYM$u#4#udV(In3SKEPyjTcAaPYk%6QDb0u1Y7+KyxQ41FeR?Hw}yNersv( zn$8P-LRE}zep8QJ35Ll@+i{KCmAk*%?#bEieffnA&WNvKA#zb!(M(a_s&}BL#qSH$ zAeMvN-!75l$!a|tXKQ9BA{XvSyuHxyfs2}c&JpVnwb7Ha!kUf~Y!M62bPE>xfVvOz z6$u#AYrCH_#T1BsZ-^~?;4FWIXT=)=+v<63`^6*8Ed}2PMfCX4S5A;#iUU_|;d7Vb4u?0iW(GiH5)7!TnWPJh%;+qO|8{BId zJLceP^rhZr+aDG)2pad~m(58Ira=8ChxOFp;0(+9hhx(^Ufpk+H7pyextn2SnC*HqBnSMN?nVFqb*@@acNi9Fk{&n6~zmo5eS$2S%!Wh8^aGMsT6~z0Xwt%ul zJ|&CuzLiCThtj5QkExl{tOWAQ8}eYJAhg65$JkT}ujI}Iz=(vBJhEuJyxjfH}{3-)Cy{L<9%Z7+3pTl7n`)vlzNZpsYOjPL zy!OHs()+YfB(@4|LQH( z{rUEKY%L(q=E1{5x6!&+Xca|zPP335(?r!q;%7W!6x`ZkFOER!#ke{P?;NPJXhG|_ zkqvj#+DVi)-B?&AB_dlT0pKq`@KRTOe47UNNe?%fEvQHtS-1DSH$7wFitY*mc# zcf)a;pg@Lm@ZBgW>Q$nXVjF6*&7=Km>kBX&DL)>}k%0sNe9)?-q;{$yFF~^{3lz?g z)t*R(K9jyDTjTwTgTg3DCwn%qq~IRRz#J?TNZ4$3d>!xC03~Qvy-)4{rO7sj zWzApV=X|#cq`FL}C>Q_Qz6?8iJ>H*kwPzDCF?SPj(}aV)M|fpZe|VVC{vc*Bbm|yy zo?KKKGn7SHv>XdNz9yb?FBwJfz5Fx?#rB*cCDhvu7a#uX9DR%lV2!mWy^rm$>XfFv zhyn>-^)4d$sbI(u_})AN9F`z51%`{7Eo&j5D@W`yB4utzJ~(;%p}@~%pU-MqxMWjw z(;dhz0{6|P(+6IXv}2VbOp+(4`dDx_O_>%DwfcVlDFlef_M)t>yg-5yIcwWlnt5% z7xl%N>rCa8mY5xvsAFd6o6J!l(O$okzNowX{5Pt#5({Dvqc%wA2G|!ULN3?h^%=5U z)FLD0()KZcIajRctXKLs>&{+II5~i-PKMaEHpB+jkJAm6DfIsH?edcihm>x8dg?^2 zaNRmN9nI7g6V1JEX%?K8%ha%`@r=G*7Q8ZKZ~Y6d951?)O9!tL`z&{e1Ub~?ACCpOVh?NFGKxx5C|tYB-kp5 zKz%Y}kR26Qc$MXTY3sBe6D_o2;|6}TRtJ(ctDKUkK{x%&7EqF5T^x(w8>XgWACer+Wc4h~qhYH$$EIHP_Zg&`D; zV%W|Q{c2)R$+=~^^A6#Qv23mbub@9#k!#wBztAmC0l(mEX*Mcx4^83-^GxcPwr z#sv}Oa)<1>rnMLqFQRl(8AdsL;YZ}nYP|KULn#MlgLDh=NkaAbWw+>~rrKZFHj!ef zb=$wiP)S{DI5(>M2e_(!yzJyuaw}3LtCtpsNpV6UtYgzs*uFbS$s(c81Kg% zk`*)!svUQJP#-Ei$0oErP_C8>v31q}YB~Z#9=b~+VnG~QE%WYW)r^YzOZi&M!9kJf@Z!Y3R{9PX&%|trY`^j3wJ#eF4ik7wY^f=u&$^fy|ywDr?}!a zVLSM}DDrhRbv_mX)OWU0T{bd}P49A7aa02Z6oAxXp*-xm5s&;7PjZV}IXA8;LEs-s zq5a!8hZE5DaB(n2cI|oCU#Hq~djMPCRrQNdF5wTBRnmIt7XJpA?#7(*Boc<@O*xPI z6vj!q(S{-OIXIg$HQ0Fp84idtM4hHMZ;0@P1`Cr5A32(yvQE+fITM7G>H*+e)r>0l+j^5v zlru5GReR^JLyyib1(hWG)Ynpj zP*5>Ev%_A5-q4?aZgh6WBP2C1#q!Vwc*eA257YQcSAGlIRz~{AGC8X@3_QhGx7`$! z7nhVOlfMugL8hT_Hz`~h;l{%|g}$Eb)WDpY2F0J6J6#vBidjy1i`|$og#~*C?tFhg zjl&ioPBmmDa+kpC7hglS@+7uCd8N>FR{EO|pk@h*lP`Z*1IDN`-PvSeDi!o9LxQx&aM(5yt`2iu|^=_}7cSLVwRWrX**@21%-#&+K3 z2+iuLUr2HUnt2hy)2|+^tUr%sVUx)ka~YUdG;;A*c#4UDESdf}#XV*!MXJNkD-1?c zR4!E!1})ucfy;jfjc9{DuJZj2x!fV{M}o%y&zs1$t)4ua&)xQY-k9F7-R@!KKMP>+ zN#Q$wbu36jry1s;jXsu?>J;WxqD$pTY5=6DFJK#1-Hxej4$ntck~{i^o`^!cW8P)> zlt1EZw1|j%`yjd7RM{1?I8Tx{&vJ{A*6ub~tQY&I7}gDJ1qVmg%Yk({g1uX^)h3=h zx&=@}pej*ph~h-?bFR%F9D^7|WDLDUZ##g)Ypi%xdjA5}4Iv1=Uz}jLz6hndyEIsu zCcKCOIw^>S%8j>aA3f5&&IAJ-$X4N@dhfjxudFagD*k4-JZKBVzJ~!B|JCEZwcYUu z%vSI7zEn#m&?_OA-YsIeJ@lUy2Rp!Xz&n-^7P%2s(j`K~TP3GYQtW-oeiJB_0A(hd z6S)EU-*lG$+_3^U^USC$x#>vGQ1$I@jaoqL4uOz59|_u88@2V_**nEKj04i9JSO&R zE7F!uT%c^og@c4v(ufoqI0=LR_f@ zNc7{!K}~~o(6pHF7=+}dKN@mc;*-Xox-Et!zq*?vC?5fcd4l`JVaD540CobMHz$Pq zvYfa+>%RIH1@>|jJ~^Fr(XS@F5@9A@0IPe{Wb2Ip7e|Z6D)(18P)xOaYYA9*y@)(D z*RaL#ix1@K*jIQ^1HKYe7C~ zvd@f*QbT?8>j5$tX=lhi|lVGi-jHhu{u7 zlOcUZI)4l2><=^sq0+2?+mN378y7Wvw(NmLsRPzf`4-gOmH(?%bj4julswc1CO9C{&s%!(%#AoeeFM|0Nhf6s-vbU4|OERDKJR>~c^TWPfjj*`fMCLX;S ztRu2T9s=Cnpv4m?=Wo{zi2vD0z=Q!+%-Hp>0F14L!9X5cRpLN?uvfs@oCr=NmCVy?S`dz!lo2JtGBn%Vyk$} za0u;0^u#pMAbXNf2t@UdnHTJ^Z=m>Pf3Ua{&e5hlex};};wwemFd3_`&cJH1M~ z`ACdky7DKYyK|*>vAtZbV*0dmRfS~kCYY@_nBh4SRKuN&3OcV%?tL<9MTcFk)>w0d z-3LeROM|rTR@e_;zTAN^#~fl+z#xDkp(!QEhYJ znY8&hUHrni-kC$x?s&6nfkR&F_HTl*bjDFLOxjDq=;G+n5L?hNK{QsldqQqj5-YX< z-@Yc&&)7^S1_9#50nz=0;_U~f=z}@Rk$F%+K&VcNVP>`1n?chK>2pY zBNU%DXR0~#^J;QQ#Obqhs|1d%b!C7$9B0leOVi2bXB6|MTm7!bIEl~fpb-ncy^h|+eB#(64R9fqkd)*f+Q-VOY zbyB9_c!EcI;F8cV6JmBnNEQaX?2kH3j<>k8To4%n75(sJF{wlYF6VNiv-IBbIWA+*_x2 zq|ms#{qSs{&=|~DMKWFrjpH7(y0pT4u@0_pGGYOnDXr=9e$Og6a24JI&W=|-)0On8 z1;_&|hC`N+MfxmIuHU?*0qZRL$c;X5t&=u*Nfh?AA@Buo=I+U{Z|@Ezom{yfH*HTY zojVV>8#4MQTiw~H^9CP3yz^(sW}o=@ck;Ir7!g{XxQP_x1}|}RXs@Wx7;1y)7dh!6 zuV_CHg32s@ILO|4mUB83T3mK%K=#upHy2Ts15w%OzE$IUIlL!67gF@{%br2O1yN0R zo?jk>Zn$b?K0<(gsQ(5(k32)rvqe})lgjFKy5RkH`tb!|4y#k{l>#?F)n(z}PS`Nu zYUY4=$JcJyNl-@3C0Up(QnMqNSJi+0@UR?_osF7wJ^(S=Xc&Pj+fdKIx{)m^t4BFJx{yFL6cCD}^cuv3V7l->cNbCq1?b zm<~V6dT^WZ7+-c}G!sG`oIo24{!j8%pa+7Xw7`I~>(-QQe3tN?^}*-Wl7^e}|a<<8Rk=2K8QN z!=>dz|9w>9FA%1labS@P=8|vzH#qS>{%WwAw#4}dZ9@Mc@HbNEzkbdb{Jvvgyq_^# z2%FiJ{6CG@4ofd|R2BL@`qCtuGsy3!!_d7zexBTJ@%bD2kZF9dV|?P^pA&2OduY0z7<;EIB1&>Tr0}SCZim6h4wCJO zU*~-bPd5agrfvTwbi!t}?C{dEOj7cQlOX7YB1e<%anfQYh-tkeDtCPfvK;n<2TkZ`toCjNt$V$_uTQdR zz1GEdQZWN_Hbvn@AF_AGyY`2u@xAKn*ijQPlL9M|Jh%2FV$Q55@gLcyeS2n~P7E(N zDYTuRBp6Jy>qOxV{&v2*8!!V+gw=#ge!Gz_ET8zXXS>>q?yZKHaftca5JjH zWvYqr61>5WnV)@%{6ID;)h4w#%_1hCuBzaS{t{iCcl6S~`cN=+k=C zxc4OqUeWP~h}q5%XcA(ec8x4FZvt7naf!lP33C3K3RQ1UnX7Ziy2k%N;zUi`2%Ie zaPKF_uD2IquY?uFS2;Y>4iH&6JZ%~y>x${yYNs1SJ;~el%bvT+^`pC(YXzPw3B zw28_E*fTmyR@(4~SI&tCFGjv3)fj{Bhwr5l4 z1Nb=*G2<>gLmBK@`?32+;k$o)@Wx!>PwZ2ft3-M&le@_N@-sEftzVM1ZjQ`M8SDUm zui<&m+d7`+o{x0Dh`nv=Hkjqhz+Sp}>CnD=t?+s)fXdDY8e{T14^VAgDb@}P4Fq^^ z8dod9rZPcOfLJWByY6hbfK!mWWR~;5dAIq(c9P!bZxwnPJm90tI(ij0E|$+cZ=n9(_P24aT76PYhM7n6YIo^g6=43Rbn^rG86&lzSj_WL&uIgMm z{%MjC(Lf48vxOrb_eU%7T&fRtXOn`_Q71c zN!R4Uv^!D<57H|b;F?oKalMSA5yuWjTP8K0cEAum&$N5G?(Q5uemutiB!qF1HszW| zU~W7;aoJ_da4@mO-NuO$3j3Per6pcTMzi7bujncBE8z>pWxvk9TBH{ZulG59Q8Rb_ z85pOYme_99dX#(v=ag);)hP?l$+#BRF4UT*W}jsWcUQ1#zBv$JjgK#9d1Q&s3~+KT z#sAH2p0@2nGmRFxMt@{7{mc8_=-zxFW`uZoFmWo`dV~N!<|R36A?4fBaA5j_ZnqSO z=Z$;J1zy4B<#br5YP=n7VM*63S8OFe6S+6?*L4)H&)sv}pFlh%a0ubd>8bGHNN zO(B`=C)X2f5iX*TGksE8?$CH;{xjC#WWbrF9Qg2*h4%7xq8k=nJeKP2)^?H}yXu|g z_bkA%+3ge)zqDnCeF2pTk8&XWC|G(tDi#+$x8`KOI__-aAMbR%*$u9=JtQ_SXX;=L z$tc`tQUS3FGOTt6hOT(s(iBn1C^exRTv9b^r)6~G-ZWqNQqenK5m%5u_z-+m3h{}h;oYre61Hy+1&a2`C-*w*x-mVjJ zE8-^_FoRjn3_+Nr>ZOSQ;d#_{l|!<2fS48Cs=9Z+*nG9Ru!8e2ys>@jmL4Q?w*x6+ z!EO|3`zTWNvup(b9q>O~)-=;sRzwpPjK%tu34cPZ182F24r)sqs9QmrEEmm=Oi=sM zNjKiamzri(>1_TcI-F$c+xy5d&yZ zG{BxLKU*06aV;CdMcIhcU#tuA-n&>;ym2&YE@3rU3L}RiT5h?d#|zk_d_`a2A$FXA z*c)VnggsbK&v#>8pRqG{edB3I3m!D=F^7Esfh;XjXfv^~ao_#PQf~osJ&E?-Wb%Vv z4L-VJcciu&xarXel`M%ES*)t_9WDtMaIB1|9KGieFBmQm?`+KplaKMwPDn0R#jRRJ zIbg13S0xm&iX3QIZ4f0Nuki9s zi&7ce3!^c}>whl7#-#91yWH{qb{;Ctav4nU$^sUp0gL{C>uGH_HqEx8U5t>*;YS$k zikTCBDDewL#3LUmerqbZwFqT3L(+xrA>251AmM73bJ}Q{?bE9^rYFB15pSn-o@ffC zzMq8CKZV%{;h$AHRwke@kseuGYiEQh>Q#EFr-C@%HxUbYAeLQ3y( zne>#6te`IymZAXzKZO?L@a#w`DRGZ$6c66(?P@s>r?P%jO?bZ%S0Rx|EUm>a4Stf# z3D)*4v0tT+7?2f48LMNqt{+DXCdpPcOQ^cnFyQ&xa}$U@iB!~cDNLSsoownxaXO*v zdj3c7Xl`VnJCT~$6j|5O*(y28rGX7>XZ*4c$bndxMt2OiThznAF`0j@l;=wBF_$Q9 zN|?FG0&r5`MHmc(*l(N=n`e#G^q1Op)8`F(_I(xghnR(nwY9yvxgF0O+S`09%kE3G2*1)J`lH<@6O+Uh07S6o7t09nTU%F$tFqkL91DAr ziisv#kQH(5?WJ17P9*TQ)m}I7tUZ{g4)NcLPu6f`<84ZVUlzEgU>FNFFGpr=Ai33T zo{!inI`EC1^uw@E?2_NEM%Hjf@N@|urd<%7SmR%Qch$7*gQQycXxvg~&39iVHp8d-Zy-+Sl+HGYQ4NVIv!hG8&oo}sDoa?-CrFH(i1O`G5y23f|=Rg%Bby9 z4q$|c{D14Q!H`1skfogi{)#wmfEH1t3@tprw>_bz9F~OEqYqu_5&{z-lvqizGdCOd zc&aiEJ%G&xtrh_q=f zPJ9NLbXxLh*gmwe?Tn_7-X}C!lb?@B zbPaZ$pxCZk%%I;&(p_we>WV8g;hrnVnz=|Xvo13ys<<}mkX$>tyFID!wy9%5B>gCK zPusrlR?c~Z+*}{H)VFa!966JVP)kg-B`{UA3V5gS_uZ0&R$G}~=(6+6@t2$4GzWc& zX3Dx&a3Zs))M1}q#Msc|DGNCJP^O8sA1PF^^WN`Hj19>0_hF=_dtF zaU3CeJMdxj){)#{iEXW~fir(@lYLg>LYR>Z7;6{r^xvd4Rfs8?gH+smqv8j;g%pMr z<0LQ@r~7dJ!<}4^ioqV#8*xwiEVi3r;~mTV{saOrLLQou`}^mc9%tTpHkvk|VcaLY zH}y?|N4{;ARXb_~()%pTCZ%txcv%zvcGki)UaXgkYBl$6@_~&l-X$#UywzM`d#}Vw z9Bj{HK92Qu4lF~4IhMC2kQU1pl*38q!lN-vyZ7e@6WMQ66x8qq^Q^6g=_wl&(sR3% z@!GAZ;&9H5VcyP_NQbX=YQB^e`pP@n%nCefbwWGCH>8Ag!9AW{yEI=h$p~p6&Bl(K zdvwIIZoyi(;((JgP!S;|J~N?SvdG%;skvUT3FQkUs$=-H&^TU7_=Lz@D5 z!jQBw6)^Nqaq+L(I7!aB#@}v}ky5Z(c+Omf*k-+7+7x$~Z2fa$Tj?n!Gw$)@=$;2_ z{{+e)l&d|iC<&V=^ZlPq{W#&b`S^0r;O%nQhkrt4@{tr&x>T`i7@7)TS8-Pn@OTXc zE8y|aU|Mav9hrYsvd=y5`G-sYGWw&Qp9-TvImz?%Q@04WRpi6KOIcS@ov%E_t+PoV8r70&w>jWN@_@}AYZc-}B+bXn%2R-8T zEB6htnYYw~-dDUwz7SS&byY&W583pf#`T~_il|o3a1VjD`?bqoRf2f+YcxWyZ?gK zE`)2{t7}i7m#WyVgW z2S=U!hAtQT?%(6id->**6?BZ_+U=r}#3KRh0%+fN$NGKoS&DV2QBP3Owi2IEJ87=sM3BlRCx5^T+*f4tkMzaR8j*m8TU*oSu_ziILI0V$){6P|b#f1ASR^97APu|>I*nbOg=wR2++h!*v+B3!-)`4@rNp#84 z4A5e=N1Mz_Avl*%emIXiWNR^vt_vDlMC|(?$s8rXe!jT>4a>SqCQ;!GQrerg7KE&hLQN5 zUt@~Pjue;@sJh63rZ{=N6%%UZDSwoDaeIOr(ZM$qT}-v!SP=G|UGEtxu}%4!9?cw) zM~CS45#Pzz;b@FL5QL%C-*awZKvwWvR!D8%Jb7{UtL9WKdy;TApz+rbQ+CNC*Q~oW zu>JEmM!mK%f4iLsI{CY`CJ8>yZc8))O2y(u{m_lz!4ts&H$5{`RMnay44Kn|WgKT) z!Gb6k7DcrtE1qQUhU@SWhQU=ycsqTwW?j5?Af)dcciVpH#QDnh?F2OKxV-XQQN@jd z?U?Ng8a5QE+=x=AYLU{>l~ZUCAU*W$Oho?4(77vdg`0`#nkDQ{^7zKGe)M81@)@(u z2AS|0_On+1kB?TK)=%GCJ2;~@3~nBYST?$!kt&6k$0DPoo=8 z9XqZbkX|#D-;5wfY?E-K!Bg5r{0%YgI-tPU$ytCDPfE-5T%q4&Nj z_e;ncqv?kD4Zc&8D84O#2_KQk8G38k)%JVN+%^}JQs&+f0>w)TrmGulC-9^Sh$(9t zgDD6mJs07gc(0l9lJRan983rLY>vN$aDy4 z5bQ$IR$lpg@P;;P&VLL=jsOlvvn9jmCWDRLi!lOdxF(vr!AKZHQjGn9=E=y(Q(b% zsULdairGai-GMnL{U}>C-M9~MWcT~?q=I~zC=T<0WiHNtw9-{n0KA*?Gi})>jwjxC ztuT?8$Vqggwb2bo8hjM#?nrM3doja7Plyxit5_wT5bDUOVM7OkmUKd}Wv^Yw!}H_^ zvl@Iv7mq(`ajY%bz}=@7ecMle=>%O5vpP#(@&`qaiBYn`^OnIMqJ&e6P{z8{>k3va zH`4;F^W)F6kezWA5dbAG#ee$pBlilC85FfP|FkDx^iLXsvS-$%LdVQRHEBZ$w*END zdPEa`4A!*K`-q;Ft=%4snl@nm8CUh!4ryxBivy{TByQPPb22E0j{pl6v^_8Anb=4V zAztjQ_4`~>wxQ-Twys*q>T+m7_#5#QH(YA66Y_iUW3BaacN=QxUW-6YxlhybdAFh= zi_}8BSz*e)V}hKgpu4#gr?q8O;{T<-+rmh;fjOgGL%ek&wOvyvs16If_;I{cK6B_f02Cw1-yv2dQ-KBsp|ysI|p@J#tuvi#o1@=7CaF zOT!rpBSYOuQu0u@iUhsj2W*#J)igdjWT1AVNeve9HdCkui96>7ZBGr32vg{x?{B|P zfgqXQ_V2#hy(d_B37ztw!WjIko2e%x4SOsvbE?~JP~EaX)IBf1??95i#wGglt1J4! zkgZU}CqOeH30BL&3CdfNM_(_(H#0@n{^!I&7+kk6&8{fn-T6t(HBmR3X_m0pIv|>+ zM4#mu>?ztjH1ibG^XHAI+`K*IfN07xM!dvN8n(&#WobUfyC7&E;Ive(tbS8Y#@u)1 zRK(Mhk8kD6nXa9xxsp~)uuB$SiGl6eo1qUmWSq8i1*|MwJQE84jY}#tv}w}TRiooY zdzgY0czw>Haa3hz0-KW+gGUCYt9d9D_TgF(CIa80>>Mi%1j5(P3k zfP9*3fzQ00+kx$;U+a@77+5PUcQFgr;IE>#Yl)MNrtGh5TEs9c7eZv?qi^@P#p}~~ z-k-BWuKW}In0-Z*USY$too6D_lNE2*K54y41e(ggPpjeI{q#tE%OD*INJ5h|V7_CC zjF8bzNqK8kQ=7|u954M|===_Qa1>6VgR>O7=hp0)tk%|>qp8@<gs$ zseB1zEu>Ggd8MNu#M<=G6Cr-gBl_I(R?+I%qCxRbQ??bxec`m%Ea4fs&_fRa-TOEn zsuS2T?ex(H&+I)wE!>1?K)>A<%L(1G+Vt>khte1B)U&R(e6yMRq21im@#`I*!b;f8oCyz#4+sF z*CC^7;by%)luQlz;Cz>Yf;`jj-c9tKUm9{=pMmu$&tmKLKH z6h1vrwkfE6Y(z-=4&2w58_F@Z>$1AtwBmvxGJcv(aInFqo80 zpA%j`Y47$Vg~MEtuJ(nFovgQRna9~O^1JEFKUz6g7FYrPYH6CoU**L^VGkx9+4o>x zeQX@j`=#cNrmLEjbEeQ<`PkJ8aC;Xxdrj2AcDr<(VAR~fRUEp}Ux7Gi3|u`vt+t>c zRZJ`Bb%saw;d)(8STAI0FD1<0>B`U)Ct-WdmsHyBQ@{i>(X*%-&9F?zAtS&;$j=8v zv`RAhIUu>5Knr08w}q_#DYWb^6+eBSd!Q7puiek`J zd&_`KS!6AmdmIoF3$~(2+eJl~%nEbHRjULq@S5e)1fFrtp1=O*7MqyN=h@vs{VrQ+ zrM>qGUsJ8DIicb3O7#moHd5sCRm2-bkmsZK-xqk^^nh$T12GYJ-?Vv+(`XCi@D+Ps z=&!LckY%j{FZZ`Nd?8FOI=pe{H%VrC=Ly2!it}XJEIe94$y?Dsg)3R-D{eU!&~&P{ z|Kaznv1cLT5A&Cr`be5tf&ee=D3C9xExtG50oS+-VGw(BNu$ow*?j$d*CMT_H+-I! zc7dU)PKo8uFZU=A6d_wRdjb-B$s_A2w@K4#YjH&3^1>FuTdb(kM8bTFMyeoy#avG|32K{^;93@__wyAzQ5ziKwwYD{3arc>_&^ zK`@o?YEIWU73f5Fn=82MfS28SUSfchg+xIz(leBb zw!56B)@velb|a`td=M(bJXL?RD@OOn=*B0KhdRC81*+jIwlimr>VCpk{0p(|c|Qur z6?KgR_aA%yn)P6^!85J6%(0@DiFhB$IiSYi&EZ)xX@Srs(C|uZ)JY(=h zjqsy0$PKl3=D!o!q^?{*gM64o0~(~t8{aE}CDL%NE{(5;dm@$G?qDPN~EYlmFYHpnoRKyHyruujMey^($=&J3oq$|OV=!Xu6b$0vHW1oc%9wUOO`U2 zQ2`9e@G9N^Qd98~O4%JtJJ8bJ6UH#Me!>|PmU^JiEemd+Ju#orf5S0IuK1$s0g6}8msxM$XvM*`@sQY9>x5g~VJ z_5EPP^K&8hc(MCRqb-7Xt#YZ9cK|8zXxqXTE;=Th8Pp;uFV|sPCry5tuSi z-Wa=8iUHHIMN(E#FV(geyVkyChlOR@d$$6)g(i;o#=2ff%McTwFA7R+$9YKWF+EmS zytIQDGzUE7@6mo=OLhEW(U$a)&sjxU-(P@@GeCE)j{h&9 zyD~gu;10ymjVa?-+beR=2Kxu ziv?)_bk`D>QdzsGII7?XRj^7hRU8=DQD^TO9ne7r15R6#4x_*dqdFQN<+I1Ei=Qqnwtn$#_)dZKi3i{r=o*9qq`&yNX7RQU;RdU& z$}3-o_usubYnA?bmBh$?BY(iABHWHAEb2DqH2W#t=|ZNUgUIamKwGRABqqUDH-A8v zi@H<2BM{~LJq{~dZ?6TDLcI#rCN>^_EjteioqaKodh1xwRn9bWl1PuwE6w_efo#Un zqz5lSLg~PR{+lB&)ZGwbbUNq%KRPn*y8gQSIMuc0BdE12k0;UW6c6o~b9xf?QG0tI z{xNqL7#l5qjcyt}aAfD#p>74|h=2YEhp!C~H>#|4OSG>xaS{mK$sn{?!7$|dM{uCMc8gMJ!pt2H{30sCSes%zYTIU#$MCq1?}PW^$Jr zjm)1U%ZPk#JGS3nVc&;Crwm_IJ-xp-xZ{V$T|IYZ(z5>-X>T45<>LR1w>YA*q=oFF z5SfT!tf5G<94cg)G9=kDwk$)kW#7iWC0io08_TF8hOw_>mt_pbzK_BD?$J4)@A*8> z_mAhget&fNqjZ(~y}Y*fJ?KYn1|+Y%xq>5NaeHhMLC@6m(28bmmbBS`$F*SP1skvu zG`W|3k$o3OTMlF0&`}=QXWFAMV?NBwfW$F*it`Rx#mEgPc8c?VLJb2Z1WvZ^|t!a%X#yoBDui4 zIHrL60Y4r07Sa@yY(J%*is`7}TrS_P$mIIz7}Z)OR#;n+A*%8?zLFl#X|t6fF$cGU zZzqMK;gR{B6D)QMWf4q6y*j3z%^BMwg#Jo`-u`uOJ96G+QE_J1$D$e6sf-ypd|4x_ zk_%!SpGeGb@7q3_eo?jB#>9Xtml5V9jxuMOuwZ&Xg$)IRjkYtgrW2ZQkITVn;GchD zOpJ!#0H3TA(zrdF-^QQk@3eH#e458GQsF32?(G|2d#p=UvFW~Y8n~2EaIJM)Zu)4U z${WHmDhQJgdoh^9Gs&hAR|4x#SS^G}RQ3gR?F4omJzJ}nU(Vq|3=c3TUOL68ABM9i zLS{s6CBVb+-%C`Wnc}f58R&8*EKK}!-m*2TP2NPI8EVIp0w>Jm?u2P7q1LUHzT$=q zU&vv^SBH79?p?0`{1VO!lPzVCsMHt3&=JeElJ2C;-%jUjK<4ZPaN$bZjf$*?uf&v? zj_NVW4OTF&3=hV0!&*czjFu4Che~mdMSSx|({`o9kqYipYC^?9DMnh%ClB?$D_*)m zhYpaSIe$|1JM(Rt^S((c&o>FhVW}}Uc;YS9Nxuq5^_Qm#iXv0lBnZ}DHYH`|1fcA? zFAa|WcMO?BGtKf;-y8K~ujrt-^lA?2&#`{k(mh z4&2uHlfidi75;L*6Z1U>T@*`$-rm28gPL6h>ocBA&(Q!Uz;E7(D7LeQ4Rak0_)do7 zimGjjO>>PjBzYzT9-DWSxHP{i(kk-)Y^()N-Ih0V^ZBB|nCac|scyxDv(fy@-0Lmz zt*e)a6v;~Iv|@aI&XdCext9fArCKakP`O{PA$=D8n2>_VK5m1;25U`}Ni30I?+$8R zSi5kognXbmJU#s-#fPkPYp}0i#bo_@2U*EcJ?ZRVIcF@dozlj<$jW@r=|@gsx}B~I zXIw^FWK$np+v`eKFlHIFw9N=3&5u2Jv>P7%_^RBTXC7B9$^4a1VihDPOAGC`OUkPU zewZ43-E$_wN<2;*k$7FHL5o!6UZx|UU0QU>n=kjL4A}HZIqQ2)%NE$I4H-snUjH`# ztU>!cL~W~DlUgV(LkB(qrI#8K8w=8(Ac!Wy=Z16Vc_%BkKls*xulnWB6xoM8rj#l@ z?Na|vhbJW_HThroOy-lAlDw75_neOHRN%YgFZz-1yoC}#MV1?0Zw8n1ch;WZ)V0qi zZAuQPf==fOtG6{#OX%b^Ug;RVnK_pS+H$C9Eu)rP8_3Kk&=O#pExE5^n{w{!)ns+` zN!v^BXOspsUfJf2s1`HutYt~J@wVM^LHq>I!8eEK4{L1Rpn~I)-M(mzOSycZ^m}P_ zF-OtuHJ?pKC<_VF*ZH^%ba0K_7}M~y)!t1o7#}3&<-C0hhxo_X(jttqzuNmv9OM{` zXHLrNC9`FEaiDH+bb5#tYiirYG`i}%Y7QuJtoRif4Jo$XcHsaZ6N;}TO*i#lW*!k!R|MqaO=JBhui!9_$p_t(vX~YyNd@u-vLR^(IAij8nyZz{60m75<)ZP-@*gz=7qqu@ptOH2N@ zDkSNK?N7CY731#u(s}*AhQ$s)YI7I`opx6t!6F-`-dsb;XO0QRW?U%LR6>i#+~nB8 zQnWcXK?f~vc)hvM%)=LUCphAxK$X-g|Gx_W8u7w91!z_sXo>40z<3wfYw{Hw(*~s8 z+7`Zvm?h@}K?gVc|9|b;4Wg~_+RJ!@6aBA_y?FmbUT*6(sEH<|fuZQXMmeZ%Z|#9> zN%-Cc%UW^j{D1xzT}wu*+A9{UDm)i+vEwt-|J1=Px_MTh4l2qk=f%FJi>N-@|6K_V zKqW*9@;R1?3Z7XU++J_V0`=W@2O8`{7X3uOso&Dk851zeC))Zt`OY<+CnboBuT1zX`@$6%lw$fj#N8=}>Rini zMn48g6&G_!T$=v`)CFktTm~i*6z_dBYtxkg?amhSB3p#z!TKT{!m_d0>`))C)pD2w z^!#$zbQGQbExMxP2G%7V%G*wB}xED{xPA$wH9C#O7isgV8|Q7l$+vOTN)dT(bj} z+~k|ZTFuWFs|z^z2O5?l2R1L1OHto~<`2Y&D8|Hy;I`+a+q z&eU0>Fu;vo+;0iyb4t(kn78~ciGPJUp89ubX4NDt1U>}Kmd~hxxA~Zcoo`ubyGqgI zp7hm-X?45_(H684S0TNNQjv!jdoF2=tRvO>qE@gBS|G=YiAoAUu_O93l=tgR&j(q*E|!BjiRSF& zXr4+E5S>n96-2@i5z|1+!x=B2^9wR+-E~wT(|lqfpqDKTUTT=?JHQb4A0umz?xq`K zzck<24m??8FuRu%fwV@{O4HP+ee6K#c`MG%aaHqqhT5W6#=cQb5`}3Z^ySdL0ONw^Ya-PpBf(|{YFWdO)lp|`9^2yJ;>T6!f zPeiPEqU_3I^MmbGVnS;#^?AuY%|)H2Tgy9ZF>jQzWI0)A|F36u*MM$~P?POL`X-7; z2Q6@V?tPizmkw-qf3^r@fRc2UETr>oT#kX+jo=eLV#dus1Dzg-ZC6KY9glQWd+TE0 zckOGxJc9;Y6a_)tJoMrkD(s*#+J`Zeg+D3Hdb6QszufJM<~I%z;fQL}TV#&$4Zb&noId0x#0R z)X>dF>8ipX3HLTH;6qZr5Qb!7lDHcN4WC3R=<#S&{mK#2&com^pmKDvy=iptXhgJq zU_NQ}j?k7_SL9D;4PqxQ`7I0a$+$SjW~eL6mEr>(N~q`2GK{gSMN|p-LAe)Sl%bOl@iP(Tcxs2I!ot{ z=L*_#KFDPc#$Gkq+BpzYGcSa ztPn`z)C6|^6KZ=q%q*~ujZK|ytkdn0-P21#h6{~`^L2}#vg?XF@cIdo$&NQh%`Ojq+U?UhnFy*KRIeLj`!>bavniBb40C#k zt$UW!I0T;vD8#DGdHu>jG%qWyn|=tquc$;GL?P`a$fS>UA&jRgze+Ft+E9$R7rIz9nnG|oIEOyjj$T+?Z3ZCA3lfk8#zRujrN zpvMcRyyW-E7Ot}{r6t^mACn1n5pT+EO_pu)5mmR5rtzYiFM03DjL{InD1=7pGX6yx z%luQ~>b5c?4%pS<*^d1#awE5&&K05k=3W4kggN9z&PrgJgEs)wzNR#OyXcvHtrcYF z;ts&{=b1`=ys-joW2?D(R_Hky?1I&}cnXW3|9xg`hSyBbM(A*PpFccRz`O6r&z9Tl zIx0m)5jJB6?Edr`H6f_8nh=4)08Fn%{_Y)<)-H8^xibdDbPS;j$|I&}oh1~{T91$En4c);kZZiXot1#C7 zT7_=S?`~j9POI5Y+v4Al`S^=-@Clwy%dqa|i!PROmcl#TncfjhBiYSS9({TQzq?}l6PgJBljFpnXp&`WaOB{<2IQ3!OfZNvmt%grG;2pP! z0CJ{gdq3&DAzDJR>pHS)(gicLF#ASL1)@Q0JaON03RV>s-P*&bLTa`#0A98$rn#-B zcnGu2i-;(WWLgwm#_feni_go96R-Cu!uA7H6(4#wOG z%Dep~eU0FG(#Wd3$D4n(v=OKO8Z2PdDO5z2qL>sWNEAT&tfL{T3B_K}mR<=+ybN$P zf(8Xx8>glg;b@5R{O|R^e#+}^{K&=&BW%9NsBo-D zsVVl)Jam8Nqs6&$&06cPo62T$f>416r~!s!reIC8^-^*p|C(=`mo>-8Pz32y{g%;X zxopt#S9`ig>?bEZ3p8fmY)lI+_=|_@Dv~Z@Q47rtCyP8_+Lc-`g-W$zY?93+fbT*W zTr8+skk&wgIPgz;%m=J_a7+m)oWVisQD3(B-nL!Xor$q2KYo8_tcFmK-RjZ~{k#)L zoBh-Z40`hUI<@m&z#Qu9SGQA#8vcdCx3FUrcA;$nMU4%q><-bu+x_@CZHSG=$ zFs5e$(ePYCa~- zf6FfdG;3>NXB>@Y8%qH=T&aI!+oBekrH#l}P3rHkPH^peHt93%3CsI=HOcZBc-m{cE}7P<&SzKUwCaV%DQ;0EK1;EhPDJil{q+$IAC%%NO{mNAg4JMgs=;Wg=7PRC zF4t$Fs;%bWyg4UHJG^upj%SNRxPDV|(H0D{OMn&avNF{kB}tEij%vHEbag9P0J-1T zTIB;YajsU~`Hd6NH*3NQeiL3Vno+xMX_M;87~?UJKhH;zn4|JO02&y3E*oG)Y~O~+ zG}B|cu-IHV_t(}#vYmDnL9@uwZ>kcS!n@n^= zIF-k@mi#Pjxyx&X2R9~bH+&b`T=R;!P zzlM;IfaXUJz4l`jg*1beO|3!8=F56+gNr7+SZ{ngAPHES>|=vL%XuZ-93m}AvT!sK z(KAyUj1Eg+KkdQE@4?+v?9(*Yj&!eh7*vAa`wb2Dd=YWv?`Rh>%R^Njx)DIviKMQm zD-j&RZC6#xi%T=L)7Z+tZh#J-)&EEO0}$M~Q)Jyl=DjQbs=k+JeJdQitqPTZ5dz-l z&UIRvDu{?2qY8idRJLe%Gd~3e3%o(qcvL^aXtso3(*=buMw#ShNmo*ZKTB<(Ol|~x zmyBuc+0_7eButLr&>hIs9Ge0~;?0PAZ)bZFFn`U5qr?M*FG?EuIb9U7s!uWRv(QSL z_Q4*}#CM7*-;KD;Qm4GvTvjZWETcW09N)-jv58VWQy#j(eMD|6uqST!HSVA)&UMK;QqC2@pGdZeMGs#^#P5k8^q*CRrM#p=1IvRA`w6lvIZ5>h_Rg2QHOV z(>hYyreMi(g_1$pU%6l|{RZa`kNRFAy0y#a&bhpg;@A~H`ejGxV~z9<^?3|Zg+ky{ z1j?W8O-ceTnk>nY%quoLvKKB>0i@cU;9sA~2R76tv$aXW8%%w98VzgR)vkRwckc7y zk8~(Y7J5)z1rl9C)aE_L9vDED5DiLozkMC;U_$u0Hr&==T4=i1`K+EQ57hs3x`O*v zQu6+&jY=p!BygMtbMk*~dvCQoluAE(LpJOyVHyzhiXDzHtF6ek8ZXeCF+mUf(va>| z%BR!h%57(DEzEOlwG9h@Q^QK^IDtBTyxQB%-_uEwn3J9-U%~!+?BuI|Txk2o>G^KT zlQ)v|p#r#dy#L1nCIfHlCzvZ&u}t&Zlf&wmLZ!+f;s!{S!<)D*+9gNF;MAzHCgE+G`8ycUMJ zo9<2F^V29d*Sp_S8iT%iM%Z<<#NuVKb#+# zz|26I8cWj@wR7_MV$ay9y%hZrc)YY7)W^%OE6P8qG%vTx+~SaT?$Jjse5GSjv`pz(tSwCN^2?$faO=DNfeouew4om!I%Vkw(}h zN{xS5Raze}k(@n!V3K^&>NA>tm|a!sAwJF zS@=u{+;r=l&>JP|yTI3+E$8l|oE*HeQXMlE zV?B69&E!a>_xN{tQmIV!*XEOlvugE-zoXa%E0aX{~i%!EI8n zS=p}~(>lF|So`%?3qAfrmgP6gI;SRflffxMeJo*|b>HeESadHS64jX1aAG?4$K8nQ zhuR#=d{+l)WCM4qj^;1g&nytK1oO>P+vI;?031P2QZ`sKM3Q8r1&2O951V&KHgrC5*?)_z zWA9rJ?@DBFTFHPcFj5wGKtGXb!bj1oOHW?0Hp|ac z5=QvdiXvsjEuguX=+!YN`=fe7v))wOyAs-q$EwgbKe}I%WfxhSb>z^yJLGyeDnGp3 zeM4pzg;o)V<7M->{QFe8WKLzWj>j&#o=xIJJqpp~X5WOsN*Q=qe9{Adh6@kJKkM3d zJjmHx*;{w)eHqKC=VYf@yuh9+7)&U1gMqbh{o)k67gpXoS59$?jQvtuRi*_M3nV1C z`S}FWINq1x!4?zWSHJ&+$EA#;$%=ZmrL0hs;hMR*K`~tuGgbc$4n^0{+MN-Z*7{p2 z?rGx5Hn9atr@{z<7X7L9IhDJ4i?hELuWY$b<8}hj>r9TaygC`I|1U zM00nC@I$l_58;wZ66l-2#KM#lI%(AECm70^(x>^)d>x@ zdiNtXN$Msd0K8+KH!u20;K!mtO33~ILq-%;aZp%+!(Mrg_ZZd=X2g-Z>(XQaF#GSK z171b>s@JoiW}}A+2#-F>W-1n(*?XH54`q#f3VJDBTOjj zXx7cIPJAg)2+`xrYRpT^v`S&w+pmmuZuB>^g@xDViklx<`V^Y+oa9I1FrA`uY9AMZ z9$*&!k8)!;r3DSdkDO*Zyvx0L!Df=-c~$*FJ;%N+1elE9SMKxPn>!y}gU);R zXSCJs7QaZ+xpPWR>jp;n{i+!xi6;62_RPIlfp78;$L`L|#j9K1L)KV>%_S|RwQ#@C zTBU}pe-}~7LE$*_8sGPlzZmT2KPtNI{o1j{{}n|8PQTlE1M3JH%}%YU?woW{M`fDD zw4T#LiK+=Dyn2iy0&YXS(i6d;MYE2VHh@*A`hsc|MrSD3G{n#pjOHFHx}fKV@<%fs zpEBm600T=5y|lSXDPW3B(uBLE_-|m$j-I-YK(xUar}v@~H)dnxwH1W<{~V*kQ-uf~ z*YOlOXK{mE=9=pxe~$_j*+2r?UxBbcer(6`BD;DKe~k&#e4^8>baq{th8s29%(q6iP+VOVt?cZ!@l7ZX1v<#B;f81R_{i))OBLAhO^oTHW zI>Cg;#rzS+kyb0HZ;+=A0|0`T9KI$j)i)eCGQBD(ly4IZ%yjiB$pVVJu{Na-gwWLW z$-VExFW^u^t8?A2bx%Ac*Jd%%CnIi!nYENNpTIorcZqbSS>aS%}oX<}8U(f1q&HSW!XY}5N=3+vv`Q7XodvkBsnzRgM zS*95CaxdwzymnlsV`5)J4GespmM)YKs(-VAJn7~9$TX(5%M9|Qh>&xhHfHP=SBsSc zX^bJQ!E43|kyiyZz zYh^C3-Ien>UN-g`GN{?BGtss&h$b+_Qj6{tifYL%cixIfKOdZ$Jj+X#4EE@gdVBWj zqh0=&ea>23o5hgYtqmsr(8XxUIY zAkN=Tq>A&lvQW&Mk>DL9DS0MW!yB;X{J!n&^royhpx1aJbA(#Q_avl4kW<|EHf!zC z90SqUaQz!7j)%O+Mn*v-k-F670U@_>VOCR3^#feMZR+8J?GgGghaV`deMwGG5pR-)kIau;y2rR@8;!N zhpnx*8Ruy?xKQ4C;X6^HO_KvY>ll1-(h}veQvR)39{4}A6)2lFbQ8QK?Tr33(J_rf zb+qm1BexB1zYIR<%rM?>Hq%mZFM)WHtE`O-L^o`U@&O&{_Coy=Q$%|Q3i5;Y57@n~ ztm;)zarGC-9ZR$Dg}r9r02tB*^%&sV@8GxOnN`{#4OevroUg1mrua&V0Qrs{U*%B` ziA-?wbK5bg@C%zDGHiPv&~@t+U0)81pE_Xfe(}<>6n9wqU%R|Ux3p(|8Nn$I*-BSZLy2~XvMhX#O8J1&T!D_^0AfQ8juD}wRl>x-&lVj!!n6!?0-b>ObVP?E+CSJ*cGm1 z5UDayd*HGxRWHa}B+39tbdUMFVNYHxc}wWDvI$Fb4yTJ2c=rO9Y!E+waIn`pyLI^* zvdk1ZdJX9mW=(kl69c>fDB;s83WqGKy2r6Bf(a_mX{g!h#fjn|Rh3c+y&27gw4X{it>K%GG)!0@p_~C>Sjq?MtZuf_w`r}N%-vagk-P)9)O>5Ab z$`^ecdEy_xDPXu=UA$(S@1|OdcximxoNhaDtLk?Zy0V#+2=k z4HIb2i*uBg)Msz3#LoM&_*S-vXidqxufMGft$!$V<^?`~y8O*0`$zcq1{n2MjbH5< zQIM+4Nec!=1Ln^=8GH6gYKwpr_1jV4s7{Kk5BiQZKr6b+x?eu)6vPxSSYXY?f{_|d zh38MoA$q6E*LB5J-0713B#{Py&-K~*b%_MvJurax=5CL~4mT9sVa4bk8;7&GXbIW4#C$-_U9y59K*O_@!-6*nR^;Z)mQ=hFa@69&Jn8lhy?0H1q6^J?#I+RW{pk>#_2}H_Bg^J@bC0^nK;gXd+=MT z95b!5hvoN$cI*3!Vyv%m!d0i_T!xAcCUBZYA2Lgrn4VgF3OZZ>RjDProD$Q)rJI20&#)b&k?%%a={H6WW zsf{Va-5{nmJ+bW`vDF;zMMck+9IX(hd?rsL74hs&$$k>^YTNa==QCweNyoKh%F8RA zo8yz3J}0+uUUsZs&2)sCsXE_^e+8o_z1ED7kq~UIDB$a+@$J^J2d^7pT=NCeQ`^94 zG?^w5K55SvsrxPLmUP2xZeoA%fD_^YV!76yaYP5V3!pvtVM@(J_;8EsI0ebpt9T!Z z`34kZ(23@=bT{hD2{zvFIE4Ai|t+hed~dc&(gZ|m`ySCEDLrx!s6E% zfB3#dnuYUxR8+*CP>XU5#)xoPww(R~mT60xntW2%76KMbyoAQ|PU|n8fYl8A+tAiQ z9(Qxm=I^7G4PS9x)dR)ewU^pV%*7WlMS*v5e?w2p26IS@qwumWK_+ekxz{V`)#AAz za!GZO4F6C;WGjP%aCJ30!kdozo?QC-n!&}(dCQ96r-`8Ly&MrN{z*{)aj`KYH6_QV zA0aM;p*kG|>v?~wOevK+!;vA*F-ub$-f+ZZzcO7ACx`^x%cxs*`*zYk3j@CZ_fWPa zi#nmAVSy56`k1J#sj}$ht&Ren47<9A$;dp0n32%cHZU0EHx1q$%Os*%%zFk}T#n zUI9&agWkHkM}mFrZ&Vcq1;>h)H-uZ63DG(E=!aC8J$bZW0`$4mDbw~iVSoGK;(z#R zkrTjIhk5#8#|yDwZWT$Y&aZEF92^8>(7%qHw8BwGK#cNIqzRtNTHm|{qB*e>;o`Km z%p6pF74;%rjlLsEc=PomjI<&Q+(Pfl5vll$xn?r|zEH!|Dvy|5)yje7SoozSJ{W?t z&-YINUq$~HzIsW;S697Xj$b&XrF2jBVcXb$X>~+qs&9_tKCy6SBQesDY=5GDS!;E? z+p4Vxs4Q2|DLx6S5B=fkaD5IJREeEGoY&k^nx7YXPe)Zr+l-S^s{YzcO-NeBeUT#4 zZBs)V8U4cx_0u-FC?9r&fF!juf8cwwOjd&LIfT)11E_xXJ})zNy{_<_j?kH;F;in# zW7a{h3<^Jp%Z-^m%Dq40L{a+=maNQNbTyisDg`J`7 zmW$KEW{RxC5`7|0*Mg}L(++V-CNAFYE!Av4WP8rP;YKZs?-;AS9*l=0L=4C z^*LSy>b6I}bhPoye>hY7olHc=V!GUt621M(+7CyifJVN|I+?h_hGD1rHV#WRek4+ug$dd@FK>*qA|b2^XJzX1^j!E<^GIy1OHpXIUwa_**!fxFbYu@tmz zy;p$Nu9n@&0YP~|I9fTA#)1X}ksiG$wSI3#M;sTF z;>sWI$H_l(6_bz5`fLu?O4a{cB2&bY|#J=s!laajnzRsfmBwJ3LdZKj4mgnrnz0-zt z|D$FmUZueX!Mo^R&3{}CNbyeWtO%NY;&ynm}Ku?iifW5HJVO!(<4R=NH_eYGiSsKI*_q*&^UWqNIXyoM=d!;Xp^-}_G})=+8IE2E~Wf9L3N8mIYTa?{QjV0ZnX`XMmjec*w8+T{iv zI6J^rLFYt}O*qXCb}1v{8wXwEPkq`HNWTOz?aC`=qb81Tk1V789a?(CEc<7YpDDVj z(n>%c;DyAGsZyG}3rgS}=+@WT@bFvGlpQNJZ~yOyLo*JSW>@2QGgFs2IO=+u@Jx+E;~9+0+rHKdeNY-NfZPmy9`kcKwxrKXz35oi zY}`L#bNZ?(*_b@%Ha2&z7-|01;Bu@tFnOHAsU-^dG&>^ksw?t|EYL{e7~6>1&YA7s z+U9SO3}a+Wg}~%4DXF=AAlel$9bMq%@WggKM&fD4C}7~w8aXxa7So53rH>$3&7Am-Rq zFGylLGaXQVib2jdgwG_+j*AF#1f|ks%5RZ;XZ0}vGZz5dwfB$g4K>mX6>u0}iE}y| zNe6vwj9^bL`hiHXD0%#EDYTpJok{7@{JYz^qh5m1eikd09^NYMzwSBh-3p$;#kVmV zv!3O_X##~eQz2qM1(WXcnq3^-;{H=t8vRd(51OOkDndBu|654EJs7RP+maRvi)#PN zBwP`kRt%F_zUTQ09S_U-TBnCWXv@p@nIRU7$1(}&A{{CK5~kCh+%V!$)Xfg_Fws{f z{0x}MVJlVgJYou>*=EZeBS5jYCv@*#Q3oKdc*NbcIpG-rw?^7*`%2X_e;lI{gDWF` zbe$lp{gzjHY8){wQ=u+TR1t3~yzJFlvJW9&SEWD%sn3!vSBOV4(&OHIB`R~fQn&DJH zhZpDisl6VTb;dOQKj5PaJyZdqR#Zf$`}_&tT%K!O*%o5Q0?j{iVjP_$;%dANQ`FXJ z_jdI{g!BpOC!7~~MgOw7^P6MI`QH7|9?bA^G7t=(%>^$4V{8}AvM~_PDQ>Ba=6v)L zz@br~bi}e8-{~aI?2TLiJOJFE3&Jg|ArWk9ghDXM&KjEQbuez@im`9x8FtJ zwy6S&BCa2xyBGTqGktV;KZ@rFn_r0|PExI13FB*;LUqLF*ph?dFUT~EIO$|ecw2uN z@A}Jv8ln6*;dS;sA^lwyO@bR&wgoM4D3WdPv^BN~E*T0VPG0@te49fH)ZB|bC3uy# z?{nD#m-9=8{!E`*@4HB>KLZ0EwGdv^3H1^ z^>tsxMDNI_+H#MgxZ9=xSIppfq-go5a>{T0E&k=z6(UPF-0N3=Q(gpvwu%Mg#xvuGy+YU{)h9q+p~x`Ee@N-n-mGA0uJC9?-y!k#4R13f|=wrXwc^7;frAPUq7n& zfo}eAP_xZ;l568A-if0?b^0y9ez%{VnlC^~z{!Z-DrR#R?;PE#0c6PVjxFGPpZb2gYB6iZ^9nW|Z|FTujqpC$`y{u!W9c?JJ=_}zM8VPH9*A#G3G{i2M%K=UJm{JD)$#(1 zz0%=*ejoj3cLH~U*3ZR!Nz)aJF4>pl>mD#kQklQOSig4=PfYo;%A8Z}1sF7#j?egZNi@QZxzyTzv7+^X^6Ezb7HjF#Zw69Ng!OJ?_1^|Vo z_!44%7Ptu^Z*)tbCXuC80^1d!vAn%**Ip|w&t-?FtN4QvXR$c6*ITLW)y?66!f;Ye zXkGF?5|)-B1y7aS1}%?rI~~Ahzbg6kjPL&yYWW#RvlGpX_6j=)=H-9h*sjivC(d~~ zG)yY&Y*fY2;T0)*u5XFlZbf_V1wK6AV`@?tYS`@b!zTz`fANT=ZwURExltM%0(x*6 z|LaFUrp_S3e?$L|%)`a)A%Vy?8RQQ^mJBF7ZxSx)A=o zV?DYoIw7`Ez=bF;IDhahSjufXO)s@(;%lijOv@AxB{=K`-o1Qk>@jU-U4y&~gX=x$ zMQqc;Y&Q@gkLggKHO7UQE=)IkdM7h6xrCa)_dxBXRNU#i)-w9Lk~5Y6jJA1)tj`hK z$%8vZkydj&I)P(S%VTLK901+}+U4XqqL{XzIuJ5JFw%OZA9YiVQzc2A&$;?9XQp6G|VeK$gVg@3JYuXVu0cF7$Or=<6FjxQ0<6 zDgwV__*sx1k5mq_kkOE^I{dj_w$jLvplM*ojK#~@#oR+kcrAnleS6i*5yQ7`2at_G zA|3F)s)nVgLl~WN*j)oBOF&k3J-p)w-5Um^<(r-t+A@KZGf+;r-{n-g^y+_`ZmfY) zQd|1;c$cjyxggyl9La-Q>pBuM_oOflCQB`vUzFl-bsm>PX>X&W;yH3DI0HB~9SdOP z5o@qzm|@P8zkg}~A~{-2J@U>@urNu1#8nO@Z8D82Bhjy=)dL+u*m$!=;w4ccbXDiBKXw*o$09^axl>?IXxKxcUl*w<2rxFHkn6u=oGGbiUz zy=maIhLFZ|%a@0}J2f;vf3J$`R`N*~ErGL&58uuBW3wg!`~ukt zENp?DD`g?c54pExj1QZUL`fX%n<$5W{`XCyw9b7@lhrz8<(0{b@B6 zCZT$QrV}2Ps<67x3aUM-=+STV)%9_ymw8qwEfZ|Q+#Ea+CBx|Z3WN`N#CVw*Qzdpu z@kuKm(g9teH(Px1|nIf zCS?LqTnL~O4E^-p6OhpZZi18eh)^uRV%l8ppxFR}2zgJ4daZyG;c=E>;1rB>u(gfL zBupN)-~J<=4qaNhpq>8gEEUu2PSh^W5eN^-0Rafr4C-G>A%P~GUa3!6&u$1fW4k-O z9$X6w+r9`&b>vCsNR8^eh{Y%Tczd&2hgM0{gp3t8xNdy7n86B_l`~DgLyMR?YrHq| zcJ2?kOp{Zv?R=Gw1t^IiUGBE2AF8aSyLo!kZ6#(_J`YDW%T$gWVLPm6I)EWtPg+M7A zMIcl{SLBhbY6_$Rl@~@BzQ{^T+%s+z~Jh zrKZug$K^CXuohpeeKW(4^b=B>CPnl6=?h;M4_|hucNrdkJ9E^Vx5@)pni!wm$}F82 zOf-{=u%A-1KrpAo;H207<=trFWY#r6lL|2SsCR`0@V^&-VRySW(p=+~78~|vc>hd2 zy6t^kIRbUtKF((mxQL%;`Fo_1FSku?ww*q8V|zPp<^fh8C`K_mgbVb%H8gLW)vBs3 znM&cvYbnQbu4Px(P+w`g@kc_sOwenum|QqYFBiD1W#@B-Wk15>1U$P@%N+ zF_g}rcrfo*?P1^S$7(kF-bm|x3rhWTI;(O!@xgyKj}qA&=Sl@Cax%#Y zS1!DoQO$00ZjbvfDf-Guc{+p3lWj$b;ah|q_3`beqLy zn+5}VJRGKFZ|JUkmd*(nkQuc#kUcM#>!Dw9`h&mV45|BnV5!rQ==MLjnQDEncmN
-wV4h_oTV-lM7X!oF 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 literal 0 HcmV?d00001 diff --git a/docs/ug/cli.md b/docs/ug/cli.md index 5fb3b4e907..e343bc014e 100644 --- a/docs/ug/cli.md +++ b/docs/ug/cli.md @@ -68,7 +68,7 @@ partial credit.
**`--config CONFIG_DIRECTORY`**: Specifies that config files located in `CONFIG_DIRECTORY` should be used to customize the report. -* Parameter: `CONFIG_DIRECTORY` The directory containing the config files. Should contain a `repo-config.csv` file. Optionally, can contain an `author-config.csv` file or/and a `group-config.csv` file or/and a `report-config.yaml` file or/and a `blurbs.md` file. +* Parameter: `CONFIG_DIRECTORY` The directory containing the config files. Should contain a `repo-config.csv` file or/and a `report-config.yaml` file. Optionally, can contain an `author-config.csv` file or/and a `group-config.csv` file or/and a `blurbs.md` file. * Alias: `-c` * Example: `java -jar RepoSense.jar --config ./config` @@ -76,6 +76,7 @@ partial credit. * Cannot be used with `--repos`. The `--repos` flag will take precedence over this flag. * If both `--repos` and `--config` are not specified, RepoSense looks for config files in the `./config` directory. +* If both `repo-config.csv` and `report-config.yaml` are present in the config file, `report-config.yaml` will take precedence as long as the `repos` field is correctly formatted. * Config files must follow [this](./configFiles.html) format.
@@ -253,6 +254,8 @@ Cannot be used with `--last-modified-date`. This may result in an incorrect last ### `--since`, `-s` +
+ **`--since START_DATE`**: Specifies the start date for the period to be analyzed. * Parameter: `START_DATE` The first day of the period to be analyzed, in the format `DD/MM/YYYY`.
Default: one month before the current date @@ -265,6 +268,7 @@ Cannot be used with `--last-modified-date`. This may result in an incorrect last * If `d1` is specified as the start date (`--since d1` or `-s d1`), then the program will search for the earliest commit date of all repositories and use that as the start date. * If `d1` is specified together with `--period`, then the program will warn that the date range being analyzed may be incorrect. +
### `--timezone`, `-t` @@ -279,6 +283,8 @@ Cannot be used with `--last-modified-date`. This may result in an incorrect last ### `--until`, `-u` +
+ **`--until END_DATE`**: Specifies the end date of the analysis period. * Parameter: `END_DATE` The last date of the period to be analyzed, in the format `DD/MM/YYYY`.
Default: current date @@ -289,6 +295,7 @@ Cannot be used with `--last-modified-date`. This may result in an incorrect last Note: If the end date is not specified, the date of generating the report will be taken as the end date. +
diff --git a/docs/ug/configFiles.md b/docs/ug/configFiles.md index abbecbdfbc..778ea09578 100644 --- a/docs/ug/configFiles.md +++ b/docs/ug/configFiles.md @@ -46,7 +46,7 @@ Given below are the details of the various config files used by RepoSense. | Ignore File Size Limit | Enter **`yes`** to ignore both the default file size limit and the file size limit possibly set by the user in `repo-config.csv`. | | Skip Ignored File Analysis | Enter **`yes`** to ignore analysis of files exceeding the file size limit entirely. If file analysis is skipped, all information about the file will be omitted from the generated report. This option can significantly improve report generation time. | | Since Date | Enter since date in the format of `yyyy/mm/dd` to signify the start date of analysis. If the field is ignored, the date will be set to the default one or the date indicated in CLI flags | -| Until Date | Enter since date in the format of `yyyy/mm/dd` to signify the end date of analysis. If the field is ignored, the date will be set to the default one or the date indicated in CLI flags | +| Until Date | Enter until date in the format of `yyyy/mm/dd` to signify the end date of analysis. If the field is ignored, the date will be set to the default one or the date indicated in CLI flags | The Shallow Cloning option is incompatible with the "--last-modified-date" CLI flag. @@ -112,9 +112,7 @@ e.g.: `example.java` in `example-repo` can either be in the `test` group or the ## `report-config.yaml` You can also optionally use a `report-config.yaml` file to quickly define the repository information for the repositories you are interested in tracking and generating your very own code portfolio. -The configurations of this file will override the configurations in the csv files if the file is present and correctly formatted. - -View this [example](report-config.yaml) for a better understanding of what repository information is required. +The configurations of this file will override the CSV files if the `repos` field of the file is present and correctly formatted. Note: All fields are optional unless specified otherwise. @@ -122,10 +120,10 @@ Note: All fields are optional unless specified otherwise. * `title`: Title of the generated report, which is also the title of the deployed dashboard. Default: "RepoSense Report". * `repos`: A list of repositories to include for analysis. * `repo` {{ mandatory }}: The URL to your repository of interest. - * `groups`: A list of the different custom groupings. - * `group-name` {{ mandatory }}: Name of the group. - * `globs` {{ mandatory }}: The list of file path globs to include for specified group. - * `branches`: A list of branches to analyse for each repository. + * `groups`: A list of the different custom code groupings. + * `group-name` {{ mandatory }}: Name of the group. + * `globs` {{ mandatory }}: The list of file path globs to include for specified group. + * `branches` {{ mandatory }}: A list of branches to analyse for each repository. * `branch`: The name of the branch. * `blurb`: The blurb to display for the branch. To display more detailed blurbs, use the `blurbs.md` file. * `authors`: A list of authors to analyse on the branch. @@ -134,9 +132,11 @@ Note: All fields are optional unless specified otherwise. * `author-git-author-name`: Author's Git host name. * `author-emails`: A list of emails associated with an author. * `ignore-glob-list`: Folders/files to ignore, specified using the [_glob format_](https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob). - * `ignore-authors-list`: The list of authors to ignore during analysis. Authors specified in `authors` field or `author-config.csv` will be also be omitted if they are in this list. Authors should be specified by their [Git Author Name](#a-note-about-git-author-name). + * `ignore-authors-list`: The list of authors to ignore during analysis. Authors should be specified by their [Git Author Name](#a-note-about-git-author-name). * `file-size-limit`: A file size limit for the repository in bytes as a single number without units, that will override the default file size limit. If not specified, the default file size limit will continue to be used. +Please refer to this [guide](./reportConfig.html) to get started. +
diff --git a/docs/ug/customizingReports.md b/docs/ug/customizingReports.md index 8192d00445..64d9519259 100644 --- a/docs/ug/customizingReports.md +++ b/docs/ug/customizingReports.md @@ -24,11 +24,11 @@ The report can be customized using several ways, as explained below. ### Customize using report-config.yaml -**Another more user-friendly way to customize the report is by using the `report-config.yaml` file.** In this case you need to use the `--config` flag instead of the `--repo` flag when running RepoSense, as follows: +**Another more user-friendly way to customize the report is by using the `report-config.yaml` file.** In this case you need to use the `--config` flag instead of the `--repo` flag when running RepoSense. - {{ embed("Appendix: **CLI syntax reference → `config` flag**", "cli.md#section-config") }} +Please refer to this [page](./reportConfig.html) to get started. -Please note that this format will override the configurations in the CSV config files. +Please note that this file will override the configurations in the CSV config files. ### Customize using CSV config files diff --git a/docs/ug/report-config.yaml b/docs/ug/report-config.yaml index f0c06093dc..100008bfa7 100644 --- a/docs/ug/report-config.yaml +++ b/docs/ug/report-config.yaml @@ -1,21 +1,28 @@ -title: Test RepoSense Report +title: RepoSense Report repos: - - repo: https://github.com/reposense/testrepo-Delta.git + - repo: https://github.com/reposense/publish-RepoSense.git groups: - group-name: code globs: - - "**.java" + - "**.py" + - group-name: tests + globs: + - "src/test**" + - group-name: docs + globs: + - "docs**" + - "**.adoc" + - "**.md" branches: - branch: master blurb: "My project" authors: - - author-git-host-id: FH-30 - author-display-name: Francis Hodianto - author-git-author-name: Francis Hodianto + - author-git-host-id: jedkohjk + author-display-name: jedkohjk + author-git-author-name: jedkohjk author-emails: - - fh@gmail.com ignore-authors-list: - bot ignore-glob-list: - - "**.md" + - "**.in" file-size-limit: 2000000 diff --git a/docs/ug/reportConfig.md b/docs/ug/reportConfig.md new file mode 100644 index 0000000000..73f5d1e909 --- /dev/null +++ b/docs/ug/reportConfig.md @@ -0,0 +1,234 @@ +{% set title = "Appendix: Getting started with `report-config.yaml`" %} + +title: "{{ title | safe }}" +pageNav: 3 + + +{% set mandatory %}mandatory{% endset %} +{% from 'scripts/macros.njk' import embed, step with context %} + +

{{ title }}

+ +RepoSense allows you to easily showcase your coding portfolio by generating visualization reports of your contributions across different repositories. This guide will help you set up your first RepoSense report using the `report-config.yaml` file. + +## What is `report-config.yaml`? + +The `report-config.yaml` file is a simple configuration file that lets you define which repositories you want to include in your RepoSense report. It's designed to be user-friendly, especially for beginners who want to quickly set up a code portfolio without dealing with complex configurations. + +## Quick Start Guide + +### Step 1: Create a `report-config.yaml` file + +Create a new file named `report-config.yaml` in your project directory with the following basic structure: + +```yaml +title: "Your Portfolio Title" +repos: + - repo: "https://github.com/repo/yourrepo.git" + branches: + - branch: main + blurb: "Short description of this project" + authors: + - author-git-host-id: Your username on GitHub, GitLab or Bitbucket + author-display-name: Your display name + author-git-author-name: Author Name Of Your Git Configuration + author-emails: + - emails-of-your-commits@email.com + - emails-of-your-git-configuration@email.com +``` + + + +The `author-display-name`, `author-git-author-name` and `author-emails` fields are optional. + +However, users are advised to add the Git author name `author-git-author-name` and emails `author-emails` in their Git configuration related to their desired commits so that +the commits can be successfully captured. + + +### Step 2: Add your repositories + +You can add multiple repositories to showcase different projects. Here's an example: + +```yaml +title: "John Doe's Code Portfolio" +repos: + - repo: https://github.com/john/ip.git + branches: + - branch: master + blurb: "Individual Project - Task Manager Application" + authors: + - author-git-host-id: johnDoe + author-display-name: John Doe + + - repo: https://github.com/john/tp.git + branches: + - branch: master + blurb: "Team Project - Event Management System" + authors: + - author-git-host-id: johnDoe + author-display-name: John Doe +``` + +### Step 3: Generate your report + +Run RepoSense with your `report-config.yaml` in your `CONFIG_DIRECTORY` using the command: + +`java -jar RepoSense.jar --config CONFIG_DIRECTORY` + +{{ embed("Appendix: **CLI syntax reference → `config` flag**", "cli.md#section-config") }} + + +Please note that the above command will only analyse your commits one month before the current date. If you would like to +specify the period for report generation, you can use the `--since` and `--until` flags. + +`java -jar RepoSense.jar --config CONFIG_DIRECTORY --since START_DATE --until END_DATE` + +{{ embed("Appendix: **CLI syntax reference → `since` flag**", "cli.md#section-since-date") }} +{{ embed("Appendix: **CLI syntax reference → `until` flag**", "cli.md#section-until-date") }} + + + +That's it! RepoSense will analyze the repositories you specified and generate a report that visualizes your contributions. + +## Example Configuration + +Here's a sample `report-config.yaml` file for a student showcasing their work across several projects: + +```yaml +title: My Code Portfolio +repos: + - repo: https://github.com/RepoSense/reposense.git + groups: + - group-name: code + globs: + - "**.java" + branches: + - branch: master + blurb: "Reposense is a code contribution analysis tool which I contributed." + authors: + - author-git-host-id: lyuanww + author-display-name: Li Yuan + author-emails: + - lyw@gmail.com + - e0123456@u.nus.edu + - repo: https://github.com/lyuanww/ip.git + groups: + - group-name: code + globs: + - "**.java" + branches: + - branch: master + blurb: "A task manager chat bot implemented in Java." + authors: + - author-git-host-id: lyuanww + author-display-name: Li Yuan + author-emails: + - lyw@gmail.com + - e0123456@u.nus.edu + - repo: https://github.com/lyuanww/tp.git + groups: + - group-name: code + globs: + - "**.java" + branches: + - branch: master + blurb: "A patient management system implemented in Java." + authors: + - author-git-host-id: lyuanww + author-display-name: Li Yuan + author-emails: + - lyw@gmail.com + - e0123456@u.nus.edu +``` +### Sample report generated +![report](../images/report-config-report.png) + + +# Advanced Report Configuration + +For users who need more advanced configuration options, `report-config.yaml` offers many additional features beyond the basic setup. The following section explains the complete syntax and capabilities. + +## Complete `report-config.yaml` Syntax + +```yaml +title: RepoSense Report +repos: + - repo: https://github.com/reposense/publish-RepoSense.git + groups: + - group-name: code + globs: + - "**.py" + - group-name: tests + globs: + - "src/test**" + - group-name: docs + globs: + - "docs**" + - "**.adoc" + - "**.md" + branches: + - branch: master + blurb: "My project" + authors: + - author-git-host-id: jedkohjk + author-display-name: jedkohjk + author-git-author-name: jedkohjk + author-emails: + - email1@example.com + - email2@example.com + ignore-authors-list: + - bot + ignore-glob-list: + - "**.in" + file-size-limit: 2000000 +``` + +## Field Descriptions + +### Top-Level Fields + +* `title`: Sets the title of your generated report, which appears as the title of the deployed dashboard. + * Default: "RepoSense Report" +* `repos`: A list of repositories to include in your analysis. + * This field is an array, so you can include multiple repositories. + +### Repository-Level Fields + +For each repository in the `repos` list, you can specify: + +* `repo` {{ mandatory }}: The URL to the repository you want to analyze. + * Example: `https://github.com/RepoSense/reposense.git` +* `groups`: Allows you to define custom groupings of files for more organized analysis. + * This is useful for separating code files, test files, documentation, etc. + * Each group has: + * `group-name` {{ mandatory }}: A name for the group (e.g., "code", "tests", "docs") + * `globs` {{ mandatory }}: File patterns to include in this group using [glob format](https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob) +* `branches` {{ mandatory }}: The branches you want to analyze in the repository. + * For each branch, you specify: + * `branch`: The name of the branch (e.g., "master", "main", "develop") + * Default: The default branch of the repository + * `blurb`: A short description of the branch that appears in the report + * For more detailed descriptions, you can use a separate `blurbs.md` file + +### Author Configuration + +Within each branch, you can specify: + +* `authors`: A list of authors whose contributions you want to track. If none specified, all authors will be shown. + * For each author: + * `author-git-host-id` {{ mandatory }}: The GitHub username of the author + * `author-display-name`: The name to display in the report (can be different from the GitHub username) + * `author-git-author-name`: The name used in Git commits + * `author-emails`: A list of email addresses associated with the author's commits + +### Filtering Options + +* `ignore-authors-list`: Authors to exclude from the analysis. + * Specified by their Git Author Name + * Useful for excluding bots or automated commit accounts +* `ignore-glob-list`: Files or directories to exclude from analysis. + * Uses the [glob format](https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob) + * Example: `"**.in"` excludes all files with the ".in" extension +* `file-size-limit`: Maximum file size (in bytes) for analysis. + * Files larger than this limit will be ignored + * Overrides the default file size limit From 6302ad1ce38c7ea7ca074b0b39c3d68cf02e9338 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Wed, 2 Apr 2025 17:10:44 +0800 Subject: [PATCH 27/42] Add more details --- docs/ug/reportConfig.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/ug/reportConfig.md b/docs/ug/reportConfig.md index 73f5d1e909..9b9a8c8f5f 100644 --- a/docs/ug/reportConfig.md +++ b/docs/ug/reportConfig.md @@ -143,12 +143,15 @@ repos: ### Sample report generated ![report](../images/report-config-report.png) +## Further customising your report -# Advanced Report Configuration +Optionally, you can provide more detailed descriptions to your report. Please refer to this [section](./customizingReports.md#add-a-title). + +## Advanced Report Configuration For users who need more advanced configuration options, `report-config.yaml` offers many additional features beyond the basic setup. The following section explains the complete syntax and capabilities. -## Complete `report-config.yaml` Syntax +### Complete `report-config.yaml` Syntax ```yaml title: RepoSense Report @@ -184,7 +187,6 @@ repos: ``` ## Field Descriptions - ### Top-Level Fields * `title`: Sets the title of your generated report, which appears as the title of the deployed dashboard. From 08f8670d291bda33291580f6e9c25ed588b8c30d Mon Sep 17 00:00:00 2001 From: LiYuan Date: Wed, 2 Apr 2025 17:17:36 +0800 Subject: [PATCH 28/42] Fix build --- src/systemtest/java/reposense/ReportConfigSystemTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/systemtest/java/reposense/ReportConfigSystemTest.java b/src/systemtest/java/reposense/ReportConfigSystemTest.java index caad82b934..cce5ee6eb5 100644 --- a/src/systemtest/java/reposense/ReportConfigSystemTest.java +++ b/src/systemtest/java/reposense/ReportConfigSystemTest.java @@ -121,7 +121,6 @@ private InputBuilder initInputBuilder() { return new InputBuilder().addConfig(configFolder) .addFormats(formats) .addTimezone(TEST_TIME_ZONE) - .addTestMode() .addOutput(OUTPUT_DIRECTORY); } From cc52c77c65af524af8ea2acc66e4dd1c877da5e0 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Wed, 2 Apr 2025 17:58:34 +0800 Subject: [PATCH 29/42] Fix systemtest --- .gitignore | 1 + .../expected/reposense_publish-RepoSense_master/summary.json | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/summary.json diff --git a/.gitignore b/.gitignore index 15049361d9..c73cad899f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ *.tmp ft_temp/ +rc_temp/ /target/ .gradle/ build/ diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/summary.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/summary.json deleted file mode 100644 index 92b9e723bc..0000000000 --- a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/summary.json +++ /dev/null @@ -1 +0,0 @@ -{"reportGeneratedTime":"Mon, 31 Mar 2025 13:28:49 SGT","reportGenerationTime":" 4.32 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"1970-01-01","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-08-02","untilDate":"2024-08-31","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense.git":"Publish branch of Reposense","https://github.com/reposense/publish-RepoSense/tree/master":"My project"}},"isPortfolio":false} From 81edf8d52c4b6bded4dbf45a1be9e4b642ba20f9 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Wed, 2 Apr 2025 18:43:08 +0800 Subject: [PATCH 30/42] Fix systemtest --- .../java/reposense/ReportConfigSystemTest.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/systemtest/java/reposense/ReportConfigSystemTest.java b/src/systemtest/java/reposense/ReportConfigSystemTest.java index cce5ee6eb5..d3805fc8d1 100644 --- a/src/systemtest/java/reposense/ReportConfigSystemTest.java +++ b/src/systemtest/java/reposense/ReportConfigSystemTest.java @@ -81,16 +81,6 @@ public void testSinceBeginningDateRangeWithShallowCloning() { "ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected"); } - @Test - public void test30DaysFromUntilDateWithShallowCloning() { - InputBuilder inputBuilder = initInputBuilder() - .addUntilDate("31/8/2024") - .addShallowCloning(); - - runTest(inputBuilder, true, - "ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected"); - } - /** * System test with a specified until date and a {@link SinceDateArgumentType#FIRST_COMMIT_DATE_SHORTHAND} * since date to capture from the first commit. From bf85c1f2882ccfcffc272ea797d72d9f0f518b77 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Wed, 2 Apr 2025 18:56:41 +0800 Subject: [PATCH 31/42] Fix systemtest --- frontend/cypress/configs/default/report-config.json | 3 --- .../{config => configs/default}/report-config.yaml | 0 frontend/cypress/configs/portfolio/report-config.json | 3 --- frontend/cypress/configs/portfolio/report-config.yaml | 1 + .../java/reposense/ReportConfigSystemTest.java | 10 ++++++++++ .../reposense_publish-RepoSense_master/authorship.json | 0 .../reposense_publish-RepoSense_master/commits.json | 0 .../expected/summary.json | 0 8 files changed, 11 insertions(+), 6 deletions(-) delete mode 100644 frontend/cypress/configs/default/report-config.json rename frontend/cypress/{config => configs/default}/report-config.yaml (100%) delete mode 100644 frontend/cypress/configs/portfolio/report-config.json create mode 100644 frontend/cypress/configs/portfolio/report-config.yaml rename src/systemtest/resources/ReportConfigSystemTest/{30DaysFromUntilDateWithShallowCloning => 30daysFromUntilDateWithShallowCloning}/expected/reposense_publish-RepoSense_master/authorship.json (100%) rename src/systemtest/resources/ReportConfigSystemTest/{30DaysFromUntilDateWithShallowCloning => 30daysFromUntilDateWithShallowCloning}/expected/reposense_publish-RepoSense_master/commits.json (100%) rename src/systemtest/resources/ReportConfigSystemTest/{30DaysFromUntilDateWithShallowCloning => 30daysFromUntilDateWithShallowCloning}/expected/summary.json (100%) diff --git a/frontend/cypress/configs/default/report-config.json b/frontend/cypress/configs/default/report-config.json deleted file mode 100644 index ea98e619e3..0000000000 --- a/frontend/cypress/configs/default/report-config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "RepoSense Test Report" -} diff --git a/frontend/cypress/config/report-config.yaml b/frontend/cypress/configs/default/report-config.yaml similarity index 100% rename from frontend/cypress/config/report-config.yaml rename to frontend/cypress/configs/default/report-config.yaml diff --git a/frontend/cypress/configs/portfolio/report-config.json b/frontend/cypress/configs/portfolio/report-config.json deleted file mode 100644 index ea98e619e3..0000000000 --- a/frontend/cypress/configs/portfolio/report-config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "title": "RepoSense Test Report" -} diff --git a/frontend/cypress/configs/portfolio/report-config.yaml b/frontend/cypress/configs/portfolio/report-config.yaml new file mode 100644 index 0000000000..a5d9bb7897 --- /dev/null +++ b/frontend/cypress/configs/portfolio/report-config.yaml @@ -0,0 +1 @@ +title: RepoSense Test Report diff --git a/src/systemtest/java/reposense/ReportConfigSystemTest.java b/src/systemtest/java/reposense/ReportConfigSystemTest.java index d3805fc8d1..cce5ee6eb5 100644 --- a/src/systemtest/java/reposense/ReportConfigSystemTest.java +++ b/src/systemtest/java/reposense/ReportConfigSystemTest.java @@ -81,6 +81,16 @@ public void testSinceBeginningDateRangeWithShallowCloning() { "ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected"); } + @Test + public void test30DaysFromUntilDateWithShallowCloning() { + InputBuilder inputBuilder = initInputBuilder() + .addUntilDate("31/8/2024") + .addShallowCloning(); + + runTest(inputBuilder, true, + "ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected"); + } + /** * System test with a specified until date and a {@link SinceDateArgumentType#FIRST_COMMIT_DATE_SHORTHAND} * since date to capture from the first commit. diff --git a/src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/authorship.json b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/authorship.json similarity index 100% rename from src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/authorship.json rename to src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/authorship.json diff --git a/src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json similarity index 100% rename from src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json rename to src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json diff --git a/src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/summary.json similarity index 100% rename from src/systemtest/resources/ReportConfigSystemTest/30DaysFromUntilDateWithShallowCloning/expected/summary.json rename to src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/summary.json From 006f5c17dba102e5fd5bcaaa2d9aa788722b82af Mon Sep 17 00:00:00 2001 From: LiYuan Date: Thu, 3 Apr 2025 01:13:28 +0800 Subject: [PATCH 32/42] Clean up --- docs/ug/configFiles.md | 21 +-------- docs/ug/customizingReports.md | 2 +- docs/ug/report-config.yaml | 28 ----------- docs/ug/reportConfig.md | 47 ++++++++++--------- .../reportconfig/ReportAuthorDetails.java | 3 -- .../model/reportconfig/ReportBranchData.java | 5 -- .../reportconfig/ReportConfiguration.java | 1 - .../reportconfig/ReportGroupNameAndGlobs.java | 1 - .../reportconfig/ReportRepoConfiguration.java | 3 -- 9 files changed, 26 insertions(+), 85 deletions(-) delete mode 100644 docs/ug/report-config.yaml diff --git a/docs/ug/configFiles.md b/docs/ug/configFiles.md index 778ea09578..1989132639 100644 --- a/docs/ug/configFiles.md +++ b/docs/ug/configFiles.md @@ -116,26 +116,7 @@ The configurations of this file will override the CSV files if the `repos` field Note: All fields are optional unless specified otherwise. -**Fields to provide**: -* `title`: Title of the generated report, which is also the title of the deployed dashboard. Default: "RepoSense Report". -* `repos`: A list of repositories to include for analysis. - * `repo` {{ mandatory }}: The URL to your repository of interest. - * `groups`: A list of the different custom code groupings. - * `group-name` {{ mandatory }}: Name of the group. - * `globs` {{ mandatory }}: The list of file path globs to include for specified group. - * `branches` {{ mandatory }}: A list of branches to analyse for each repository. - * `branch`: The name of the branch. - * `blurb`: The blurb to display for the branch. To display more detailed blurbs, use the `blurbs.md` file. - * `authors`: A list of authors to analyse on the branch. - * `author-git-host-id` {{ mandatory }}: Git host username of the author. - * `author-display-name`: Display name of the author. - * `author-git-author-name`: Author's Git host name. - * `author-emails`: A list of emails associated with an author. - * `ignore-glob-list`: Folders/files to ignore, specified using the [_glob format_](https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob). - * `ignore-authors-list`: The list of authors to ignore during analysis. Authors should be specified by their [Git Author Name](#a-note-about-git-author-name). - * `file-size-limit`: A file size limit for the repository in bytes as a single number without units, that will override the default file size limit. If not specified, the default file size limit will continue to be used. - -Please refer to this [guide](./reportConfig.html) to get started. +Please refer to this [guide](./reportConfig.html#advanced-report-configuration). diff --git a/docs/ug/customizingReports.md b/docs/ug/customizingReports.md index 64d9519259..93dc381ada 100644 --- a/docs/ug/customizingReports.md +++ b/docs/ug/customizingReports.md @@ -24,7 +24,7 @@ The report can be customized using several ways, as explained below. ### Customize using report-config.yaml -**Another more user-friendly way to customize the report is by using the `report-config.yaml` file.** In this case you need to use the `--config` flag instead of the `--repo` flag when running RepoSense. +**The `report-config.yaml` file provides a more user-friendly way for users to customise their reports in more detail compared to the CLI flags.** Please refer to this [page](./reportConfig.html) to get started. diff --git a/docs/ug/report-config.yaml b/docs/ug/report-config.yaml deleted file mode 100644 index 100008bfa7..0000000000 --- a/docs/ug/report-config.yaml +++ /dev/null @@ -1,28 +0,0 @@ -title: RepoSense Report -repos: - - repo: https://github.com/reposense/publish-RepoSense.git - groups: - - group-name: code - globs: - - "**.py" - - group-name: tests - globs: - - "src/test**" - - group-name: docs - globs: - - "docs**" - - "**.adoc" - - "**.md" - branches: - - branch: master - blurb: "My project" - authors: - - author-git-host-id: jedkohjk - author-display-name: jedkohjk - author-git-author-name: jedkohjk - author-emails: - ignore-authors-list: - - bot - ignore-glob-list: - - "**.in" - file-size-limit: 2000000 diff --git a/docs/ug/reportConfig.md b/docs/ug/reportConfig.md index 9b9a8c8f5f..db392311a1 100644 --- a/docs/ug/reportConfig.md +++ b/docs/ug/reportConfig.md @@ -13,7 +13,8 @@ RepoSense allows you to easily showcase your coding portfolio by generating visu ## What is `report-config.yaml`? -The `report-config.yaml` file is a simple configuration file that lets you define which repositories you want to include in your RepoSense report. It's designed to be user-friendly, especially for beginners who want to quickly set up a code portfolio without dealing with complex configurations. +The `report-config.yaml` file is a simple configuration file that lets you define which repositories you want to include in your RepoSense report in more detail compared to the CLI flags. +It's designed to be user-friendly, especially for beginners who want to quickly set up a code portfolio without dealing with complex configurations. ## Quick Start Guide @@ -41,7 +42,7 @@ repos: The `author-display-name`, `author-git-author-name` and `author-emails` fields are optional. -However, users are advised to add the Git author name `author-git-author-name` and emails `author-emails` in their Git configuration related to their desired commits so that +However, users are encouraged to add their Git author name `author-git-author-name` and emails `author-emails` in their Git configuration related to their commits so that the commits can be successfully captured. @@ -109,7 +110,7 @@ repos: - author-git-host-id: lyuanww author-display-name: Li Yuan author-emails: - - lyw@gmail.com + - user@gmail.com - e0123456@u.nus.edu - repo: https://github.com/lyuanww/ip.git groups: @@ -123,7 +124,7 @@ repos: - author-git-host-id: lyuanww author-display-name: Li Yuan author-emails: - - lyw@gmail.com + - user@gmail.com - e0123456@u.nus.edu - repo: https://github.com/lyuanww/tp.git groups: @@ -137,7 +138,7 @@ repos: - author-git-host-id: lyuanww author-display-name: Li Yuan author-emails: - - lyw@gmail.com + - user@gmail.com - e0123456@u.nus.edu ``` ### Sample report generated @@ -190,8 +191,8 @@ repos: ### Top-Level Fields * `title`: Sets the title of your generated report, which appears as the title of the deployed dashboard. - * Default: "RepoSense Report" -* `repos`: A list of repositories to include in your analysis. + * Default: "RepoSense Report". +* `repos`: A list of repositories to include in your analysis.. * This field is an array, so you can include multiple repositories. ### Repository-Level Fields @@ -203,14 +204,14 @@ For each repository in the `repos` list, you can specify: * `groups`: Allows you to define custom groupings of files for more organized analysis. * This is useful for separating code files, test files, documentation, etc. * Each group has: - * `group-name` {{ mandatory }}: A name for the group (e.g., "code", "tests", "docs") - * `globs` {{ mandatory }}: File patterns to include in this group using [glob format](https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob) + * `group-name` {{ mandatory }}: A name for the group (e.g., "code", "tests", "docs"). + * `globs` {{ mandatory }}: File patterns to include in this group using [glob format](https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob). * `branches` {{ mandatory }}: The branches you want to analyze in the repository. * For each branch, you specify: - * `branch`: The name of the branch (e.g., "master", "main", "develop") - * Default: The default branch of the repository - * `blurb`: A short description of the branch that appears in the report - * For more detailed descriptions, you can use a separate `blurbs.md` file + * `branch`: The name of the branch (e.g., "master", "main", "develop"). + * Default: The default branch of the repository. + * `blurb`: A short description of the branch that appears in the report. + * For more detailed descriptions, you can use a separate `blurbs.md` file. ### Author Configuration @@ -218,19 +219,19 @@ Within each branch, you can specify: * `authors`: A list of authors whose contributions you want to track. If none specified, all authors will be shown. * For each author: - * `author-git-host-id` {{ mandatory }}: The GitHub username of the author - * `author-display-name`: The name to display in the report (can be different from the GitHub username) - * `author-git-author-name`: The name used in Git commits - * `author-emails`: A list of email addresses associated with the author's commits + * `author-git-host-id` {{ mandatory }}: The GitHub username of the author. + * `author-display-name`: The name to display in the report (can be different from the GitHub username). + * `author-git-author-name`: The name used in Git commits. + * `author-emails`: A list of email addresses associated with the author's commits. ### Filtering Options * `ignore-authors-list`: Authors to exclude from the analysis. - * Specified by their Git Author Name - * Useful for excluding bots or automated commit accounts + * Specified by their Git Author Name. + * Useful for excluding bots or automated commit accounts. * `ignore-glob-list`: Files or directories to exclude from analysis. - * Uses the [glob format](https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob) - * Example: `"**.in"` excludes all files with the ".in" extension + * Uses the [glob format](https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob). + * Example: `"**.in"` excludes all files with the ".in" extension. * `file-size-limit`: Maximum file size (in bytes) for analysis. - * Files larger than this limit will be ignored - * Overrides the default file size limit + * Files larger than this limit will be ignored. + * Overrides the default file size limit. diff --git a/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java b/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java index d3a637ad8e..f4fd31456f 100644 --- a/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java +++ b/src/main/java/reposense/model/reportconfig/ReportAuthorDetails.java @@ -11,11 +11,8 @@ */ public class ReportAuthorDetails { private final List authorEmails; - private final String authorGitHostId; - private final String authorDisplayName; - private final String authorGitAuthorName; @JsonCreator diff --git a/src/main/java/reposense/model/reportconfig/ReportBranchData.java b/src/main/java/reposense/model/reportconfig/ReportBranchData.java index 447e5287f2..f1de0edef7 100644 --- a/src/main/java/reposense/model/reportconfig/ReportBranchData.java +++ b/src/main/java/reposense/model/reportconfig/ReportBranchData.java @@ -14,15 +14,10 @@ public class ReportBranchData { public static final Long DEFAULT_FILE_SIZE_LIMIT = 1000000L; private final String branch; - private final String blurb; - private final List reportAuthorDetails; - private final List ignoreGlobList; - private final List ignoreAuthorList; - private final Long fileSizeLimit; @JsonCreator diff --git a/src/main/java/reposense/model/reportconfig/ReportConfiguration.java b/src/main/java/reposense/model/reportconfig/ReportConfiguration.java index be760c78db..baed9d96cd 100644 --- a/src/main/java/reposense/model/reportconfig/ReportConfiguration.java +++ b/src/main/java/reposense/model/reportconfig/ReportConfiguration.java @@ -21,7 +21,6 @@ public class ReportConfiguration { private static final Logger logger = LogsManager.getLogger(ReportConfiguration.class); private String title; - private List reportRepoConfigurations; public ReportConfiguration() {} diff --git a/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java b/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java index 1da9bfd056..65b49791cd 100644 --- a/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java +++ b/src/main/java/reposense/model/reportconfig/ReportGroupNameAndGlobs.java @@ -12,7 +12,6 @@ */ public class ReportGroupNameAndGlobs { private final String groupName; - private final List globs; @JsonCreator diff --git a/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java index d5a8f31b18..d07ba202fc 100644 --- a/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java +++ b/src/main/java/reposense/model/reportconfig/ReportRepoConfiguration.java @@ -16,11 +16,8 @@ * configuration. */ public class ReportRepoConfiguration { - private final String repo; - private final List groups; - private final List branches; @JsonCreator From 81dd9dfff3c3f56bddffb336640b4fbc691f15fc Mon Sep 17 00:00:00 2001 From: LiYuan Date: Thu, 3 Apr 2025 01:20:49 +0800 Subject: [PATCH 33/42] Fix docs --- docs/ug/configFiles.md | 2 -- docs/ug/reportConfig.md | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ug/configFiles.md b/docs/ug/configFiles.md index 1989132639..fea352151b 100644 --- a/docs/ug/configFiles.md +++ b/docs/ug/configFiles.md @@ -114,8 +114,6 @@ e.g.: `example.java` in `example-repo` can either be in the `test` group or the You can also optionally use a `report-config.yaml` file to quickly define the repository information for the repositories you are interested in tracking and generating your very own code portfolio. The configurations of this file will override the CSV files if the `repos` field of the file is present and correctly formatted. -Note: All fields are optional unless specified otherwise. - Please refer to this [guide](./reportConfig.html#advanced-report-configuration). diff --git a/docs/ug/reportConfig.md b/docs/ug/reportConfig.md index db392311a1..58eccbdb08 100644 --- a/docs/ug/reportConfig.md +++ b/docs/ug/reportConfig.md @@ -188,6 +188,8 @@ repos: ``` ## Field Descriptions +Note: All fields are optional unless specified otherwise. + ### Top-Level Fields * `title`: Sets the title of your generated report, which appears as the title of the deployed dashboard. From f17a5f0d5dd8789e68d258e435ae887c7e585cba Mon Sep 17 00:00:00 2001 From: LiYuan Date: Fri, 4 Apr 2025 02:22:23 +0800 Subject: [PATCH 34/42] Enhance docs --- docs/ug/cli.md | 1 - docs/ug/customizingReports.md | 24 +++++++++++++++++++++--- docs/ug/reportConfig.md | 8 ++++++-- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/docs/ug/cli.md b/docs/ug/cli.md index e343bc014e..73fda00ce0 100644 --- a/docs/ug/cli.md +++ b/docs/ug/cli.md @@ -77,7 +77,6 @@ partial credit. * Cannot be used with `--repos`. The `--repos` flag will take precedence over this flag. * If both `--repos` and `--config` are not specified, RepoSense looks for config files in the `./config` directory. * If both `repo-config.csv` and `report-config.yaml` are present in the config file, `report-config.yaml` will take precedence as long as the `repos` field is correctly formatted. -* Config files must follow [this](./configFiles.html) format.
diff --git a/docs/ug/customizingReports.md b/docs/ug/customizingReports.md index 93dc381ada..9befc5a571 100644 --- a/docs/ug/customizingReports.md +++ b/docs/ug/customizingReports.md @@ -15,6 +15,21 @@ The report can be customized using several ways, as explained below. +### Configuration Options Overview + +RepoSense offers multiple ways to customize your reports, with varying levels of complexity and flexibility: + +1. **CLI flags** - The simplest, most direct approach for basic customization +2. **report-config.yaml** - A user-friendly single file for typical use cases +3. **CSV config files** - A more powerful option with granular control for advanced scenarios + +When deciding which configuration method to use: +- **CLI flags** are ideal for quick, one-off reports with minimal customization. +- **report-config.yaml** is recommended for most users as it provides a good balance of usability and features in a single file. +- **CSV config files** offer the most flexibility for complex scenarios, such as managing large teams or requiring fine-grained control. + + + ### Customize using CLI flags **The simplest approach is to provide additional flags when running RepoSense.** The various flags are given in the panel below. @@ -22,18 +37,21 @@ The report can be customized using several ways, as explained below. {{ embed("Appendix: **CLI syntax reference**", "cli.md") }} + ### Customize using report-config.yaml -**The `report-config.yaml` file provides a more user-friendly way for users to customise their reports in more detail compared to the CLI flags.** +**The `report-config.yaml` file provides a more user-friendly way for users to customize their reports in more detail compared to the CLI flags.** Please refer to this [page](./reportConfig.html) to get started. -Please note that this file will override the configurations in the CSV config files. +Note that configurations in `report-config.yaml` will override the configurations in the CSV config files when both are present. ### Customize using CSV config files -**Another, more powerful, way to customize the report is by using dedicated config files.** In this case you need to use the `--config` flag instead of the `--repo` flag when running RepoSense, as follows: +**Another, more powerful, way to customize the report is by using dedicated config files.** You can refer to this [section](./configFiles.html) to find out more about the CSV file formats. + +In this case you need to use the `--config` flag instead of the `--repo` flag when running RepoSense, as follows: {{ embed("Appendix: **CLI syntax reference → `config` flag**", "cli.md#section-config") }} diff --git a/docs/ug/reportConfig.md b/docs/ug/reportConfig.md index 58eccbdb08..5647ef77cf 100644 --- a/docs/ug/reportConfig.md +++ b/docs/ug/reportConfig.md @@ -9,13 +9,17 @@ pageNav: 3

{{ title }}

-RepoSense allows you to easily showcase your coding portfolio by generating visualization reports of your contributions across different repositories. This guide will help you set up your first RepoSense report using the `report-config.yaml` file. +RepoSense allows you to easily showcase your coding portfolio by generating visualization reports of your contributions across different repositories. +This guide will help you set up your first RepoSense report using the `report-config.yaml` file. ## What is `report-config.yaml`? The `report-config.yaml` file is a simple configuration file that lets you define which repositories you want to include in your RepoSense report in more detail compared to the CLI flags. It's designed to be user-friendly, especially for beginners who want to quickly set up a code portfolio without dealing with complex configurations. +For more sophisticated use cases that require additional flexibility, +RepoSense's [CSV config files](./customizingReports.html#customize-using-csv-config-files) offer an alternative configuration approach with expanded capabilities. + ## Quick Start Guide ### Step 1: Create a `report-config.yaml` file @@ -144,7 +148,7 @@ repos: ### Sample report generated ![report](../images/report-config-report.png) -## Further customising your report +## Further customizing your report Optionally, you can provide more detailed descriptions to your report. Please refer to this [section](./customizingReports.md#add-a-title). From 463d291db08d15ddbb1127346bff8d446f475fca Mon Sep 17 00:00:00 2001 From: LiYuan Date: Fri, 4 Apr 2025 04:10:37 +0800 Subject: [PATCH 35/42] lint FE fix --- .gitignore | 1 - docs/ug/cli.md | 1 + docs/ug/reportConfig.md | 8 ++++---- src/systemtest/java/reposense/ReportConfigSystemTest.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index c73cad899f..15049361d9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ *.tmp ft_temp/ -rc_temp/ /target/ .gradle/ build/ diff --git a/docs/ug/cli.md b/docs/ug/cli.md index 73fda00ce0..6c56bf13e7 100644 --- a/docs/ug/cli.md +++ b/docs/ug/cli.md @@ -266,6 +266,7 @@ Cannot be used with `--last-modified-date`. This may result in an incorrect last * If the start date is not specified, only commits made one month before the end date (if specified) or the date of generating the report, will be captured and analyzed. * If `d1` is specified as the start date (`--since d1` or `-s d1`), then the program will search for the earliest commit date of all repositories and use that as the start date. * If `d1` is specified together with `--period`, then the program will warn that the date range being analyzed may be incorrect. +

g&*Zeg zd$}%8@;!zFwAQC?y!{^B#r?%A^9m4s3@X+=U#_K^RO?Z>cXf63Y3^SMv~dpyUr}e> zcN*sJI`%WjJEm`^ug;G74+5@vAScIYdCxvlRj0DzdJyyqb?J_e(k)p^K#n#{fpd!t zo()R%(6LpHsR3TFB2P$OJbf=AFQhO1OUPJaLGNgOWla~oEc7=B;!CLXX)F$XY%{=k8@oTtjR=!WYW@N=|h>RxaELfi!Kat$6l$$sGf z7Pztgdw3~#KUVDwbTT08Lm;2GcKhuX0ju&i9{vj8VVrh(8)=_nwns?I?Vd}Qpr3eK zdHDa)_2%(Vu;2T5NqbpRsSr{jWl|yA*pfC&sbrmy5JQ$Rc6m}Np|WQgQe;oovCI_N zMwYS+29sroF*A&1%#1O=d-Oc-_w#=JzMp^kr&ls_pL3scu5(@2{S8Zierqt4V<2{P zHq+4u)al8|D%NIa4+E)>MBmSo((C9m+--4a@(Dx3Ix(T|xBDy*cwUPZX}Ny%N6NQz z6%CylG;8_oq&iz-duM07%4fyro>nmrp^?u?$kb(y^1OVQwzhjFf+o)YX4&hxH}bAA z#VKBA#@gSu(P4Eb=WP7xhpAUS#I1-U5sVz?m@168J>?A1;^R8MThQ*j0?&*^mrDx# zsLknvM0oo-3&7vSC~M$1g6~`w-NB{6wAIhVP-|#1GTy3gOFUFM`}f^i+4I!EGCXG(69|oDsjx&A1&+qObxL5 z&VV1nVZ1xhOh$VCHGmJs&)wlEg0Bf}Ouf>C`ki10iI+VtsFD*bHrNFix-+gR%)^CNq>3*7t_6W=A{Kpj~@9Ngl7** zTj^$ZIK6%3DM4F1#9lQ$hUBdz-r{s$ed6ugaC%~g7Wv_r|BiLI`u1&=;h}iJxC^IW zeZ4bDT-%j@EG*E|vo}ovbLlxSAt;rAb^$@T?he?UBnbST&6{t!nvW^-iTe$=k>(zA z-@8wLT#>zSyCKl=CDLC{?cQp7V&7{W8KZGIaT~3a> zfynGPHb`(u78V$CJ|beW>p1I9skej>z1aH=c@;9EI@O0M;WXAVYp4g3YwYr;c#IB% zt>d?%J8z0<<`;qYRsf($8Was)NHZM?B<5tP`_U4{6buEK8D(l)^v8<|4@h$#r}tyw zM*z~Q^5YyVO4ymK|K9V+UKw{C-vSxsHQjr6rj5+5IG`V06S(1@2qHEs#aZo6 zTl*<=Np|pr`-BVIrK*^nn&F)&W`9u*|GS^#enCg%+q-c3*n2O)2ukhdNJz&qcGmmp zDqfi1gHw%M<~$a$DVLOqi#hU!Wg%1OrIK1Rh{Cn%PGaygxj?s)$kVMfS4@%**lPUf zQ2wSc1*C{n){Pu_KbfOG%6NzAk%u|3kL20?V_Cd zm;OTOSC4cOd3!A}`6jO~2LLOeUebHV&Gi;2LMkc#kM4-Bl^~3R9bAKCKA#4|p5FLG zDI&$;^f~I_3t>8T_-uap6JFZu{ah{kOUFv`Na0Zj zs1UX4&SbbcUNUI9)liOByv0}%QUHGL4v!h9s50EZL+XRj6S7;A*|zyP9gRxX_w9-< zN)p}{JNQv9XiU1M8i!z3^nSZb#{q#)iF9V*n{cmh>Yk&=kU-wqu(FaeYMUrzX9J}X zwv)zl()kQA*>e={J%vTPWmUyzM?9(Z==5WCe;*tmlzt{ok(KQjGx3@Vl_ob~bUD$?8a$qg7mb~mj zS$f|x@jLX-UHVN+Hps&!Rk3gcjfzL16hFMn}Q??AT_2syZFI)qY_T2z?^x8#T zIjo)Cx2LMF5-T``W76mqTEIbYzN{kb$O6jRw*Ngy6{4l5RCdmmJ4H9qUF7L|znL2QJp#$eyNS*LcIa0}=y`N%V`XNP${quBiIG^L ziJIFzD=V)RM?2%%O7e)V4pt|9G96Xx7i2$;xXj~Uv~)D`c=Ap7{F{9uMWwIj)*3<)aMGRmb%rJq^9P4*YG>B|H#K?9~u}Gi~bYU|Hp{_9e=eMYX%q> zD>Y%J(N17GufQ)R+Om_`Ju$YD^*P%~e)oYN(LbF;TsR%y!L`3+Y9hVvO$N*aHOdf3 z^wayA*2$ka5lTEsG~9CXEz*+Fb!c2 zm=8_cv)C)}54r3y!>NlQF8nuY2?kMv%*)ijGA7B)49vDoraT!zh}k>DbelubHd#}!s&a(Ud}Te zUjpgPYn~_m;>#1<*+?6LJ|K=! z>|D|q){_Qo>a^?R*bG6#(5E;A?+FV0Gzfc?0< zY7*#Ob;2%Omx=4__S@xlNEJPG25Y2lEnE zeS-2&6s@gCQ9|exqx@q~g@nj{KDQeS6JC(aTB(#8=3lEn#k7a5MVkHdsuIo?3FDrB z`}LOXNEzBawZ14gK$#yww*ge=vc$J(zVR->7^duUEG{PKwBU5o&^kz5BrGLt-E2XymJjt!@dDe# z!K0aAWL+6>+nXy1Hr}RMfh}*k9tP?P{pgnaf(~Pq^`E*&NTd+}yIimjssg4Hb69J--n8#tU zt-5j$P)+86z$7Kb-MvUEJ`lIfv(`M-7z6DRd%d$}ub$ea=@gIM#=cLc>zm#8AFjy9 znDtaxHM@%I2e?JK3LNcew&B@?%56P`4>Z0Nc-7LJJq9QVGcEbcDXx&78^Z?Ccb+-6vk#ECx67V4MtFY1V)C9JI_vamtq#mVeWUdya z&<;ZlMK&-sjSg-n0d}QxF6zoUO8nJje7%At%7&jkTtpA21M*U1-7@O~AV;4M7N^LG zLW&Y(Ks~GgDl>%J8yQ=BiF;w~aq@5$Kn(OPKN!5fjER zk6-B0@ZK`PC)-z*PKN&%xto2!|1jY3nwkkLV)5#O!O;)qv{@$INjKOx z&LyzL3=9}wS2Ti$kyJHR?uhq~b1Qcb-#WK>okXa9VMETFR@C(Uk5^qI1ug*H00`ns zd!L}Dh)|WX*DPrmyfs;-`1^B)ZyKEc22spt=MI1xqx??T2=m~V>c_Wu5@+?p)cLbD z%`qOKPd&}P=ZL-e!0HN+4n=rvV?MCzC=d8v?2!h6FRGt+tP}hE!|R}7pMz2vDhaa) zU#g3GMDy`-^I;@bULMpBFM?O6zls4dz4>yGLqj`7%<|>Ui_gYWy5NBCZ&Ni{p;#a| zUS=eRcW^y$>DEM28xY)^XFjeBpqQ3dzDMXA=LfC0l@YB2QJofX(`pq|>pV{Qr#!Ou zL^0goY5_s;FCV4-Y(=3Xfn8Tk6ew2&E12JpVK%O2_OC!7r4k??8?8NmeaKKQ;u~@Vpf}(K`{R(yPuI?Wk%~+^-QNgJq z4}a;u_?SPYz0s_;+Hl&jLA|fJ>cI+7(-JAb`GLB=p;>X!dVA$X(q8u=sq6uy{@~jYiq; zaumUdjCmJ$znoDzxp2WI=IWpsq%zI7)T`HZ*c7Yh`YU@m({Ip2E~F*Iassf?HPIDX z%MIPsTnB=zPsBDNV(E2fNj(?+*PZA+^ca<z@F*6x2c?;H6Se+fPT zsUdXc_OwvaTK%lfO0S^h1-ZGx@gSJlaFbG-Zg00?^iB5m z%-i_T78rpJ2J(u92Mt7`7F&dYAx%XTWv7`Ip|WIw@m%ZgqZvTaTrQOj{yhJ%0BAdO zGTP8Tw~+{273WRfGk(dT;`43EPg(mcH>s8jHg<>T&+9LDA6Xv~C1>FaG}-6VuMTFW zww!N9-VZ2nbA8!&qv_SMf!Ha@rP^Ra4!qUbi2Zyz#*$#82r6r*!S_WC{a?vv#oRp` zxn{4hGugxdF1=e&N2gene z{ehujaNLkv1GEHC?Q@g=Tun=pmJ)y6$rJS75epH;%}nSJii!~^5g%Jb3*jF~7zrTZ zqkIZ5CTq^;oIDLyJIh*O!f(x$_TPZ&$9(HgOxCjPB@uv3S}AO94NgfN zSq21wb-Qg0tCZivTP4*DFNWq}!u%Saw;s9y%og_knE~7JO0~y_qvs!q;E-oC#XBpI z98Stt5nLiZ5hv0ZEc=XDhCT0Str@r}l&RT{-P+d!2=WcDAznE`>$|LnZ~Z#qwtk`h zf1Mv61yu6p>0gnJtyxt-PPp1^*5tWo8n`{R`KDfoR!O~x@gSxW24@mg$mEm;^`P$k z2v&Ay7zdDEXY}@i`drZlb}9zZp!`A>iHF#T*Pu)`-@`D5{hHg6gDsmHWgE8VcIB&Q ziqmRAakbOP4BC*z(Ri}WN6kEY56(f&txl;QH3R;AHQNl+xr6Bx&9i#^Uo6;;%RD^u z+7PBljJid^pi%!|7hb*aO}4h?xi7ftD;M zf8J>3CM|Tt`)SKaHRI?DdQK%;SdmAC9DEbu_0y{cB>_s{J|nXj6I)G;!!+>N1{cCq zjFf8S-DV>QMFuHjLNI9v9tI@EKnc2UOLn0$q6OPA45Y_L9y#I5J%S=CW$;2k_%wZG z-m06EN!^wily$ytHNYy)6k{QT+j-dJp>i z{(mhP-V?Yp@d(BmNz`{=rg%05WY!Qf(H-E^0=jIFc*Zrlptt) zu7J*)7BgQwrV;lXUixs|gIBWt^pu*^gu|f;^my+!(lqa%tcITCihBG9aL=(T=&%<6 z{+Er|8AXbS?a}vfL!!R&P+)8HFXoDc-d#${nvv(QA?Hlwy}5Q#Tlrh0$}i=osO4Js zcqtCZPI5aT(A2n#)NJG%GqZIYekqZSb;8EpLf;%;^LxrChI>t4orwS>i$rEdM=H>5 zBenUa%mb;@-Q%(i#(|w9bpSx>`FeSBXdjSDGV|7MMgi8vihZT7&ef%`D0JMOBkyzi zeGAQa;QFnIT8Ymtu0|37$mrAmBBQZxQ+N7}fS*>w ze50GioLeAGfd*)e#Yf_0rVa$bdgbJ~UOXkPIH263aBFVK6iYnhR$`M^hIuf(@UuxQTBloalKpg`vk z!dTAmJ1FS!IWgll`;8@w)f>e_OzkmSEL{SID&7ARU;`dC6_Gq>x3RlcStp}V@bwn? zq}>1ZxCi*ohVf8y%>` zNB;Tv6Wh>WwvU(G!OYqei0hQwd1N_Q#WeJDPX}Il1$Hq~f8^M_U5s+8Z}gMl3v$ig zv(I;CVWIpM&rC4`gahevV2m1ys0>QKuaREIr8gF%>Acw%ECbeJ?9BBuj`3HgD)2sm zc9V9ZhhWryv-uTSph7G&_0q1QAdmQ~I;wii4On0_-zAsPD6wsjRkt_J{^&!%CJfr( z^tcO|*F z5O0z=3GjY-=qF*~(Ti7z)qP;E+Xqa0hu@!G1=;;s9*}JxNq24J!^HLq-G_UB7{W6tI5{c4TRE{cf1Mkt@k`Eo?CPoRAIceQk)Lo*vXXl?UiX)&z#G34nL(vpr;L5# zKPYC3%ccQ_RWP4KP=)_LU>L+RvUkbEH3HDXhfr7ArTY>sJWK$xao`WKp$4?|DfcwC zJ4n{eRcZK8W=}7IEy>SMBL>ILQO)c}eZ11o3j{+6Av1;^RaTu4K^sk=Mru6gDJ??pf)#B(YUvZ~y0L_FnFpFS67|%38nN zn{Fyl_r%`)G4-{KYJz#HPKQ|&&jtkGqmIaV`+0X{{#-f6A}A-1HaWS4>R#=FwXzah zwcl!TFsE?Q2({R7FQctLZgkP?+Xc6{*y1##q@^0oSX8^KYq z-z68U=4RpSk*zlbZx*jqF#vlFPm`j227<6^^;fi?$_-vai~u-q!^W3SmJHJmNc&&D_vy!}?RTBY#rsp5rQ$vIP0ZWLZ}VYq zQRNsJ2o;~t6hHdVW@ZmAP_!t@&8@)$cs%{_G>18uBT&_&sij+5he0xA=1B%-o^n)- z9zD%_prU^gXtmvLP`qAJj_aXU9To33IuX=Cb!_(-41KuE3u@A~We?6%Pwnc<^`(Zw z=7j)9$A++;I{(pNAC1~KrUDW|UoX!z#ZP$15iNu;o%_O-U-g;l$VGHAnH#f$?CfB< zF)BuKiJ0lwkc^l&dUD#Hs|D=2CXz7V<=DY~qYj=&uR$hxKAIQrRn6DY1YXx1Q`$=_ z)xD5^018!@hC@U#%tIhu%ab-;DHwOp* zC)|?c`QN%}et7lu*d3PR!ND6rb9NFeOybI`(iQtpaM@xT;A43Yd$NMJOFj;E$+D{7 z9E;a)-s?TvNPAA@S3c3{2+XXouOK<)uG%-DD~6QmU=={woi8qhMjME^2h1PF4@GIZ zXq;L)-nqS65KKzY6cv0Al&kImCZ$JTX3V)kE0Gq^3sogrZiJcL+eiTY@W)O~E{pQ3 z1KIWLq#iJ&n~IF>Fl?Rruog!CzG&B_>qw-io@iY@^=ur>!A%Y3SBD>VSVXL}oW}IW zeVEkY)CCxU3$_1W7wSz76IsLBVLds9mi_`$?SdT47IrB8NRf$9;)MJ2z~4-U2;Wq5YB#YocvsR1YQW@jq`D+(LJP6GYbP5x0?gu>xSG zqg)4{9DcH3lBd%SvA(8m$G;eEyAR5EViOPf!>as;N8BgWC42Pdk_qU&He4+zRf&J#vfOB%R3=eN8{)47H-tg=#>CX8YBYIce`V`yP=^?n& z_&9_LP~v0$K~gl)=7h(lwnE-Y=$i~0 zzHf^No-SW=KMuJ4Di`FiWz`%q?5(N5XC|Noy!_fS2T64yI2o;*QLXV=<8Q8knV;e+l$w0HB- z)-hVV;uXvYuA+x~VHPg#>PvnUozKxX!RF@_g14tCgnkDA>om|N_Xjwn2o3gbCmAh& z2}13o(?oy=dQDvs9({{5Ul^7$Xx`Y337*fN&B;6kE`{hj1U+s}XSxwIQ`1~>er?eT zvL8mbJoL0fEi=_25a8oWQ`9`b6;Hg~4ocLuBpx=*yjonXkM(~)vYqqF0;-a4 zwQXzh$Z57FMCk332G0=uGxdhmz5Ft$ij5r#=DaynkpihbQ-+6BN9@Kc3?3faD{18L zThn(eWVA6Nw!zCmYv9Y?^}6PKa3jZT*eAve@3S5594p822xS#ty!Mf|)d)KJdr53^ z!u9L`RYwQAZA8Mt!^xjMHcv^B|A<)EiLX?P{T;izv?~>^0`LXIuZ+V?K$>`2j3PA! z$WwwAEddxzF8y&v!f>S7-WjG8JWZGh+`5rdJ3~fmUKgTpNWC}UDffUApZ*pQ-I^ zssYUmuqtDFS2Ql(&Ec3m%?BHJ1>XI~W0D%);F&31q%bY&{0rE_~-EuU%)0 zYG<@g6G7t@18|#RTReFvZ*7GqrMfj=<^fwh!FF+ujM|qh(^t{e93;PU%2-~%E*d(! zM#2VcNwnL}jNro)X16vkPZif%4lbZ;9{j;=o^FQ~zaRXLWu$5V(>!#3Or7!=-x-@Z z&66njd}I*Nm=BJg*m>_{b(B}`CW{xv;#$9tz_g5|1mokB-gCZQY|~qlj%n<|*t2ex z6r73%6XE!QE&YUbI3kEO#8!cV^6DI=Bt(sTzm)YKAu0bNjZ|ydd!GpG#D}n zkOA)B6Crn#YNd075oBaM4{wPFarX(Y1em&!3gOj4gcf zIkB6zySpWRwAM}&U)q=%lyXhLVt=N1?ZVfMMW-d?!A3Jo#xGW*=+X*F>J&`o{Q&2Oupl@gBrP+o{_IbN}zV3Y0(s z?9xVPrj|SPdNXFjeNYU!Z2;!=7;+h?Xr8wOyT^Uqk}-GRhyx4A^v4O)zJGmL_ZTZ% z+1`3-FWdClhI{S*Iu0k{FHNc;Hz4KCdQ8wD)UK#?bn7p4HK^<>)0$9{JU?9Mva5-h zOa5Ctd_1Cac+uF`?Zx_!I4UJHxU2c8YRG3R(YnlbUm@Hm83J29WI);nW<;J{h3dtk z+7mgi@6Fyf2cDjXoeya!N8R)K)uY051`uYY%tgw6uG=m76IDdsU7^ljk|db=X65F5 z_t)GH#saw*@{BT=={?D>D$Cmeuhau!RLCJ5vgY>A+J-U*B3l3#SRo`J4uY+P3H<=URtL6hyr;yjjB#El{Nqu#%YQ3zqN z&#-h(4ZUZfi+G90FF%ti#dB^BXw)pLs5q#Z8I)1EUo$gR_VqZcL&F@HYZcoLKTJt^ zEP{*7{n`H9#W>xxC^!pd7(H}uu_2@hEX!avh)`~KHvj-7PfSvGCK{T^VWBAVv;bUE z)tE%(+L?&B`C#9D^Dous%s)i@aOS9MFi4&T?ZVzt7aIp1$YDubBTVxWPnW(*s2ymN4%wTLKn)X4E1u-O%(w4q|L&jR~(Kmxt(9E$nHJ1j4+SucUnw(d4+@Q$yvKso97t0wWz%rA%NE6grnp zTgs+{igCEwbi!`gxN#pZO~bY$C{%E`mr#@L*!}0={VrJ)_TZ+^Ku0Bgs}`RW3ff$Z z)czy6E~feu?tAo^(3%%=#u6(CbRg{5naZ z`BS&+t0Co}@1b}KdR#eCDm@?A2vuHOn}~oiGG7M{{j!LseYnvw+;h zp+%g!x3;;$xEzK@sneN-VEwwhg&O$h;+e?ub!g}Bhcdg4^YtH{GC_P;>4lWu#?TRX zjld9R%ZOQtg|POLY^A5$siuxZ^ptqW%*pZz_kbK0co~h?Qr{4oODo!CFDOTpC{ZB%spF}mfY9f8fH%5^~i?5J;pUMYj;a# z`n^&;mhoZb%XX9In3y*&^1R+&)bsD2;va^&zqy|Nd8AjRjuBklt6aW}dLyz>NuHJ2 zpqCV~e`)EEY|c_*F^Y9+JIN}g2y2mQs(7{c`S@EviRxrqQTE^%!y#GVQ4htlC&>TG zc%{D49lE%u-c;66B%rZHyZ}yR+1U$B;6k(FP^kr@O>lKGOUlKzkyy#1=W$N9l9|@p zj4So3&&FKE6n=={4$36{8GIE|+`-^G8aEdeRR{u@w6CIQ&_M5bc93C?&LRowqxc6Y7l&3%r%Kqi}3$*xB3~_R(O?2xa?nArN zNNvC0d`7@uv0@v^TK}eSWA5Xld5cN!LD37hk$v*z=p+sdp9%VE(JwqZeu5`VN;nEl zlecJ@^O4691eZ!eai%}C2U5qQkG{}ny2IAZW?IP{hU6)t)mP($uI*!JmS5*)VnF+0 zdW`sr`z{qPp7&Bmch}B8xh`Pe^5((g$Bp2nD>HI9of#hn@68?Sd5tbn+)nDrr*E#g zsnxVXvVnO)Q0BDrz{7qT4pV9 zOOdi&pKmVJtuWA=8a69B?7jJrZ=bl1(O%SxlUYA%$9#(Z5-tF%UVVx@_h|*f^ z=wFh{bo_^(88QF5MUcIkqVlxvm8zTM;4&i4Ykb$oUXG}F3rRvAh*Ua3@=@?rkvr1fqpvL}ll zCrF?@TZ~-NCc8UiFOUx?WKXWq4b0fQztMb%D)Sugi}T2mA?f&sw|Jl(uMRq)ZW&Ys zCF%I1+doQDCVu(5$ri^@NYfa@LL2DsN0_kY?-XBN3khZFh4Rk|> z2+tlQ6dhJK9ii1)mIdOKDItztiDMj2&lPu4anqbP%{|6?z)^>Up&=t_@qC z*f4x{t3>SWAzuTko@YwK82Z}VkrDsVPv-FCJ4tQ3AGjuY`Rk7h#Nj275Zp}uB(F6)rH{-~I5D1~l(@II5SpnCG{P8VE zMM5d3*wYC-Y*Z?8B*v?I*c2&%Y-VlqYi4Z{9MTvLASaZ$bT1z<4QZCpP|sA$WLm@*dbg6|*opOvTLg*vGQY$I~DY_+LiNSNuaA)>PG}rd^AM`_)aMZp@Wb zh58BM6A%*)ANhK=xE$>;LCrLsh(zL+G|Vg2iYw$}*(o5k$a{6?D9HEzn{gV_^jrz# zdtK={XKmcMEjX*Pee5KHSXsN9a3xV~F7%~xjs{=G`jWJHCZtG1JjPZ7q+L_)RJbo?tJ@yVW>aSH7!BT)|S{R~`G)a@O+ysazie$~Fh z=L{}qeohfVR<@(7wmYh&<~v9>`(xbCShXy1^z5%ngWIOuHKfx@RE|5@9b;^T{LcC8 zt7aNPNU+HQkEl6vS8Y{CoGv(PW!r5cAt;H7g)l5xR8_@;8f2Guc4=o^bIAcS0mKyH z&i;+GJCc>E;7c5=MmPz}#oYD3mTz7xr$_co$r0b%iEscnc5(;$++ZsSM+Wie!JQ{H zwoH(OaWMlGt`FnAGBi=0>?b{5k_&K~s+!u9*ysa$_jWqIYu6=hftRjY1S?J@e5v|x zEc#Jw)At`S9r5Ah)pC*`l8JmIB>5%lP(Tk0$#{=+SF7FPp?*7np}r4$Xb<-H_4F;9 zG2;kb0`D5;diQZ%Y@jStfjDFgrG}5h)RZ8x(((!5u{FSW_!h?t#Dc16S%W$zKW^N- z$l=glYKP-nJ`#E;qr%<_-exe6gehZfuDNfyMvUt&(4bnX44+bQ7DG@3PY~ff-)lF6 z>TQ%W27Y0BV|^9tqXZxSdK`YaA#{LLZ&n*7NopvYbLqawY!B!WmJ{48vXPH}zj4O& zUw()-=3l-De}03_xcn1T!9Bl4j~=|~WMWh4NoI9)_7iJ!e2WQ?FuILsLABJW`-9>A zpbL;S9oY#JU7?;*_G`_Ma^YQif+KIr+b&Cu$TYKil50Y?)VL5|>?HX6&<8qx6VKLM z+<+31>&%KZbb#}b(!LDM+H73(j5Zzdfbrvyt!4iSAfkhsd>aAWb0`1Yb8($ZZ(r_r z6ld!wFO52v$h40V!%7*#Q6LH^O2 zGqmy#$BsKYbS*at>$n0TA14FaSgm!+6gtBWpIPgxJM11%XH_VoP1dLh;6CWyL=zmm zSk7^FHf4s1efApIHu0miP1{B5;govThxTtK9v%aI{?gqC8<&bs`=5w9ZQBD5xjAir z7~ShYuyq*SxfEtKzAdhE>cAMoky-BxG#D*3DLXfLToyYuI zrL|Jb#ly5vT7N~!o%qD~HWH&+N!@=<`Mp<~oQ9mKtzrrR)I5(~X_xUPxO~i(!<1f7 z>;(AnRcuUocErq+7nKThS}F1)0nF_jJaAMar=!Vgr^ z(}S6-(uPHsQxt35soO|tunlVWrwR?MhJ|r7byRbGNLKp(eOI{g4decvwzty+an#> zTYFKC+o1}d zPYU(FdvyYs?Y=fOBvuI~H)`ceq52t66hD%i?G+E~(`_{|GEcEp+2|K{lZGGiCbx)*7d(qGlJA!`;&t+LIq>VGmM z4c#mg5m2m&f`gihw+YO%waQcIHlUt0gJ+qP*n8Tr8+s&rmc1;^kwp@d)|TejR;Q%p zl)*!ImM5yXxwqJkaPZvNZ!OwNr%#NA;@j?tPJz(*RQ5RL>~|&6tXOrRT?8ea8}~q@!4w>lwe!csa$K>1OuH~mLo-%l1Qd02bv0>CX_fF zr}USQ%~fakNLHbqLDv;467^k}71byCW`6YH;uT)#_!Yp=UMGx9I{UVOKJ)q7x4o2a zEKSnq2nk^qs_hCl6W%|Squt!9w!oZM6JNv{w3i&EFs51(#x6YuB7e8{OB`V67hjI z5ZtCr?QSAd`V%7|RRt@(pY9x{b)r0*_oWeP*cQm5qIAQ2Mpog>=NXQi8FZ?dn3ROW ze`NPQ*WH;-WtAt@McbxSaP9G~QSZV!LJ`1SpV2T6RmG1RHcl}_3pkQYckud^!^eQ3Miqf!q(Od#ccw1kV}&GQOMEB1 zIt}|shq+4W5r3>RtV`@E(B5IB*Y{5{%9%3kZpZ2~8;Y)LUyFn*mtW^R)S8JSb~STm zZc-f(!_+F6=xvvo=F_5eDTANWLGfzXwz$(f0rJe8&?Z_In6NX>{PIxowv)}LnN4Fk zQyxXL)9Hcc89^(jk;6}_y;=&~gS*dWSYvZ%?F%HAg7wN{5XBKGwzS*iml~HW;o7XB zo}8HwG`5zXzJQ@>Ib*d>@i|w5r@@JXm-E`GJF53N8g7qEB>Nr`k;7Vr4ux)|y8Gj; zhs$$j%0E<}>GjQH{K`%`$#~ znvkR4UMI@lB&3@7)mSHd7c{^1lxpOI0uU^w{Dlk&2n1a3UjVSZmyMLa!8Q2xuc z(oJpeNHXfIGC;6uO7~u)-P{YlUyuKMzpk&N;kS4wal{%w3F=$?8va>Do3%`fL6P)r&2x7;`xTJm*E4dhXAcwc}(H z`bLIzMo`y6+3KMxsxm#Jx}*pMj2rKg^LP-Ni5Cp46%Z4WtO%Md)QAnx$tP1jPRM7Ci`s3X zdHDn?Fb1#|iiw=` zg)#Z)9ts_$JV@JDY4iHNfO_`e;urQ~TQhS)%+oFLl2y_QiM2Wcj(x$I{#_jgNkP8d z5?TiLbHMC6u!6VPS4na)&Ctxd)>*f#R0NJaTf(JrV}6^~bVo6i^@tkepG|c_w~^Zx z7St}1L;k86x{nBf5)MpO6$J+0sH-UJ0wdgF=FFMrj1+*m9bd=X&Qlih>W8^g*zV}XDfMW^N-N8mt4BmWBS-Drupu-ne zC`WA$y{?^YtvczQ@&mO`;r3q2gcoTE7}AKzg?8Cm{bVhrbhA1HpNm&`oN#xlbh|2O zSwbG-6mZ<1AOAU9mr%CC9YJ6Hv&`?(0A$rd7^; z>z?-RT067SD4DLN4V8RG=^XK_ypCN!)S4R+kad`T^6zpNHstBHIOL2mrTBdU3-UU_m!q;=kUW{Q;^Z6D5 z-IW2qNr+K@jGIy!?ttlJ1g-f#h}O*j90)|poMEPMD$~I&c|ub4z6Z~ ze&px`nFf+r*NbwaX0|R|&slv6rJCo=sN|7F*=sYA+8T?;BhbIKe8x|J#Ju}R2aB4G zzUzn0h52dV5)F;X=Gu^$U;e|J7eE6S$YI^wT|5b|vC#~x7W5YNAn&aF@Zr3=zw^sK z^NZBd-&y3*v%#`#xTh(xv(C*_^Z}mY5=Q6UhDfjV45~swUfm+lkfB|!GvHcm%Aj*9 z?3i?+mQH3h0~O487!W9YAm558;aID#?!OmYTaXyH5c7ud9dRba&!-AC@6oEM*`8po z_I}ytd;d%Q-(!pNm8HR-dN9<3?ud;SL`w6ft%vwa#5;ORVwfK0-D?SW+GSL7`Su7( zYZzU(*s6_&ir5{Oh<#PmiE69EHlFxv;r}0d-yPLdy1hMv1;r6`1VL#kqtX#jdb5Fzq8NHn5J4cI^Z+4) zh&ZAkf^-Oq^d?e5kBSsQ2)%_61ww$31VRZAl6-IU&b@c7-<^Bq&u^{y$Hj7yaL#*n zdG@oPy-%xziB7LNhVU#<-c(Q$LmF2>9bkSsy;vDabFB2dAY4h|dG5c3kY+LzX6FCv zRGJibTl<#czVNak&WmE_A#U3`9KE=_opC5B0DA`EU+ao-xn<+o zNS_pO6%#S4_hO&IRy%oRPFXfsQZ!_S^{Tc{@El4C@mq~t#F+u*LQnrCPK~jUan>9X zxw}r+MYw+@^W|v*w?}K685b9!D3RYvGBiA-^ok}{4eZKzpV0D$-ZbiJU#jQr z7S;>A`yknKt+Ct!->1LaxSXpzr}CEXh3e6Z_SRZ)$;LmE`SlgryB)BGO`5coou~T*JeRGqN&FO{+MmN5J zKhoHx{&ZdEqw3s>8Fe~S2vIu_qj*C>^Kb&ZxwC#L?$R+sVOWe>Z#-OZU z=iC+hpxLY15buCG5lUx!TezcT^_9;hR;4wX`LH;gPQ4tt!i$pd$jh2ggQX2V$W#l8W#4lf%P1v;Aoo z?+IrP!QwTB734^TH5UoMS1}+ z{Z9)GR@q;&Si;5&63*XQiO2RL$WKh{(nq$okva_BSJu|Omj};n?+4y95jonA z;h7C#%5XOjAJl3}L=$6Ly%7^-Z@iN7iqc|w=}DT;gIRTjHuCz4C2le!P-}Vm&BI)T z{s|AzK5%7}DnRXsM7W=39RO|fw_TrHywMn+v{X-`u=IVfx0zR6WU@rQ8|(=wu7yzl z;KWmNAdEg-aer#nZp zDJeECHOkQ1CC7r|BB~h8CF9k)Ajg_%3SmAaS$keqMnS1^_?^!&Mi?I!sqo-2U_(vCfiL#A03e>R{+XP1 zXp9SxGDydhv)2*XwQot@lul>i?sq3zd~t3sXcbvaRXY8B5$clrf}OjntN!@TFAI(b z<8G;TE??F*5er=?@T`6v|9YKOeNYnLy%ru>!tP`S&Nc^q-C(Jf6gKEa5lRb1c%s)p z0Og=}hph`qatuL1 ztZkKM{Q1?w4Va&;1(dKQI@R=;ik*3J4Xy@Cxrhw&-`gPz3vJRc=jHKHUB0=}x$b7r z2oSmpW8sjBEqa!&mk@n9loqEC->vP+42d>{tz8S@wEg1f%2N`0VyCGfEbk2F7`;Tp zXn!`-i#=Xe%BH3yWC^pF6npW+>b1(HCr`Z*>J@;wRZqyNpR)6u=Cnb2XRfS`YQn+G zhu!;R47JIA14TL%yF0T3GBsHbcgD3i+b$>=`yQT83X4iI&9=|bLOYA~nFSXQRN8nr z1764nNA7h5qesD`1n3j*2Q)7R%)X6ET~tC%?As$Lj?9XG$nV1V`D}$HfsTZ%4DohAH8cG7`-nX1242j4YiM3j}bhxQ8*T;La~*39KT5h z3@ASuN?KPJEG!`RfwL~l7vVO1fr~&4@_pSEvb&{eu$&on65cy-vNm&{NQqI}9-KST{xQPW z6uq}ivpq8Kg@)tKIL{=?)~%YS>$dT5SkR{$%5J0#s$!Nh;K3Rt;_Wb_H& ztyQ4CvBFgIJMA=6+p;D|OWHmw;>`l!o*kp)O8ZzJUMax??m3DiRf?erFYIVvrFv4YbUqU{%2s@STb;Vg1)qeIyY3GYD z4#bcw7WHk|W0lr?Kr(s)*5`JEiy-y%TLAATUcfQ4HaG|)k3wBvw(jXjd%x!&yk?|E zglHwTj*Ea-C%*ETX!7Ul+taeHtb!A*idrck&Z;FClTn_9vqt@kt|>}atAi;;j3*f* z9(|VZrIB&vg2d=Mpm%R`HVRWqyABYPL=sVn3ED5(-ty*yW?S2^@$KY~2^uQy>6Tr3 zurY83X7u|7yS@2%N_PhxR?wAfTsb^w z{Rj)dYTt662rq_|aweUivF4Dlx2-H4dv8Y^lEfSfGDF2SPL}QM8tdDc<@@2Dw&~V3 zO^G)HQAT3njIC`!Bh_x9pcncmd{xurlqb(~+2-ZEY@O>2tvot+T4c(VZ{3s94b{Rs z$aWI_!TijLmY(#3lISmO-tC3nQw3CHdnhewc;=+Gvi7x%y1G)4?Shg>%GSXFpF0lM zuZPm)BMxw#-6cB!%JjNL?f%Y^9W6mIwDRMr1?|iOz9ET=Au#K;$h6D>1=O%-h!lGJ zzc`Tnl6Xx+{99;eFBJ4lfRv$X-|2gxi-OGC5l7e3j-TWTr5#;+BNuI?&>R^~IP9eG zy=nS0SJ*ai#{A2u^tFogr*Y_dT-n>QjE$RsnPtk)@|8WU(UF}}l895tu7vJAbRz%V znXPS^9W;KyI>&PrvvCR!%nfb``@uX=@{?kuw#>uCw;qD}p!;#trUf_qdRrShnBt`U zpz&cfcZK3yc-?5yO~x~l+($AeudSD_k7HJ3J$L}^Bzx{z`Dh5m#uVwL4(vE%!=^+S zR_LLSL#cadVPhjQHoP;6zAKJexHFl!eOueEKh%op<$K#m`d2zn$4@Ug*dSeJ&xRZ{ zTqkpUx#J(c{E*|z@HOvh8m%d{Bq*JZWi@)PMx;Y@H8+~H+ncL1f=34{uvECWy!Gxl zG)tFsZ+2x zdt#S)TAuO+SaR|RL`Gi;W4iUdcUqmShodFz6c?Tf@XC*y+AT8Ji z^v$f&l(E#sIj@%-r3Yy-xYzm1RS#peY;OckUYwJdj1WsuZ5Ix)T3VRL*Hmh@)4`zW zCJ+#pGhGTm;SiUX($+#4VAs+Qj+`KoM(kbNzb7jdJKqw+WIMj{JN}AoCq-rYb>g;+ zUqxv&wGs$-hG7C>8faaQ1 zWv@f0tg;X*PlD~tb2Pxa?viTsU~%n8|Nj0@?ScA|)F&>Wyf?+QP7s!1A&FkqM!Z3M z3=mj^q{aPV!V@Ad>qbMy60&s&ULM5bhfH*IRV$hgnb14lsC2AncIjU{@Q>>D`PNx| z9hWjTpht7X+4-iRSK(_ridl99&59YB!H%_0-MRaEPhf{2j&!EInB~1hyB%e}WM;|! z%r)`rG`$R`>RwaHh#m38=I`<}NNeO%lLnF3-yNH{f@i!-zwH3qu<<`-qBBPuy=#kf z0OF|qRzuh|yE)4tyQipSH;?*Tb-O9q1I+@GR60HL4yvtXQfheyPy)phyT5D6cHO6)53q_Nv2A9hq;a z`{Erk>b@l^yn3%X;&bPc=u0g$yJMiA-qN2DP!U{gE+^QYS>(Dl`xc7L?rlqR>jj#W z+OO1H&5Ao7E>Z({*sz2Hgv?K(j|D-xjYjn|f}Ml+$~>P;llnlAp=gMkZ6N^p$yZl| z6=?F;YXc@()SHV?X04GKZe;9#f6Dst6tV&@cc;=+{>&82>K>;0bdAN>lBG|vFp8;z zkxxh(K^T&q#|TP__}jGv%t*m9?)VA&e}S>}d;rFPoJkenFru}N{I)jJ@3k>Q+lWB8 zy!Ng77{er@r$(@mi(Xq>uv;4<RMZINjNFJIL8j5?=k8w5aLcNU|D%b$hP>UM|H3L8#(Zfi@Jny8cwwDmvY zS?0(^fX2lg{wB2AdhB8bC9F_|fzHa02?_H9oXnD(MIjelh5tR z)@`&|^5{)h0vxo9L>JY$w&3NwtF`Y}a_+wN0$)$;+lJ#O#mG&7WfuG#VQY7?XN^;1 z6pPq(IN_qqZwx(a4L~)h6+uU98WvOQVfR!j-&r5{j=Zud3DKYKm3NodU*&SCCZC4( z6}A7nMLXl%#LBw4i{TH0L)9Y6BrNPyJ1En#)Q%*) zZMnRO*w0HfgfuJAM%J(F>n~Cmq^jC` zWZEay)p-av_F>4fbZpHc6zhG*7N=qd#d`J*sI|#{TniLoy1F_JnGB^1NqPo$Si$*h zc;l$k!HQDZoXX-cmWEicQv>$gP8t(WNPx5x0|B}Kp&9BiR;Nrp6?9|o$#!TH^c1+d zg(9?CgUzkZ;KJxG|1}l;0~pgM*FhOw?0F3YhiW5vVN_|MRs}%Zg3V&VcZq{X!Bz~w z2UfcP^-{DK;ZC0(42>yk$GT|ZmktD!o3aY)p|x#$U4FGF^d^9Yt*Q9Cm_m~ z3u?$MZ)x`|{Tsw?!_iRLEP`)|Cw55}yd>1LE+vmV)d&*3=REog zXGQ)ZV>I-Ij0}LH*m42tdjPCb>Vq3Q9sCY&A;5%zAOn79i~=A))~DRT@5+?#f{5?( zzyI++UB)-^7t0L7n(s}UW4yjc>3;|+e&q8O=1!UDGm{m)j!|G2~Nz6n6k342Zxy)DmR6ZgmY!=Xpz51BN46xYKY|L1r8 z*bnc5Iocta9I=1!Xg~ai$B8$>jrN*RzqRZy?aNh;b z-fY7=Xy(dgdL2=__mAW0iM(3#?;4g-p7?Z+hP5f5#@(tuY$&!;PBd`(ARC-Y?G76twTs8jKhg>n3J(MuC0^0JCG|+yx(4bUJ^n{6TeBtz1P*2y26RBWF z!H3o7m*p>ZH4qkq_NVT}7x-Jy!QCl<=Vc|Dd+L9f{ ztC3qbqO}QQxp8slITapUe#n=%vC6sh-A(=&qQfbB>hp;;K@=6zWT&oK-bL7 z%q)Rdcqlc+I(L=(kw<)H^K!Bsqtvr2EgqnCnKQ;Yk@7XUesI}fS}ZcvZvMXD(t{d2 zy@8(9u7Iusi+}gtTp{YR*Uk&~-*-mB{WRDciv^KE6azg*ktf{kG5kQw$u@Af6Vc}q zfrPQ4QgiPlqTPDz`N~)jb!53wR%CF(1~=u8X5g0i*zOZw& z{}xaF(kF)|w#nDdyXKXdZ_GmS_8CzJ&1^G3LsQN2=~Q zE-27t?{_TRXpXJYYKo*H}9#rmZz1vzsC-a56P93OuP zcydb&|NA4qiIlyB8DrL@@#C20_V!L~`sh-BNl1kS|8l0s+}U97iFj*|q%uCmYilWE zOW9t_u#V^&40srr-Uywgmz2s({8s^lS{p;8-AI?YRQlkH0X(%fjr{qes*cWbp7C<< zUcHRE!do0o!w<$Ei%VRJ$*Y?9wN8%&3C*CPn-KzZiP##a_IOIoju-4X(+9l8?-Fc;WNG@I@fbL6w=OG;vt#%%796Jn^n(^1E#_`vgY z_>E#Sqrj#qnErxG&nYNrsM+znWo59?GFDI!wRe;?O4LbcU^A(L^i152l7t+!Du?p< zvwYeoi)^aj*5xNgudruJSosXOXik8@h1KTVZQr){29m1diK{tPzwp#^j&&8*W}5z- z^@Wd>;U#}aa`E~bO11dYk-z{;XT8U8E7 z&N;33o0oD8w56E~;_Q3Xt#w3_{;Is{0HCdh4)kAL-vIIBZGc$aY7^LZo91&3X6{Cm zFbCnK3%vKumoi}ODaX}ywK(}%^t%A9m?K+7Z!V!(B(%J~YDwi9mCd5!OxE7~JOdVd zZG9u5CWc*I%F(rjHY~7U#Iyt*V9kOAJ9^Dv)!L#Yv(E0eMa;!pij5+*p?h1DG$ z;-j(LXBwm;pli7Yf7cS_VY|07+5{2Y4klnuM|-=g@+J#Gv{@!oGZ&2mudOSv326RM zro}Onbf?i@eFa$te*TO1cgbIHPKVvT$EJr+={-~yh3Yp* zNNROV4_TkN!(3l(dbJ}_Jb3;+%y$B6I#!k!+Dg!$*8V_M)L0L~Gsd!|pOPGZBGJ)o zChqDdiAU@WqONY!q0hurZ=+>#LW&RrH--IU%XkEM)7I;u!Nd=zf&~L->1r*0 z5*OGxi1nPA>=chn3qtyPSc#5N7Z(iUwOoPZ|BUXLkxjrXzZ|_uN&NZ%YQOCxE!W9w zi~Oe-c1X7@Ekq;gG?id>NKf_}lC2JQ7TL>E8a4)kHwxG&9ab`60(fcTG zvkJrd5GRjXa^+ICYJHwi5;K%9_(qJT&g8SR*I>Hb+uHmM^nK%WTHbY+1T@L;M4{@* z&>4^y5W2}YAfXARU0@=;+N7FzaX`Fw*%QIQ)@st1OAy-dnalRll$L#8t0tmI#RZ)k z9rz8yn)wgABAdc^pS#vqiH8z)q*fWf`*Rn7d<%bO5TE88!yLWd??b63?q4p@RWOki z8Ao5GuW3_Z#x=9~1&`%eWIPM1$OXeqwIAMCRevx|KVuXoMy$huoRKiYt-E?UaHR}s z`((d(`?j{SDC-}M$Dr3+y~_r8J??!Ha=Fl1AgQemvzIq|gBW8VmNd53=|9z^jUrra z266Owc0$8ambTNnoJ%Zy&RjL#Kxq9%Nw8BlCNoMSUo&?;LjyVhCu-uQX!RUFMscX~ z>qDXk`qx(00CKoc0w=pgGDY~~sAuLLCY1sWS@q;|eobM}^k58)UyP%Z3d=e#ae{wj zts>Vdi$O)2FOP;y2;--Dt;Hp5Tc!t~*+SQJ)2kHEGhnY5RD3h9Z2YDd9D9VfOXv(6 z#QJk!$_zeYMwL>X)oC%_XA`{8biNkJTFR+?KbOAScmGG!egMwwpOTMno2`sV*d3Va zom68TO4AT%|C2TS-w`yFAh|~((!fL`JE4BgX$F{Hup%1HSXs(9ug_0JeAt)%j5Qt2 z%1xxw?Rk;9Uw-DhfM5|3Vv63yV!gN5JAE;=&UFse2pXv> z4Vp^%&~=gFNi7qfTcPGfRxdoN3Gohn^o|tOoGpA>Cz=k8l?MA8I_{2Z*+ZjFKj96* z`3?#&7MoQY6t$OL@{uXl56Tc56XJrW$wJVet|efjs!SZ7X?u0Dyw~V*bihZ6?E=(d z28b5OPEBgvIIA~(rF;n_EmSR#(%$LK_F<4w{(?}Di<(X^f+w7{BevzSAiy&-pNvDi zP@jWu>*I)J7#+Z$6r8jOe24LPZl5ijg}zIQ?^mx;UghIcmF_kSPk zKT$+z_?}0pv%+KZ4?W~hy-kZ*c#@TscRpI=G@SSH4BuXk|15zxQSSjMooiAExfj?Io8IRS#|nU)6dkgZ>7k~jZv&uKZ&14A!t2NI zXwOg^Q0bf>)L{%`D#_zrgk&+|>D`p5Fkcfyp~RoX+z%un*#~-Jl4>I%0HG#>8zB0s zdN&D}zecYYprt7s$Y*Q}5JwGZ_VySjx=L6X$ggmKfNe~=6F6P&r_aQ&Pl72wbBlD9 zJ-sLIvaeIDy#D)1?jRTlp~?J~Uo)e1YM=|Hal+Oabt1u0C+M8x^qGDc(K&M~Lq}0)%bhgjb*pPD?1x+<{5Ee_a zy)oLTAhbqV;PNcLF5kR0dfD3BtSMGbUse1hj;KSQ**@G-is8iJ-Wp$0O-gWAm{=UI z`s6boVwyjv7P{SefmChs`f!}SGD?Qw_D_5x>U@6&yL1_U#Ud+ehMu!|ZquVXnU>hORhG!sazryDt^-qW{@ zQEz|P5yZTmPd-9zlAwx=wuLGjo1jG6a*A$P4RT>CSQG}Wklojd)s{8O&J zJttCz!pGc7>U$JhM3h}jalRl*U>RSybuId;>Gjk!P!F_ZwQ}Jn#ON1#dwZqU;hpNl z*IJi?Wup~@uNWHoqRP6IK?l{I1M#+quj-*n5VYfn=i~27jMRlhLl@CZEV`&T-J2bu zyK!^fo=rjswA(G`n`gCXO(}X0Bv9Q)t4cv6FkvZH99T!(zI_ZjR#%O^vb7rlRzT$d zKMR`quuEZ1p53d<>{T~@Eab~w0T&T0XfDMFtGG+*OGRI8#TtDT z?o`~3C5B*K7;5wa@pi08Vc4woW(b?1*llfLVpKGnbv{u`e@)()5SoBwj1seRw8?h} z{o0G3A&V)dlNb4PR=@B|Ao-G~V(FumP-}k^q6>oqEopB#Db%!+sPku_)^YANJiR0- z!h9N{Y6xOR*oaH)RVG0JVW}k+Hu6V{Yy^}0MWip3A)-zY>eARASDeDB_l_V$DT?P; z=#M0#a%E}`wd?c7al89yaE&O|vzAMY`4Fp(ZkpD-Du^j%xdGtl<(N=qB4hUKEAX!dIK`Fa3eb?O09Cspp1D6#h&cET}y zmH&)4k&uZI;;R0+VBw<;S$jfPgwnLq-z>;IP}4&!sv?x(>%B<%bu;G5r#J9oY|c$F zV$C|tG9~O(g72p69@K8quqQ@hrcb7r#s2j&9MVL)c8_IOYhv>f0NvW!M0L!Jl&RhP ztT|fa{4|3DnG|<;!)#$8HZaWSZTH39o&je^-a1__od$u~^YPw6up}=5U6*pWvppXr z&IHruBNc9(`X9C1nSQ;_?R3f$*Wr45&AlvktAS3B=W;3`bESH82t;D^YYd#?-Sld( zZtB!bOmy%q*z*0(Xnx7)my4jh^!G#VFic~E+BhbJlMFSN1`*7`ncuhYPS<)9Je})+ zy~^D0?fDuVGFZ=c$Y#gpzx@A_cPxrtO#Kdw^Uf;4NXzlm4^%$K!n&prhQ$xW_C zC~S*C^C_=f-`@3QQm^$GE#9Gj0;HCuw+Z!y#TNdSVmmjkMh9MBpU`DZP-wKzYK!Rv zKQt&9Uwe0|=U6O_3^u5w-Tkl=*ROcOM=Y+U_Ki~Su`6N_{oT$eP8eByM0(-jM=iFh zkS?+;%MLN&AaW1*YSw5hy#@k9zheM#F~}Gw?d^_T+DCS#n3Z&LU=Jh#)G)1#y4^!1!Ot+ zKW2dci4Fei4?)TAzSSSfooboWW)D8G#}!x(ofpRXS!*TWV)qi?yL6Ir1!QgITv^TNSp>MvRxF$ar zLHl$(N3eeSdzk*jS#U46X#6X&*Pt_HUsZXloNoWbH*ej1=Mczg^w|KRxR z4dtH~hnv+_s_)dI0b}s^ceuL@YY%*UA9DIYcRf%|uU{1-x>^$$AhFAN;y1;v+Hl1< zGv9IRn4A>2=7M6(*X}*GVM`T(f`^yuhH; zdakdw=%%+h?U$w|X_f*?ng1{?RhTsdbVn&YgR?N~ z4U9wCy`Ly3dMB&eY z#UDccQ!;y!TK(%uwz$xEHlV!@iTd_kg{N3odpQLH3UzchkLU2)qiKF4cceayE1t)( zSMV(B8W6=(D4fQ?8X+NvlMiq!65fnLXYB$kD;I`I@u-o41ACUDmB&0`}p zeIe@~1RI_?Y+XvEcq1A73@rIwqBDq*)Tk$}DLMx7>v&La_i##waeE5`F*O=Sbu7R! zCQ=9VAZn5oLKY%>W7C&jQqgR zyYk?ZJ!51rn?tF3o1p9SqVYWRZ?vW3xRQcW5c;lw>GSUVky zl+{0oAvoTeWv}V7ogd#_%b0nv&*(x)V1Gf6T@bln&aCs6pPzpJ{zswDteUkb4 zK6jh_V)Ec?107n7Hk|pD4`C7Pb|l&_eb>1W)?q%_1;ADZH@?)U*lkV>G!CJ~_U^J@ z{Sd60<~v|16=L@VFw`?a^G(KF=T0zo8V7u^5P!Bdw+I?PfdF(OJwS7Zq7&!*fdpzA z=hEmFu^@L?e;u)%`#M&CUAwb?olq%Ax>2<9elz{bcez|dBqBruI9r746kxtC1Of1# zs-bvr4G${mEKVy#d&?e9dC453GVL_hCS!?xe`pe}+{U!&>}rEyc9bCi19e3eAMDMc z@bNz*IsKI*IR_=&6ejzQrE0q{oT8iBaw}&RIrq$|(NT9P3HD;zJF>YZlqn z2X+VkYB@IKw{=v46xez=f}ia$@7TM#_4e)C*M4cO=X9koI*iwie7eD%t&0=bYm9im zpo}`W`HSB+QLTQH35MR|*%23QYHIyM@#Ek48;diXCiV3D9>^wgu|`^Cmd7 z2m#Yg^z*=59c;!*t%mA>N`UH*5 zw41*ly*BWE?cetQDU1HEUi5D+c|!@f;_-=ll?j`B&^z`_gCnCJF5RRf{>&A77^plC zM3>cXHjL|mnQIu|AKNy2p^3i$NBk6!NXZpI?9C05G@M1vTvD z%@{i8wwA!z(4Sds&Obo_|Fj3llb*B;+if;4W*7izLBvO{&0gr0halD5(&}7!{;%Wu zvFGx)EB@7q{q2f>ZH0fk;$Oyuzg_XK1JB=C@h{W!-&ygm;_xB8zq8_BBIUmU>Ypg@ z{|4m00qS2N<-Y;yU&i6TYsJ4#-+$MNe;J4Wt`+|}eg93{{8bzVuJ->CZIj`f5$x*f ziu&VoXD6i+iaqhyk>WoaLwo^XDZJpf$by^we0tL&!H7+ zreAsBGcn&L(SWeBRlBry{oIc|#84+dFgCc0UD7r%O3H(0XINbP2{5HrXmQHyS!t97~R z2U{=iR<|8HW}*Y;azHDvr?2n2RjEVM>d}fS*=;KBLtXy2Mme%JV?Oxw*#trem6E0b zWUjp&9oLf2ggORh6bMG6M$1OIxMC%{jy=Ln{(73cBc7x3H9X%^qjP^Ac5)evlDQaR zyK@q0>vc-&NjZ{bL-5Z3A>Mq)#*C%4bX;d4MA(PUINdS5)BE_srFavUvfs)-J-=1( z;7nGWw$sIOg-cGmoy>o`d@=qKUN#H<;@sL{w`o->iINQFraq5)Q>T65;uLl7cv4O0 zVro(Yq7LJ{NAIVfw|@UeZD=b?Ih6LtzT;LX58UXob%pB-0a=Ur8@2K5yu|}5?uonh z4dj`&@CeG!b;7$}ov#dBcID_uPPud)GSv!UtvT1Ojz?y$oVwHVo8ZSMH8C#^{OU4L zm?URcr*>Xk&Z_*)Sbb2A1%_uU%Z)2(?=imQnc9<@TqAiMnii*QUoHZ@h>Tjtt@iiu zSm>ACTssc^{?iZsqnEr@N!!G(!WAMoC1+bxE()8BF?)SsFIZ9>LB`b%f@w&{{8=r} zvAUCn*ZP~3NA8DuLC(ru&o$Anom>_9)ui7F=jw@;EU_RX{1Tn&%?)@X#Xo8=!PF382D1sH##e~vO)14D$V!CjC1 zV-7ErQ^dPPJ@@|Wk^JwUe%;gBr?)oMk+P4+wZx{n2jVw|^0enNJt<+5zNZ&)JLfnu zxaT3S=xnDs-|d2iRb|@USk#R{jb7532YraHPCqNk8&U=8)K@f(S(YC^1$zi?Q1ur zi+A*#q*?^H^^ zfX$>D>>_Bs57_JJBq(1D-c#~aL|y5Z`3_ZlB8wvc_4{-*;lZHut-}^CQ}tl_zbX5< zn}NyoN~jyqyHz|tdb!4Rah(;&S|gfXG2?5O>?(fa7Rt5lnmb={mv%n&%(l!5#1Sa5 zGc7xRo-pb#bM98c5mz4SU# z7VG@s!A^O-;-Ae5ExtB?`UFnzmsrL=y!{zMj6iMJ&+Z+N!|u4tt}Jv5#c(=$z|Ho} zc;GFBB`p1qtsj{-y|ln~In-+>|CRIqbmjkTH+vp&45yl_0kN?V0SA)5Y!jz9Qa0*0 z-u+ZdOb;5JMYGo1x2c6OK3W|!5xYEvM%f8`!j|8eFxmi$-%mZRH3uK)t-ZH>Im+Fp zyFQ)C|8)*1S3d8Tv$|y;vX;deNZzzCtq3#)t!(vjY3mknmt8f@x%hL&04DC3sX&`9)N>M5&3nwj3< z`{N5d#6(mJxJJ6C#6?u=+bvJwmpUUR{w?_ZuM-R1VtB}O3*nE{4Im$LJ9;VZZtZ@l zYag$#saCAm?~E&0zguimJ&NwT@(ODHNY^-Bt6N3%Bnbpk=pqs0ABrWkCs>cw!IXVG zlm;7lORe;`5b|vI;jrt1p1h?~@mBcmsLSMFrlPqWENj;ZRnPPlJ=iP9)zTyOgqN7p zZ=8I?X)d8OUQvyz3^EWSx->-!`uAS#f_-{^)B`qBe#b2PO49s#Ev2U2NRxaLMeQ~a zhYG8DluMarSDmQQLc&*uzerUdqVF@-`Z^#r1aCgzJhVc_+WsC42FwTQgZwqWKG=7K z4{rmeLi~WV6ML^k)Oprg9|VAxAbt;tqVInFFfS6jn}!hvJLd)3e5ttytIkl87g;?U zTHZ%Y(ybClg`G~mBQq{y+{)+uZ-vv!SA;EJ+O!OUoPl$@Lzbyv?u^Sh_R;Sj`R0Ee zIVUe2a^Ct@+obr!-KPgnjSia?+YHCc+a}K@$!H~YtK1tu2j=8cNX-!f5JQ~#70>>B z$lJ$_?-%%=k^Ayu!FKw_ZRFxwiiYWl1_YbsfdV2&%=JIi-h#QHetsoL?L?I3+{Pk? zkE(R@!&Y-J8JP@yQ?ZLr|S^RDS*C))|mILtDT+~$b_@t%hz z(g4mkYbScjH7*UvaHXOQZ-q-|k;D$3ba^%tBxSaE8Y(UsU14ujLSJ0McMqRJTHN&7 zR^Y4as`EbH^d-&ZEA>g|QJFKdrwhJ#6@v7!@W)BpNSCKPgzoDW5kHDI!0-Jg|BMX6 z&jo>7kanalde?6`D!4+PxPnV19|CyZNk7Rmxn0fyNCjZxAG-tsUaeF3S>aQzlLPgv zb<)+wI-tIObLoZjQO!=eeWkmlYB55b3<+f@IyV1SFi<#O^`Y^PauA6-xA|3+d?51} z7D#O^IN^TFRL|!9jZ@)B0i`JSFm66x?lysVNTQm|dPjnS{bbiCX$>?mb15z=&rr0G z;)n1I0rwTTtIFXy0u)FF9(7JSE+>n9CY)y&44o5ejTiENFXf`QnwFW=B1@y>P5>p{JDmUaVKH zvug`C?=J|kTl4R^#eXAtAb8?rM)BE(!h-w=uub84hw_N3X+BGvkTnA3@Qc>Ah29)v6tKxE*OkCMXYkwJb~o;9n5lIJ=#+xw>n9y7z#lVA zu4*HN*z02Eeu-<@73-Gn+I_$4z-}*wx6#t65fLAE{l>o1PTfqVzGlgj^t0vjLAQm@ zrO1#rlO5R4^(wcw)%sJ#ibaZMzwpc->v=0}Qd}zF2RURTkRsslIrg~mW2tRttMj{x zqCUk_8+h_``zM25#)~5i$2Ix ztHVC0!p}pJa40aVKe7!0Ibivo)ao~rmGH2!GhbTUsV%P6)@Jqk*9mDz{)3iJ`5|Fp z?Bzjw`ME;rxzZ`5ONI^6tkw^z35FeP9$6l?64y(3KmWC{@odgYZDsOP zEV-UOwu_z@4y<87`$0xq9Y`WRgJ)klRj5KcL+t2OqljYZ=ippv&~3WAse2wqJ(i}1nB zp60=!T=O5GZ+-d|Rp>gIVC$EZl9``haH+01A^CoWbDD3w^? zAR_2;hd+W0+bs%#8b;H-`N@G#qrRA};=cy~bG2=P=|0RKCe`ZW(`qVtb`{JV>`Ybf zEgC$l$x%#K{fegKd_$G*2eotcf~LG2X-R+w8-0eeFE!79L_#yY*m`LhtE3tLx7Q zU|)tK04zZP-X~1N3Y2O}LJGsK&kS=*gbB0?tnPkD410e^H?hQujHixP_g+enCy#BH zNpyywLu~3h)3r6{GTx!i)Wpce=&b?6Un)p>O)M>%3LWJbF7x`ob8JU42v}paNZUy@D?JZZS_UB3UPOsJE%fFz&3(c)YVvL z?KKv{%)_5c;R}|z0!kNrZiJLb{*AiiIRSaTN_+@I&PLHzfJGptQ0=J#7aG6_{@g=h zkGp*OtML0J4ATr7yb}ju-E`Hq*jutj-PwWQR3f%a1%53cey$8yaPY_Z@J%0%;8bqh zsG_BZY>_+txhL-4fWE z1r))4zhmUU_5Xg|*fkDPbn z;gJFX?w}Z7CJidoHF2M;aCm=zTk*lW0Ta5rcyedePC*n_h&f#|Ao)%W6uk8Xz!LWs zZ`>axuzEe$lAM>5{+5%8otQhfKQf3GBXf0GAtB6sPkfQUG~b`Q0QgtUmVF_HX6NT_ z2#rx=FJx%NTh5E{e>6MgSy8^d&2d>bVEzUO7f@i%sVz=wf@?beAT{Mav!ssp7$F?;)E5&DFxzT|-;q$7ff(%LK~)bCtgd%+>*n6*6-F$V~HvA}X zW9I66o+ zFo8@4o$6_rd~zop`|>QeX{*hI$MD;xr{c^Y}73c#utB@wR9{19f9?#6sr zKiDW$PadrcB^50nAhq(V#O6*4ghuw@C0^UI;<#h%4}7g5rH{YX6#^{^SFH>KGI{Qhskx zgQh&Ug;3L};jgXriu;$P`dn_VrKDbGG++k&NJXfJwdtDv#f-6T9(apUy4En%lrRs9 z&}PnNp!tTFT{^jGV|vx>ka@IK2Dc~wnSrxh09ss8NtG@nDdz^ArI#Yq&hk~er06(< zcTs?G={dFfg~iWpfHU|o8S=sM0ANG*cS6`;ia$r~m`r|%f%j#_ zyBcPUm687Ziy)od{@>lN1;z9CrZxi>0f@ ztxIBV_MQ9C%^^>`2yN9K{QBnN4gI-S8$&i049wMGt#V=6ad`Sk&FJzodDG8Jz`YJFW!4e+SZT1lan@80}=2S z@9G42`va=Tm-5L0MaeWkrI-{=835lp6EZ8f8SO259=+y}GjN(({W$j}n{ zNXH5+ah47hSRnu$w3zA1Y8)J-W}1tyMhQxymwGPS&RgE}6ffspc}W0}cJP={>a-bm z*ir*~%Tbf!d>YI9^ z6DKzQsDJ6iX*vam^M4ewd~@i{%~HSJ+!ejTlJpl-5by%Cea64&KG^5>?#o$G9vKEz z0FAB$Gb#hjUEABsz#9(av}|*X(h~79A)PTL-5^fp8MS!U?ebE#+?FN;NTEIXuqP%~Dj7?_ne4 zuvLF=KmRWfJ(HKX)-P`Ji55Ti0!qaKP$o9^??Woa0ekT1z)z3>9CtW)Ily2Yq{70m z|D|Wm%rO*rE!gAe1Io?eP4L8JunIv5B4KV_7f>pUH9v(^_s)JH4Qcb)ecjSx@ml{a zh%s+v;jH<~VIBeLs(Zjuy@CKk7S?#oy3-|N)cifK*=-DX;P?A=mU8c|fmK*9+$n*| z9;0eOUqZP@10kP5P-(=`INT%8lJt`#t6kgMDyV>MvpJ+YP}yT(WQ2o+tA7f9ishs zlOStaG_6-p1*(k$#~9RsC6RW;P0-graorvklPobWN$*HhY#dgwc=OH`r0QAU)>%E+ z3AN(8+0qULQXAQHS7Tjfxm3V3*kyc_nYRfQzYm+x%5*Ux{=1MFI;HLLOGAB8k|cUa0PnNZ9gvNo1_jVrwf zmj+8HZtN4GcnWnJdyG4MVp|(i`xW=0>xP;YDtEu=$ymI}z;@8Z*D~}Abv7*43zsN3 z%>j=)b|uooJW$fh$DEykO$$7y?Y%or$zKrzuZQ64pd$$=jse@2Ceea-h5u5 zovHRmLzJlIfOczFK17B-8@;&r8KhR7p!K6W@JayJ!OuN>t(<9$_n&kFe!X(hIM+r1 zLl9Y-pUBG5MYz`uUML7)WIG9m(#l;*6@IXb!C@bsT|0!`Lg27_hiwhd|7;GaH!5=S z&9IqZ{QmkwDbKJ}pJ?Hc|Hs~&heO%^f8%#aiqJxmP^rjTglt18Dp^X%uI%f`zRgUM z+?A!WWv8-an6d8+DP+$!wlQQkX6y!I%yZ7?dw)Ln{T#>l`F+##=kxo+(Huvvab4$i zp6})Le!pIC)M=BkFTwS?toy?_PwA=16nq2u_F+QM2j;uT3Rf!-1#ALv=`h<8GpfW-nNwliS@#5ZB(455PwGT2AP{U(gycSn zecM1OX?+ffHUhN!@#LY@Ye9E06|Ml5uEcI{t~3tgXVu-T$8n4(b zQe$iOQ!m#YcuuQNTlS_+!|)8lzw2hdfMZ@gOouVeK%vO>JdR%Gy6HJ$XE$&}2n0EP zSF1iZSAu9livPWyr&TZ{$hjp(O#DrjC$qlr#;JkFjLk5+`xFEK%ie~RlMOkA45n)a z3$J&B??LKiKlmQTeW-125WGZQm+EIUhW&?J>w1r#zf<Kx0aP@ZOg& zH^2cM1Ofi>%a4~!xerD$G2kI~=n;2$otl76H!g=hldaX+V<EvIIDstIyE3oIC4&>Gguo#q$TE zpu=T9g!OY~GCpKiX1C_d4Lg8%i^U+-OXNBN!0xs{MdyV-kBA3PjyP|QOYS0S@4N$Z zKch%WWptXdA-52QfT}ckqZ|DL3h2SEZ$?5ir9%Mywq6+Id>JcU8WQE5RbPtR;-w4W z)UEQhw+P8f^Wc6^xnq=V{g(@sc55A_tnP32L|n{4T|y6nQ$&o?)q2t*$17w|)^C8p zp5n}jaGGU}Vp`Y)neO|&?KQ<9l2N*vYJOP9bIXo6o0-C9aDmMJ=*E!5UCo-K9HqQi%TVG)G%^sw zEo2%5|0pJcP?#O1l|KxC3~zI!1mh^RUi_pZh_PG{{IB@0x~6`13&^t%1;K0_U)ACY zD82_Hbx;ezO&R6^oeUQT)$!bQE8R8Bt%PJpfHV}S9*hzk|8whtWK`~@;=a$Hmn)1G z>ljOarYQUUagCkjo1-Cjdw8OMmG38OWBqnHu@wvR-Mm{BPI`eX`y!2QngAKNOt{jq zFXysess7|3KqKS!PC&AdhZjJ2YXhQIShg29&e(GSH{JAF^-_(aJ$r516YW9N<F$8Kg zY4s-3KL-kJhmNS98XFN=&9TTHe8$LPlG;-ZbX0WKTBSNX96#t4^GVr+u7SAR8GY~L z3xfKXZ!WvcLRwxnyD>Yjd=CJ}4-bn~CB{)n^qR;v=Z#ztYbXKE_t=xiV&{$u833G8 zUtfBLUi8BunR~Ja3ymfof~^u;FE^byKIH|hOI&s)@???l4T9M{PH)B=^k**B1`c#>!V5Jv_1ek#Pm>#V&m_(tc>86DsKB<6t69*@S^$DjyaxzJ>R<7MS zE|dbwWA-CksO1!CW$e5U6{IsJYc8glioVNVPu(Zyd>P#`;SkYp1%WAtJAQsJ&v(Vf zTf$D<@2XnhT2qEE0mGV|V+P;{E+O*t=j^EpgPv7bR)~6*4&TN1+tpuapsM690=xMn z_}d!qn0X}s(n6(0@CpKZL2R?;EY`NqfA3`6`P<67i11XVn5~RbO~%w12aY;txB3Ir zMzF}=3t$RA*L(e_Umx7|9w@LDKTN=GVlAxgFyeiVmvm5zS$o&*?X#ceoc6HU*UY(U zU;*g|c|C*+RGxKx6eXK@MZTiOB@4|N6ZI~dXH}MjAynXwe4&S&=sZrVtKrjVN zu9=Gf3y+eUN&ACw?3i<`ncrtte#mg}M37#iB`^NNO%`&Vx+|qCT|@Ul#QKnQ>|8Ih zvea+pC&(;T5+2uN|HKHlQ%W!(M5}tRWK6y)M%nj_y?^80!6^Ti{pdQK^6Ulzrmki- zE%Kq5eySp}ej=V7z@rAiYr9#YcKQl!gAD76-lksg14ehfSy>|r=(thVwT7BguMR@q z5(-3gP|O6ZskdptSOO>;19(-VM0`VS`Z9^j3h5a=RyhItL;9hRFONi1A9bU>p)=iauq=TDjNDOGFg2 zgd1yst7&@>r_}%8Vj0jB9Mg**0%-jSIDh$@IO>*b+1{E8^kbZMo!o}2`-Ec9neEY? zI%_>k13eeLB{q5_^wJ~}luiaTWpNV~`iED6G$b?Ic#NQSvJ*((JzKI*lSGtg;G|!O zIC$*Br>kn5Q){kIQR%|Z33}xAjiqtto$bx_S_by70iCKT-7?ODWe6iS8Q9bod0`>@ zJrmYZZrLpPiv2q!P%ODfnjC$S{RJIzJOJ3#G$14X1%=u1BX2GdEUV`dAB@QxZ7B6! zewl+E1}->{Mkh28d)i0>G8-Y5Kw6OPmIdmOExI zNP%9JxzmBXAY-v+K7stawajT?a-=`r?cTLHG-hXzBSylcEbrnz(>#m1CQLg>DMnq9y@7(~LX<;xl!zp1r-uq}+{Lo@kL8us%OP3Jd``@Ou&{ z3%wqsmrjfKR`1t5r<0uaJ$-Bxw7viwgK7i9-cq3Z-9T+gkcF0o0JyOqG-^15ZK7Xb z^?lI~245{7Dtq#gy41)|s`6}~699GoG3o~~ySsYH_-g-BUeM~5G5R???D!(KdJE@` zkDE%nR0UyRCaaHh|7f`FYzq2Hba`Tl2j)BW=3k$*KNw|e@ZKbRUo&~S@@4s%U+#CR zMHrrL0Y>O$JT7pV4q@r}_sIop&1QOiJ)&+kHQQ+*!vY$XvM-=&5g)`rPR8gRECHhz z%jxPfJ7VZKp+!*mPJmyol{Hl@Zbj*kDASyQ42GvAvv@^x_ztBKD(_+*q?vPmM;$+< zT-eYf7bRg1DXjR&zZ(~-d=YGyW^3qQ`w=AUk1ZQQUluSS+0F&F0` zDJ@8ecjg#M0kq-CtMCI+nWrBy?tQd}v4xW(g6pWZRf=nfVcubGc*o4){4Xoq#Mtp& zvsVWEi53V?JugIM0H z=brii-Dc%Smoj|-i@>myec5&wukf#Loj-h9hcB%Pl{3v=zUeec3>3nd1!f$lTAfGYlBtyIF94-0kNgYQ@01+M)!=IN#djzjx^l;m&y^*0~E zi+7Josr$Y6o_GxGI`{R^(BIm16ItL?otm_D-SwfOZ@tsg`@n>vL?r=%e=O#&--nzq zu&?Ty+^7ED#=H2`B)4bpq?~O>(=MX4zwhn;#C)YR>p~Mb zzq5zJ|6ZS6d^&Y?FJkD7@AAWus*s0udqa$pJKeO3JQ$m4!zJ~>-`yX734OVXult8d zblMxj*$Ul?#fh~AF1Hz%44`m`a^IPwG4m}y~k~S^WN^$!-R?>{(tZD-F#w72L?N6xxnrIZ*6;(IpAz`WgoNu zmc1WM@8Ze#>|?tF?wx7Udh*k*F8lW{2lg<6jL79H-!I?pKMzFhHIRjEgCR$W z9KW@6Zz{ln=?UxKO}*pqWx6T^?wvm>Hahb9Z-s{c|6%{XJnR*S#2$dvIs?$95Okax z1De8m#O=$k8Sbl)-;y#Jh-N>f-cU=205uDj>bpGgeW#B5a@*b=sSh-@nH5dd-MqCX zIN0X{P}2228f7AqLm*bzbP#mUzj;gztav>7RDHhJz^tUZ<7$0IhtGXr~F}E6JOZ|wb$bnXITOHBr;mWJK0nx?F(EI zxzW7=pwJw%BoUmJfO_K zki*+rppji+IqT!mcoBu&m_epz^gzGtP6}ct&U7g2(W|%oyplb0eaP6yu(Kk7lp;%~ zfC0W-=PQnj@0!jS)Z5Y!TF6U}jKC~UEm$Y} z=)%H0TT};ZnwQX@R=r}nI70c?e8BI6i2b0oKlw+#d$Xcu?Gc4zLJ@!5o^)1QSa?vO zZMPD5LWo~+;xf`;A^UlIMAB|}(4t0ms`;)~A4e%)55$v{i(!R2zpXV3W?yp~2N6^neKp3F@xp1~CcZ3aAXc`mz$VxK4 z%jf(@cPPUZ-*q#GKz~Bgl;LKrSm5%N?kBZ-PLD2t`r}LUoY4Kv$N24O1Cc7egQ#zT zCxx_!3~ky+_BBg2zBT*x^|7FVNF%uoYdz+_Fpx_eEW2lZa_N;``OI14QW95Xkqv7p zY6ylSPQ{4f+>CZMYMqT3CIQW=lsHt{0jxk|%$U={NcCsFafaONsvWS4?dO!m6bPqQ zCCse7lPs%!fFlq!YoVpn#t+zmZ(+ zH?-84V)6QTHBs>z(`)SEMN>3m5wCZ zM_#CY#O%DYL|Jf20g}kAcM`oSl6-!?^fD7V+z$+;OZCo%3cclLY8j}hS5h5&a;(F6 z@FyTLgMdp2>IPNiY57_O8Ex@HW%-s)n~nTKwkf$@m9ZE7QJ`O_Ww6NE7LXN6yr)|8 z*&z?DKw&dq0`u_L$aF!AL@B`Ghk&N~JE!*meQ%a)wgt88b@wJ)Vu$hout|5P4L`ea zI^r_Iq9qXUg7Fd;oCA5JyEhjYp(M~!@*PlLe>PAzGgF4QScD(s`K_S}8f0Z|A1P~Z z^1L@4Pkvn=G6ZT&^9g8-*Z9TS06N_a@YTjeC-NP!;$WuPkf!^RCc#5l+`LB7YV)lE zmhLkEA!`z4JyHE>d~1z(tOC(Dm>z59f;B}{JL3wzjl@yHFD7m|HWAluN5aTY%%w9> zW1zQ;vj$RO0A>yLbYgUKpqP*^*e0fCN91lgC~N9r3I zE@oy+OeR!#d_U?x8GU5Eo~-=5v2(2>h1Thc`D(>2b zr5z>)bLkfut731G9~Na$X4APmVFn>NH6AHSMC1I&=PNI*qU{T9=#=5bv~hDDTbARk zrO_e=FO)N5d$NvuhBik1v zW^9AhN4dIpQjOD_Z}yeoa${tnBa2ld`D@e;i=*plN(%(X*#K>G0M<7Z{#`a{;vvK{sf|xB3pL z5yvVOU%#4q&k>1;TU*d-l=kQQ=^xqnw--Lae@ujSQr{#meei0(vx~f8l>c8=SY%_= z2P|s-wD}1>KI)IUAg8Yy5*6gSR9~Nv6D-n>CQby^Ju^Pe%RRGRV9eu?lDBtpoK!~R zP;ao&bZZjy<|~ea3z@>mQoI*ZEMNh*SYgM)ETI)AFIIK&L`GPS&JI|!LFurQWfr+# z^1E%TxTTx;NTNlf6c*I$l7P9-=f9j8wHqqdN?R68zRO%lfnCsgZ&B?BnZY*%z^o^P zxTe+=gcgm9@0_Q}kdW8I$zGKb1CZHj@wo}WFC>ysNs((@3!0b^z7j`tWjk?MS150A zvt4UU1zt2R?_I9IG3*@c6v^(5G}@4xj>>O*amZ^-kNAn{*p!5i#K6vv5yhpAOa7z* z+YipnXydhkcvnWKvW<^rXmD40NB+_=b8UUjt4dhUhE@BtkbR`^%XhlwL>#gCY4v;& zmD(T3IeVATRq|b6far`B^`ggM`ikk8g-<~kt?GxIGIib^Fc&YEW`K}O{NX8Qa|}wo zW90kwpK#b>Z-Tmhu=Z+V7e(%F5p8+e+@eE?T*Z4YOUGyMGh;IsVlmKj&PRO%&dR^h z@vP?c3#Fw`dbvhmVZ~CQN!n5+7>f4ORJg zVnlDGWJOC)=6p%S0j5dAXOF%Md);@lrOH+B*FUI&?V z;gXDw-eQYGudBcap1P;<^S8FKA9_!UoC*3Qs)m4BzA%?5$H;N$qb?qFrR&5>hw}9G zOy(#1kq4)nlZH;3;A(@#Z9Dv0KoOBigoD|F4}1XiyxV9Diq>nEa`p0qVy}FjZ0D?I zKub6MQXkGM)xbB0ZLBu%DYnTvx(dqmglu&v)g9^FpLgcLcYo)sJ8ou`BCd(-3c^)f z+(KZa7M$iW>ys};`$3dqN1M;Xxn~tYhoXq0IseolI$Ml01duDOh^l&|acMp0nv#3}D%)?QYEyFz_RLZ^z*NSA6_m3kQ7I(z;BIOVr+^u}SJ>e5QHYEZRxf%)4`;?1 zP4zUGq*$!Yb!#ar87^K`w6Hki)6tx1Qob@=rIF|rPpw(<_FwCbt=F4e8@Lzji3eq8 z%UE-LkpMHzc$vc2;@KxF&=DRO@x=xn=yT;l+2PK9KQeKet=IcywpB;GbV~rE3y!BQ z13qyA?Yq(gs=HLVL-F?TV)5$MB=4o4-*6HlilEFr=3n?%;8ve`L<02uZxuV39o^aX zM&yMg@l-_Yuu-)!9+!MNRj?5d4fkoWJXLnhY&HJP&AuS1tpI|1l*p>G-adXQzXv{S zMx6|*ARut(Y*?eFIi_`#8`Y~}`|BePxwh;?kj#~=(lRU)UZ9HM7q4^D%mbDfDH|Un zhGk4GO&#%Pb05@UK$E0?)yB(p1g-RYhE~#Jeo>rs zCWv3f$E>L02ZEWom`&@IoyHqj(^A3AG!{m~BSP9e^&Ju;pyvhR+G&C0-&)9q&Tg{N zfC@#fU-9eV>YNaPhEU$=N{#h7<&Zj9my}t7mT%B5pqv}@y67L@n@b~A11FeB+SyvX z++Fk}ED2$Dzjv_DdC&-3sf(UDnTTXh0(S1BEqXNLc?_TeIIaakPfn zT=-IUhP%&BFju5rcAp=kdV;LuhntntBQ9PV$UYajx5_ZeY6-R>%(D_@LiH_Muox}U zz*cJ3jW@~ouZtS6nh@yOCgXhp^2>F|h-aKKA{qC@|NbAo8yW)lpC@28ezG|Bq}C8h zpqL#m+!{6{WMj3PW*FHZTSl(lS}kq}gC2r*KjS-E_Q_ALRBOzzZ9o=*o~=d&ljQW2 z=p&A(;!n&M2wu=Ehb4~Ejuv!y!JX*)XcDQhs^Xd|&m~u#S395tZd-YlqekB`!19>x zQ^=3wgo?S1@`Wl(UWFoW)7j+idKP>8on*cD+eA5c-BVyD_EaYW|C*_tNuVENoO=l0 z-g_+iDGLM0Qe6Kh`+{*#s7DPlvRcYE+?YoCa`XG%h0kG0IfX*dD&HIxUws%GwW^oa zT56Md$>3HwyjouTggDl)aQb=MPsV0*zb9p^z8-lB-50xU`VDQVlsC`y#UEb64~D>B zUK2Yp%m%NqgxT&}NW1s3agY&k6yV$Z)MnMnlHB?+lC}Yg&EO=y#5tk2tSnw3#m}Hu zHaME9lsfL2lrY@iuLr?;&pz|!stEBd60?(r?o@uyE_STsyHiFQbTqM07*u)>(vi2D zN^29x)?2%^?S)BPXC7E96>Kg9wH}HhjNFb;pBR3&0Q%a4C^y9$DQpYfm9b$6v5U8A zJOcI; z1}C%0#tu^qXsfdlC~@u{JYJ3I)MHDAjJH`0-S;1_PXP^|NE#nI!~$9c`Jva#`obKB zXFk^srzCN#giFivdmY-}%=xA=+r^nH`N32D>CXDdRgLG

diff --git a/docs/ug/reportConfig.md b/docs/ug/reportConfig.md index 5647ef77cf..e5593d8a1b 100644 --- a/docs/ug/reportConfig.md +++ b/docs/ug/reportConfig.md @@ -14,10 +14,10 @@ This guide will help you set up your first RepoSense report using the `report-co ## What is `report-config.yaml`? -The `report-config.yaml` file is a simple configuration file that lets you define which repositories you want to include in your RepoSense report in more detail compared to the CLI flags. +The `report-config.yaml` file is a simple configuration file that lets you define which repositories you want to include in your RepoSense report in more detail compared to the CLI flags. It's designed to be user-friendly, especially for beginners who want to quickly set up a code portfolio without dealing with complex configurations. -For more sophisticated use cases that require additional flexibility, +For more sophisticated use cases that require additional flexibility, RepoSense's [CSV config files](./customizingReports.html#customize-using-csv-config-files) offer an alternative configuration approach with expanded capabilities. ## Quick Start Guide @@ -46,7 +46,7 @@ repos: The `author-display-name`, `author-git-author-name` and `author-emails` fields are optional. -However, users are encouraged to add their Git author name `author-git-author-name` and emails `author-emails` in their Git configuration related to their commits so that +However, users are encouraged to add their Git author name `author-git-author-name` and emails `author-emails` in their Git configuration related to their commits so that the commits can be successfully captured.
@@ -64,7 +64,7 @@ repos: authors: - author-git-host-id: johnDoe author-display-name: John Doe - + - repo: https://github.com/john/tp.git branches: - branch: master diff --git a/src/systemtest/java/reposense/ReportConfigSystemTest.java b/src/systemtest/java/reposense/ReportConfigSystemTest.java index cce5ee6eb5..44340bfd87 100644 --- a/src/systemtest/java/reposense/ReportConfigSystemTest.java +++ b/src/systemtest/java/reposense/ReportConfigSystemTest.java @@ -26,7 +26,7 @@ public class ReportConfigSystemTest { private static final List TESTING_FILE_FORMATS = Arrays.asList("py", "sh"); private static final String TEST_TIME_ZONE = "Asia/Singapore"; - private static final String OUTPUT_DIRECTORY = "rc_temp"; + private static final String OUTPUT_DIRECTORY = "ft_temp"; private static final Path REPORT_DIRECTORY_PATH = Paths.get(OUTPUT_DIRECTORY, "reposense-report"); private static boolean didNotCloneRepoNormally = true; From 3f9addaf9a3c305e56d5367d8d65363a2b7d81c1 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Fri, 4 Apr 2025 15:22:19 +0800 Subject: [PATCH 36/42] System test --- .gitignore | 1 + .../java/reposense/ReportConfigSystemTest.java | 18 +++++++++++++++++- .../30daysFromUntilDate/expected/summary.json | 2 +- .../expected/summary.json | 2 +- .../resources/ReportConfigSystemTest/blurbs.md | 2 +- .../authorship.json | 1 + .../commits.json | 1 + .../expected/summary.json | 1 + .../expected/summary.json | 2 +- .../expected/summary.json | 2 +- .../expected/summary.json | 2 +- 11 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/reposense_publish-RepoSense_master/authorship.json create mode 100644 src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/reposense_publish-RepoSense_master/commits.json create mode 100644 src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/summary.json diff --git a/.gitignore b/.gitignore index 15049361d9..c73cad899f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ *.tmp ft_temp/ +rc_temp/ /target/ .gradle/ build/ diff --git a/src/systemtest/java/reposense/ReportConfigSystemTest.java b/src/systemtest/java/reposense/ReportConfigSystemTest.java index 44340bfd87..b918bdc073 100644 --- a/src/systemtest/java/reposense/ReportConfigSystemTest.java +++ b/src/systemtest/java/reposense/ReportConfigSystemTest.java @@ -26,7 +26,7 @@ public class ReportConfigSystemTest { private static final List TESTING_FILE_FORMATS = Arrays.asList("py", "sh"); private static final String TEST_TIME_ZONE = "Asia/Singapore"; - private static final String OUTPUT_DIRECTORY = "ft_temp"; + private static final String OUTPUT_DIRECTORY = "rc_temp"; private static final Path REPORT_DIRECTORY_PATH = Paths.get(OUTPUT_DIRECTORY, "reposense-report"); private static boolean didNotCloneRepoNormally = true; @@ -106,6 +106,22 @@ public void testSinceBeginningDateRangeWithPortfolio() { "ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected"); } + + /** + * System test with a specified since date and until date, with the last modified date time in each + * line of code. + */ + @Test + public void testDateRangeWithModifiedDateTimeInLines() { + InputBuilder inputBuilder = initInputBuilder() + .addSinceDate("1/1/2024") + .addUntilDate("31/12/2024") + .addLastModifiedDateFlags(); + + runTest(inputBuilder, false, + "ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected"); + } + /** * Returns a {@link InputBuilder} that is initialized with some default values. *
Config Folder Path: {@code ConfigSystemTest} diff --git a/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/summary.json index c7e6d1cd91..7ef89463c6 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/summary.json +++ b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/summary.json @@ -1 +1 @@ -{"reportGeneratedTime":"Mon, 31 Mar 2025 13:26:14 SGT","reportGenerationTime":" 6.92 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"2024-07-31","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-07-31","untilDate":"2024-08-31","isSinceDateProvided":false,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense.git":"Publish branch of Reposense","https://github.com/reposense/publish-RepoSense/tree/master":"My project"}},"isPortfolio":false} +{"reportGeneratedTime":"Fri, 4 Apr 2025 15:03:26 SGT","reportGenerationTime":" 1.98 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"2024-07-31","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-07-31","untilDate":"2024-08-31","isSinceDateProvided":false,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"isPortfolio":false} diff --git a/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/summary.json index 76b36937d0..126acb7c58 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/summary.json +++ b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/summary.json @@ -1 +1 @@ -{"reportGeneratedTime":"Mon, 31 Mar 2025 13:34:36 SGT","reportGenerationTime":" 3.40 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"2024-07-31","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-07-31","untilDate":"2024-08-31","isSinceDateProvided":false,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense.git":"Publish branch of Reposense","https://github.com/reposense/publish-RepoSense/tree/master":"My project"}},"isPortfolio":false} +{"reportGeneratedTime":"Fri, 4 Apr 2025 15:09:32 SGT","reportGenerationTime":" 4.39 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"2024-07-31","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-07-31","untilDate":"2024-08-31","isSinceDateProvided":false,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"isPortfolio":false} diff --git a/src/systemtest/resources/ReportConfigSystemTest/blurbs.md b/src/systemtest/resources/ReportConfigSystemTest/blurbs.md index d260bfda9d..25be4a7c78 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/blurbs.md +++ b/src/systemtest/resources/ReportConfigSystemTest/blurbs.md @@ -1,2 +1,2 @@ -https://github.com/reposense/publish-RepoSense.git +https://github.com/reposense/publish-RepoSense/tree/master Publish branch of Reposense diff --git a/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/reposense_publish-RepoSense_master/authorship.json b/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/reposense_publish-RepoSense_master/authorship.json new file mode 100644 index 0000000000..f310626ff7 --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/reposense_publish-RepoSense_master/authorship.json @@ -0,0 +1 @@ +[{"path":"get-reposense.py","fileType":"other","lines":[{"lineNumber":1,"author":{"gitId":"-"},"content":"#!/usr/bin/env python","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":2,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":3,"author":{"gitId":"-"},"content":"import argparse","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":4,"author":{"gitId":"-"},"content":"import sys","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":5,"author":{"gitId":"-"},"content":"import os","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":6,"author":{"gitId":"-"},"content":"import shutil","lastModifiedDate":"2019-01-26","isFullCredit":false},{"lineNumber":7,"author":{"gitId":"-"},"content":"import requests","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":8,"author":{"gitId":"-"},"content":"import subprocess","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":9,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":10,"author":{"gitId":"-"},"content":"JAR_FILENAME \u003d \u0027RepoSense.jar\u0027","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":11,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":12,"author":{"gitId":"-"},"content":"def parse_args():","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":13,"author":{"gitId":"-"},"content":" parser \u003d argparse.ArgumentParser(description\u003d\u0027Downloads a specific version of RepoSense.jar from our repository.\u0027)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":14,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":15,"author":{"gitId":"-"},"content":" group \u003d parser.add_mutually_exclusive_group()","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":16,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-r\u0027, \u0027--release\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Get RepoSense.jar from the latest release (Stable)\u0027)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":17,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-m\u0027, \u0027--master\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Get RepoSense.jar from the latest master (Beta)\u0027)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":18,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-t\u0027, \u0027--tag\u0027, help\u003d\u0027Get RepoSense.jar of a specific release version tag\u0027)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":19,"author":{"gitId":"jedkohjk"},"content":" group.add_argument(\u0027-l\u0027, \u0027--latest\u0027, help\u003d\u0027Get RepoSense.jar of the latest release of a specific version tag\u0027)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":20,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-c\u0027, \u0027--commit\u0027, help\u003d\u0027Get RepoSense.jar of a specific commit\u0027)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":21,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":22,"author":{"gitId":"-"},"content":" parser.add_argument(\u0027-o\u0027, \u0027--overwrite\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Overwrite RepoSense.jar file, if exists. Default: false\u0027)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":23,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":24,"author":{"gitId":"-"},"content":" return parser.parse_args()","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":25,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":26,"author":{"gitId":"jedkohjk"},"content":"def handle_latest_tag(tag):","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":27,"author":{"gitId":"jedkohjk"},"content":" page \u003d 1","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":28,"author":{"gitId":"jedkohjk"},"content":" major \u003d tag.strip(\u0027. \u0027).split(\u0027.\u0027)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":29,"author":{"gitId":"jedkohjk"},"content":" while True:","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":30,"author":{"gitId":"jedkohjk"},"content":" response \u003d requests.get(f\u0027https://api.github.com/repos/reposense/RepoSense/releases?per_page\u003d100\u0026page\u003d{page}\u0027)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":31,"author":{"gitId":"jedkohjk"},"content":" if response.status_code in [403, 500]:","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":32,"author":{"gitId":"jedkohjk"},"content":" print(\u0027GitHub API has exceed the rate limit.\u0027)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":33,"author":{"gitId":"jedkohjk"},"content":" exit(1)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":34,"author":{"gitId":"jedkohjk"},"content":" releases \u003d response.json()","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":35,"author":{"gitId":"jedkohjk"},"content":" if not releases:","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":36,"author":{"gitId":"jedkohjk"},"content":" print(\u0027Error, the provided tag does not exist!\u0027)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":37,"author":{"gitId":"jedkohjk"},"content":" exit(1)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":38,"author":{"gitId":"jedkohjk"},"content":" for i in releases:","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":39,"author":{"gitId":"jedkohjk"},"content":" release_tag \u003d i[\u0027tag_name\u0027]","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":40,"author":{"gitId":"jedkohjk"},"content":" if release_tag.strip(\u0027. \u0027).split(\u0027.\u0027)[:len(major)] \u003d\u003d major:","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":41,"author":{"gitId":"jedkohjk"},"content":" handle_specific_release(release_tag)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":42,"author":{"gitId":"jedkohjk"},"content":" exit()","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":43,"author":{"gitId":"jedkohjk"},"content":" page +\u003d 1","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":44,"author":{"gitId":"jedkohjk"},"content":"","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":45,"author":{"gitId":"-"},"content":"def handle_specific_commit(commit):","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":46,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/commits/\u0027 + commit, commit\u003dcommit)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":47,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":48,"author":{"gitId":"-"},"content":"def handle_specific_release(tag):","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":49,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/releases/tags/\u0027 + tag, tag\u003dtag)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":50,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":51,"author":{"gitId":"-"},"content":"def handle_latest_release():","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":52,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/releases/latest\u0027)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":53,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":54,"author":{"gitId":"-"},"content":"def get_reposense_jar(url, tag\u003dNone, commit\u003dNone):","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":55,"author":{"gitId":"-"},"content":" response \u003d requests.get(url)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":56,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":57,"author":{"gitId":"-"},"content":" if tag and response.status_code \u003d\u003d 404:","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":58,"author":{"gitId":"-"},"content":" print(\u0027Error, the provided tag does not exist!\u0027)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":59,"author":{"gitId":"-"},"content":" exit(1)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":60,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":61,"author":{"gitId":"-"},"content":" if commit and response.status_code in [404, 422]:","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":62,"author":{"gitId":"-"},"content":" print(\u0027Error, the provided commit does not exist!\u0027)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":63,"author":{"gitId":"-"},"content":" exit(1)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":64,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":65,"author":{"gitId":"-"},"content":" if response.status_code in [403, 500]:","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":66,"author":{"gitId":"-"},"content":" print(\u0027GitHub API has exceed the rate limit. Falling back to alternative method...\u0027)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":67,"author":{"gitId":"-"},"content":" clone_and_make_reposense(tag\u003dtag, commit\u003dcommit)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":68,"author":{"gitId":"-"},"content":" return","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":69,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":70,"author":{"gitId":"-"},"content":" if commit:","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":71,"author":{"gitId":"-"},"content":" clone_and_make_reposense(commit\u003dcommit)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":72,"author":{"gitId":"-"},"content":" return","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":73,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":74,"author":{"gitId":"-"},"content":" url \u003d response.json()[\u0027assets\u0027][0][\u0027browser_download_url\u0027]","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":75,"author":{"gitId":"-"},"content":" download_file(url)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":76,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":77,"author":{"gitId":"-"},"content":"def clone_and_make_reposense(tag\u003dNone, commit\u003dNone):","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":78,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":79,"author":{"gitId":"-"},"content":" # Cleanup cached RepoSense folder","lastModifiedDate":"2019-01-26","isFullCredit":false},{"lineNumber":80,"author":{"gitId":"-"},"content":" shutil.rmtree(\u0027RepoSense\u0027, ignore_errors\u003dTrue)","lastModifiedDate":"2019-01-26","isFullCredit":false},{"lineNumber":81,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-26","isFullCredit":false},{"lineNumber":82,"author":{"gitId":"-"},"content":" command \u003d \\","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":83,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":84,"author":{"gitId":"-"},"content":" git clone \u0027https://github.com/reposense/RepoSense.git\u0027 \u0026\u0026","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":85,"author":{"gitId":"-"},"content":" cd RepoSense \u0026\u0026","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":86,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":87,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":88,"author":{"gitId":"-"},"content":" if tag:","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":89,"author":{"gitId":"-"},"content":" command +\u003d \u0027git checkout tags/{} -b deploy \u0026\u0026\u0027.format(tag)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":90,"author":{"gitId":"-"},"content":" elif commit:","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":91,"author":{"gitId":"-"},"content":" command +\u003d \u0027git checkout {} -b deploy \u0026\u0026\u0027.format(commit)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":92,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":93,"author":{"gitId":"-"},"content":" command +\u003d \\","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":94,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":95,"author":{"gitId":"-"},"content":" ./gradlew zipreport shadowjar \u0026\u0026","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":96,"author":{"gitId":"-"},"content":" mv build/jar/RepoSense.jar ../","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":97,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":98,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":99,"author":{"gitId":"-"},"content":" subprocess.check_call(command, shell\u003dTrue)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":100,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":101,"author":{"gitId":"-"},"content":"def download_file(url):","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":102,"author":{"gitId":"-"},"content":" response \u003d requests.get(url, allow_redirects\u003dTrue)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":103,"author":{"gitId":"-"},"content":" open(JAR_FILENAME, \u0027wb\u0027).write(response.content)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":104,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":105,"author":{"gitId":"-"},"content":"if __name__ \u003d\u003d \"__main__\":","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":106,"author":{"gitId":"-"},"content":" args \u003d parse_args()","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":107,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":108,"author":{"gitId":"-"},"content":" if os.path.exists(JAR_FILENAME) and args.overwrite is False:","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":109,"author":{"gitId":"-"},"content":" print(JAR_FILENAME + \u0027 already exists. Quitting.\u0027)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":110,"author":{"gitId":"-"},"content":" exit()","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":111,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":112,"author":{"gitId":"-"},"content":" if args.tag:","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":113,"author":{"gitId":"-"},"content":" handle_specific_release(args.tag)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":114,"author":{"gitId":"-"},"content":" exit()","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":115,"author":{"gitId":"-"},"content":" ","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":116,"author":{"gitId":"jedkohjk"},"content":" if args.latest:","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":117,"author":{"gitId":"jedkohjk"},"content":" handle_latest_tag(args.latest)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":118,"author":{"gitId":"jedkohjk"},"content":" exit()","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":119,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":120,"author":{"gitId":"-"},"content":" if args.commit:","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":121,"author":{"gitId":"-"},"content":" handle_specific_commit(args.commit)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":122,"author":{"gitId":"-"},"content":" exit()","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":123,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":124,"author":{"gitId":"-"},"content":" if args.master:","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":125,"author":{"gitId":"-"},"content":" clone_and_make_reposense()","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":126,"author":{"gitId":"-"},"content":" exit()","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":127,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":128,"author":{"gitId":"-"},"content":" # If no arguments are provided or --release","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":129,"author":{"gitId":"-"},"content":" handle_latest_release()","lastModifiedDate":"2019-01-21","isFullCredit":false}],"authorContributionMap":{"jedkohjk":23,"-":106}},{"path":"run.sh","fileType":"other","lines":[{"lineNumber":1,"author":{"gitId":"-"},"content":"#!/bin/bash","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":2,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":3,"author":{"gitId":"-"},"content":"# Downloads a specific version of RepoSense.jar of your choice from our repository","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":4,"author":{"gitId":"-"},"content":"## Examples of supported options:","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":5,"author":{"gitId":"-"},"content":"### ./get-reposense.py --release # Gets the latest release (Stable)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":6,"author":{"gitId":"-"},"content":"### ./get-reposense.py --master # Gets the latest master (Beta)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":7,"author":{"gitId":"-"},"content":"### ./get-reposense.py --tag v1.6.1 # Gets a specific version","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":8,"author":{"gitId":"jedkohjk"},"content":"### ./get-reposense.py --latest v1.6 # Gets the latest version with the given tag prefix e.g. v1.6.1","lastModifiedDate":"2024-08-13","isFullCredit":false},{"lineNumber":9,"author":{"gitId":"-"},"content":"### ./get-reposense.py --commit abc123 # Gets a specific commit","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":10,"author":{"gitId":"-"},"content":"### ./get-reposense.py --release --overwrite # Overwrite RepoSense.jar, if exists, with the latest release","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":11,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":12,"author":{"gitId":"-"},"content":"./get-reposense.py --release","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":13,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":14,"author":{"gitId":"-"},"content":"# Executes RepoSense","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":15,"author":{"gitId":"-"},"content":"# Do not change the default output folder name (reposense-report)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":16,"author":{"gitId":"-"},"content":"## Examples of other valid options; For more, please view the user guide","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":17,"author":{"gitId":"-"},"content":"### java -jar RepoSense.jar --repos https://github.com/reposense/RepoSense.git","lastModifiedDate":"2019-03-15","isFullCredit":false},{"lineNumber":18,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":19,"author":{"gitId":"-"},"content":"java -jar RepoSense.jar --config ./configs","lastModifiedDate":"2019-03-15","isFullCredit":false}],"authorContributionMap":{"jedkohjk":1,"-":18}}] diff --git a/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/reposense_publish-RepoSense_master/commits.json b/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/reposense_publish-RepoSense_master/commits.json new file mode 100644 index 0000000000..e35d0f493e --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/reposense_publish-RepoSense_master/commits.json @@ -0,0 +1 @@ +{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":24}},"authorContributionVariance":{"jedkohjk":2.7689133},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} diff --git a/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/summary.json new file mode 100644 index 0000000000..26b661cb34 --- /dev/null +++ b/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/summary.json @@ -0,0 +1 @@ +{"reportGeneratedTime":"Fri, 4 Apr 2025 15:14:41 SGT","reportGenerationTime":" 2.28 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"2024-01-01","untilDate":"2024-12-31"}],"errorSet":[],"sinceDate":"2024-01-01","untilDate":"2024-12-31","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"isPortfolio":false} diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/summary.json index 04dbec9ca1..f017260a70 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/summary.json +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/summary.json @@ -1 +1 @@ -{"reportGeneratedTime":"Mon, 31 Mar 2025 02:46:49 SGT","reportGenerationTime":" 1.68 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"1970-01-01","untilDate":"2024-12-31"}],"errorSet":[],"sinceDate":"2024-08-02","untilDate":"2024-12-31","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense.git":"Publish branch of Reposense","https://github.com/reposense/publish-RepoSense/tree/master":"My project"}},"isPortfolio":false} +{"reportGeneratedTime":"Fri, 4 Apr 2025 15:01:49 SGT","reportGenerationTime":" 2.08 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"1970-01-01","untilDate":"2024-12-31"}],"errorSet":[],"sinceDate":"2024-08-02","untilDate":"2024-12-31","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"isPortfolio":false} diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/summary.json index 76d44e39c2..5238092906 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/summary.json +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/summary.json @@ -1 +1 @@ -{"reportGeneratedTime":"Mon, 31 Mar 2025 13:58:30 SGT","reportGenerationTime":" 1.44 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"1970-01-01","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-08-02","untilDate":"2024-08-31","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense.git":"Publish branch of Reposense","https://github.com/reposense/publish-RepoSense/tree/master":"My project"}},"isPortfolio":true} +{"reportGeneratedTime":"Fri, 4 Apr 2025 15:11:44 SGT","reportGenerationTime":" 2.56 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"1970-01-01","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-08-02","untilDate":"2024-08-31","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"isPortfolio":true} diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/summary.json index 92b9e723bc..3e383c8a2b 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/summary.json +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/summary.json @@ -1 +1 @@ -{"reportGeneratedTime":"Mon, 31 Mar 2025 13:28:49 SGT","reportGenerationTime":" 4.32 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"1970-01-01","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-08-02","untilDate":"2024-08-31","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense.git":"Publish branch of Reposense","https://github.com/reposense/publish-RepoSense/tree/master":"My project"}},"isPortfolio":false} +{"reportGeneratedTime":"Fri, 4 Apr 2025 15:05:24 SGT","reportGenerationTime":" 4.23 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"1970-01-01","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-08-02","untilDate":"2024-08-31","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"isPortfolio":false} From cf0f32c0b6a116891cd129fe2e580ac5cb1ce8e8 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Mon, 7 Apr 2025 14:02:07 +0800 Subject: [PATCH 37/42] Fix nits --- docs/dg/cli.md | 2 +- docs/ug/cli.md | 1 - src/main/java/reposense/parser/ReportConfigYamlParser.java | 4 +++- src/systemtest/java/reposense/ReportConfigSystemTest.java | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/dg/cli.md b/docs/dg/cli.md index db708f46c4..066b8b0806 100644 --- a/docs/dg/cli.md +++ b/docs/dg/cli.md @@ -17,7 +17,7 @@ syntax reference under Appendix section of User Guide. -* Used in `ConfigSystemTest`. +* Used in `ConfigSystemTest` and `ReportConfigSystemTest` classes. * Some test cases performs shallow cloning while some does not. Fresh cloning ensures that the test cases that does not perform shallow cloning will clone the repo again if the previous test case uses shallow cloning, ensuring correctness of the analysis. diff --git a/docs/ug/cli.md b/docs/ug/cli.md index 6c56bf13e7..73fda00ce0 100644 --- a/docs/ug/cli.md +++ b/docs/ug/cli.md @@ -266,7 +266,6 @@ Cannot be used with `--last-modified-date`. This may result in an incorrect last * If the start date is not specified, only commits made one month before the end date (if specified) or the date of generating the report, will be captured and analyzed. * If `d1` is specified as the start date (`--since d1` or `-s d1`), then the program will search for the earliest commit date of all repositories and use that as the start date. * If `d1` is specified together with `--period`, then the program will warn that the date range being analyzed may be incorrect. -
diff --git a/src/main/java/reposense/parser/ReportConfigYamlParser.java b/src/main/java/reposense/parser/ReportConfigYamlParser.java index 85f0268b40..8206fdf48f 100644 --- a/src/main/java/reposense/parser/ReportConfigYamlParser.java +++ b/src/main/java/reposense/parser/ReportConfigYamlParser.java @@ -16,6 +16,8 @@ */ public class ReportConfigYamlParser extends YamlParser { public static final String REPORT_CONFIG_FILENAME = "report-config.yaml"; + public static final String REPORT_CONFIG_PARSED_SUCCESSFULLY_MESSAGE = + "report-config.yaml file parsed successfully!"; @Override public Type getType() { @@ -38,7 +40,7 @@ public ReportConfiguration parse(Path path) throws IOException { ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); mapper.findAndRegisterModules(); reportConfiguration = mapper.readValue(new File(path.toString()), ReportConfiguration.class); - logger.log(Level.INFO, "report-config.yaml file parsed successfully!"); + logger.log(Level.INFO, REPORT_CONFIG_PARSED_SUCCESSFULLY_MESSAGE); return reportConfiguration; } diff --git a/src/systemtest/java/reposense/ReportConfigSystemTest.java b/src/systemtest/java/reposense/ReportConfigSystemTest.java index b918bdc073..4521f147f2 100644 --- a/src/systemtest/java/reposense/ReportConfigSystemTest.java +++ b/src/systemtest/java/reposense/ReportConfigSystemTest.java @@ -124,7 +124,7 @@ public void testDateRangeWithModifiedDateTimeInLines() { /** * Returns a {@link InputBuilder} that is initialized with some default values. - *
Config Folder Path: {@code ConfigSystemTest} + *
Config Folder Path: {@code ReportConfigSystemTest} *
Formats: {@link ReportConfigSystemTest#TESTING_FILE_FORMATS TESTING_FILE_FORMATS} *
Timezone: {@link ReportConfigSystemTest#TEST_TIME_ZONE TEST_TIME_ZONE} *
Output Folder Path: {@link ReportConfigSystemTest#OUTPUT_DIRECTORY OUTPUT_DIRECTORY} From 92bea0692adb5312474ade050ec60e0055f4e059 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Wed, 9 Apr 2025 17:01:10 +0800 Subject: [PATCH 38/42] Resolve comments --- docs/ug/reportConfig.md | 26 +++++++++---------- src/main/java/reposense/RepoSense.java | 2 +- .../java/reposense/model/CliArguments.java | 8 +++--- .../commits.json | 2 +- .../30daysFromUntilDate/expected/summary.json | 2 +- .../commits.json | 2 +- .../expected/summary.json | 2 +- .../authorship.json | 2 +- .../commits.json | 2 +- .../expected/summary.json | 2 +- .../{blurbs.md => repo-blurbs.md} | 0 .../commits.json | 2 +- .../expected/summary.json | 2 +- .../commits.json | 2 +- .../expected/summary.json | 2 +- .../commits.json | 2 +- .../expected/summary.json | 2 +- .../OneStopConfigRunConfigurationTest.java | 10 +++---- .../model/RunConfigurationDeciderTest.java | 3 --- .../reportconfig/ReportConfigurationTest.java | 12 ++++----- 20 files changed, 42 insertions(+), 45 deletions(-) rename src/systemtest/resources/ReportConfigSystemTest/{blurbs.md => repo-blurbs.md} (100%) diff --git a/docs/ug/reportConfig.md b/docs/ug/reportConfig.md index e5593d8a1b..e9931db1ef 100644 --- a/docs/ug/reportConfig.md +++ b/docs/ug/reportConfig.md @@ -148,15 +148,15 @@ repos: ### Sample report generated ![report](../images/report-config-report.png) -## Further customizing your report +## Further Customizing Your Report -Optionally, you can provide more detailed descriptions to your report. Please refer to this [section](./customizingReports.md#add-a-title). +Optionally, you can provide more detailed descriptions to your report. Please refer to this [section](./customizingReports.md#personalizing-reports). ## Advanced Report Configuration For users who need more advanced configuration options, `report-config.yaml` offers many additional features beyond the basic setup. The following section explains the complete syntax and capabilities. -### Complete `report-config.yaml` Syntax +### Complete `report-config.yaml` Example ```yaml title: RepoSense Report @@ -191,46 +191,46 @@ repos: file-size-limit: 2000000 ``` -## Field Descriptions +### Field Descriptions Note: All fields are optional unless specified otherwise. -### Top-Level Fields +#### Top-Level Fields * `title`: Sets the title of your generated report, which appears as the title of the deployed dashboard. * Default: "RepoSense Report". * `repos`: A list of repositories to include in your analysis.. * This field is an array, so you can include multiple repositories. -### Repository-Level Fields +#### Repository-Level Fields For each repository in the `repos` list, you can specify: -* `repo` {{ mandatory }}: The URL to the repository you want to analyze. +* `repo` {{ mandatory }} : The URL to the repository you want to analyze. * Example: `https://github.com/RepoSense/reposense.git` * `groups`: Allows you to define custom groupings of files for more organized analysis. * This is useful for separating code files, test files, documentation, etc. * Each group has: - * `group-name` {{ mandatory }}: A name for the group (e.g., "code", "tests", "docs"). - * `globs` {{ mandatory }}: File patterns to include in this group using [glob format](https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob). -* `branches` {{ mandatory }}: The branches you want to analyze in the repository. + * `group-name` {{ mandatory }} : A name for the group (e.g., "code", "tests", "docs"). + * `globs` {{ mandatory }} : File patterns to include in this group using [glob format](https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob). +* `branches` {{ mandatory }} : The branches you want to analyze in the repository. * For each branch, you specify: * `branch`: The name of the branch (e.g., "master", "main", "develop"). * Default: The default branch of the repository. * `blurb`: A short description of the branch that appears in the report. * For more detailed descriptions, you can use a separate `blurbs.md` file. -### Author Configuration +#### Author Configuration Within each branch, you can specify: * `authors`: A list of authors whose contributions you want to track. If none specified, all authors will be shown. * For each author: - * `author-git-host-id` {{ mandatory }}: The GitHub username of the author. + * `author-git-host-id` {{ mandatory }} : The GitHub username of the author. * `author-display-name`: The name to display in the report (can be different from the GitHub username). * `author-git-author-name`: The name used in Git commits. * `author-emails`: A list of email addresses associated with the author's commits. -### Filtering Options +#### Filtering Options * `ignore-authors-list`: Authors to exclude from the analysis. * Specified by their Git Author Name. diff --git a/src/main/java/reposense/RepoSense.java b/src/main/java/reposense/RepoSense.java index f63985b24c..53ec0c948d 100644 --- a/src/main/java/reposense/RepoSense.java +++ b/src/main/java/reposense/RepoSense.java @@ -56,7 +56,7 @@ public static void main(String[] args) { configs = RunConfigurationDecider.getRunConfiguration(cliArguments).getRepoConfigurations(); reportConfig = cliArguments.getReportConfiguration(); - repoBlurbMap = cliArguments.mergeWithReportConfigBlurbMap(); + repoBlurbMap = cliArguments.mergeWithReportConfigRepoBlurbMap(); authorBlurbMap = cliArguments.getAuthorBlurbMap(); RepoConfiguration.setFormatsToRepoConfigs(configs, cliArguments.getFormats()); diff --git a/src/main/java/reposense/model/CliArguments.java b/src/main/java/reposense/model/CliArguments.java index b1b29f6a91..f7bbbfc4b1 100644 --- a/src/main/java/reposense/model/CliArguments.java +++ b/src/main/java/reposense/model/CliArguments.java @@ -174,15 +174,15 @@ public AuthorBlurbMap getAuthorBlurbMap() { * * @return the merged blurb map. */ - public RepoBlurbMap mergeWithReportConfigBlurbMap() { + public RepoBlurbMap mergeWithReportConfigRepoBlurbMap() { if (reportConfiguration == null) { return repoBlurbMap; } - RepoBlurbMap repoConfigBlurbMap = reportConfiguration.getRepoBlurbMap(); + RepoBlurbMap repoConfigRepoBlurbMap = reportConfiguration.getRepoBlurbMap(); for (Map.Entry entry : repoBlurbMap.getAllMappings().entrySet()) { - repoConfigBlurbMap.withRecord(entry.getKey(), entry.getValue()); + repoConfigRepoBlurbMap.withRecord(entry.getKey(), entry.getValue()); } - return repoConfigBlurbMap; + return repoConfigRepoBlurbMap; } public boolean isViewModeOnly() { diff --git a/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/reposense_publish-RepoSense_master/commits.json b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/reposense_publish-RepoSense_master/commits.json index df2e506560..5c4f422026 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/reposense_publish-RepoSense_master/commits.json +++ b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/reposense_publish-RepoSense_master/commits.json @@ -1 +1 @@ -{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":24}},"authorContributionVariance":{"jedkohjk":41.209164},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} +{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02T00:00:00","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13T00:00:00","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":24}},"authorContributionVariance":{"jedkohjk":41.209164},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} diff --git a/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/summary.json index 7ef89463c6..e84da20e0b 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/summary.json +++ b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDate/expected/summary.json @@ -1 +1 @@ -{"reportGeneratedTime":"Fri, 4 Apr 2025 15:03:26 SGT","reportGenerationTime":" 1.98 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"2024-07-31","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-07-31","untilDate":"2024-08-31","isSinceDateProvided":false,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"isPortfolio":false} +{"reportGeneratedTime":"Wed, 9 Apr 2025 15:46:34 SGT","reportGenerationTime":" 1.74 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"2024-07-31T00:00:00","untilDate":"2024-08-31T23:59:59"}],"errorSet":[],"sinceDate":"2024-07-31T00:00:00","untilDate":"2024-08-31T23:59:59","isSinceDateProvided":false,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"repoBlurbs":{"blurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"authorBlurbs":{"blurbMap":{}},"isPortfolio":false} diff --git a/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json index df2e506560..5c4f422026 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json +++ b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json @@ -1 +1 @@ -{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":24}},"authorContributionVariance":{"jedkohjk":41.209164},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} +{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02T00:00:00","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13T00:00:00","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":24}},"authorContributionVariance":{"jedkohjk":41.209164},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} diff --git a/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/summary.json index 126acb7c58..7dc0ab07a0 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/summary.json +++ b/src/systemtest/resources/ReportConfigSystemTest/30daysFromUntilDateWithShallowCloning/expected/summary.json @@ -1 +1 @@ -{"reportGeneratedTime":"Fri, 4 Apr 2025 15:09:32 SGT","reportGenerationTime":" 4.39 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"2024-07-31","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-07-31","untilDate":"2024-08-31","isSinceDateProvided":false,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"isPortfolio":false} +{"reportGeneratedTime":"Wed, 9 Apr 2025 15:57:59 SGT","reportGenerationTime":" 4.00 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"2024-07-31T00:00:00","untilDate":"2024-08-31T23:59:59"}],"errorSet":[],"sinceDate":"2024-07-31T00:00:00","untilDate":"2024-08-31T23:59:59","isSinceDateProvided":false,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"repoBlurbs":{"blurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"authorBlurbs":{"blurbMap":{}},"isPortfolio":false} diff --git a/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/reposense_publish-RepoSense_master/authorship.json b/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/reposense_publish-RepoSense_master/authorship.json index f310626ff7..3ac1ac7878 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/reposense_publish-RepoSense_master/authorship.json +++ b/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/reposense_publish-RepoSense_master/authorship.json @@ -1 +1 @@ -[{"path":"get-reposense.py","fileType":"other","lines":[{"lineNumber":1,"author":{"gitId":"-"},"content":"#!/usr/bin/env python","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":2,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":3,"author":{"gitId":"-"},"content":"import argparse","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":4,"author":{"gitId":"-"},"content":"import sys","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":5,"author":{"gitId":"-"},"content":"import os","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":6,"author":{"gitId":"-"},"content":"import shutil","lastModifiedDate":"2019-01-26","isFullCredit":false},{"lineNumber":7,"author":{"gitId":"-"},"content":"import requests","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":8,"author":{"gitId":"-"},"content":"import subprocess","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":9,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":10,"author":{"gitId":"-"},"content":"JAR_FILENAME \u003d \u0027RepoSense.jar\u0027","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":11,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":12,"author":{"gitId":"-"},"content":"def parse_args():","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":13,"author":{"gitId":"-"},"content":" parser \u003d argparse.ArgumentParser(description\u003d\u0027Downloads a specific version of RepoSense.jar from our repository.\u0027)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":14,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":15,"author":{"gitId":"-"},"content":" group \u003d parser.add_mutually_exclusive_group()","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":16,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-r\u0027, \u0027--release\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Get RepoSense.jar from the latest release (Stable)\u0027)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":17,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-m\u0027, \u0027--master\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Get RepoSense.jar from the latest master (Beta)\u0027)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":18,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-t\u0027, \u0027--tag\u0027, help\u003d\u0027Get RepoSense.jar of a specific release version tag\u0027)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":19,"author":{"gitId":"jedkohjk"},"content":" group.add_argument(\u0027-l\u0027, \u0027--latest\u0027, help\u003d\u0027Get RepoSense.jar of the latest release of a specific version tag\u0027)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":20,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-c\u0027, \u0027--commit\u0027, help\u003d\u0027Get RepoSense.jar of a specific commit\u0027)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":21,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":22,"author":{"gitId":"-"},"content":" parser.add_argument(\u0027-o\u0027, \u0027--overwrite\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Overwrite RepoSense.jar file, if exists. Default: false\u0027)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":23,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":24,"author":{"gitId":"-"},"content":" return parser.parse_args()","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":25,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":26,"author":{"gitId":"jedkohjk"},"content":"def handle_latest_tag(tag):","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":27,"author":{"gitId":"jedkohjk"},"content":" page \u003d 1","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":28,"author":{"gitId":"jedkohjk"},"content":" major \u003d tag.strip(\u0027. \u0027).split(\u0027.\u0027)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":29,"author":{"gitId":"jedkohjk"},"content":" while True:","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":30,"author":{"gitId":"jedkohjk"},"content":" response \u003d requests.get(f\u0027https://api.github.com/repos/reposense/RepoSense/releases?per_page\u003d100\u0026page\u003d{page}\u0027)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":31,"author":{"gitId":"jedkohjk"},"content":" if response.status_code in [403, 500]:","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":32,"author":{"gitId":"jedkohjk"},"content":" print(\u0027GitHub API has exceed the rate limit.\u0027)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":33,"author":{"gitId":"jedkohjk"},"content":" exit(1)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":34,"author":{"gitId":"jedkohjk"},"content":" releases \u003d response.json()","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":35,"author":{"gitId":"jedkohjk"},"content":" if not releases:","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":36,"author":{"gitId":"jedkohjk"},"content":" print(\u0027Error, the provided tag does not exist!\u0027)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":37,"author":{"gitId":"jedkohjk"},"content":" exit(1)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":38,"author":{"gitId":"jedkohjk"},"content":" for i in releases:","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":39,"author":{"gitId":"jedkohjk"},"content":" release_tag \u003d i[\u0027tag_name\u0027]","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":40,"author":{"gitId":"jedkohjk"},"content":" if release_tag.strip(\u0027. \u0027).split(\u0027.\u0027)[:len(major)] \u003d\u003d major:","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":41,"author":{"gitId":"jedkohjk"},"content":" handle_specific_release(release_tag)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":42,"author":{"gitId":"jedkohjk"},"content":" exit()","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":43,"author":{"gitId":"jedkohjk"},"content":" page +\u003d 1","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":44,"author":{"gitId":"jedkohjk"},"content":"","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":45,"author":{"gitId":"-"},"content":"def handle_specific_commit(commit):","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":46,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/commits/\u0027 + commit, commit\u003dcommit)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":47,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":48,"author":{"gitId":"-"},"content":"def handle_specific_release(tag):","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":49,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/releases/tags/\u0027 + tag, tag\u003dtag)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":50,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":51,"author":{"gitId":"-"},"content":"def handle_latest_release():","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":52,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/releases/latest\u0027)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":53,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":54,"author":{"gitId":"-"},"content":"def get_reposense_jar(url, tag\u003dNone, commit\u003dNone):","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":55,"author":{"gitId":"-"},"content":" response \u003d requests.get(url)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":56,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":57,"author":{"gitId":"-"},"content":" if tag and response.status_code \u003d\u003d 404:","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":58,"author":{"gitId":"-"},"content":" print(\u0027Error, the provided tag does not exist!\u0027)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":59,"author":{"gitId":"-"},"content":" exit(1)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":60,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":61,"author":{"gitId":"-"},"content":" if commit and response.status_code in [404, 422]:","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":62,"author":{"gitId":"-"},"content":" print(\u0027Error, the provided commit does not exist!\u0027)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":63,"author":{"gitId":"-"},"content":" exit(1)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":64,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":65,"author":{"gitId":"-"},"content":" if response.status_code in [403, 500]:","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":66,"author":{"gitId":"-"},"content":" print(\u0027GitHub API has exceed the rate limit. Falling back to alternative method...\u0027)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":67,"author":{"gitId":"-"},"content":" clone_and_make_reposense(tag\u003dtag, commit\u003dcommit)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":68,"author":{"gitId":"-"},"content":" return","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":69,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":70,"author":{"gitId":"-"},"content":" if commit:","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":71,"author":{"gitId":"-"},"content":" clone_and_make_reposense(commit\u003dcommit)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":72,"author":{"gitId":"-"},"content":" return","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":73,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":74,"author":{"gitId":"-"},"content":" url \u003d response.json()[\u0027assets\u0027][0][\u0027browser_download_url\u0027]","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":75,"author":{"gitId":"-"},"content":" download_file(url)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":76,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":77,"author":{"gitId":"-"},"content":"def clone_and_make_reposense(tag\u003dNone, commit\u003dNone):","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":78,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":79,"author":{"gitId":"-"},"content":" # Cleanup cached RepoSense folder","lastModifiedDate":"2019-01-26","isFullCredit":false},{"lineNumber":80,"author":{"gitId":"-"},"content":" shutil.rmtree(\u0027RepoSense\u0027, ignore_errors\u003dTrue)","lastModifiedDate":"2019-01-26","isFullCredit":false},{"lineNumber":81,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-26","isFullCredit":false},{"lineNumber":82,"author":{"gitId":"-"},"content":" command \u003d \\","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":83,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":84,"author":{"gitId":"-"},"content":" git clone \u0027https://github.com/reposense/RepoSense.git\u0027 \u0026\u0026","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":85,"author":{"gitId":"-"},"content":" cd RepoSense \u0026\u0026","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":86,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":87,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":88,"author":{"gitId":"-"},"content":" if tag:","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":89,"author":{"gitId":"-"},"content":" command +\u003d \u0027git checkout tags/{} -b deploy \u0026\u0026\u0027.format(tag)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":90,"author":{"gitId":"-"},"content":" elif commit:","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":91,"author":{"gitId":"-"},"content":" command +\u003d \u0027git checkout {} -b deploy \u0026\u0026\u0027.format(commit)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":92,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":93,"author":{"gitId":"-"},"content":" command +\u003d \\","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":94,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":95,"author":{"gitId":"-"},"content":" ./gradlew zipreport shadowjar \u0026\u0026","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":96,"author":{"gitId":"-"},"content":" mv build/jar/RepoSense.jar ../","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":97,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":98,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":99,"author":{"gitId":"-"},"content":" subprocess.check_call(command, shell\u003dTrue)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":100,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":101,"author":{"gitId":"-"},"content":"def download_file(url):","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":102,"author":{"gitId":"-"},"content":" response \u003d requests.get(url, allow_redirects\u003dTrue)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":103,"author":{"gitId":"-"},"content":" open(JAR_FILENAME, \u0027wb\u0027).write(response.content)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":104,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":105,"author":{"gitId":"-"},"content":"if __name__ \u003d\u003d \"__main__\":","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":106,"author":{"gitId":"-"},"content":" args \u003d parse_args()","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":107,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":108,"author":{"gitId":"-"},"content":" if os.path.exists(JAR_FILENAME) and args.overwrite is False:","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":109,"author":{"gitId":"-"},"content":" print(JAR_FILENAME + \u0027 already exists. Quitting.\u0027)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":110,"author":{"gitId":"-"},"content":" exit()","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":111,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":112,"author":{"gitId":"-"},"content":" if args.tag:","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":113,"author":{"gitId":"-"},"content":" handle_specific_release(args.tag)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":114,"author":{"gitId":"-"},"content":" exit()","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":115,"author":{"gitId":"-"},"content":" ","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":116,"author":{"gitId":"jedkohjk"},"content":" if args.latest:","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":117,"author":{"gitId":"jedkohjk"},"content":" handle_latest_tag(args.latest)","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":118,"author":{"gitId":"jedkohjk"},"content":" exit()","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":119,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2024-08-02","isFullCredit":false},{"lineNumber":120,"author":{"gitId":"-"},"content":" if args.commit:","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":121,"author":{"gitId":"-"},"content":" handle_specific_commit(args.commit)","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":122,"author":{"gitId":"-"},"content":" exit()","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":123,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":124,"author":{"gitId":"-"},"content":" if args.master:","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":125,"author":{"gitId":"-"},"content":" clone_and_make_reposense()","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":126,"author":{"gitId":"-"},"content":" exit()","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":127,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":128,"author":{"gitId":"-"},"content":" # If no arguments are provided or --release","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":129,"author":{"gitId":"-"},"content":" handle_latest_release()","lastModifiedDate":"2019-01-21","isFullCredit":false}],"authorContributionMap":{"jedkohjk":23,"-":106}},{"path":"run.sh","fileType":"other","lines":[{"lineNumber":1,"author":{"gitId":"-"},"content":"#!/bin/bash","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":2,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":3,"author":{"gitId":"-"},"content":"# Downloads a specific version of RepoSense.jar of your choice from our repository","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":4,"author":{"gitId":"-"},"content":"## Examples of supported options:","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":5,"author":{"gitId":"-"},"content":"### ./get-reposense.py --release # Gets the latest release (Stable)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":6,"author":{"gitId":"-"},"content":"### ./get-reposense.py --master # Gets the latest master (Beta)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":7,"author":{"gitId":"-"},"content":"### ./get-reposense.py --tag v1.6.1 # Gets a specific version","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":8,"author":{"gitId":"jedkohjk"},"content":"### ./get-reposense.py --latest v1.6 # Gets the latest version with the given tag prefix e.g. v1.6.1","lastModifiedDate":"2024-08-13","isFullCredit":false},{"lineNumber":9,"author":{"gitId":"-"},"content":"### ./get-reposense.py --commit abc123 # Gets a specific commit","lastModifiedDate":"2021-03-31","isFullCredit":false},{"lineNumber":10,"author":{"gitId":"-"},"content":"### ./get-reposense.py --release --overwrite # Overwrite RepoSense.jar, if exists, with the latest release","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":11,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":12,"author":{"gitId":"-"},"content":"./get-reposense.py --release","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":13,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":14,"author":{"gitId":"-"},"content":"# Executes RepoSense","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":15,"author":{"gitId":"-"},"content":"# Do not change the default output folder name (reposense-report)","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":16,"author":{"gitId":"-"},"content":"## Examples of other valid options; For more, please view the user guide","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":17,"author":{"gitId":"-"},"content":"### java -jar RepoSense.jar --repos https://github.com/reposense/RepoSense.git","lastModifiedDate":"2019-03-15","isFullCredit":false},{"lineNumber":18,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21","isFullCredit":false},{"lineNumber":19,"author":{"gitId":"-"},"content":"java -jar RepoSense.jar --config ./configs","lastModifiedDate":"2019-03-15","isFullCredit":false}],"authorContributionMap":{"jedkohjk":1,"-":18}}] +[{"path":"get-reposense.py","fileType":"other","lines":[{"lineNumber":1,"author":{"gitId":"-"},"content":"#!/usr/bin/env python","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":2,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":3,"author":{"gitId":"-"},"content":"import argparse","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":4,"author":{"gitId":"-"},"content":"import sys","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":5,"author":{"gitId":"-"},"content":"import os","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":6,"author":{"gitId":"-"},"content":"import shutil","lastModifiedDate":"2019-01-26T21:42:39","isFullCredit":false},{"lineNumber":7,"author":{"gitId":"-"},"content":"import requests","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":8,"author":{"gitId":"-"},"content":"import subprocess","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":9,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":10,"author":{"gitId":"-"},"content":"JAR_FILENAME \u003d \u0027RepoSense.jar\u0027","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":11,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":12,"author":{"gitId":"-"},"content":"def parse_args():","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":13,"author":{"gitId":"-"},"content":" parser \u003d argparse.ArgumentParser(description\u003d\u0027Downloads a specific version of RepoSense.jar from our repository.\u0027)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":14,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":15,"author":{"gitId":"-"},"content":" group \u003d parser.add_mutually_exclusive_group()","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":16,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-r\u0027, \u0027--release\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Get RepoSense.jar from the latest release (Stable)\u0027)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":17,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-m\u0027, \u0027--master\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Get RepoSense.jar from the latest master (Beta)\u0027)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":18,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-t\u0027, \u0027--tag\u0027, help\u003d\u0027Get RepoSense.jar of a specific release version tag\u0027)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":19,"author":{"gitId":"jedkohjk"},"content":" group.add_argument(\u0027-l\u0027, \u0027--latest\u0027, help\u003d\u0027Get RepoSense.jar of the latest release of a specific version tag\u0027)","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":20,"author":{"gitId":"-"},"content":" group.add_argument(\u0027-c\u0027, \u0027--commit\u0027, help\u003d\u0027Get RepoSense.jar of a specific commit\u0027)","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":21,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":22,"author":{"gitId":"-"},"content":" parser.add_argument(\u0027-o\u0027, \u0027--overwrite\u0027, action\u003d\u0027store_true\u0027, help\u003d\u0027Overwrite RepoSense.jar file, if exists. Default: false\u0027)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":23,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":24,"author":{"gitId":"-"},"content":" return parser.parse_args()","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":25,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":26,"author":{"gitId":"jedkohjk"},"content":"def handle_latest_tag(tag):","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":27,"author":{"gitId":"jedkohjk"},"content":" page \u003d 1","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":28,"author":{"gitId":"jedkohjk"},"content":" major \u003d tag.strip(\u0027. \u0027).split(\u0027.\u0027)","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":29,"author":{"gitId":"jedkohjk"},"content":" while True:","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":30,"author":{"gitId":"jedkohjk"},"content":" response \u003d requests.get(f\u0027https://api.github.com/repos/reposense/RepoSense/releases?per_page\u003d100\u0026page\u003d{page}\u0027)","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":31,"author":{"gitId":"jedkohjk"},"content":" if response.status_code in [403, 500]:","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":32,"author":{"gitId":"jedkohjk"},"content":" print(\u0027GitHub API has exceed the rate limit.\u0027)","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":33,"author":{"gitId":"jedkohjk"},"content":" exit(1)","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":34,"author":{"gitId":"jedkohjk"},"content":" releases \u003d response.json()","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":35,"author":{"gitId":"jedkohjk"},"content":" if not releases:","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":36,"author":{"gitId":"jedkohjk"},"content":" print(\u0027Error, the provided tag does not exist!\u0027)","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":37,"author":{"gitId":"jedkohjk"},"content":" exit(1)","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":38,"author":{"gitId":"jedkohjk"},"content":" for i in releases:","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":39,"author":{"gitId":"jedkohjk"},"content":" release_tag \u003d i[\u0027tag_name\u0027]","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":40,"author":{"gitId":"jedkohjk"},"content":" if release_tag.strip(\u0027. \u0027).split(\u0027.\u0027)[:len(major)] \u003d\u003d major:","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":41,"author":{"gitId":"jedkohjk"},"content":" handle_specific_release(release_tag)","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":42,"author":{"gitId":"jedkohjk"},"content":" exit()","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":43,"author":{"gitId":"jedkohjk"},"content":" page +\u003d 1","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":44,"author":{"gitId":"jedkohjk"},"content":"","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":45,"author":{"gitId":"-"},"content":"def handle_specific_commit(commit):","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":46,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/commits/\u0027 + commit, commit\u003dcommit)","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":47,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":48,"author":{"gitId":"-"},"content":"def handle_specific_release(tag):","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":49,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/releases/tags/\u0027 + tag, tag\u003dtag)","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":50,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":51,"author":{"gitId":"-"},"content":"def handle_latest_release():","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":52,"author":{"gitId":"-"},"content":" get_reposense_jar(\u0027https://api.github.com/repos/reposense/RepoSense/releases/latest\u0027)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":53,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":54,"author":{"gitId":"-"},"content":"def get_reposense_jar(url, tag\u003dNone, commit\u003dNone):","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":55,"author":{"gitId":"-"},"content":" response \u003d requests.get(url)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":56,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":57,"author":{"gitId":"-"},"content":" if tag and response.status_code \u003d\u003d 404:","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":58,"author":{"gitId":"-"},"content":" print(\u0027Error, the provided tag does not exist!\u0027)","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":59,"author":{"gitId":"-"},"content":" exit(1)","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":60,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":61,"author":{"gitId":"-"},"content":" if commit and response.status_code in [404, 422]:","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":62,"author":{"gitId":"-"},"content":" print(\u0027Error, the provided commit does not exist!\u0027)","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":63,"author":{"gitId":"-"},"content":" exit(1)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":64,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":65,"author":{"gitId":"-"},"content":" if response.status_code in [403, 500]:","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":66,"author":{"gitId":"-"},"content":" print(\u0027GitHub API has exceed the rate limit. Falling back to alternative method...\u0027)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":67,"author":{"gitId":"-"},"content":" clone_and_make_reposense(tag\u003dtag, commit\u003dcommit)","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":68,"author":{"gitId":"-"},"content":" return","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":69,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":70,"author":{"gitId":"-"},"content":" if commit:","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":71,"author":{"gitId":"-"},"content":" clone_and_make_reposense(commit\u003dcommit)","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":72,"author":{"gitId":"-"},"content":" return","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":73,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":74,"author":{"gitId":"-"},"content":" url \u003d response.json()[\u0027assets\u0027][0][\u0027browser_download_url\u0027]","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":75,"author":{"gitId":"-"},"content":" download_file(url)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":76,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":77,"author":{"gitId":"-"},"content":"def clone_and_make_reposense(tag\u003dNone, commit\u003dNone):","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":78,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":79,"author":{"gitId":"-"},"content":" # Cleanup cached RepoSense folder","lastModifiedDate":"2019-01-26T21:42:39","isFullCredit":false},{"lineNumber":80,"author":{"gitId":"-"},"content":" shutil.rmtree(\u0027RepoSense\u0027, ignore_errors\u003dTrue)","lastModifiedDate":"2019-01-26T21:42:39","isFullCredit":false},{"lineNumber":81,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-26T21:42:39","isFullCredit":false},{"lineNumber":82,"author":{"gitId":"-"},"content":" command \u003d \\","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":83,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":84,"author":{"gitId":"-"},"content":" git clone \u0027https://github.com/reposense/RepoSense.git\u0027 \u0026\u0026","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":85,"author":{"gitId":"-"},"content":" cd RepoSense \u0026\u0026","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":86,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":87,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":88,"author":{"gitId":"-"},"content":" if tag:","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":89,"author":{"gitId":"-"},"content":" command +\u003d \u0027git checkout tags/{} -b deploy \u0026\u0026\u0027.format(tag)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":90,"author":{"gitId":"-"},"content":" elif commit:","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":91,"author":{"gitId":"-"},"content":" command +\u003d \u0027git checkout {} -b deploy \u0026\u0026\u0027.format(commit)","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":92,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":93,"author":{"gitId":"-"},"content":" command +\u003d \\","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":94,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":95,"author":{"gitId":"-"},"content":" ./gradlew zipreport shadowjar \u0026\u0026","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":96,"author":{"gitId":"-"},"content":" mv build/jar/RepoSense.jar ../","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":97,"author":{"gitId":"-"},"content":" \u0027\u0027\u0027","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":98,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":99,"author":{"gitId":"-"},"content":" subprocess.check_call(command, shell\u003dTrue)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":100,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":101,"author":{"gitId":"-"},"content":"def download_file(url):","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":102,"author":{"gitId":"-"},"content":" response \u003d requests.get(url, allow_redirects\u003dTrue)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":103,"author":{"gitId":"-"},"content":" open(JAR_FILENAME, \u0027wb\u0027).write(response.content)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":104,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":105,"author":{"gitId":"-"},"content":"if __name__ \u003d\u003d \"__main__\":","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":106,"author":{"gitId":"-"},"content":" args \u003d parse_args()","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":107,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":108,"author":{"gitId":"-"},"content":" if os.path.exists(JAR_FILENAME) and args.overwrite is False:","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":109,"author":{"gitId":"-"},"content":" print(JAR_FILENAME + \u0027 already exists. Quitting.\u0027)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":110,"author":{"gitId":"-"},"content":" exit()","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":111,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":112,"author":{"gitId":"-"},"content":" if args.tag:","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":113,"author":{"gitId":"-"},"content":" handle_specific_release(args.tag)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":114,"author":{"gitId":"-"},"content":" exit()","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":115,"author":{"gitId":"-"},"content":" ","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":116,"author":{"gitId":"jedkohjk"},"content":" if args.latest:","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":117,"author":{"gitId":"jedkohjk"},"content":" handle_latest_tag(args.latest)","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":118,"author":{"gitId":"jedkohjk"},"content":" exit()","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":119,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2024-08-02T00:19:00","isFullCredit":false},{"lineNumber":120,"author":{"gitId":"-"},"content":" if args.commit:","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":121,"author":{"gitId":"-"},"content":" handle_specific_commit(args.commit)","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":122,"author":{"gitId":"-"},"content":" exit()","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":123,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":124,"author":{"gitId":"-"},"content":" if args.master:","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":125,"author":{"gitId":"-"},"content":" clone_and_make_reposense()","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":126,"author":{"gitId":"-"},"content":" exit()","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":127,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":128,"author":{"gitId":"-"},"content":" # If no arguments are provided or --release","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":129,"author":{"gitId":"-"},"content":" handle_latest_release()","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false}],"authorContributionMap":{"jedkohjk":23,"-":106}},{"path":"run.sh","fileType":"other","lines":[{"lineNumber":1,"author":{"gitId":"-"},"content":"#!/bin/bash","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":2,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":3,"author":{"gitId":"-"},"content":"# Downloads a specific version of RepoSense.jar of your choice from our repository","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":4,"author":{"gitId":"-"},"content":"## Examples of supported options:","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":5,"author":{"gitId":"-"},"content":"### ./get-reposense.py --release # Gets the latest release (Stable)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":6,"author":{"gitId":"-"},"content":"### ./get-reposense.py --master # Gets the latest master (Beta)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":7,"author":{"gitId":"-"},"content":"### ./get-reposense.py --tag v1.6.1 # Gets a specific version","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":8,"author":{"gitId":"jedkohjk"},"content":"### ./get-reposense.py --latest v1.6 # Gets the latest version with the given tag prefix e.g. v1.6.1","lastModifiedDate":"2024-08-13T20:16:35","isFullCredit":false},{"lineNumber":9,"author":{"gitId":"-"},"content":"### ./get-reposense.py --commit abc123 # Gets a specific commit","lastModifiedDate":"2021-03-31T17:56:18","isFullCredit":false},{"lineNumber":10,"author":{"gitId":"-"},"content":"### ./get-reposense.py --release --overwrite # Overwrite RepoSense.jar, if exists, with the latest release","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":11,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":12,"author":{"gitId":"-"},"content":"./get-reposense.py --release","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":13,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":14,"author":{"gitId":"-"},"content":"# Executes RepoSense","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":15,"author":{"gitId":"-"},"content":"# Do not change the default output folder name (reposense-report)","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":16,"author":{"gitId":"-"},"content":"## Examples of other valid options; For more, please view the user guide","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":17,"author":{"gitId":"-"},"content":"### java -jar RepoSense.jar --repos https://github.com/reposense/RepoSense.git","lastModifiedDate":"2019-03-15T16:04:37","isFullCredit":false},{"lineNumber":18,"author":{"gitId":"-"},"content":"","lastModifiedDate":"2019-01-21T14:24:59","isFullCredit":false},{"lineNumber":19,"author":{"gitId":"-"},"content":"java -jar RepoSense.jar --config ./configs","lastModifiedDate":"2019-03-15T16:04:37","isFullCredit":false}],"authorContributionMap":{"jedkohjk":1,"-":18}}] diff --git a/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/reposense_publish-RepoSense_master/commits.json b/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/reposense_publish-RepoSense_master/commits.json index e35d0f493e..728a37ab0d 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/reposense_publish-RepoSense_master/commits.json +++ b/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/reposense_publish-RepoSense_master/commits.json @@ -1 +1 @@ -{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":24}},"authorContributionVariance":{"jedkohjk":2.7689133},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} +{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02T00:00:00","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13T00:00:00","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":24}},"authorContributionVariance":{"jedkohjk":2.7689133},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} diff --git a/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/summary.json index 26b661cb34..a4a5abd6dd 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/summary.json +++ b/src/systemtest/resources/ReportConfigSystemTest/dateRangeWithModifiedDateTimeInLines/expected/summary.json @@ -1 +1 @@ -{"reportGeneratedTime":"Fri, 4 Apr 2025 15:14:41 SGT","reportGenerationTime":" 2.28 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"2024-01-01","untilDate":"2024-12-31"}],"errorSet":[],"sinceDate":"2024-01-01","untilDate":"2024-12-31","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"isPortfolio":false} +{"reportGeneratedTime":"Wed, 9 Apr 2025 16:02:12 SGT","reportGenerationTime":" 2.47 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"2024-01-01T00:00:00","untilDate":"2024-12-31T23:59:59"}],"errorSet":[],"sinceDate":"2024-01-01T00:00:00","untilDate":"2024-12-31T23:59:59","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"repoBlurbs":{"blurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"authorBlurbs":{"blurbMap":{}},"isPortfolio":false} diff --git a/src/systemtest/resources/ReportConfigSystemTest/blurbs.md b/src/systemtest/resources/ReportConfigSystemTest/repo-blurbs.md similarity index 100% rename from src/systemtest/resources/ReportConfigSystemTest/blurbs.md rename to src/systemtest/resources/ReportConfigSystemTest/repo-blurbs.md diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/reposense_publish-RepoSense_master/commits.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/reposense_publish-RepoSense_master/commits.json index a94c9995df..5cf4f434d4 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/reposense_publish-RepoSense_master/commits.json +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/reposense_publish-RepoSense_master/commits.json @@ -1 +1 @@ -{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":25}},"authorContributionVariance":{"jedkohjk":47.354168},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} +{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02T00:00:00","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13T00:00:00","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":25}},"authorContributionVariance":{"jedkohjk":47.354168},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/summary.json index f017260a70..329979b408 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/summary.json +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRange/expected/summary.json @@ -1 +1 @@ -{"reportGeneratedTime":"Fri, 4 Apr 2025 15:01:49 SGT","reportGenerationTime":" 2.08 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"1970-01-01","untilDate":"2024-12-31"}],"errorSet":[],"sinceDate":"2024-08-02","untilDate":"2024-12-31","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"isPortfolio":false} +{"reportGeneratedTime":"Wed, 9 Apr 2025 15:45:33 SGT","reportGenerationTime":" 2.15 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"1970-01-01T07:30:00","untilDate":"2024-12-31T23:59:59"}],"errorSet":[],"sinceDate":"2024-08-02T00:00:00","untilDate":"2024-12-31T23:59:59","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"repoBlurbs":{"blurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"authorBlurbs":{"blurbMap":{}},"isPortfolio":false} diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/reposense_publish-RepoSense_master/commits.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/reposense_publish-RepoSense_master/commits.json index a94c9995df..5cf4f434d4 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/reposense_publish-RepoSense_master/commits.json +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/reposense_publish-RepoSense_master/commits.json @@ -1 +1 @@ -{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":25}},"authorContributionVariance":{"jedkohjk":47.354168},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} +{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02T00:00:00","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13T00:00:00","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":25}},"authorContributionVariance":{"jedkohjk":47.354168},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/summary.json index 5238092906..a1df721fc6 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/summary.json +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithPortfolio/expected/summary.json @@ -1 +1 @@ -{"reportGeneratedTime":"Fri, 4 Apr 2025 15:11:44 SGT","reportGenerationTime":" 2.56 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"1970-01-01","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-08-02","untilDate":"2024-08-31","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"isPortfolio":true} +{"reportGeneratedTime":"Wed, 9 Apr 2025 15:59:47 SGT","reportGenerationTime":" 2.43 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"1970-01-01T07:30:00","untilDate":"2024-08-31T23:59:59"}],"errorSet":[],"sinceDate":"2024-08-02T00:00:00","untilDate":"2024-08-31T23:59:59","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"repoBlurbs":{"blurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"authorBlurbs":{"blurbMap":{}},"isPortfolio":true} diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json index a94c9995df..5cf4f434d4 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/reposense_publish-RepoSense_master/commits.json @@ -1 +1 @@ -{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":25}},"authorContributionVariance":{"jedkohjk":47.354168},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} +{"authorDailyContributionsMap":{"jedkohjk":[{"date":"2024-08-02T00:00:00","commitResults":[{"hash":"80185806433b561004c3d0245997a77afcc674f3","isMergeCommit":false,"messageTitle":"Add flag to get the latest release from a specified major version (#12)","messageBody":"The `--tag` flag fetches the exact specified RepoSense version, but provides no way to fetch the latest release of a specified major version.\r\n\r\nLet\u0027s introduce the `--latest` or `-l` flag to get the latest release from a specified major version.","fileTypesAndContributionMap":{"other":{"insertions":25,"deletions":0}}}]},{"date":"2024-08-13T00:00:00","commitResults":[{"hash":"3a748c5843b273dafa3080956eb138093b4b58c9","isMergeCommit":false,"messageTitle":"Clarify description of --latest flag in run.sh (#13)","messageBody":"Information about the function of the `--latest` flag is unclear.\r\n\r\nLet\u0027s clarify the description of `--latest` flag in `run.sh`.\r\n","fileTypesAndContributionMap":{"other":{"insertions":1,"deletions":1}}}]}]},"authorFileTypeContributionMap":{"jedkohjk":{"code":0,"tests":0,"docs":0,"other":25}},"authorContributionVariance":{"jedkohjk":47.354168},"authorDisplayNameMap":{"jedkohjk":"jedkohjk"}} diff --git a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/summary.json b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/summary.json index 3e383c8a2b..74f359e2ca 100644 --- a/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/summary.json +++ b/src/systemtest/resources/ReportConfigSystemTest/sinceBeginningDateRangeWithShallowCloning/expected/summary.json @@ -1 +1 @@ -{"reportGeneratedTime":"Fri, 4 Apr 2025 15:05:24 SGT","reportGenerationTime":" 4.23 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"1970-01-01","untilDate":"2024-08-31"}],"errorSet":[],"sinceDate":"2024-08-02","untilDate":"2024-08-31","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"blurbs":{"urlBlurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"isPortfolio":false} +{"reportGeneratedTime":"Wed, 9 Apr 2025 15:48:40 SGT","reportGenerationTime":" 4.64 second(s)","zoneId":"Asia/Singapore","reportTitle":"RepoSense Report","repos":[{"location":{"location":"https://github.com/reposense/publish-RepoSense.git","repoName":"publish-RepoSense","organization":"reposense","domainName":"github"},"branch":"master","displayName":"reposense/publish-RepoSense[master]","outputFolderName":"reposense_publish-RepoSense_master","sinceDate":"1970-01-01T07:30:00","untilDate":"2024-08-31T23:59:59"}],"errorSet":[],"sinceDate":"2024-08-02T00:00:00","untilDate":"2024-08-31T23:59:59","isSinceDateProvided":true,"isUntilDateProvided":true,"supportedDomainUrlMap":{"NOT_RECOGNIZED":{"BRANCH":"","REPO_URL":"UNSUPPORTED","BASE_URL":"UNSUPPORTED","HISTORY_PATH":"","COMMIT_PATH":"","BLAME_PATH":""},"github":{"BRANCH":"tree/$BRANCH","REPO_URL":"https://github.com/$ORGANIZATION/$REPO_NAME/","BASE_URL":"https://github.com/","HISTORY_PATH":"commits/$BRANCH/$FILE_PATH","COMMIT_PATH":"commit/$COMMIT_HASH","BLAME_PATH":"blame/$BRANCH/$FILE_PATH"}},"isAuthorshipAnalyzed":false,"repoBlurbs":{"blurbMap":{"https://github.com/reposense/publish-RepoSense/tree/master":"Publish branch of Reposense"}},"authorBlurbs":{"blurbMap":{}},"isPortfolio":false} diff --git a/src/test/java/reposense/model/OneStopConfigRunConfigurationTest.java b/src/test/java/reposense/model/OneStopConfigRunConfigurationTest.java index 1817250656..bba8a27fc5 100644 --- a/src/test/java/reposense/model/OneStopConfigRunConfigurationTest.java +++ b/src/test/java/reposense/model/OneStopConfigRunConfigurationTest.java @@ -15,10 +15,10 @@ class OneStopConfigRunConfigurationTest { - private static CliArguments testCliArguments; + private static CliArguments testSetUp; private static List expectedRepoConfigurations; - public static void setUpTestCliArguments() { + public static void setUpActualRepoConfigurations() { CliArguments.Builder testCliBuilder = new CliArguments.Builder(); List globPatterns = List.of("**.java"); @@ -41,7 +41,7 @@ public static void setUpTestCliArguments() { List repos = List.of(repo); ReportConfiguration expectedReportConfig = new ReportConfiguration("Test RepoSense Report", repos); testCliBuilder.reportConfiguration(expectedReportConfig); - testCliArguments = testCliBuilder.build(); + testSetUp = testCliBuilder.build(); } public static void setUpExpectedRepoConfigurations() throws InvalidLocationException { @@ -81,10 +81,10 @@ public static void setUpExpectedRepoConfigurations() throws InvalidLocationExcep @Test public void getRepoConfigurations_withValidInputs_returnsRepoConfigurations() throws InvalidLocationException { - setUpTestCliArguments(); + setUpActualRepoConfigurations(); setUpExpectedRepoConfigurations(); - OneStopConfigRunConfiguration config = new OneStopConfigRunConfiguration(testCliArguments); + OneStopConfigRunConfiguration config = new OneStopConfigRunConfiguration(testSetUp); List actualRepoConfigurations = config.getRepoConfigurations(); Assertions.assertEquals(expectedRepoConfigurations, actualRepoConfigurations); } diff --git a/src/test/java/reposense/model/RunConfigurationDeciderTest.java b/src/test/java/reposense/model/RunConfigurationDeciderTest.java index c5a68b3dd8..f85d49c222 100644 --- a/src/test/java/reposense/model/RunConfigurationDeciderTest.java +++ b/src/test/java/reposense/model/RunConfigurationDeciderTest.java @@ -39,9 +39,6 @@ public class RunConfigurationDeciderTest { private static final String TEST_REPO_REPOSENSE = "https://github.com/reposense/RepoSense.git"; private static final String TEST_REPO_BETA = "https://github.com/reposense/testrepo-Beta.git"; - - - @BeforeEach public void before() { DEFAULT_INPUT_BUILDER.reset().addConfig(CONFIG_FOLDER_ABSOLUTE); diff --git a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java index 78157fc077..29a7d6e4af 100644 --- a/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java +++ b/src/test/java/reposense/model/reportconfig/ReportConfigurationTest.java @@ -7,7 +7,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import reposense.model.BlurbMap; +import reposense.model.RepoBlurbMap; public class ReportConfigurationTest { @@ -58,19 +58,19 @@ public void getTitle_equalsDefaultReturnValue_success() { @Test public void getBlurbMap_withValidInputs_success() { - BlurbMap expectedBlurbMap = new BlurbMap(); + RepoBlurbMap expectedBlurbMap = new RepoBlurbMap(); expectedBlurbMap.withRecord("https://github.com/reposense/testrepo-Delta/tree/master", "My project"); - Assertions.assertEquals(expectedBlurbMap, expectedReportConfig.getBlurbMap()); + Assertions.assertEquals(expectedBlurbMap, expectedReportConfig.getRepoBlurbMap()); } @Test public void getBlurbMap_withInvalidInputs_returnEmptyBlurbMap() { - BlurbMap expectedBlurbMap = new BlurbMap(); + RepoBlurbMap expectedBlurbMap = new RepoBlurbMap(); expectedBlurbMap.withRecord("https://github.com/reposense/testrepo-Delta/tree/master", "My project"); - Assertions.assertNotEquals(expectedBlurbMap, invalidReportConfig.getBlurbMap()); - Assertions.assertEquals(new BlurbMap(), invalidReportConfig.getBlurbMap()); + Assertions.assertNotEquals(expectedBlurbMap, invalidReportConfig.getRepoBlurbMap()); + Assertions.assertEquals(new RepoBlurbMap(), invalidReportConfig.getRepoBlurbMap()); } @Test From e24d51c199eaeac12b934ae8f357e620473a1268 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Wed, 9 Apr 2025 23:08:25 +0800 Subject: [PATCH 39/42] Fix tests --- .../reposense/model/ReportConfiguration.java | 0 .../parser/SummaryJsonParserTest.java | 20 +++++++++---------- .../reposense/report/ReportGeneratorTest.java | 2 +- .../reposense/report/SummaryJsonTest.java | 2 +- 4 files changed, 11 insertions(+), 13 deletions(-) delete mode 100644 src/main/java/reposense/model/ReportConfiguration.java diff --git a/src/main/java/reposense/model/ReportConfiguration.java b/src/main/java/reposense/model/ReportConfiguration.java deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/test/java/reposense/parser/SummaryJsonParserTest.java b/src/test/java/reposense/parser/SummaryJsonParserTest.java index ce65497024..f78db02a03 100644 --- a/src/test/java/reposense/parser/SummaryJsonParserTest.java +++ b/src/test/java/reposense/parser/SummaryJsonParserTest.java @@ -19,7 +19,6 @@ import reposense.model.RepoBlurbMap; import reposense.model.RepoConfiguration; import reposense.model.RepoLocation; -import reposense.model.ReportConfiguration; import reposense.report.SummaryJson; public class SummaryJsonParserTest { @@ -37,7 +36,7 @@ public static void setUp() throws Exception { String reportGenerationTime = " 1 minute(s) 19.83 second(s)"; String repoSenseVersion = "48a60d6c0d"; ZoneId zoneId = ZoneId.of("Asia/Singapore"); - ReportConfiguration reportConfig = new ReportConfiguration(); + String reportTitle = "RepoSense Report"; LocalDateTime sinceDate = LocalDate.parse("2025-02-16").atStartOfDay(); LocalDateTime untilDate = LocalDateTime.parse("2025-03-16T23:59:59"); boolean isSinceDateProvided = false; @@ -68,7 +67,7 @@ public static void setUp() throws Exception { AuthorBlurbMap authorBlurbs = new AuthorBlurbMap(); authorBlurbs.withRecord("nbriannl", "Test for author-blurbs.md"); - expectedValidSummaryJson = new SummaryJson(repos, reportConfig, reportGeneratedTime, sinceDate, untilDate, + expectedValidSummaryJson = new SummaryJson(repos, reportTitle, reportGeneratedTime, sinceDate, untilDate, isSinceDateProvided, isUntilDateProvided, repoSenseVersion, errorSet, reportGenerationTime, zoneId, isAuthorshipAnalyzed, repoBlurbs, authorBlurbs, isPortfolio); } @@ -101,14 +100,13 @@ public void summaryJson_parseValidJsonFile_success() throws Exception { .build(); SummaryJson actualSummaryJson = new SummaryJson(List.of(repoConfig), - new ReportConfiguration(parsedSummaryJson.getReportTitle()), - parsedSummaryJson.getReportGeneratedTime(), parsedSummaryJson.getSinceDate(), - parsedSummaryJson.getUntilDate(), parsedSummaryJson.isSinceDateProvided(), - parsedSummaryJson.isUntilDateProvided(), parsedSummaryJson.getRepoSenseVersion(), - parsedSummaryJson.getErrorSet(), parsedSummaryJson.getReportGenerationTime(), - parsedSummaryJson.getZoneId(), parsedSummaryJson.isAuthorshipAnalyzed(), - parsedSummaryJson.getRepoBlurbs(), parsedSummaryJson.getAuthorBlurbs(), - parsedSummaryJson.isPortfolio()); + parsedSummaryJson.getReportTitle(), parsedSummaryJson.getReportGeneratedTime(), + parsedSummaryJson.getSinceDate(), parsedSummaryJson.getUntilDate(), + parsedSummaryJson.isSinceDateProvided(), parsedSummaryJson.isUntilDateProvided(), + parsedSummaryJson.getRepoSenseVersion(), parsedSummaryJson.getErrorSet(), + parsedSummaryJson.getReportGenerationTime(), parsedSummaryJson.getZoneId(), + parsedSummaryJson.isAuthorshipAnalyzed(), parsedSummaryJson.getRepoBlurbs(), + parsedSummaryJson.getAuthorBlurbs(), parsedSummaryJson.isPortfolio()); Assertions.assertEquals(expectedValidSummaryJson, actualSummaryJson); } diff --git a/src/test/java/reposense/report/ReportGeneratorTest.java b/src/test/java/reposense/report/ReportGeneratorTest.java index 1cf1049e1b..c4069ff227 100644 --- a/src/test/java/reposense/report/ReportGeneratorTest.java +++ b/src/test/java/reposense/report/ReportGeneratorTest.java @@ -21,7 +21,7 @@ import reposense.model.AuthorBlurbMap; import reposense.model.RepoBlurbMap; -import reposense.model.ReportConfiguration; +import reposense.model.reportconfig.ReportConfiguration; import reposense.parser.SummaryJsonParser; import reposense.util.TimeUtil; diff --git a/src/test/java/reposense/report/SummaryJsonTest.java b/src/test/java/reposense/report/SummaryJsonTest.java index 01cf0b59bc..38743cf533 100644 --- a/src/test/java/reposense/report/SummaryJsonTest.java +++ b/src/test/java/reposense/report/SummaryJsonTest.java @@ -20,8 +20,8 @@ import reposense.model.RepoBlurbMap; import reposense.model.RepoConfiguration; import reposense.model.RepoLocation; -import reposense.model.ReportConfiguration; import reposense.model.SupportedDomainUrlMap; +import reposense.model.reportconfig.ReportConfiguration; import reposense.parser.SummaryJsonParserTest; public class SummaryJsonTest { From d8d222dd6dea86ff91d15627fa445549483b3851 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Wed, 9 Apr 2025 23:48:52 +0800 Subject: [PATCH 40/42] Enhance docs --- docs/dg/cli.md | 2 +- docs/ug/cli.md | 2 +- docs/ug/reportConfig.md | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/dg/cli.md b/docs/dg/cli.md index 066b8b0806..e642e15f20 100644 --- a/docs/dg/cli.md +++ b/docs/dg/cli.md @@ -17,7 +17,7 @@ syntax reference under Appendix section of User Guide. -* Used in `ConfigSystemTest` and `ReportConfigSystemTest` classes. +* Used in `ConfigSystemTest` and `ReportConfigSystemTest`. * Some test cases performs shallow cloning while some does not. Fresh cloning ensures that the test cases that does not perform shallow cloning will clone the repo again if the previous test case uses shallow cloning, ensuring correctness of the analysis. diff --git a/docs/ug/cli.md b/docs/ug/cli.md index 1ee20a1111..994688cb47 100644 --- a/docs/ug/cli.md +++ b/docs/ug/cli.md @@ -76,7 +76,7 @@ partial credit. * Cannot be used with `--repos`. The `--repos` flag will take precedence over this flag. * If both `--repos` and `--config` are not specified, RepoSense looks for config files in the `./config` directory. -* If both `repo-config.csv` and `report-config.yaml` are present in the config file, `report-config.yaml` will take precedence as long as the `repos` field is correctly formatted. +* If both `repo-config.csv` and `report-config.yaml` are present in the config file, `report-config.yaml` will take precedence over **all CSV files** as long as the `repos` field is correctly formatted. * Config files must follow [this](./configFiles.html) format. diff --git a/docs/ug/reportConfig.md b/docs/ug/reportConfig.md index e9931db1ef..1d04941e69 100644 --- a/docs/ug/reportConfig.md +++ b/docs/ug/reportConfig.md @@ -93,6 +93,11 @@ specify the period for report generation, you can use the `--since` and `--until + + +Additionally, you can add a `--portfolio` flag to generate a more optimized view of your report. + + That's it! RepoSense will analyze the repositories you specified and generate a report that visualizes your contributions. ## Example Configuration From 53f7487966f7bf2d524d35eee70696547bd87861 Mon Sep 17 00:00:00 2001 From: LiYuan Date: Sun, 13 Apr 2025 16:36:13 +0800 Subject: [PATCH 41/42] Enhance docs --- docs/ug/reportConfig.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ug/reportConfig.md b/docs/ug/reportConfig.md index 1d04941e69..1cc29b495e 100644 --- a/docs/ug/reportConfig.md +++ b/docs/ug/reportConfig.md @@ -155,7 +155,7 @@ repos: ## Further Customizing Your Report -Optionally, you can provide more detailed descriptions to your report. Please refer to this [section](./customizingReports.md#personalizing-reports). +Optionally, you can provide more detailed descriptions to your report such as an introduction at the top of your dashboard or explanatory blurbs for each repository. Please refer to this [section](./customizingReports.md#personalizing-reports). ## Advanced Report Configuration From 00ab6707e8faf26f0e3b8e17c13dda67b674208e Mon Sep 17 00:00:00 2001 From: LiYuan Date: Sun, 13 Apr 2025 21:52:24 +0800 Subject: [PATCH 42/42] Remove --config flag description --- docs/ug/reportConfig.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/ug/reportConfig.md b/docs/ug/reportConfig.md index 1cc29b495e..7a50b86537 100644 --- a/docs/ug/reportConfig.md +++ b/docs/ug/reportConfig.md @@ -18,7 +18,7 @@ The `report-config.yaml` file is a simple configuration file that lets you defin It's designed to be user-friendly, especially for beginners who want to quickly set up a code portfolio without dealing with complex configurations. For more sophisticated use cases that require additional flexibility, -RepoSense's [CSV config files](./customizingReports.html#customize-using-csv-config-files) offer an alternative configuration approach with expanded capabilities. +RepoSense's [CSV configuration files](./customizingReports.html#customize-using-csv-config-files) offer an alternative configuration approach with expanded capabilities. ## Quick Start Guide @@ -76,17 +76,18 @@ repos: ### Step 3: Generate your report -Run RepoSense with your `report-config.yaml` in your `CONFIG_DIRECTORY` using the command: +To run RepoSense with `report-config.yaml`: -`java -jar RepoSense.jar --config CONFIG_DIRECTORY` - -{{ embed("Appendix: **CLI syntax reference → `config` flag**", "cli.md#section-config") }} +1. Create a `config` folder in the same directory as the RepoSense.jar file. +2. Add your `report-config.yaml` to the `config` folder. +3. Run the command:`java -jar RepoSense.jar` + Please note that the above command will only analyse your commits one month before the current date. If you would like to specify the period for report generation, you can use the `--since` and `--until` flags. -`java -jar RepoSense.jar --config CONFIG_DIRECTORY --since START_DATE --until END_DATE` +`java -jar RepoSense.jar --since START_DATE --until END_DATE` {{ embed("Appendix: **CLI syntax reference → `since` flag**", "cli.md#section-since-date") }} {{ embed("Appendix: **CLI syntax reference → `until` flag**", "cli.md#section-until-date") }}