Skip to content

Instantly share code, notes, and snippets.

@bmvakili
Last active August 29, 2015 14:26
Show Gist options
  • Save bmvakili/1d8298387e12a2354a7e to your computer and use it in GitHub Desktop.
Save bmvakili/1d8298387e12a2354a7e to your computer and use it in GitHub Desktop.
A utility for some code reviews
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.AllPermission;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Utility to help some code reviews
* What?
* Utility to help some code reviews; specifically, to review code in a
* topic branch that may have been merged with other branches; though
* needs a code review in it's entirety.
*
* Why?
* There is no straightforward command for code reviewing work in a
* topic branch that may have been merged with other branches
* several times and try to only review the code that was committed
* as part of that branch.
* A simple {@code git diff} would also include changes merged in
* from other branches whereas this utility allows code reviewer
* to ignore all of those files and only focus on files that
* the committer has touched as part of the commits in the
* topic branch.
*
* How?
* The method that summarizes the entire logic is as follows
* {@link #createNewReviewBranchBasedOnBranchAndCommitAndFiles(String, String, String, String)}
* This utility uses the local git repository's references log to
* locate local commits to a topic branch; then it creates a new
* branch and checks out only files changed by the user to the
* new branch for review.
*
* Note others cannot run this utility for the developer; because
* the reflogs for that branch will only exists on the developer's
* local repository. As such this tool needs to be run by local
* developer to aid in the code review process.
*
* Note this utility depends on the configuration property {@code gc.reflogExpire}
* By default it is set to 90 days.
* http://git-scm.com/docs/git-gc
*
* Note this utility has been tested on projects using {@code git merge}
* to merge; and has not been tested on projects using {@code git rebase}
* The effect of the latter is that the utility will ignore commits brought
* in to the topic branch using git rebase since the reflog information
* is not preserved during rebase operation; instead shows as {@code %H@HEAD..}
*
* TODO: create script to fetch this code using bat and sh files and
* run it with one simple command assuming javac is on classpath.
* @author bmvakili
*
*/
public class GitCodeReviewUtility {
private static String gitPathValue = "/opt/git/incomplete/newmerge";
/**
*
* @param args this is not used though note the following variables
* startCommit - how far back do you want to go back for the
* code review? Put the value of the commit here.
* branchName - which topic branch do you want to use for the
* code review? Put name of the branch here.
* pathToGitWorkingDirectory - where is your git directory? Put
* the value in the gitPathValue
* attribute.
*/
public static void main(String[] args) {
String startCommit = "91a1e01";
String branchName = "SIH_1";
String pathToGitWorkingDirectory = getGitPathValue();
String diffFiles = getStringOfFilesModifiedInBranchSinceCommit(
startCommit, branchName);
createNewReviewBranchBasedOnBranchAndCommitAndFiles(startCommit, branchName, diffFiles, pathToGitWorkingDirectory);
}
private static void createNewReviewBranchBasedOnBranchAndCommitAndFiles(
String startCommit, String branchName, String diffFiles, String path) {
String reviewBranchName = branchName + REVIEW_BRANCH_SUFFIX;
checkoutBranchAndResetHard(startCommit, reviewBranchName);
checkoutFilesFromBranch(branchName, diffFiles);
commitFilesToCurrentBranchForReview(startCommit, branchName, diffFiles);
resetHard();
}
private static void resetHard() {
String gitResetHardCommand = getGitResetHardCommand();
runCommand(gitResetHardCommand);
}
private static String getGitResetHardCommand() {
String gitResetHardCommand = getBaseGitCommand();
gitResetHardCommand += addCommandArgumentAndSpace(RESET);
gitResetHardCommand += addCommandArgumentAndSpace(GIT_HARD);
return gitResetHardCommand;
}
private static void commitFilesToCurrentBranchForReview(String commit, String branch, String diffFiles) {
String gitAddAllCurrentFilesCommit = getAddAllCurrentFilesCommandForPath(diffFiles);
runCommand(gitAddAllCurrentFilesCommit);
String commitMessage = getCodeReviewCommitMessage(commit, branch);
String gitCommitCommand = getCommitCommand(commitMessage);
runCommand(gitCommitCommand);
}
private static String getCodeReviewCommitMessage(String commit,
String branch) {
String commitMessage = REVIEW_COMMIT_MESSAGE + SPACE + commit + GIT_DOUBLEDOT + branch;
return commitMessage;
}
private static String getCommitCommand(String commitMessage) {
String gitCommitCommand = getBaseGitCommand();
gitCommitCommand += addCommandArgumentAndSpace(COMMIT);
gitCommitCommand += addCommandArgumentAndSpace(GIT_MESSAGE_FLAG);
gitCommitCommand += QUOTE;
gitCommitCommand += commitMessage;
gitCommitCommand += QUOTE;
return gitCommitCommand;
}
private static String getAddAllCurrentFilesCommandForPath(String diffFiles) {
String gitAddAllCurrentFiles = getBaseGitCommand();
gitAddAllCurrentFiles += addCommandArgumentAndSpace(ADD);
gitAddAllCurrentFiles += addCommandArgumentAndSpace(GIT_ALL_FLAG);
gitAddAllCurrentFiles += addCommandArgumentAndSpace(DASHDASH);
gitAddAllCurrentFiles += addCommandArgumentAndSpace(diffFiles);
return gitAddAllCurrentFiles;
}
private static void checkoutFilesFromBranch(String branchName,
String diffFiles) {
String gitCheckoutFilesCommand = getCheckoutFilesCommand(branchName, diffFiles);
runCommand(gitCheckoutFilesCommand);
}
private static String getCheckoutFilesCommand(String branchName,
String diffFiles) {
String gitCheckoutCommand = getCheckoutCommand(branchName);
gitCheckoutCommand += addCommandArgumentAndSpace(DASHDASH);
gitCheckoutCommand += addCommandArgumentAndSpace(diffFiles);
return gitCheckoutCommand;
}
private static void checkoutBranchAndResetHard(String startCommit,
String branchName) {
String gitCheckoutCommand = getCheckoutCommand(branchName);
runCommand(gitCheckoutCommand);
String gitResetHardCommand = getResetHardCommand(startCommit);
runCommand(gitResetHardCommand);
}
private static String getResetHardCommand(String startCommit) {
String gitCommand = getBaseGitCommand();
gitCommand += addCommandArgumentAndSpace(RESET);
gitCommand += addCommandArgumentAndSpace(startCommit);
return gitCommand;
}
private static String getCheckoutCommand(String branchName) {
String gitCommand = getBaseGitCommand();
gitCommand += addCommandArgumentAndSpace(CHECKOUT);
gitCommand += addCommandArgumentAndSpace(GIT_NEW_BRANCH_FLAG);
gitCommand += addCommandArgumentAndSpace(branchName);
return gitCommand;
}
private static String getStringOfFilesModifiedInBranchSinceCommit(
String startCommit, String branchName) {
String[] files = getFilesModifiedInBranchSinceCommit(startCommit, branchName);
String diffFiles = String.join(SPACE, files);
return diffFiles;
}
private static String getDiffCommandForFiles(String startCommit,
String branchName, String diffFiles) {
String gitCommand = getDiffCommand(startCommit, branchName);
gitCommand += addCommandArgumentAndSpace(DASHDASH);
gitCommand += addCommandArgumentAndSpace(diffFiles);
return gitCommand;
}
private static String getDiffCommand(String startCommit, String branchName) {
String gitCommand = getBaseGitCommand();
gitCommand += addCommandArgumentAndSpace(DIFF);
gitCommand += addCommandArgumentAndSpace(startCommit);
gitCommand += addCommandArgumentAndSpace(branchName);
return gitCommand;
}
private static String[] getFilesModifiedInBranchSinceCommit(
String startCommit, String branchName) {
String[] revs = getRevList(startCommit, branchName);
String[] branchCommits = getCommitsForBranchFromReflog(revs, branchName);
return getFilesForCommits(branchCommits);
}
private static String[] getFilesForCommits(String[] branchCommits) {
Set<String> setOfFiles = getSetOfFilesForCommits(branchCommits);
String[] filesForCommits = new String[setOfFiles.size()];
setOfFiles.toArray(filesForCommits);
return filesForCommits;
}
private static Set<String> getSetOfFilesForCommits(String[] branchCommits) {
String gitDiffTreeBaseCommand = getDiffTreeCommand();
Set<String> setOfFiles = new HashSet<String>();
setOfFiles = getAllFilesForCommitsAndBranch(branchCommits, gitDiffTreeBaseCommand);
return setOfFiles;
}
private static Set<String> getAllFilesForCommitsAndBranch(String[] branchCommits,
String gitDiffTreeBaseCommand) {
Set<String> setOfFiles = new HashSet<String>();
for (String branchCommit : branchCommits) {
String[] arrayOfFiles = getFilesForCommitsAndBranch(
gitDiffTreeBaseCommand, branchCommit);
setOfFiles.addAll(Arrays.asList(arrayOfFiles));
}
return setOfFiles;
}
private static String[] getFilesForCommitsAndBranch(
String gitDiffTreeBaseCommand, String branchCommit) {
String gitDiffTreeCommand = gitDiffTreeBaseCommand + branchCommit;
String[] commandOutputs = runCommand(gitDiffTreeCommand);
String files = commandOutputs[OUTPUT_INDEX];
String[] arrayOfFiles = splitStringByLines(files);
arrayOfFiles = wrapWithQuotes(arrayOfFiles);
return arrayOfFiles;
}
private static String[] wrapWithQuotes(String[] arrayOfFiles) {
for (int index = 0; index < arrayOfFiles.length; index++) {
arrayOfFiles[index] = QUOTE + arrayOfFiles[index] + QUOTE;
}
return arrayOfFiles;
}
private static String getDiffTreeCommand() {
String gitCommand = getBaseDiffTreeCommand();
gitCommand += addCommandArgumentAndSpace(GIT_NO_COMMIT_ID);
gitCommand += addCommandArgumentAndSpace(GIT_NAME_ONLY);
gitCommand += addCommandArgumentAndSpace(GIT_RECURSE_INTO_SUBTREES);
return gitCommand;
}
private static String addCommandArgumentAndSpace(String gitCommandArgument) {
return gitCommandArgument + SPACE;
}
private static String getBaseDiffTreeCommand() {
String gitCommand = getBaseGitCommand();
gitCommand += addCommandArgumentAndSpace(DIFF_TREE);
return gitCommand;
}
private static String[] getCommitsForBranchFromReflog(String[] revs,
String branchName) {
String gitCommand = getRefLogCommandForAllRefs();
String[] commandOutput = runCommand(gitCommand);
String[] referenceLogs = splitStringByLines(commandOutput[OUTPUT_INDEX]);
String[] branchSpecificCommits = getReferencesSpecificToBranch(
branchName, referenceLogs);
return branchSpecificCommits;
}
private static String[] splitStringByLines(String string) {
return string.split(NEWLINE_REGULAR_EXPRESSION);
}
private static String[] getReferencesSpecificToBranch(String branchName,
String[] referenceLogs) {
List<String> commits = addBranchSpecificCommits(branchName,
referenceLogs);
String[] branchSpecificCommits = new String[commits.size()];
branchSpecificCommits = commits.toArray(branchSpecificCommits);
return branchSpecificCommits;
}
private static List<String> addBranchSpecificCommits(String branchName,
String[] referenceLogs) {
List<String> commits = new ArrayList<String>();
for (String referenceLog : referenceLogs) {
String[] referenceLogLineArray = referenceLog.split(SPACE);
String reference = referenceLogLineArray[REFLINEARRAY_REF_INDEX];
if (reference.startsWith(branchName + AT_SYMBOL)) {
String commit = referenceLogLineArray[REFLINEARRAY_HASH_INDEX];
if(isNotMergeCommit(commit)) {
commits.add(commit);
}
}
}
return commits;
}
private static boolean isNotMergeCommit(String commit) {
String gitCommand = getBaseGitCommand();
gitCommand += addCommandArgumentAndSpace(SHOW);
gitCommand += addCommandArgumentAndSpace(GIT_SUMMARY);
gitCommand += GIT_FORMAT;
gitCommand += EQUALS;
gitCommand += addCommandArgumentAndSpace(GIT_FORMAT_PARENT_HASHES);
gitCommand += commit;
String[] commandOutputs = runCommand(gitCommand);
String parentsLine = commandOutputs[OUTPUT_INDEX];
boolean hasMultipleParents = false;
if (parentsLine.split(SPACE).length > 1) {
hasMultipleParents = true;
}
boolean isNotMerge = !hasMultipleParents;
return isNotMerge;
}
private static String getRefLogCommandForAllRefs() {
String gitCommand = getRefLogCommandShowAll();
gitCommand += getReflogFormatOptions();
return gitCommand;
}
private static String getReflogFormatOptions() {
String gitCommand = GIT_FORMAT;
gitCommand += EQUALS;
gitCommand += QUOTE;
gitCommand += GIT_REFLOG_FORMAT;
gitCommand += QUOTE;
return gitCommand;
}
private static String getRefLogCommandShowAll() {
String gitCommand = getRefLogCommandShow();
gitCommand += addCommandArgumentAndSpace(GIT_ALL);
return gitCommand;
}
private static String getRefLogCommandShow() {
String gitCommand = getRefLogCommand();
gitCommand += addCommandArgumentAndSpace(SHOW);
return gitCommand;
}
private static String getRefLogCommand() {
String gitCommand = getBaseGitCommand();
gitCommand += addCommandArgumentAndSpace(REFLOG);
return gitCommand;
}
private static String[] getRevList(String commitFrom, String commitTo) {
String gitCommand = getRevListCommand(commitFrom, commitTo);
String[] revsArray = runCommand(gitCommand);
return revsArray;
}
private static String getRevListCommand(String commitFrom, String commitTo) {
String gitCommand = getRevListCommand();
gitCommand += addCommandArgumentAndSpace(commitFrom);
gitCommand += commitTo;
return gitCommand;
}
private static String getRevListCommand() {
String gitCommand = getBaseGitCommand();
gitCommand += addCommandArgumentAndSpace(REV_LIST);
return gitCommand;
}
public static String getMergedCommitHashes() {
String gitCommand = getBranchMergesCommand();
String[] mergeCommits = runCommand(gitCommand);
return mergeCommits[OUTPUT_INDEX];
}
public static String getLogCommand() {
String gitCommand = getBaseGitCommand();
gitCommand += addCommandArgumentAndSpace(LOG);
return gitCommand;
}
public static String getBranchMergesCommand() {
String gitCommand = getLogCommand();
gitCommand += addCommandArgumentAndSpace(GIT_SHOW_ONLY_MERGES);
gitCommand += GIT_FORMAT;
gitCommand += EQUALS;
gitCommand += GIT_FORMAT_PARENT_HASHES;
return gitCommand;
}
public static String getBaseGitCommand() {
String gitCommand = addCommandArgumentAndSpace(GIT_COMMAND);
gitCommand += addCommandArgumentAndSpace(GIT_PATH);
gitCommand += addCommandArgumentAndSpace(GIT_PATH_VALUE);
return gitCommand;
}
public static String getShowBranchesCommand() {
String gitCommand = getBaseGitCommand();
gitCommand += BRANCH;
return gitCommand;
}
public static void printCommandResults(String gitCommand) {
String[] output = runCommand(gitCommand);
System.out.println(output[OUTPUT_INDEX]);
System.out.println(output[ERROR_INDEX]);
}
private static String[] runCommand(String gitCommand) {
String shellCommand = getShellCommand();
String shellCommandFlagCommand = getShellCommandFlagCommand();
String[] commands = {
shellCommand, shellCommandFlagCommand, gitCommand
};
return runCommands(commands);
}
private static String getShellCommandFlagCommand() {
if (File.separatorChar == UNIX_SEPARATOR_CHARACTER) {
return UNIX_COMMAND_FLAG;
}
return WINDOWS_COMMAND_FLAG;
}
private static String getShellCommand() {
if (File.separatorChar == '/') {
return UNIX_COMMAND;
}
return WINDOWS_COMMAND;
}
private static String[] runCommands(String[] commands) {
Runtime rt = Runtime.getRuntime();
Process proc = runCommand(commands, rt);
String[] retVal = readOutputErrorStreams(proc);
return retVal;
}
private static String[] readOutputErrorStreams(Process proc) {
String[] retVal = new String[2];
retVal[OUTPUT_INDEX] = readInputStream(proc.getInputStream());
retVal[ERROR_INDEX] = readInputStream(proc.getErrorStream());
return retVal;
}
private static String readInputStream(InputStream input) {
BufferedReader stdInput = new BufferedReader(new
InputStreamReader(input));
String content = readLines(stdInput);
return content;
}
private static String readLines(BufferedReader stdInput) {
String content = BLANK;
String s = null;
try {
while ((s = stdInput.readLine()) != null) {
content += s + UNIX_NEWLINE;
}
} catch (IOException e) {
e.printStackTrace();
}
return content;
}
private static String getGitPathValue() {
if (gitPathValue == null || gitPathValue.trim().isEmpty()) {
return GIT_PATH_VALUE;
} else {
return gitPathValue;
}
}
private static Process runCommand(String[] commands, Runtime rt) {
Process proc = null;
try {
proc = rt.exec(commands);
} catch (IOException e) {
e.printStackTrace();
}
return proc;
}
private static final String GIT_COMMAND = "git";
private static final String DASHDASH = "--";
private static final int OUTPUT_INDEX = 0;
private static final int ERROR_INDEX = 1;
private static final int REFLINEARRAY_HASH_INDEX = 0;
private static final int REFLINEARRAY_REF_INDEX = 1;
private static final String GIT_PATH = "-C";
private static final String GIT_PATH_VALUE = "/opt/git/incomplete/newmerge";
private static final String EQUALS = "=";
private static final String SPACE = " ";
private static final String BRANCH = "branch";
private static final String GIT_SHOW_ONLY_MERGES = "--merges";
private static final String SHOW = "show";
private static final String LOG = "log";
private static final String GIT_FORMAT = "--format";
private static final String GIT_FORMAT_PARENT_HASHES = "%P";
private static final String NEWLINE_REGULAR_EXPRESSION = "\\r?\\n";
private static final String REV_LIST = "rev-list";
private static final String REFLOG = "reflog";
private static final String GIT_ALL = "--all";
private static final String GIT_REFLOG_FORMAT = "%H %gd";
private static final String QUOTE = "\"";
private static final String AT_SYMBOL = "@";
private static final String DIFF_TREE = "diff-tree";
private static final String GIT_NO_COMMIT_ID = "--no-commit-id";
private static final String GIT_NAME_ONLY = "--name-only";
private static final String GIT_RECURSE_INTO_SUBTREES = "-r";
private static final String GIT_DOUBLEDOT = "..";
private static final String GIT_SUMMARY = "--summary";
private static final String GIT_HARD = "--hard";
private static final String DIFF = "diff";
private static final String CHECKOUT = "checkout";
private static final String GIT_NEW_BRANCH_FLAG = "-b";
private static final String RESET = "reset";
private static final String ADD = "add";
private static final String COMMIT = "commit";
private static final String REVIEW_BRANCH_SUFFIX = "_REVIEW_TMP";
private static final String REVIEW_COMMIT_MESSAGE = "Temporary commit for code review";
private static final String GIT_MESSAGE_FLAG = "-m";
private static final String GIT_ALL_FLAG = "-A";
private static final String UNIX_NEWLINE = "\n";
private static final String BLANK = "";
private static final String UNIX_COMMAND = "/bin/bash";
private static final String UNIX_COMMAND_FLAG = "-c";
private static final String WINDOWS_COMMAND = "bash.exe";
private static final String WINDOWS_COMMAND_FLAG = "-c";
private static final char UNIX_SEPARATOR_CHARACTER = '/';
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment