Skip to content

Commit

Permalink
[#944] Implement authorship analysis (#2140)
Browse files Browse the repository at this point in the history
A line is credited to the author who last modified it.

Another author might have written the line initially and the current
author only modified it slightly. In such a case, the current author
gets credited for work that is not entirely done by him/her.

Let's analyze how similar a line is as compared to its ancestor lines
(previous versions of the line) and give full or partial credit to the
last author based on the analysis.
  • Loading branch information
SkyBlaise99 committed Apr 28, 2024
1 parent bf78bf2 commit 29b9f5f
Show file tree
Hide file tree
Showing 66 changed files with 1,355 additions and 134 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 42 additions & 2 deletions docs/ug/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,36 @@ The command `java -jar RepoSense.jar` takes several flags.
**Examples**:

An example of a command using most parameters:<br>
`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:<br>
`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`
</box>

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`

<box type="info" seamless>

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.
</box>

<!-- ------------------------------------------------------------------------------------------------------ -->

### `--assets`, `-a`

<div id="section-assets">
Expand Down Expand Up @@ -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].<br>
Default: `0.51`
* Alias: `-ot`
* Example: `--originality-threshold 0.66` or `-ot 0.66`

<box type="info" seamless>

* 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.
</box>

<!-- ------------------------------------------------------------------------------------------------------ -->

### `--output`, `-o`

**`--output OUTPUT_DIRECTORY`**: Indicates where to save the report generated.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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);
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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)');
});
});
15 changes: 11 additions & 4 deletions frontend/src/components/c-segment.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<template lang="pug">
.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")
Expand All @@ -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}` }"
Expand Down Expand Up @@ -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: {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/styles/_colors.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/styles/hightlight-js-style.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

.hljs-section,
.hljs-name {
color: #63a35c;
color: #468C5A;
}

.hljs-tag {
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export interface Repo extends RepoRaw {

export interface AuthorshipFileSegment {
knownAuthor: string | null;
isFullCredit: boolean;
lineNumbers: number[];
lines: string[];
}
Expand All @@ -83,3 +84,9 @@ export interface Bar {
color?: string;
tooltipText?: string;
}

export interface SegmentState {
id: number;
author: string | null;
isFullCredit: boolean;
}
1 change: 1 addition & 0 deletions frontend/src/types/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions frontend/src/types/zod/authorship-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/types/zod/summary-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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,
});

Expand Down
1 change: 1 addition & 0 deletions frontend/src/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } = {};
Expand Down
Loading

0 comments on commit 29b9f5f

Please sign in to comment.