Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/ug/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ partial credit.

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

### `--author-dedup-mode`

**`--author-dedup-mode`**: Deduplicates authors based on the `author-config.csv` file, while preserving all commit authors.
* Default: this feature is turned **_off_** by default
* Example: `--author-dedup-mode`

<box type="info" seamless>

* Must be used in conjunction with the `--config` flag and requires an `author-config.csv` file to be present.
* When enabled, all commit authors will be included in the report while respecting the aliases configured in the `author-config.csv` file.
* Authors not found in the `author-config.csv` file will be added as per normal with their commit names.
</box>

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

### `--config`, `-c`

<div id="section-config">
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/reposense/RepoSense.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ public static void main(String[] args) {
cliArguments.isShallowCloningPerformed());
RepoConfiguration.setIsFindingPreviousAuthorsPerformedToRepoConfigs(configs,
cliArguments.isFindingPreviousAuthorsPerformed());
RepoConfiguration.setIsAuthorDedupModeToRepoConfigs(configs,
cliArguments.isAuthorDedupMode());

List<String[]> globalGitConfig = GitConfig.getGlobalGitLfsConfig();
if (globalGitConfig.size() != 0) {
Expand Down
18 changes: 17 additions & 1 deletion src/main/java/reposense/model/CliArguments.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public class CliArguments {
private boolean isPortfolio;
private boolean isFreshClonePerformed = ArgsParser.DEFAULT_SHOULD_FRESH_CLONE;
private boolean isOnlyTextRefreshed;
private boolean isAuthorDedupMode;

private List<String> locations;
private boolean isViewModeOnly;
Expand Down Expand Up @@ -210,6 +211,10 @@ public boolean isOnlyTextRefreshed() {
return isOnlyTextRefreshed;
}

public boolean isAuthorDedupMode() {
return isAuthorDedupMode;
}

@Override
public boolean equals(Object other) {
// short circuit if same object
Expand Down Expand Up @@ -253,7 +258,8 @@ public boolean equals(Object other) {
&& this.isAuthorshipAnalyzed == otherCliArguments.isAuthorshipAnalyzed
&& Objects.equals(this.originalityThreshold, otherCliArguments.originalityThreshold)
&& this.isPortfolio == otherCliArguments.isPortfolio
&& this.isOnlyTextRefreshed == otherCliArguments.isOnlyTextRefreshed;
&& this.isOnlyTextRefreshed == otherCliArguments.isOnlyTextRefreshed
&& this.isAuthorDedupMode == otherCliArguments.isAuthorDedupMode;
}

/**
Expand Down Expand Up @@ -552,6 +558,16 @@ public Builder isOnlyTextRefreshed(boolean isOnlyTextRefreshed) {
return this;
}

/**
* Adds the {@code isAuthorDedupMode} to CLIArguments.
*
* @param isAuthorDedupMode Is author dedup mode.
*/
public Builder isAuthorDedupMode(boolean isAuthorDedupMode) {
this.cliArguments.isAuthorDedupMode = isAuthorDedupMode;
return this;
}

/**
* Builds CliArguments.
*
Expand Down
44 changes: 36 additions & 8 deletions src/main/java/reposense/model/RepoConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class RepoConfiguration {
private transient boolean isLastModifiedDateIncluded;
private transient boolean isShallowCloningPerformed = false;
private transient boolean isFindingPreviousAuthorsPerformed = false;
private transient boolean isAuthorDedupMode = false;
private transient boolean isFormatsOverriding = false;
private transient boolean isIgnoreGlobListOverriding;
private transient boolean isIgnoreCommitListOverriding = false;
Expand Down Expand Up @@ -328,7 +329,19 @@ public Builder isShallowCloningPerformed(boolean isShallowCloningPerformed) {
}

/**
* Updates the {@code isFindingPreviousAuthorsPerformed} for {@code RepoConfiguration}.
* Updates the {@code isAuthorDedupMode} for {@code RepoConfiguration}.
*
* @param isAuthorDedupMode Checks if author dedup mode is enabled.
* @return This builder object.
*/
public Builder isAuthorDedupMode(boolean isAuthorDedupMode) {
this.repoConfiguration.isAuthorDedupMode = isAuthorDedupMode;
return this;
}

/**
* Updates the {@code isFindingPreviousAuthorsPerformed} for
* {@code RepoConfiguration}.
*
* @param isFindingPreviousAuthorsPerformed Checks if finding previous authors is performed.
* @return This builder object.
Expand Down Expand Up @@ -530,28 +543,35 @@ public static void setZoneIdToRepoConfigs(List<RepoConfiguration> configs, ZoneI
}

public static void setIsLastModifiedDateIncludedToRepoConfigs(List<RepoConfiguration> configs,
boolean isLastModifiedDateIncluded) {
boolean isLastModifiedDateIncluded) {
for (RepoConfiguration config : configs) {
config.setIsLastModifiedDateIncluded(isLastModifiedDateIncluded);
}
}

public static void setIsShallowCloningPerformedToRepoConfigs(List<RepoConfiguration> configs,
boolean isShallowCloningPerformed) {
boolean isShallowCloningPerformed) {
if (isShallowCloningPerformed) {
configs.stream().forEach(config -> config.setIsShallowCloningPerformed(true));
}
}

public static void setIsFindingPreviousAuthorsPerformedToRepoConfigs(List<RepoConfiguration> configs,
boolean isFindingPreviousAuthorsPerformed) {
boolean isFindingPreviousAuthorsPerformed) {
if (isFindingPreviousAuthorsPerformed) {
configs.stream().forEach(config -> config.setIsFindingPreviousAuthorsPerformed(true));
}
}

public static void setIsAuthorDedupModeToRepoConfigs(List<RepoConfiguration> configs,
boolean isAuthorDedupMode) {
if (isAuthorDedupMode) {
configs.stream().forEach(config -> config.setIsAuthorDedupMode(true));
}
}

public static void setHasAuthorConfigFileToRepoConfigs(List<RepoConfiguration> configs,
boolean setHasAuthorConfigFile) {
boolean setHasAuthorConfigFile) {
configs.stream().forEach(config -> config.setHasAuthorConfigFile(setHasAuthorConfigFile));
}

Expand All @@ -568,8 +588,8 @@ public static void merge(List<RepoConfiguration> repoConfigs, List<AuthorConfigu
continue;
}

List<RepoConfiguration> locationMatchingRepoConfigs =
getMatchingRepoConfigsByLocation(repoConfigs, authorConfig.getLocation());
List<RepoConfiguration> locationMatchingRepoConfigs = getMatchingRepoConfigsByLocation(repoConfigs,
authorConfig.getLocation());

if (locationMatchingRepoConfigs.isEmpty()) {
logger.warning(String.format(
Expand Down Expand Up @@ -678,7 +698,7 @@ public static void setStandaloneConfigIgnoredToRepoConfigs(List<RepoConfiguratio
* {@code ignoreFilesizeLimit} is true.
*/
public static void setFileSizeLimitIgnoredToRepoConfigs(List<RepoConfiguration> configs,
boolean ignoreFileSizeLimit) {
boolean ignoreFileSizeLimit) {
if (ignoreFileSizeLimit) {
configs.forEach(config -> config.setFileSizeLimitIgnored(true));
}
Expand Down Expand Up @@ -881,6 +901,10 @@ public void setIsFindingPreviousAuthorsPerformed(boolean isFindingPreviousAuthor
this.isFindingPreviousAuthorsPerformed = isFindingPreviousAuthorsPerformed;
}

public void setIsAuthorDedupMode(boolean isAuthorDedupMode) {
this.isAuthorDedupMode = isAuthorDedupMode;
}

public boolean isLastModifiedDateIncluded() {
return this.isLastModifiedDateIncluded;
}
Expand Down Expand Up @@ -1081,6 +1105,10 @@ public boolean isFindingPreviousAuthorsPerformed() {
return isFindingPreviousAuthorsPerformed;
}

public boolean isAuthorDedupMode() {
return isAuthorDedupMode;
}

public boolean isHasUpdatedSinceDateInConfig() {
return hasUpdatedSinceDateInConfig;
}
Expand Down
29 changes: 28 additions & 1 deletion src/main/java/reposense/parser/ArgsParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
Expand Down Expand Up @@ -79,6 +80,7 @@ public class ArgsParser {
public static final String[] ORIGINALITY_THRESHOLD_FLAGS = new String[] {"--originality-threshold", "-ot"};
public static final String[] PORTFOLIO_FLAG = new String[] {"--portfolio", "-P"};
public static final String[] REFRESH_ONLY_TEXT_FLAG = new String[] {"--text", "-T"};
public static final String[] AUTHOR_DEDUP_MODE_FLAGS = new String[] {"--author-dedup-mode"};

private static final Logger logger = LogsManager.getLogger(ArgsParser.class);

Expand All @@ -99,6 +101,10 @@ public class ArgsParser {
"\"Since Date\" cannot be later than \"Until Date\".";
private static final String MESSAGE_SINCE_DATE_LATER_THAN_TODAY_DATE =
"\"Since Date\" must not be later than today's date.";
private static final String MESSAGE_AUTHOR_DEDUP_MODE_WITHOUT_CONFIG =
"--author-dedup-mode flag is used without --config flag. The flag will be ignored.";
private static final String MESSAGE_AUTHOR_CONFIG_FILE_NOT_FOUND =
"--author-dedup-mode flag is used but author-config.csv file not found at %s. The flag will be ignored.";
private static final Path EMPTY_PATH = Paths.get("");
private static final Path DEFAULT_CONFIG_PATH = Paths.get(System.getProperty("user.dir")
+ File.separator + "config" + File.separator);
Expand Down Expand Up @@ -231,6 +237,11 @@ private static ArgumentParser getArgumentParser() {
.action(Arguments.storeTrue())
.help("Refreshes only the text content of the report, without analyzing the repositories again.");

parser.addArgument(AUTHOR_DEDUP_MODE_FLAGS)
.dest(AUTHOR_DEDUP_MODE_FLAGS[0])
.action(Arguments.storeTrue())
.help("Deduplicates authors based on the author-config file, while preserving all commit authors.");

// Mutex flags - these will always be the last parameters in help message.
mutexParser.addArgument(CONFIG_FLAGS)
.dest(CONFIG_FLAGS[0])
Expand Down Expand Up @@ -316,6 +327,7 @@ public static CliArguments parse(String[] args) throws HelpScreenException, Pars
int numAnalysisThreads = results.get(ANALYSIS_THREADS_FLAG[0]);
boolean shouldPerformFreshCloning = results.get(FRESH_CLONING_FLAG[0]);
boolean shouldRefreshOnlyText = results.get(REFRESH_ONLY_TEXT_FLAG[0]);
boolean isAuthorDedupMode = results.get(AUTHOR_DEDUP_MODE_FLAGS[0]);

CliArguments.Builder cliArgumentsBuilder = new CliArguments.Builder()
.configFolderPath(configFolderPath)
Expand All @@ -335,7 +347,8 @@ public static CliArguments parse(String[] args) throws HelpScreenException, Pars
.originalityThreshold(originalityThreshold)
.isPortfolio(isPortfolio)
.isFreshClonePerformed(shouldPerformFreshCloning)
.isOnlyTextRefreshed(shouldRefreshOnlyText);
.isOnlyTextRefreshed(shouldRefreshOnlyText)
.isAuthorDedupMode(isAuthorDedupMode);
Comment on lines 330 to +351

Copilot AI Feb 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no validation to ensure that the --author-dedup-mode flag is used correctly. According to the documentation, this flag "must be used in conjunction with the --config flag and requires an author-config.csv file to be present." However, the code doesn't enforce this requirement.

If a user specifies --author-dedup-mode with --repos (instead of --config), or if they use --author-dedup-mode with --config but no author-config.csv exists, the flag will be silently ignored without any warning or error message. This could lead to user confusion.

Consider adding validation in ArgsParser.parse() to check if isAuthorDedupMode is true, and if so, verify that:

  1. configFolderPath is not the default path (or that --config was explicitly provided)
  2. Optionally, check if the author-config.csv file exists and warn if it doesn't

Alternatively, you could add a warning log in ReportGenerator.updateAuthorList() when isAuthorDedupMode is true but the conditions aren't met.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yizhong187 agreed, and i think it should just be limited to a warning. meaning if dedup flag is used but config flag is absent or config csv is absent, you can consider displaying a warning saying the dedup flag will be ignored. giving an error might be too much

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!


LogsManager.setLogFolderLocation(outputFolderPath);

Expand All @@ -361,6 +374,20 @@ public static CliArguments parse(String[] args) throws HelpScreenException, Pars
}
cliArgumentsBuilder.isAutomaticallyLaunching(isAutomaticallyLaunching);

// Validate author-dedup-mode flag
if (isAuthorDedupMode) {
// Check if --config flag was explicitly provided
if (configFolderPath.equals(DEFAULT_CONFIG_PATH)) {
logger.warning(MESSAGE_AUTHOR_DEDUP_MODE_WITHOUT_CONFIG);
} else {
// Check if author-config.csv exists
Path authorConfigPath = configFolderPath.resolve(AuthorConfigCsvParser.AUTHOR_CONFIG_FILENAME);
if (!Files.exists(authorConfigPath)) {
logger.warning(String.format(MESSAGE_AUTHOR_CONFIG_FILE_NOT_FOUND, authorConfigPath));
}
}
}

return cliArgumentsBuilder.build();
}

Expand Down
21 changes: 21 additions & 0 deletions src/main/java/reposense/report/ReportGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,27 @@ private void updateAuthorList(RepoConfiguration config) throws NoAuthorsWithComm
}

config.setAuthorList(authorList);
} else if (config.isAuthorDedupMode() && config.getAuthorConfig().hasAuthorConfigFile()) {
// In dedup mode, add all commit authors to the config while keeping configured aliases
logger.info(String.format("Author dedup mode enabled. Including all commit authors while "
+ "preserving configured aliases for %s (%s).", config.getLocation(), config.getBranch()));
List<Author> authorList = GitShortlog.getAuthors(config);

if (authorList.isEmpty()) {
throw new NoAuthorsWithCommitsFoundException();
}

// Add all commit authors to the config, but skip those that match configured aliases
for (Author commitAuthor : authorList) {
String gitId = commitAuthor.getGitId();
Author configuredAuthor = config.getAuthorConfig()
.getAuthor(gitId, gitId);

if (configuredAuthor == Author.UNKNOWN_AUTHOR) {
// Not in configured authors/aliases, add as new author
config.addAuthor(commitAuthor);
}
}
}
config.removeIgnoredAuthors();
}
Expand Down
Loading
Loading