diff --git a/build.gradle b/build.gradle
index 789f245400..b5827976aa 100644
--- a/build.gradle
+++ b/build.gradle
@@ -196,7 +196,7 @@ def serveTestReportInBackground = tasks.register('serveTestReportInBackground',
workingDir = 'build/serveTestReport'
main = mainClassName
classpath = sourceSets.main.runtimeClasspath
- args = ['--config', './exampleconfig', '--since', 'd1', '--view']
+ args = ['--config', './exampleconfig', '--since', 'd1', '--view', '-A']
String versionJvmArgs = '-Dversion=' + getRepoSenseVersion()
jvmArgs = [ versionJvmArgs ]
waitForPort = 9000
diff --git a/docs/ug/cli.md b/docs/ug/cli.md
index db0cbe5243..550713969c 100644
--- a/docs/ug/cli.md
+++ b/docs/ug/cli.md
@@ -16,16 +16,36 @@ The command `java -jar RepoSense.jar` takes several flags.
**Examples**:
An example of a command using most parameters:
-`java -jar RepoSense.jar --repos https://github.com/reposense/RepoSense.git --output ./report_folder --since 31/1/2017 --until 31/12/2018 --formats java adoc xml --view --ignore-standalone-config --last-modified-date --timezone UTC+08 --find-previous-authors`
+`java -jar RepoSense.jar --repos https://github.com/reposense/RepoSense.git --output ./report_folder --since 31/1/2017 --until 31/12/2018 --formats java adoc xml --view --ignore-standalone-config --last-modified-date --timezone UTC+08 --find-previous-authors --analyze-authorship --originality-threshold 0.66`
Same command as above but using most parameters in alias format:
-`java -jar RepoSense.jar -r https://github.com/reposense/RepoSense.git -o ./report_folder -s 31/1/2017 -u 31/12/2018 -f java adoc xml -v -i -l -t UTC+08 -F`
+`java -jar RepoSense.jar -r https://github.com/reposense/RepoSense.git -o ./report_folder -s 31/1/2017 -u 31/12/2018 -f java adoc xml -v -i -l -t UTC+08 -F -A -ot 0.66`
The section below provides explanations for each of the flags.
+### `--analyze-authorship`, `-A`
+
+**`--analyze-authorship`**: Performs further analysis to distinguish between partial and full credit attribution for
+lines of code assigned to the author.
+
+* Default: this feature is turned ***off*** by default and the author will receive partial credits for all lines of
+ code, as the code lines are at least partial credit but may not qualify for full credit.
+* Alias: `-A` (upper case)
+* Example: `--analyze-authorship` or `-A`
+
+
+
+A darker background colour represents full credit, while a lighter background colour represents partial credit.
+
+If the code is attributed to a different author by the user via `@@author` tag, then the new author will be given
+partial credit.
+
+
+
+
### `--assets`, `-a`
@@ -147,6 +167,26 @@ This flag overrides the `Ignore file size limit` field in the CSV config file.
+### `--originality-threshold`, `-ot`
+
+**`--originality-threshold [VALUE]`**: Specifies the cut-off point for partial and full credit
+in `--analyze-authorship`. Author will be given full credit if their contribution exceeds this threshold, else partial
+credit is given.
+
+* Parameter: `VALUE` Optional. Acceptable range: [0.0, 1.0].
+ Default: `0.51`
+* Alias: `-ot`
+* Example: `--originality-threshold 0.66` or `-ot 0.66`
+
+
+
+* Requires `--analyze-authorship` flag.
+* An author's contribution, or `originality score`, is calculated using Levenshtein Distance (Edit Distance) algorithm.
+ We compare the difference between current code line and its previous versions.
+
+
+
+
### `--output`, `-o`
**`--output OUTPUT_DIRECTORY`**: Indicates where to save the report generated.
diff --git a/frontend/cypress/tests/codeView/codeView_codeHighlighting.cy.js b/frontend/cypress/tests/codeView/codeView_codeHighlighting.cy.js
index 727478e8f0..79c3334528 100644
--- a/frontend/cypress/tests/codeView/codeView_codeHighlighting.cy.js
+++ b/frontend/cypress/tests/codeView/codeView_codeHighlighting.cy.js
@@ -38,7 +38,7 @@ describe('code highlighting works properly', () => {
cy.get('.hljs-comment').contains('* Represents a Git Author.')
.parent() // .line-content
.parent() // .code
- .should('have.css', 'background-color', 'rgb(230, 255, 237)'); // #e6ffed
+ .should('have.css', 'background-color', 'rgb(191, 246, 207)'); // #BFF6CF
});
it('should highlight code when multiple authors are merged in a repo group', () => {
@@ -62,13 +62,13 @@ describe('code highlighting works properly', () => {
cy.get('.hljs-comment').contains('* MUI Colors module') // eugenepeh
.parent() // .line-content
.parent() // .code
- .should('have.css', 'background-color', 'rgba(30, 144, 255, 0.19)') // #1e90ff, transparencyValue 30
+ .should('have.css', 'background-color', 'rgba(30, 144, 255, 0.314)') // #1e90ff, transparencyValue 50
.then((firstAuthorColor) => {
// eslint-disable-next-line quotes
cy.get('.line-content').contains("'red': (") // jamessspanggg
.parent() // .code
- // #f08080, transparencyValue 30
- .should('have.css', 'background-color', 'rgba(240, 128, 128, 0.19)')
+ // #f08080, transparencyValue 50
+ .should('have.css', 'background-color', 'rgba(240, 128, 128, 0.314)')
.and('not.eq', firstAuthorColor);
});
});
diff --git a/frontend/cypress/tests/codeView/codeView_creditBackgroundColour.cy.js b/frontend/cypress/tests/codeView/codeView_creditBackgroundColour.cy.js
new file mode 100644
index 0000000000..684f977e59
--- /dev/null
+++ b/frontend/cypress/tests/codeView/codeView_creditBackgroundColour.cy.js
@@ -0,0 +1,55 @@
+describe('credit background colour', () => {
+ it('check if background colour match the credit information for Eugene', () => {
+ // open the code panel
+ cy.get('.icon-button.fa-code')
+ .should('exist')
+ .first()
+ .click();
+
+ // src/main/java/reposense/model/Author.java
+ // line 9: full credit - #BFF6CF
+ cy.get(':nth-child(1) > .file-content > .segment-collection > :nth-child(2) > .java > .code')
+ .should('have.css', 'background-color')
+ .and('eq', 'rgb(191, 246, 207)');
+
+ // src/main/java/reposense/model/Author.java
+ // line 15: partial credit - #E6FFED
+ cy.get(':nth-child(1) > .file-content > .segment-collection > :nth-child(4) > .java > .code')
+ .should('have.css', 'background-color')
+ .and('eq', 'rgb(230, 255, 237)');
+ });
+
+ it('check if background colour match the credit information when group is merged', () => {
+ // check merge group checkbox
+ cy.get('#summary label.merge-group > input')
+ .should('be.visible')
+ .check()
+ .should('be.checked');
+
+ // open the code panel
+ cy.get('.icon-button.fa-code')
+ .should('exist')
+ .first()
+ .click();
+
+ // frontend/src/styles/_colors.scss
+ // line 35: full credit - #F0808050
+ cy.get(':nth-child(7) > .scss > :nth-child(1)')
+ .should('have.css', 'background-color')
+ .and('eq', 'rgba(240, 128, 128, 0.314)');
+
+ // FileInfoExtractor.java is too far away to be loaded, use filter to go to it directly
+ cy.get('#search')
+ .click()
+ .type('FileInfoExtractor.java');
+
+ cy.get('#submit-button')
+ .click();
+
+ // src/main/java/reposense/authorship/FileInfoExtractor.java
+ // line 23: partial credit - #1E90FF20
+ cy.get(':nth-child(10) > .java > :nth-child(1)')
+ .should('have.css', 'background-color')
+ .and('eq', 'rgba(30, 144, 255, 0.125)');
+ });
+});
diff --git a/frontend/src/components/c-segment.vue b/frontend/src/components/c-segment.vue
index 0fcec1a4be..362bed5427 100644
--- a/frontend/src/components/c-segment.vue
+++ b/frontend/src/components/c-segment.vue
@@ -1,8 +1,8 @@
.segment(
- v-bind:class="{ untouched: !segment.knownAuthor, active: isOpen }",
+ v-bind:class="{ untouched: !segment.knownAuthor, active: isOpen, isNotFullCredit: !segment.isFullCredit }",
v-bind:style="{ 'border-left': `0.25rem solid ${authorColors[segment.knownAuthor]}` }",
- v-bind:title="`Author: ${segment.knownAuthor || \"Unknown\"}`"
+ v-bind:title="`${segment.isFullCredit ? 'Author' : 'Co-author'}: ${segment.knownAuthor || \"Unknown\"}`"
)
.closer(v-if="canOpen",
v-on:click="toggleCode", ref="topButton")
@@ -17,6 +17,7 @@
v-bind:title="'Click to hide code'"
)
div(v-if="isOpen", v-hljs="path")
+ //- author color is applied only when the author color exists, else it takes the default mui color value
.code(
v-for="(line, index) in segment.lines", v-bind:key="index",
v-bind:style="{ 'background-color': `${authorColors[segment.knownAuthor]}${transparencyValue}` }"
@@ -57,7 +58,7 @@ export default defineComponent({
return {
isOpen: (this.segment.knownAuthor !== null) || this.segment.lines.length < 5 as boolean,
canOpen: (this.segment.knownAuthor === null) && this.segment.lines.length > 4 as boolean,
- transparencyValue: '30' as string,
+ transparencyValue: (this.segment.isFullCredit ? '50' : '20') as string,
};
},
computed: {
@@ -81,7 +82,7 @@ export default defineComponent({
border-left: .25rem solid mui-color('green');
.code {
- background-color: mui-color('github', 'authored-code-background');
+ background-color: mui-color('github', 'full-authored-code-background');
padding-left: 1rem;
}
@@ -114,6 +115,12 @@ export default defineComponent({
word-break: break-word;
}
+ &.isNotFullCredit {
+ .code {
+ background-color: mui-color('github', 'partial-authored-code-background');
+ }
+ }
+
&.untouched {
$grey: mui-color('grey', '400');
border-left: .25rem solid $grey;
diff --git a/frontend/src/styles/_colors.scss b/frontend/src/styles/_colors.scss
index ca250b293a..1c8d58a78f 100644
--- a/frontend/src/styles/_colors.scss
+++ b/frontend/src/styles/_colors.scss
@@ -303,7 +303,8 @@ $mui-colors: (
'github': (
'title-background': #FAFBFC,
'border': #E1E4E8,
- 'authored-code-background': #E6FFED,
+ 'full-authored-code-background': #BFF6CF,
+ 'partial-authored-code-background': #E6FFED,
),
'grey': (
'50': #FAFAFA,
diff --git a/frontend/src/styles/hightlight-js-style.css b/frontend/src/styles/hightlight-js-style.css
index 40c88537f4..c23be59baf 100644
--- a/frontend/src/styles/hightlight-js-style.css
+++ b/frontend/src/styles/hightlight-js-style.css
@@ -34,7 +34,7 @@
.hljs-section,
.hljs-name {
- color: #63a35c;
+ color: #468C5A;
}
.hljs-tag {
diff --git a/frontend/src/types/types.ts b/frontend/src/types/types.ts
index b825eb8593..63fcc0c6dd 100644
--- a/frontend/src/types/types.ts
+++ b/frontend/src/types/types.ts
@@ -61,6 +61,7 @@ export interface Repo extends RepoRaw {
export interface AuthorshipFileSegment {
knownAuthor: string | null;
+ isFullCredit: boolean;
lineNumbers: number[];
lines: string[];
}
@@ -83,3 +84,9 @@ export interface Bar {
color?: string;
tooltipText?: string;
}
+
+export interface SegmentState {
+ id: number;
+ author: string | null;
+ isFullCredit: boolean;
+}
diff --git a/frontend/src/types/window.ts b/frontend/src/types/window.ts
index ffc01a5b5a..b8a0a60524 100644
--- a/frontend/src/types/window.ts
+++ b/frontend/src/types/window.ts
@@ -68,6 +68,7 @@ declare global {
repoSenseVersion: string;
isSinceDateProvided: boolean;
isUntilDateProvided: boolean;
+ isAuthorshipAnalyzed: boolean;
DOMAIN_URL_MAP: DomainUrlMap;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
app: any;
diff --git a/frontend/src/types/zod/authorship-type.ts b/frontend/src/types/zod/authorship-type.ts
index 5455267144..1e2708b943 100644
--- a/frontend/src/types/zod/authorship-type.ts
+++ b/frontend/src/types/zod/authorship-type.ts
@@ -4,6 +4,7 @@ const lineSchema = z.object({
lineNumber: z.number(),
author: z.object({ gitId: z.string() }),
content: z.string(),
+ isFullCredit: z.boolean().default(false), // for backwards compatability
});
const fileResult = z.object({
diff --git a/frontend/src/types/zod/summary-type.ts b/frontend/src/types/zod/summary-type.ts
index a390bf4957..4ee1635406 100644
--- a/frontend/src/types/zod/summary-type.ts
+++ b/frontend/src/types/zod/summary-type.ts
@@ -31,7 +31,6 @@ const urlSchema = z.object({
const supportedDomainUrlMapSchema = z.record(urlSchema);
// Contains the zod validation schema for the summary.json file
-
export const summarySchema = z.object({
repoSenseVersion: z.string(),
reportGeneratedTime: z.string(),
@@ -44,6 +43,7 @@ export const summarySchema = z.object({
untilDate: z.string(),
isSinceDateProvided: z.boolean(),
isUntilDateProvided: z.boolean(),
+ isAuthorshipAnalyzed: z.boolean().default(false), // for backwards compatability
supportedDomainUrlMap: supportedDomainUrlMapSchema,
});
diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts
index 1ac4d3c96b..940dedffa7 100644
--- a/frontend/src/utils/api.ts
+++ b/frontend/src/utils/api.ts
@@ -217,6 +217,7 @@ window.api = {
window.repoSenseVersion = data.repoSenseVersion;
window.isSinceDateProvided = data.isSinceDateProvided;
window.isUntilDateProvided = data.isUntilDateProvided;
+ window.isAuthorshipAnalyzed = data.isAuthorshipAnalyzed;
document.title = data.reportTitle || document.title;
const errorMessages: { [key: string]: ErrorMessage } = {};
diff --git a/frontend/src/views/c-authorship.vue b/frontend/src/views/c-authorship.vue
index f85017d2d6..7e3ef1007d 100644
--- a/frontend/src/views/c-authorship.vue
+++ b/frontend/src/views/c-authorship.vue
@@ -85,6 +85,13 @@
)
span {{ ignoredFilesCount }} ignored file(s)
+ .background-color-legend(v-if="isAuthorshipAnalyzed")
+ .bold Legend:
+ .color-circle.full-credit-color(v-bind:class="{'isMergeGroup': info.isMergeGroup}")
+ span [darker shades] Mostly contributed by author.
+ .color-circle.partial-credit-color(v-bind:class="{'isMergeGroup': info.isMergeGroup}")
+ span [lighter shades] Contributed by author, with non-trivial contribution from others.
+
.files(v-if="isLoaded")
.empty(v-if="info.files.length === 0") nothing to see here :(
template(v-for="(file, index) in selectedFiles", v-bind:key="file.path")
@@ -104,7 +111,7 @@ import cFileTypeCheckboxes from '../components/c-file-type-checkboxes.vue';
import getNonRepeatingColor from '../utils/random-color-generator';
import { StoreState } from '../types/vuex.d';
import { FileResult, Line } from '../types/zod/authorship-type';
-import { AuthorshipFile, AuthorshipFileSegment } from '../types/types';
+import { AuthorshipFile, AuthorshipFileSegment, SegmentState } from '../types/types';
import { FilesSortType, FilterType } from '../types/authorship';
const filesSortDict = {
@@ -281,6 +288,10 @@ export default defineComponent({
info: (state: unknown) => (state as StoreState).tabAuthorshipInfo,
authorColors: (state: unknown) => (state as StoreState).tabAuthorColors,
}),
+
+ isAuthorshipAnalyzed(): boolean {
+ return window.isAuthorshipAnalyzed;
+ },
},
watch: {
@@ -442,8 +453,7 @@ export default defineComponent({
splitSegments(lines: Array): { segments: Array; blankLineCount: number; } {
// split into segments separated by knownAuthor
- let lastState: string | null;
- let lastId = -1;
+ const lastState : SegmentState = { id: -1, author: null, isFullCredit: true };
const segments: Array = [];
let blankLineCount = 0;
@@ -452,22 +462,25 @@ export default defineComponent({
? !this.isUnknownAuthor(line.author.gitId)
: line.author.gitId === this.info.author;
const knownAuthor = (line.author && isAuthorMatched) ? line.author.gitId : null;
+ const isFullCredit = line.isFullCredit;
- if (knownAuthor !== lastState || lastId === -1) {
+ if (lastState.id === -1 || lastState.author !== knownAuthor
+ || (knownAuthor && lastState.isFullCredit !== isFullCredit)) {
segments.push({
knownAuthor,
+ isFullCredit,
lineNumbers: [],
lines: [],
});
- lastId += 1;
- lastState = knownAuthor;
+ lastState.id += 1;
+ lastState.author = knownAuthor;
+ lastState.isFullCredit = isFullCredit;
}
const content = line.content || ' ';
- segments[lastId].lines.push(content);
-
- segments[lastId].lineNumbers.push(lineCount + 1);
+ segments[lastState.id].lines.push(content);
+ segments[lastState.id].lineNumbers.push(lineCount + 1);
if (line.content === '' && knownAuthor) {
blankLineCount += 1;
@@ -731,5 +744,40 @@ export default defineComponent({
.empty {
text-align: center;
}
+
+ .background-color-legend {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+
+ .bold {
+ font-weight: bold;
+ }
+
+ .color-circle {
+ border: 1px solid lightgrey;
+ border-radius: 50%;
+ margin-left: 5px;
+ margin-right: 5px;
+ min-height: 15px;
+ min-width: 15px;
+ }
+
+ .full-credit-color {
+ background-color: mui-color('github', 'full-authored-code-background');
+
+ &.isMergeGroup {
+ background-color: mui-color('grey', '400');
+ }
+ }
+
+ .partial-credit-color {
+ background-color: mui-color('github', 'partial-authored-code-background');
+
+ &.isMergeGroup {
+ background-color: mui-color('grey', '200');
+ }
+ }
+ }
}
diff --git a/src/main/java/reposense/RepoSense.java b/src/main/java/reposense/RepoSense.java
index 20a8c9188d..be54dd663b 100644
--- a/src/main/java/reposense/RepoSense.java
+++ b/src/main/java/reposense/RepoSense.java
@@ -79,7 +79,8 @@ public static void main(String[] args) {
cliArguments.getSinceDate(), cliArguments.getUntilDate(),
cliArguments.isSinceDateProvided(), cliArguments.isUntilDateProvided(),
cliArguments.getNumCloningThreads(), cliArguments.getNumAnalysisThreads(),
- TimeUtil::getElapsedTime, cliArguments.getZoneId(), cliArguments.isFreshClonePerformed());
+ TimeUtil::getElapsedTime, cliArguments.getZoneId(), cliArguments.isFreshClonePerformed(),
+ cliArguments.isAuthorshipAnalyzed(), cliArguments.getOriginalityThreshold());
FileUtil.zipFoldersAndFiles(reportFoldersAndFiles, cliArguments.getOutputFilePath().toAbsolutePath(),
".json");
diff --git a/src/main/java/reposense/authorship/AuthorshipReporter.java b/src/main/java/reposense/authorship/AuthorshipReporter.java
index 24f55ce8f4..25cb0b76d8 100644
--- a/src/main/java/reposense/authorship/AuthorshipReporter.java
+++ b/src/main/java/reposense/authorship/AuthorshipReporter.java
@@ -11,7 +11,6 @@
import reposense.model.RepoConfiguration;
import reposense.system.LogsManager;
-
/**
* Generates the authorship summary data for each repository.
*/
@@ -29,16 +28,18 @@ public class AuthorshipReporter {
private final FileInfoAnalyzer fileInfoAnalyzer = new FileInfoAnalyzer();
private final FileResultAggregator fileResultAggregator = new FileResultAggregator();
-
/**
* Generates and returns the authorship summary for each repo in {@code config}.
+ * Further analyzes the authorship of each line in the commit if {@code shouldAnalyzeAuthorship} is true, based on
+ * {code originalityThreshold}.
*/
- public AuthorshipSummary generateAuthorshipSummary(RepoConfiguration config) {
+ public AuthorshipSummary generateAuthorshipSummary(RepoConfiguration config, boolean shouldAnalyzeAuthorship,
+ double originalityThreshold) {
List textFileInfos = fileInfoExtractor.extractTextFileInfos(config);
int numFiles = textFileInfos.size();
int totalNumLines = textFileInfos.stream()
- .mapToInt(fileInfo -> fileInfo.getNumOfLines())
+ .mapToInt(FileInfo::getNumOfLines)
.sum();
if (totalNumLines > HIGH_NUMBER_LINES_THRESHOLD) {
@@ -46,7 +47,8 @@ public AuthorshipSummary generateAuthorshipSummary(RepoConfiguration config) {
}
List fileResults = textFileInfos.stream()
- .map(fileInfo -> fileInfoAnalyzer.analyzeTextFile(config, fileInfo))
+ .map(fileInfo -> fileInfoAnalyzer.analyzeTextFile(config, fileInfo, shouldAnalyzeAuthorship,
+ originalityThreshold))
.filter(Objects::nonNull)
.collect(Collectors.toList());
diff --git a/src/main/java/reposense/authorship/FileInfoAnalyzer.java b/src/main/java/reposense/authorship/FileInfoAnalyzer.java
index 5efd8ce7c3..6fc01757d8 100644
--- a/src/main/java/reposense/authorship/FileInfoAnalyzer.java
+++ b/src/main/java/reposense/authorship/FileInfoAnalyzer.java
@@ -12,6 +12,7 @@
import java.util.logging.Logger;
import reposense.authorship.analyzer.AnnotatorAnalyzer;
+import reposense.authorship.analyzer.AuthorshipAnalyzer;
import reposense.authorship.model.FileInfo;
import reposense.authorship.model.FileResult;
import reposense.authorship.model.LineInfo;
@@ -45,10 +46,13 @@ public class FileInfoAnalyzer {
/**
* Analyzes the lines of the file, given in the {@code fileInfo}, that has changed in the time period provided
* by {@code config}.
+ * Further analyzes the authorship of each line in the commit if {@code shouldAnalyzeAuthorship} is true, based on
+ * {@code originalityThreshold}.
* Returns null if the file is missing from the local system, or none of the
* {@link Author} specified in {@code config} contributed to the file in {@code fileInfo}.
*/
- public FileResult analyzeTextFile(RepoConfiguration config, FileInfo fileInfo) {
+ public FileResult analyzeTextFile(RepoConfiguration config, FileInfo fileInfo, boolean shouldAnalyzeAuthorship,
+ double originalityThreshold) {
String relativePath = fileInfo.getPath();
if (Files.notExists(Paths.get(config.getRepoRoot(), relativePath))) {
@@ -60,10 +64,10 @@ public FileResult analyzeTextFile(RepoConfiguration config, FileInfo fileInfo) {
return null;
}
- aggregateBlameAuthorModifiedAndDateInfo(config, fileInfo);
+ aggregateBlameAuthorModifiedAndDateInfo(config, fileInfo, shouldAnalyzeAuthorship, originalityThreshold);
fileInfo.setFileType(config.getFileType(fileInfo.getPath()));
- AnnotatorAnalyzer.aggregateAnnotationAuthorInfo(fileInfo, config.getAuthorConfig());
+ AnnotatorAnalyzer.aggregateAnnotationAuthorInfo(fileInfo, config.getAuthorConfig(), shouldAnalyzeAuthorship);
if (!config.getAuthorList().isEmpty() && fileInfo.isAllAuthorsIgnored(config.getAuthorList())) {
return null;
@@ -101,9 +105,8 @@ private FileResult generateTextFileResult(FileInfo fileInfo) {
authorContributionMap.put(author, authorContributionMap.getOrDefault(author, 0) + 1);
}
- return FileResult.createTextFileResult(
- fileInfo.getPath(), fileInfo.getFileType(), fileInfo.getLines(), authorContributionMap,
- fileInfo.exceedsFileLimit());
+ return FileResult.createTextFileResult(fileInfo.getPath(), fileInfo.getFileType(), fileInfo.getLines(),
+ authorContributionMap, fileInfo.exceedsFileLimit());
}
/**
@@ -140,8 +143,11 @@ private FileResult generateBinaryFileResult(RepoConfiguration config, FileInfo f
* The {@code config} is used to obtain the root directory for running git blame as well as other parameters used
* in determining which author to assign to each line and whether to set the last modified date for a
* {@code lineInfo}.
+ * Further analyzes the authorship of each line in the commit if {@code shouldAnalyzeAuthorship} is true, based on
+ * {@code originalityThreshold}.
*/
- private void aggregateBlameAuthorModifiedAndDateInfo(RepoConfiguration config, FileInfo fileInfo) {
+ private void aggregateBlameAuthorModifiedAndDateInfo(RepoConfiguration config, FileInfo fileInfo,
+ boolean shouldAnalyzeAuthorship, double originalityThreshold) {
String blameResults;
if (!config.isFindingPreviousAuthorsPerformed()) {
@@ -160,14 +166,15 @@ private void aggregateBlameAuthorModifiedAndDateInfo(RepoConfiguration config, F
String authorName = blameResultLines[lineCount + 1].substring(AUTHOR_NAME_OFFSET);
String authorEmail = blameResultLines[lineCount + 2]
.substring(AUTHOR_EMAIL_OFFSET).replaceAll("<|>", "");
- Long commitDateInMs = Long.parseLong(blameResultLines[lineCount + 3].substring(AUTHOR_TIME_OFFSET)) * 1000;
+ long commitDateInMs = Long.parseLong(blameResultLines[lineCount + 3].substring(AUTHOR_TIME_OFFSET)) * 1000;
LocalDateTime commitDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(commitDateInMs),
config.getZoneId());
Author author = config.getAuthor(authorName, authorEmail);
- if (!fileInfo.isFileLineTracked(lineCount / 5) || author.isIgnoringFile(filePath)
+ int lineNumber = lineCount / 5;
+ if (!fileInfo.isFileLineTracked(lineNumber) || author.isIgnoringFile(filePath)
|| CommitHash.isInsideCommitList(commitHash, config.getIgnoreCommitList())
- || commitDate.compareTo(sinceDate) < 0 || commitDate.compareTo(untilDate) > 0) {
+ || commitDate.isBefore(sinceDate) || commitDate.isAfter(untilDate)) {
author = Author.UNKNOWN_AUTHOR;
}
@@ -177,9 +184,16 @@ private void aggregateBlameAuthorModifiedAndDateInfo(RepoConfiguration config, F
MESSAGE_SHALLOW_CLONING_LAST_MODIFIED_DATE_CONFLICT, config.getRepoName()));
}
- fileInfo.setLineLastModifiedDate(lineCount / 5, commitDate);
+ fileInfo.setLineLastModifiedDate(lineNumber, commitDate);
+ }
+ fileInfo.setLineAuthor(lineNumber, author);
+
+ if (shouldAnalyzeAuthorship && !author.equals(Author.UNKNOWN_AUTHOR)) {
+ String lineContent = fileInfo.getLine(lineNumber + 1).getContent();
+ boolean isFullCredit = AuthorshipAnalyzer.analyzeAuthorship(config, fileInfo.getPath(), lineContent,
+ commitHash, author, originalityThreshold);
+ fileInfo.setIsFullCredit(lineNumber, isFullCredit);
}
- fileInfo.setLineAuthor(lineCount / 5, author);
}
}
diff --git a/src/main/java/reposense/authorship/analyzer/AnnotatorAnalyzer.java b/src/main/java/reposense/authorship/analyzer/AnnotatorAnalyzer.java
index 8e32caa994..51ac0a7a7c 100644
--- a/src/main/java/reposense/authorship/analyzer/AnnotatorAnalyzer.java
+++ b/src/main/java/reposense/authorship/analyzer/AnnotatorAnalyzer.java
@@ -48,8 +48,10 @@ public class AnnotatorAnalyzer {
*
* @param fileInfo FileInfo to be further analyzed with author annotations.
* @param authorConfig AuthorConfiguration for current analysis.
+ * @param shouldAnalyzeAuthorship whether credit info needs to be overwritten.
*/
- public static void aggregateAnnotationAuthorInfo(FileInfo fileInfo, AuthorConfiguration authorConfig) {
+ public static void aggregateAnnotationAuthorInfo(FileInfo fileInfo, AuthorConfiguration authorConfig,
+ boolean shouldAnalyzeAuthorship) {
Optional currentAnnotatedAuthor = Optional.empty();
Path filePath = Paths.get(fileInfo.getPath());
for (LineInfo lineInfo : fileInfo.getLines()) {
@@ -60,7 +62,7 @@ public static void aggregateAnnotationAuthorInfo(FileInfo fileInfo, AuthorConfig
boolean isUnknownAuthorSegment = !currentAnnotatedAuthor.isPresent() && !newAnnotatedAuthor.isPresent();
if (isEndOfAnnotatedSegment) {
- lineInfo.setAuthor(currentAnnotatedAuthor.get());
+ lineInfo.updateAuthorAndCredit(currentAnnotatedAuthor.get(), shouldAnalyzeAuthorship);
currentAnnotatedAuthor = Optional.empty();
} else if (isUnknownAuthorSegment) {
currentAnnotatedAuthor = Optional.of(Author.UNKNOWN_AUTHOR);
@@ -68,7 +70,7 @@ public static void aggregateAnnotationAuthorInfo(FileInfo fileInfo, AuthorConfig
currentAnnotatedAuthor = newAnnotatedAuthor.filter(author -> !author.isIgnoringFile(filePath));
}
}
- currentAnnotatedAuthor.ifPresent(lineInfo::setAuthor);
+ currentAnnotatedAuthor.ifPresent(author -> lineInfo.updateAuthorAndCredit(author, shouldAnalyzeAuthorship));
}
}
diff --git a/src/main/java/reposense/authorship/analyzer/AuthorshipAnalyzer.java b/src/main/java/reposense/authorship/analyzer/AuthorshipAnalyzer.java
new file mode 100644
index 0000000000..26789847cb
--- /dev/null
+++ b/src/main/java/reposense/authorship/analyzer/AuthorshipAnalyzer.java
@@ -0,0 +1,250 @@
+package reposense.authorship.analyzer;
+
+import java.nio.file.Paths;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import reposense.authorship.model.CandidateLine;
+import reposense.authorship.model.FileDiffInfo;
+import reposense.git.GitBlame;
+import reposense.git.GitDiff;
+import reposense.git.GitLog;
+import reposense.git.model.GitBlameLineInfo;
+import reposense.model.Author;
+import reposense.model.CommitHash;
+import reposense.model.RepoConfiguration;
+import reposense.system.LogsManager;
+import reposense.util.StringsUtil;
+
+/**
+ * Analyzes a line to find out if the author should be assigned partial or full credit.
+ */
+public class AuthorshipAnalyzer {
+ private static final Logger logger = LogsManager.getLogger(AuthorshipAnalyzer.class);
+ private static final Pattern DIFF_FILE_CHUNK_PATTERN = Pattern.compile("\ndiff --git a/.*\n");
+ private static final Pattern FILE_CHANGED_PATTERN =
+ Pattern.compile("\n(-){3} a?/(?.*)\n(\\+){3} b?/(?.*)\n");
+ private static final String PRE_IMAGE_FILE_PATH_GROUP_NAME = "preImageFilePath";
+ private static final String POST_IMAGE_FILE_PATH_GROUP_NAME = "postImageFilePath";
+ private static final String FILE_ADDED_SYMBOL = "dev/null";
+ private static final Pattern HUNK_PATTERN = Pattern.compile("\n@@ ");
+ private static final int LINES_CHANGED_HEADER_INDEX = 0;
+ private static final Pattern STARTING_LINE_NUMBER_PATTERN =
+ Pattern.compile("-(?\\d+),?\\d* \\+\\d+,?\\d* @@");
+ private static final String PREIMAGE_START_LINE_GROUP_NAME = "preImageStartLine";
+ private static final String MATCH_GROUP_FAIL_MESSAGE_FORMAT = "Failed to match the %s group for:\n%s";
+ private static final String ADDED_LINE_SYMBOL = "+";
+ private static final String DELETED_LINE_SYMBOL = "-";
+
+ private static final ConcurrentHashMap GIT_LOG_CACHE = new ConcurrentHashMap<>();
+ private static final ConcurrentHashMap> GIT_DIFF_CACHE = new ConcurrentHashMap<>();
+
+ /**
+ * Analyzes the authorship of {@code lineContent} in {@code filePath} based on {@code originalityThreshold}.
+ * Returns {@code true} if {@code currentAuthor} should be assigned full credit, {@code false} otherwise.
+ */
+ public static boolean analyzeAuthorship(RepoConfiguration config, String filePath, String lineContent,
+ String commitHash, Author currentAuthor, double originalityThreshold) {
+ // Empty lines are ignored and given full credit
+ if (lineContent.isEmpty()) {
+ return true;
+ }
+
+ Optional deletedLineOptional = getDeletedLineWithLowestOriginality(config, filePath, lineContent,
+ commitHash);
+
+ // Give full credit if there are no deleted lines found
+ if (!deletedLineOptional.isPresent()) {
+ return true;
+ }
+
+ CandidateLine deletedLine = deletedLineOptional.get();
+
+ // Give full credit if deleted line's originality score exceeds the originality threshold
+ if (deletedLine.getOriginalityScore() > originalityThreshold) {
+ return true;
+ }
+
+ GitBlameLineInfo deletedLineInfo = GitBlame.blameLine(config.getRepoRoot(), deletedLine.getGitBlameCommitHash(),
+ deletedLine.getFilePath(), deletedLine.getLineNumber());
+ Author previousAuthor = config.getAuthor(deletedLineInfo.getAuthorName(), deletedLineInfo.getAuthorEmail());
+ long sinceDateInMilliseconds = ZonedDateTime.of(config.getSinceDate(), config.getZoneId()).toEpochSecond();
+
+ // Give full credit if author is unknown, is before since date, is in ignored list, or is an ignored file
+ if (previousAuthor.equals(Author.UNKNOWN_AUTHOR)
+ || deletedLineInfo.getTimestampMilliseconds() < sinceDateInMilliseconds
+ || CommitHash.isInsideCommitList(deletedLineInfo.getCommitHash(), config.getIgnoreCommitList())
+ || previousAuthor.isIgnoringFile(Paths.get(deletedLine.getFilePath()))) {
+ return true;
+ }
+
+ // Give partial credit if currentAuthor is not the author of the previous version
+ if (!currentAuthor.equals(previousAuthor)) {
+ return false;
+ }
+
+ // Check the previous version as currentAuthor is the same as author of the previous version
+ return analyzeAuthorship(config, deletedLine.getFilePath(), deletedLine.getLineContent(),
+ deletedLineInfo.getCommitHash(), previousAuthor, originalityThreshold);
+ }
+
+ /**
+ * Returns the deleted line in {@code commitHash} that has the lowest originality with {@code lineContent}.
+ */
+ private static Optional getDeletedLineWithLowestOriginality(RepoConfiguration config,
+ String filePath, String lineContent, String commitHash) {
+ CandidateLine lowestOriginalityLine = null;
+
+ String gitLogCacheKey = config.getRepoRoot() + commitHash;
+ String[] parentCommits;
+ if (GIT_LOG_CACHE.containsKey(gitLogCacheKey)) {
+ parentCommits = GIT_LOG_CACHE.get(gitLogCacheKey);
+ } else {
+ String gitLogResults = GitLog.getParentCommits(config.getRepoRoot(), commitHash);
+ parentCommits = StringsUtil.SPACE.split(gitLogResults);
+ GIT_LOG_CACHE.put(gitLogCacheKey, parentCommits);
+ }
+
+ for (String parentCommit : parentCommits) {
+ String gitDiffCacheKey = config.getRepoRoot() + parentCommit + commitHash;
+ ArrayList fileDiffInfoList;
+
+ if (GIT_DIFF_CACHE.containsKey(gitDiffCacheKey)) {
+ fileDiffInfoList = GIT_DIFF_CACHE.get(gitDiffCacheKey);
+ } else {
+ fileDiffInfoList = getFileDiffInfoList(config, parentCommit, commitHash);
+ GIT_DIFF_CACHE.put(gitDiffCacheKey, fileDiffInfoList);
+ }
+
+ for (FileDiffInfo fileDiffInfo : fileDiffInfoList) {
+ // If file name does not match
+ if (!fileDiffInfo.getPostImageFilePath().equals(filePath)) {
+ continue;
+ }
+
+ CandidateLine candidateLine = getDeletedLineWithLowestOriginalityInDiff(
+ fileDiffInfo.getFileDiffResult(), lineContent, parentCommit,
+ fileDiffInfo.getPreImageFilePath());
+ if (candidateLine == null) {
+ continue;
+ }
+
+ if (lowestOriginalityLine == null
+ || candidateLine.getOriginalityScore() < lowestOriginalityLine.getOriginalityScore()) {
+ lowestOriginalityLine = candidateLine;
+ }
+ }
+ }
+
+ return Optional.ofNullable(lowestOriginalityLine);
+ }
+
+ private static ArrayList getFileDiffInfoList(RepoConfiguration config, String parentCommit,
+ String commitHash) {
+ ArrayList fileDiffInfoList = new ArrayList<>();
+
+ // Generate diff between commit and parent commit
+ String gitDiffResult = GitDiff.diffCommits(config.getRepoRoot(), parentCommit, commitHash);
+ String[] fileDiffResults = DIFF_FILE_CHUNK_PATTERN.split(gitDiffResult);
+
+ for (String fileDiffResult : fileDiffResults) {
+ Matcher filePathMatcher = FILE_CHANGED_PATTERN.matcher(fileDiffResult);
+ if (!filePathMatcher.find()) {
+ continue;
+ }
+
+ // If file was added in the commit
+ String preImageFilePath = filePathMatcher.group(PRE_IMAGE_FILE_PATH_GROUP_NAME);
+ if (preImageFilePath.equals(FILE_ADDED_SYMBOL)) {
+ continue;
+ }
+
+ String postImageFilePath = filePathMatcher.group(POST_IMAGE_FILE_PATH_GROUP_NAME);
+
+ fileDiffInfoList.add(new FileDiffInfo(fileDiffResult, preImageFilePath, postImageFilePath));
+ }
+
+ return fileDiffInfoList;
+ }
+
+ /**
+ * Returns the deleted line in {@code fileDiffResult} that has the lowest originality with {@code lineContent}.
+ */
+ private static CandidateLine getDeletedLineWithLowestOriginalityInDiff(String fileDiffResult, String lineContent,
+ String commitHash, String filePath) {
+ CandidateLine lowestOriginalityLine = null;
+
+ String[] hunks = HUNK_PATTERN.split(fileDiffResult);
+
+ // skip the diff header, index starts from 1
+ for (int index = 1; index < hunks.length; index++) {
+ String hunk = hunks[index];
+
+ // skip hunk if lines added in the hunk does not include lineContent
+ if (!hunk.contains(ADDED_LINE_SYMBOL + lineContent)) {
+ continue;
+ }
+
+ String[] linesChanged = StringsUtil.NEWLINE.split(hunk);
+ int currentPreImageLineNumber = getPreImageStartingLineNumber(linesChanged[LINES_CHANGED_HEADER_INDEX]);
+
+ // skip the lines changed header, index starts from 1
+ for (int lineIndex = 1; lineIndex < linesChanged.length; lineIndex++) {
+ String lineChanged = linesChanged[lineIndex];
+
+ if (lineChanged.startsWith(DELETED_LINE_SYMBOL)) {
+ String deletedLineContent = lineChanged.substring(DELETED_LINE_SYMBOL.length());
+ double lowestOriginalityScore = lowestOriginalityLine == null
+ ? Integer.MAX_VALUE
+ : lowestOriginalityLine.getOriginalityScore();
+ double originalityScore = computeOriginalityScore(lineContent, deletedLineContent,
+ lowestOriginalityScore);
+
+ if (lowestOriginalityLine == null
+ || originalityScore < lowestOriginalityLine.getOriginalityScore()) {
+ lowestOriginalityLine = new CandidateLine(
+ currentPreImageLineNumber, deletedLineContent, filePath, commitHash,
+ originalityScore);
+ }
+ }
+
+ if (!lineChanged.startsWith(ADDED_LINE_SYMBOL)) {
+ currentPreImageLineNumber++;
+ }
+ }
+ }
+
+ return lowestOriginalityLine;
+ }
+
+ /**
+ * Returns the pre-image starting line number by matching the pattern inside {@code linesChangedHeader}.
+ *
+ * @throws AssertionError if lines changed header matcher failed to find anything.
+ */
+ private static int getPreImageStartingLineNumber(String linesChangedHeader) {
+ Matcher linesChangedHeaderMatcher = STARTING_LINE_NUMBER_PATTERN.matcher(linesChangedHeader);
+
+ if (!linesChangedHeaderMatcher.find()) {
+ logger.severe(
+ String.format(MATCH_GROUP_FAIL_MESSAGE_FORMAT, PREIMAGE_START_LINE_GROUP_NAME, linesChangedHeader));
+ throw new AssertionError(
+ "Should not have error matching line number pattern inside lines changed header!");
+ }
+
+ return Integer.parseInt(linesChangedHeaderMatcher.group(PREIMAGE_START_LINE_GROUP_NAME));
+ }
+
+ /**
+ * Calculates the originality score of {@code s} with {@code baseString}.
+ */
+ private static double computeOriginalityScore(String s, String baseString, double limit) {
+ double levenshteinDistance = StringsUtil.getLevenshteinDistance(s, baseString, limit * baseString.length());
+ return levenshteinDistance / baseString.length();
+ }
+}
diff --git a/src/main/java/reposense/authorship/model/CandidateLine.java b/src/main/java/reposense/authorship/model/CandidateLine.java
new file mode 100644
index 0000000000..6916b9b5c2
--- /dev/null
+++ b/src/main/java/reposense/authorship/model/CandidateLine.java
@@ -0,0 +1,41 @@
+package reposense.authorship.model;
+
+/**
+ * Stores the information of a candidate line used in {@code AuthorshipAnalyzer}.
+ */
+public class CandidateLine {
+ private final int lineNumber;
+ private final String lineContent;
+ private final String filePath;
+ private final String gitBlameCommitHash;
+ private final double originalityScore;
+
+ public CandidateLine(int lineNumber, String lineContent, String filePath, String gitBlameCommitHash,
+ double originalityScore) {
+ this.lineNumber = lineNumber;
+ this.lineContent = lineContent;
+ this.filePath = filePath;
+ this.gitBlameCommitHash = gitBlameCommitHash;
+ this.originalityScore = originalityScore;
+ }
+
+ public int getLineNumber() {
+ return lineNumber;
+ }
+
+ public String getLineContent() {
+ return lineContent;
+ }
+
+ public String getFilePath() {
+ return filePath;
+ }
+
+ public String getGitBlameCommitHash() {
+ return gitBlameCommitHash;
+ }
+
+ public double getOriginalityScore() {
+ return originalityScore;
+ }
+}
diff --git a/src/main/java/reposense/authorship/model/FileDiffInfo.java b/src/main/java/reposense/authorship/model/FileDiffInfo.java
new file mode 100644
index 0000000000..7bb9b5f088
--- /dev/null
+++ b/src/main/java/reposense/authorship/model/FileDiffInfo.java
@@ -0,0 +1,28 @@
+package reposense.authorship.model;
+
+/**
+ * Stores the information of processed file diff used in {@code AuthorshipAnalyzer}.
+ */
+public class FileDiffInfo {
+ private final String fileDiffResult;
+ private final String preImageFilePath;
+ private final String postImageFilePath;
+
+ public FileDiffInfo(String fileDiffResult, String preImageFilePath, String postImageFilePath) {
+ this.fileDiffResult = fileDiffResult;
+ this.preImageFilePath = preImageFilePath;
+ this.postImageFilePath = postImageFilePath;
+ }
+
+ public String getFileDiffResult() {
+ return fileDiffResult;
+ }
+
+ public String getPreImageFilePath() {
+ return preImageFilePath;
+ }
+
+ public String getPostImageFilePath() {
+ return postImageFilePath;
+ }
+}
diff --git a/src/main/java/reposense/authorship/model/FileInfo.java b/src/main/java/reposense/authorship/model/FileInfo.java
index c5acb7f5db..4ccce204d5 100644
--- a/src/main/java/reposense/authorship/model/FileInfo.java
+++ b/src/main/java/reposense/authorship/model/FileInfo.java
@@ -73,14 +73,14 @@ public void setFileSize(long fileSize) {
this.fileSize = fileSize;
}
- public void setFileAnalyzed(boolean isFileAnalyzed) {
- this.isFileAnalyzed = isFileAnalyzed;
- }
-
public boolean isFileAnalyzed() {
return isFileAnalyzed;
}
+ public void setFileAnalyzed(boolean isFileAnalyzed) {
+ this.isFileAnalyzed = isFileAnalyzed;
+ }
+
public boolean exceedsFileLimit() {
return exceedsFileLimit;
}
@@ -110,6 +110,13 @@ public boolean isFileLineTracked(int lineNumber) {
return getLines().get(lineNumber).isTracked();
}
+ /**
+ * Sets whether {@code lineNumber} is fully credited to its {@code author}.
+ */
+ public void setIsFullCredit(int lineNumber, boolean isFullCredit) {
+ lines.get(lineNumber).setIsFullCredit(isFullCredit);
+ }
+
@Override
public boolean equals(Object other) {
if (this == other) {
diff --git a/src/main/java/reposense/authorship/model/LineInfo.java b/src/main/java/reposense/authorship/model/LineInfo.java
index f60fb916fa..41100daf15 100644
--- a/src/main/java/reposense/authorship/model/LineInfo.java
+++ b/src/main/java/reposense/authorship/model/LineInfo.java
@@ -13,6 +13,7 @@ public class LineInfo {
private Author author;
private String content;
private LocalDateTime lastModifiedDate;
+ private boolean isFullCredit;
private transient boolean isTracked;
@@ -21,6 +22,9 @@ public LineInfo(int lineNumber, String content) {
this.content = content;
isTracked = true;
+
+ // The code is at least partial credit but may not be full credit if authorship analysis is not performed.
+ isFullCredit = false;
}
public Author getAuthor() {
@@ -39,10 +43,6 @@ public String getContent() {
return content;
}
- public void setTracked(boolean isTracked) {
- this.isTracked = isTracked;
- }
-
public LocalDateTime getLastModifiedDate() {
return lastModifiedDate;
}
@@ -55,6 +55,33 @@ public boolean isTracked() {
return isTracked;
}
+ public void setTracked(boolean isTracked) {
+ this.isTracked = isTracked;
+ }
+
+ public boolean isFullCredit() {
+ return isFullCredit;
+ }
+
+ public void setIsFullCredit(boolean isFullCredit) {
+ this.isFullCredit = isFullCredit;
+ }
+
+ /**
+ * If {@code newAuthor} is not the same as current author, then {@code newAuthor} will get partial credit.
+ * Else nothing happens since the 2 authors are the same, the credit info is retained.
+ * {@code isFullCredit} is only updated if {@code shouldAnalyzeAuthorship} is set to True.
+ */
+ public void updateAuthorAndCredit(Author newAuthor, boolean shouldAnalyzeAuthorship) {
+ if (!author.equals(newAuthor)) {
+ author = newAuthor;
+
+ if (shouldAnalyzeAuthorship) {
+ isFullCredit = false;
+ }
+ }
+ }
+
@Override
public boolean equals(Object other) {
if (this == other) {
@@ -70,8 +97,7 @@ public boolean equals(Object other) {
&& Objects.equals(author, otherLineInfo.author)
&& content.equals(otherLineInfo.content)
&& isTracked == otherLineInfo.isTracked
- && ((lastModifiedDate == null && otherLineInfo.lastModifiedDate == null)
- || (lastModifiedDate.equals(otherLineInfo.lastModifiedDate)));
+ && Objects.equals(lastModifiedDate, otherLineInfo.lastModifiedDate)
+ && isFullCredit == otherLineInfo.isFullCredit;
}
}
-
diff --git a/src/main/java/reposense/git/GitBlame.java b/src/main/java/reposense/git/GitBlame.java
index e6f129364c..5f238c18b9 100644
--- a/src/main/java/reposense/git/GitBlame.java
+++ b/src/main/java/reposense/git/GitBlame.java
@@ -6,6 +6,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
+import reposense.git.model.GitBlameLineInfo;
import reposense.util.StringsUtil;
/**
@@ -20,8 +21,15 @@ public class GitBlame {
private static final String AUTHOR_EMAIL_REGEX = "(^author-mail .*)";
private static final String AUTHOR_TIME_REGEX = "(^author-time [0-9]+)";
private static final String AUTHOR_TIMEZONE_REGEX = "(^author-tz .*)";
+ private static final String COMMIT_TIME_REGEX = "(^committer-time .*)";
private static final String COMBINATION_REGEX = COMMIT_HASH_REGEX + "|" + AUTHOR_NAME_REGEX + "|"
+ AUTHOR_EMAIL_REGEX + "|" + AUTHOR_TIME_REGEX + "|" + AUTHOR_TIMEZONE_REGEX;
+ private static final String COMBINATION_WITH_COMMIT_TIME_REGEX = COMBINATION_REGEX + "|" + COMMIT_TIME_REGEX;
+
+ private static final int AUTHOR_NAME_OFFSET = "author ".length();
+ private static final int AUTHOR_EMAIL_OFFSET = "author-mail ".length();
+ private static final int FULL_COMMIT_HASH_LENGTH = 40;
+ private static final int COMMIT_TIME_OFFSET = "committer-time ".length();
/**
* Returns the raw git blame result for the {@code fileDirectory}, performed at the {@code root} directory.
@@ -48,4 +56,33 @@ public static String blameWithPreviousAuthors(String root, String fileDirectory)
return StringsUtil.filterText(runCommand(rootPath, blameCommandWithFindingPreviousAuthors), COMBINATION_REGEX);
}
+
+ /**
+ * Returns the git blame result for {@code lineNumber} of {@code fileDirectory} at {@code commitHash}.
+ */
+ public static GitBlameLineInfo blameLine(String root, String commitHash, String fileDirectory, int lineNumber) {
+ Path rootPath = Paths.get(root);
+
+ String blameCommand = String.format("git blame -w --line-porcelain %s -L %d,+1 -- %s",
+ commitHash, lineNumber, fileDirectory);
+
+ String blameResult = StringsUtil.filterText(runCommand(rootPath, blameCommand),
+ COMBINATION_WITH_COMMIT_TIME_REGEX);
+
+ return processGitBlameResultLine(blameResult);
+ }
+
+ /**
+ * Returns the processed result of {@code blameResult}.
+ */
+ private static GitBlameLineInfo processGitBlameResultLine(String blameResult) {
+ String[] blameResultLines = StringsUtil.NEWLINE.split(blameResult);
+
+ String commitHash = blameResultLines[0].substring(0, FULL_COMMIT_HASH_LENGTH);
+ String authorName = blameResultLines[1].substring(AUTHOR_NAME_OFFSET);
+ String authorEmail = blameResultLines[2].substring(AUTHOR_EMAIL_OFFSET).replaceAll("[<>]", "");
+ long timestampMilliseconds = Long.parseLong(blameResultLines[5].substring(COMMIT_TIME_OFFSET));
+
+ return new GitBlameLineInfo(commitHash, authorName, authorEmail, timestampMilliseconds);
+ }
}
diff --git a/src/main/java/reposense/git/GitDiff.java b/src/main/java/reposense/git/GitDiff.java
index 20119281dd..beb2be2a1d 100644
--- a/src/main/java/reposense/git/GitDiff.java
+++ b/src/main/java/reposense/git/GitDiff.java
@@ -37,4 +37,12 @@ public static List getModifiedFilesList(Path repoRoot) {
String diffResult = runCommand(repoRoot.toAbsolutePath(), diffCommand);
return Arrays.asList(StringsUtil.NEWLINE.split(diffResult));
}
+
+ /**
+ * Returns the git diff result of {@code currentCommitHash} compared to {@code baseCommitHash}.
+ */
+ public static String diffCommits(String root, String baseCommitHash, String currentCommitHash) {
+ Path rootPath = Paths.get(root);
+ return runCommand(rootPath, String.format("git diff %s...%s", baseCommitHash, currentCommitHash));
+ }
}
diff --git a/src/main/java/reposense/git/GitLog.java b/src/main/java/reposense/git/GitLog.java
index 1da51148b2..b0db85001c 100644
--- a/src/main/java/reposense/git/GitLog.java
+++ b/src/main/java/reposense/git/GitLog.java
@@ -77,4 +77,16 @@ public static List getFileAuthors(RepoConfiguration config, String fil
: authorAndEmailArray)
.collect(Collectors.toList());
}
+
+ /**
+ * Returns the git log result containing the parents of {@code commitHash}.
+ */
+ public static String getParentCommits(String root, String commitHash) {
+ Path rootPath = Paths.get(root);
+
+ String command = "git log --pretty=%P -1 ";
+ command += commitHash;
+
+ return runCommand(rootPath, command).trim();
+ }
}
diff --git a/src/main/java/reposense/git/model/GitBlameLineInfo.java b/src/main/java/reposense/git/model/GitBlameLineInfo.java
new file mode 100644
index 0000000000..2434215eae
--- /dev/null
+++ b/src/main/java/reposense/git/model/GitBlameLineInfo.java
@@ -0,0 +1,51 @@
+package reposense.git.model;
+
+/**
+ * Stores the git blame info of a single line.
+ */
+public class GitBlameLineInfo {
+ private final String commitHash;
+ private final String authorName;
+ private final String authorEmail;
+ private final long timestampMilliseconds;
+
+ public GitBlameLineInfo(String commitHash, String authorName, String authorEmail, long timestampMilliseconds) {
+ this.commitHash = commitHash;
+ this.authorName = authorName;
+ this.authorEmail = authorEmail;
+ this.timestampMilliseconds = timestampMilliseconds;
+ }
+
+ public String getCommitHash() {
+ return commitHash;
+ }
+
+ public String getAuthorName() {
+ return authorName;
+ }
+
+ public String getAuthorEmail() {
+ return authorEmail;
+ }
+
+ public long getTimestampMilliseconds() {
+ return timestampMilliseconds;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (!(other instanceof GitBlameLineInfo)) {
+ return false;
+ }
+
+ GitBlameLineInfo otherLineInfo = (GitBlameLineInfo) other;
+ return commitHash.equals(otherLineInfo.commitHash)
+ && authorName.equals(otherLineInfo.authorName)
+ && authorEmail.equals(otherLineInfo.authorEmail)
+ && timestampMilliseconds == otherLineInfo.timestampMilliseconds;
+ }
+}
diff --git a/src/main/java/reposense/model/CliArguments.java b/src/main/java/reposense/model/CliArguments.java
index 0b70dd2b0e..d1b3564002 100644
--- a/src/main/java/reposense/model/CliArguments.java
+++ b/src/main/java/reposense/model/CliArguments.java
@@ -35,6 +35,8 @@ public class CliArguments {
private int numAnalysisThreads;
private ZoneId zoneId;
private boolean isFindingPreviousAuthorsPerformed;
+ private boolean isAuthorshipAnalyzed;
+ private double originalityThreshold;
private boolean isTestMode = ArgsParser.DEFAULT_IS_TEST_MODE;
private boolean isFreshClonePerformed = ArgsParser.DEFAULT_SHOULD_FRESH_CLONE;
@@ -163,6 +165,14 @@ public boolean isViewModeOnly() {
return isViewModeOnly;
}
+ public boolean isAuthorshipAnalyzed() {
+ return isAuthorshipAnalyzed;
+ }
+
+ public double getOriginalityThreshold() {
+ return originalityThreshold;
+ }
+
@Override
public boolean equals(Object other) {
// short circuit if same object
@@ -200,7 +210,9 @@ public boolean equals(Object other) {
&& Objects.equals(this.repoConfigFilePath, otherCliArguments.repoConfigFilePath)
&& Objects.equals(this.authorConfigFilePath, otherCliArguments.authorConfigFilePath)
&& Objects.equals(this.groupConfigFilePath, otherCliArguments.groupConfigFilePath)
- && Objects.equals(this.reportConfigFilePath, otherCliArguments.reportConfigFilePath);
+ && Objects.equals(this.reportConfigFilePath, otherCliArguments.reportConfigFilePath)
+ && this.isAuthorshipAnalyzed == otherCliArguments.isAuthorshipAnalyzed
+ && Objects.equals(this.originalityThreshold, otherCliArguments.originalityThreshold);
}
/**
@@ -432,8 +444,8 @@ public Builder reportDirectoryPath(Path reportDirectoryPath) {
*/
public Builder configFolderPath(Path configFolderPath) {
this.cliArguments.configFolderPath = configFolderPath.equals(EMPTY_PATH)
- ? configFolderPath.toAbsolutePath()
- : configFolderPath;
+ ? configFolderPath.toAbsolutePath()
+ : configFolderPath;
this.cliArguments.repoConfigFilePath = configFolderPath.resolve(
RepoConfigCsvParser.REPO_CONFIG_FILENAME);
this.cliArguments.authorConfigFilePath = configFolderPath.resolve(
@@ -455,6 +467,26 @@ public Builder reportConfiguration(ReportConfiguration reportConfiguration) {
return this;
}
+ /**
+ * Adds the {@code isAuthorshipAnalyzed} to CliArguments.
+ *
+ * @param isAuthorshipAnalyzed Is authorship analyzed.
+ */
+ public Builder isAuthorshipAnalyzed(boolean isAuthorshipAnalyzed) {
+ this.cliArguments.isAuthorshipAnalyzed = isAuthorshipAnalyzed;
+ return this;
+ }
+
+ /**
+ * Adds the {@code originalityThreshold} to CliArguments.
+ *
+ * @param originalityThreshold the originality threshold.
+ */
+ public Builder originalityThreshold(double originalityThreshold) {
+ this.cliArguments.originalityThreshold = originalityThreshold;
+ return this;
+ }
+
/**
* Builds CliArguments.
*
diff --git a/src/main/java/reposense/parser/ArgsParser.java b/src/main/java/reposense/parser/ArgsParser.java
index 245be1a6a3..35d6afc3a4 100644
--- a/src/main/java/reposense/parser/ArgsParser.java
+++ b/src/main/java/reposense/parser/ArgsParser.java
@@ -52,6 +52,7 @@ public class ArgsParser {
public static final int DEFAULT_NUM_ANALYSIS_THREADS = Runtime.getRuntime().availableProcessors();
public static final boolean DEFAULT_IS_TEST_MODE = false;
public static final boolean DEFAULT_SHOULD_FRESH_CLONE = false;
+ public static final double DEFAULT_ORIGINALITY_THRESHOLD = 0.51;
public static final String[] HELP_FLAGS = new String[] {"--help", "-h"};
public static final String[] CONFIG_FLAGS = new String[] {"--config", "-c"};
@@ -70,12 +71,12 @@ public class ArgsParser {
public static final String[] VERSION_FLAGS = new String[] {"--version", "-V"};
public static final String[] LAST_MODIFIED_DATE_FLAGS = new String[] {"--last-modified-date", "-l"};
public static final String[] FIND_PREVIOUS_AUTHORS_FLAGS = new String[] {"--find-previous-authors", "-F"};
-
public static final String[] CLONING_THREADS_FLAG = new String[] {"--cloning-threads"};
public static final String[] ANALYSIS_THREADS_FLAG = new String[] {"--analysis-threads"};
-
public static final String[] TEST_MODE_FLAG = new String[] {"--test-mode"};
public static final String[] FRESH_CLONING_FLAG = new String[] {"--fresh-cloning"};
+ public static final String[] ANALYZE_AUTHORSHIP_FLAGS = new String[] {"--analyze-authorship", "-A"};
+ public static final String[] ORIGINALITY_THRESHOLD_FLAGS = new String[] {"--originality-threshold", "-ot"};
private static final Logger logger = LogsManager.getLogger(ArgsParser.class);
@@ -213,6 +214,22 @@ private static ArgumentParser getArgumentParser() {
+ "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)");
+ parser.addArgument(ANALYZE_AUTHORSHIP_FLAGS)
+ .dest(ANALYZE_AUTHORSHIP_FLAGS[0])
+ .action(Arguments.storeTrue())
+ .help("Performs further analysis to distinguish between partial and full credit attribution for "
+ + "lines of code assigned to the author. A darker background colour represents full credit, "
+ + "while a lighter background colour represents partial credit.");
+
+ parser.addArgument(ORIGINALITY_THRESHOLD_FLAGS)
+ .dest(ORIGINALITY_THRESHOLD_FLAGS[0])
+ .metavar("(0.0 ~ 1.0)")
+ .type(new OriginalityThresholdArgumentType())
+ .setDefault(DEFAULT_ORIGINALITY_THRESHOLD)
+ .help("Specifies the cut-off point for partial and full credit when further analysis of authorship "
+ + "is performed. Author will be given full credit if their contribution exceeds this "
+ + "threshold, else partial credit is given.");
+
// Mutex flags - these will always be the last parameters in help message.
mutexParser.addArgument(CONFIG_FLAGS)
.dest(CONFIG_FLAGS[0])
@@ -298,6 +315,8 @@ public static CliArguments parse(String[] args) throws HelpScreenException, Pars
boolean shouldPerformShallowCloning = results.get(SHALLOW_CLONING_FLAGS[0]);
boolean shouldFindPreviousAuthors = results.get(FIND_PREVIOUS_AUTHORS_FLAGS[0]);
boolean isTestMode = results.get(TEST_MODE_FLAG[0]);
+ boolean isAuthorshipAnalyzed = results.get(ANALYZE_AUTHORSHIP_FLAGS[0]);
+ double originalityThreshold = results.get(ORIGINALITY_THRESHOLD_FLAGS[0]);
int numCloningThreads = results.get(CLONING_THREADS_FLAG[0]);
int numAnalysisThreads = results.get(ANALYSIS_THREADS_FLAG[0]);
@@ -316,7 +335,9 @@ public static CliArguments parse(String[] args) throws HelpScreenException, Pars
.isFindingPreviousAuthorsPerformed(shouldFindPreviousAuthors)
.numCloningThreads(numCloningThreads)
.numAnalysisThreads(numAnalysisThreads)
- .isTestMode(isTestMode);
+ .isTestMode(isTestMode)
+ .isAuthorshipAnalyzed(isAuthorshipAnalyzed)
+ .originalityThreshold(originalityThreshold);
LogsManager.setLogFolderLocation(outputFolderPath);
@@ -339,7 +360,6 @@ public static CliArguments parse(String[] args) throws HelpScreenException, Pars
}
cliArgumentsBuilder.isAutomaticallyLaunching(isAutomaticallyLaunching);
-
boolean shouldPerformFreshCloning = isTestMode
? results.get(FRESH_CLONING_FLAG[0])
: DEFAULT_SHOULD_FRESH_CLONE;
diff --git a/src/main/java/reposense/parser/OriginalityThresholdArgumentType.java b/src/main/java/reposense/parser/OriginalityThresholdArgumentType.java
new file mode 100644
index 0000000000..a94c6713a0
--- /dev/null
+++ b/src/main/java/reposense/parser/OriginalityThresholdArgumentType.java
@@ -0,0 +1,25 @@
+package reposense.parser;
+
+import net.sourceforge.argparse4j.inf.Argument;
+import net.sourceforge.argparse4j.inf.ArgumentParser;
+import net.sourceforge.argparse4j.inf.ArgumentParserException;
+import net.sourceforge.argparse4j.inf.ArgumentType;
+
+/**
+ * Verifies and parses a string-formatted double, between 0.0 and 1.0, to an {@link Double} object.
+ */
+public class OriginalityThresholdArgumentType implements ArgumentType {
+ private static final String PARSE_EXCEPTION_MESSAGE_THRESHOLD_OUT_OF_BOUND =
+ "Invalid threshold. It must be a number between 0.0 and 1.0.";
+
+ @Override
+ public Double convert(ArgumentParser parser, Argument arg, String value) throws ArgumentParserException {
+ double threshold = Double.parseDouble(value);
+
+ if (Double.compare(threshold, 0.0) < 0 || Double.compare(threshold, 1.0) > 0) {
+ throw new ArgumentParserException(PARSE_EXCEPTION_MESSAGE_THRESHOLD_OUT_OF_BOUND, parser);
+ }
+
+ return threshold;
+ }
+}
diff --git a/src/main/java/reposense/report/ReportGenerator.java b/src/main/java/reposense/report/ReportGenerator.java
index 39d87a343e..fd1f9744bd 100644
--- a/src/main/java/reposense/report/ReportGenerator.java
+++ b/src/main/java/reposense/report/ReportGenerator.java
@@ -110,14 +110,16 @@ public class ReportGenerator {
* @param reportGenerationTimeProvider Supplier for time taken to generate the report.
* @param zoneId The timezone to adjust all date-times to.
* @param shouldFreshClone The boolean variable for whether to clone a repo again during tests.
+ * @param shouldAnalyzeAuthorship The boolean variable for whether to further analyze authorship.
+ * @param originalityThreshold The double variable for originality threshold in analyze authorship.
* @return the list of file paths that were generated.
- * @throws IOException if templateZip.zip does not exists in jar file.
+ * @throws IOException if templateZip.zip does not exist 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 {
+ boolean shouldFreshClone, boolean shouldAnalyzeAuthorship, double originalityThreshold) throws IOException {
prepareTemplateFile(outputPath);
if (Files.exists(Paths.get(assetsPath))) {
FileUtil.copyDirectoryContents(assetsPath, outputPath, assetsFilesWhiteList);
@@ -126,8 +128,8 @@ public List generateReposReport(List configs, String ou
earliestSinceDate = null;
progressTracker = new ProgressTracker(configs.size());
- List reportFoldersAndFiles = cloneAndAnalyzeRepos(configs, outputPath,
- numCloningThreads, numAnalysisThreads, shouldFreshClone);
+ List reportFoldersAndFiles = cloneAndAnalyzeRepos(configs, outputPath, numCloningThreads,
+ numAnalysisThreads, shouldFreshClone, shouldAnalyzeAuthorship, originalityThreshold);
LocalDateTime reportSinceDate = (TimeUtil.isEqualToArbitraryFirstDateConverted(cliSinceDate, zoneId))
? earliestSinceDate : cliSinceDate;
@@ -136,7 +138,7 @@ public List generateReposReport(List configs, String ou
new SummaryJson(configs, reportConfig, generationDate,
reportSinceDate, untilDate, isSinceDateProvided,
isUntilDateProvided, RepoSense.getVersion(), ErrorSummary.getInstance().getErrorSet(),
- reportGenerationTimeProvider.get(), zoneId),
+ reportGenerationTimeProvider.get(), zoneId, shouldAnalyzeAuthorship),
getSummaryResultPath(outputPath));
summaryPath.ifPresent(reportFoldersAndFiles::add);
@@ -185,11 +187,14 @@ private Map> groupConfigsByRepoLocation(
* To turn off multi-threading, run the program with the flags
* {@code --cloning-threads 1 --analysis-threads 1}.
* For test environments, cloning is skipped if it has been done before and {@code shouldFreshClone} is false.
+ * Further analyzes the authorship of each line in the commit if {@code shouldAnalyzeAuthorship} is true, based on
+ * {@code originalityThreshold}.
*
* @return A list of paths to the JSON report files generated for each repository.
*/
- private List cloneAndAnalyzeRepos(List configs, String outputPath,
- int numCloningThreads, int numAnalysisThreads, boolean shouldFreshClone) {
+ private List cloneAndAnalyzeRepos(List configs, String outputPath, int numCloningThreads,
+ int numAnalysisThreads, boolean shouldFreshClone, boolean shouldAnalyzeAuthorship,
+ double originalityThreshold) {
Map> repoLocationMap = groupConfigsByRepoLocation(configs);
List repoLocationList = new ArrayList<>(repoLocationMap.keySet());
@@ -212,7 +217,9 @@ private List cloneAndAnalyzeRepos(List configs, String
// Note that the `analyzeExecutor` is passed as a parameter to ensure that the number of threads used
// for analysis is no more than `numAnalysisThreads`.
CompletableFuture analyzeFuture = cloneFuture.thenApplyAsync(
- cloneJobOutput -> analyzeRepos(outputPath, configsToAnalyze, cloneJobOutput), analyzeExecutor);
+ cloneJobOutput -> analyzeRepos(outputPath, configsToAnalyze, cloneJobOutput,
+ shouldAnalyzeAuthorship, originalityThreshold),
+ analyzeExecutor);
analyzeJobFutures.add(analyzeFuture);
}
@@ -234,7 +241,7 @@ private List cloneAndAnalyzeRepos(List configs, String
List cloneFailLocations = jobOutputs
.stream()
.filter(jobOutput -> !jobOutput.isCloneSuccessful())
- .map(jobOutput -> jobOutput.getLocation())
+ .map(AnalyzeJobOutput::getLocation)
.collect(Collectors.toList());
cloneFailLocations.forEach(location -> handleCloningFailed(configs, location));
@@ -272,38 +279,41 @@ private CloneJobOutput cloneRepo(RepoConfiguration config, RepoLocation location
/**
* Analyzes all repos in {@code configsToAnalyze} and generates their report at {@code outputPath}.
* Uses {@code cloneJobOutput} to find repo location, default branch and whether cloning was successful.
+ * Further analyzes the authorship of each line in the commit if {@code shouldAnalyzeAuthorship} is true, based on
+ * {@code originalityThreshold}.
*
* @return An {@link AnalyzeJobOutput} object comprising the {@code location} of the repo, whether the cloning was
* successful, the list of {@code generatedFiles} by the analysis and a list of {@code analysisErrors} encountered.
*/
private AnalyzeJobOutput analyzeRepos(String outputPath, List configsToAnalyze,
- CloneJobOutput cloneJobOutput) {
+ CloneJobOutput cloneJobOutput, boolean shouldAnalyzeAuthorship, double originalityThreshold) {
RepoLocation location = cloneJobOutput.getLocation();
boolean cloneSuccessful = cloneJobOutput.isCloneSuccessful();
List generatedFiles = new ArrayList<>();
List analysisErrors = new ArrayList<>();
RepoCloner repoCloner = new RepoCloner();
+
if (!cloneSuccessful) {
repoCloner.cleanupRepo(configsToAnalyze.get(0));
return new AnalyzeJobOutput(location, cloneSuccessful, generatedFiles, analysisErrors);
}
- Iterator itr = configsToAnalyze.iterator();
- while (itr.hasNext()) {
+
+ for (RepoConfiguration configToAnalyze : configsToAnalyze) {
progressTracker.incrementProgress();
- RepoConfiguration configToAnalyze = itr.next();
configToAnalyze.updateBranch(cloneJobOutput.getDefaultBranch());
Path repoReportDirectory = Paths.get(outputPath, configToAnalyze.getOutputFolderName());
- logger.info(
- String.format(progressTracker.getProgress() + " "
- + MESSAGE_START_ANALYSIS, configToAnalyze.getLocation(), configToAnalyze.getBranch()));
+ logger.info(String.format(progressTracker.getProgress() + " " + MESSAGE_START_ANALYSIS,
+ configToAnalyze.getLocation(), configToAnalyze.getBranch()));
+
try {
GitRevParse.assertBranchExists(configToAnalyze, FileUtil.getBareRepoPath(configToAnalyze));
GitClone.cloneFromBareAndUpdateBranch(Paths.get("."), configToAnalyze);
FileUtil.createDirectory(repoReportDirectory);
- generatedFiles.addAll(analyzeRepo(configToAnalyze, repoReportDirectory.toString()));
+ generatedFiles.addAll(analyzeRepo(configToAnalyze, repoReportDirectory.toString(),
+ shouldAnalyzeAuthorship, originalityThreshold));
} catch (IOException ioe) {
String logMessage = String.format(MESSAGE_ERROR_CREATING_DIRECTORY,
configToAnalyze.getLocation(), configToAnalyze.getBranch());
@@ -329,18 +339,21 @@ private AnalyzeJobOutput analyzeRepos(String outputPath, List
String.format(LOG_UNEXPECTED_ERROR, configToAnalyze.getLocation(), sw.toString())));
}
}
+
repoCloner.cleanupRepo(configsToAnalyze.get(0));
return new AnalyzeJobOutput(location, cloneSuccessful, generatedFiles, analysisErrors);
}
/**
* Analyzes repo specified by {@code config} and generates the report at {@code repoReportDirectory}.
+ * Further analyzes the authorship of each line in the commit if {@code shouldAnalyzeAuthorship} is true, based on
+ * {@code originalityThreshold}.
*
* @return A list of paths to the JSON report files generated for the repo specified by {@code config}.
* @throws NoAuthorsWithCommitsFoundException if there are no authors with commits found for the repo.
*/
- private List analyzeRepo(RepoConfiguration config, String repoReportDirectory)
- throws NoAuthorsWithCommitsFoundException {
+ private List analyzeRepo(RepoConfiguration config, String repoReportDirectory,
+ boolean shouldAnalyzeAuthorship, double originalityThreshold) throws NoAuthorsWithCommitsFoundException {
// preprocess the config and repo
updateRepoConfig(config);
updateAuthorList(config);
@@ -351,7 +364,8 @@ private List analyzeRepo(RepoConfiguration config, String repoReportDirect
}
AuthorshipReporter authorshipReporter = new AuthorshipReporter();
- AuthorshipSummary authorshipSummary = authorshipReporter.generateAuthorshipSummary(config);
+ AuthorshipSummary authorshipSummary = authorshipReporter.generateAuthorshipSummary(config,
+ shouldAnalyzeAuthorship, originalityThreshold);
CommitsReporter commitsReporter = new CommitsReporter();
CommitContributionSummary commitSummary = commitsReporter.generateCommitSummary(config);
diff --git a/src/main/java/reposense/report/SummaryJson.java b/src/main/java/reposense/report/SummaryJson.java
index 4d264f938f..f709fb0998 100644
--- a/src/main/java/reposense/report/SummaryJson.java
+++ b/src/main/java/reposense/report/SummaryJson.java
@@ -28,10 +28,12 @@ public class SummaryJson {
private final boolean isSinceDateProvided;
private final boolean isUntilDateProvided;
private final Map> supportedDomainUrlMap;
+ private final boolean isAuthorshipAnalyzed;
public SummaryJson(List repos, ReportConfiguration reportConfig, String reportGeneratedTime,
LocalDateTime sinceDate, LocalDateTime untilDate, boolean isSinceDateProvided, boolean isUntilDateProvided,
- String repoSenseVersion, Set