/* git-reports.c for creating git equivalent of cvs-reports. * * Copyright Galt Barber 2010. * * Anyone is free to use it. * Just include me in your credits. * Likewise cvs-reports was created by Mark Diekhans. */ #include "common.h" #include "options.h" #include "dystring.h" #include "errabort.h" #include "hash.h" #include "linefile.h" #include "htmshell.h" #include "portable.h" struct hash *userHash = NULL; struct slName *users = NULL; char *startTag = NULL; char *endTag = NULL; char *startDate = NULL; char *endDate = NULL; char *title = NULL; char *repoDir = NULL; char *outDir = NULL; char *outPrefix = NULL; int contextSize; char gitCmd[1024]; char *tempMakeDiffName = NULL; struct files { struct files *next; char type; char *path; int linesChanged; }; struct commit { struct commit *next; int commitNumber; // used for sorting fileviews char *commitId; char *merge; char *author; char *date; char *comment; struct files *files; }; struct comFile { struct comFile *next; struct files *f; struct commit *commit; }; void usage(char *msg) /* Explain usage and exit. */ { errAbort( "%s\n\n" "git-reports - produce source code reports useful for code-review on git repository \n" "\n" "Usage:\n" " git-reports startTag endTag startDate endDate title repoDir outDir outPrefix\n" "where " " startTag and endTag are repository tags marking the beginning and end of the git range\n" " startDate and endDate and title are just strings that get printed on the report\n" " title is usually the branch number, e.g. v225\n" " repoDir is where the git repository (use absolute path)\n" " outDir is the output directory (use absolute path).\n" " outPrefix is typically \"branch\" or \"review\" directory.\n" " --context=N - show N lines of context around a change\n" " --help - this help screen\n", msg); } static struct optionSpec options[] = { {"-context", OPTION_INT}, {"-help", OPTION_BOOLEAN}, {NULL, 0}, }; void runShell(char *cmd) /* Run a command and do simple error-checking */ { int exitCode = system(cmd); if (exitCode != 0) errAbort("system command [%s] failed with exitCode %d", cmd, exitCode); } void makeDiffAndSplit(struct commit *c, char *u, boolean full); // FOREWARD REFERENCE struct commit* getCommits() /* Get all commits from startTag to endTag */ { int numCommits = 0; safef(gitCmd,sizeof(gitCmd), "" "git log %s..%s --name-status > commits.tmp" , startTag, endTag); runShell(gitCmd); struct lineFile *lf = lineFileOpen("commits.tmp", TRUE); int lineSize; char *line; struct commit *commits = NULL, *commit = NULL; struct files *files = NULL, *f = NULL; char *sep = ""; while (lineFileNext(lf, &line, &lineSize)) { char *w = nextWord(&line); AllocVar(commit); if (!sameString("commit", w)) errAbort("expected keyword commit parsing commits.tmp\n"); commit->commitId = cloneString(nextWord(&line)); commit->commitNumber = ++numCommits; lineFileNext(lf, &line, &lineSize); w = nextWord(&line); if (sameString("Merge:", w)) { commit->merge = cloneString(line); lineFileNext(lf, &line, &lineSize); w = nextWord(&line); } if (!sameString("Author:", w)) errAbort("expected keyword Author: parsing commits.tmp\n"); /* by request, keep just the email account name */ char *lc = strchr(line, '<'); if (!lc) errAbort("expected '<' char in email address in Author: parsing commits.tmp\n"); ++lc; char *rc = strchr(lc, '>'); if (!rc) errAbort("expected '>' char in email address in Author: parsing commits.tmp\n"); char *ac = strchr(lc, '@'); if (ac) rc = ac; commit->author = cloneStringZ(lc, rc-lc); lineFileNext(lf, &line, &lineSize); w = nextWord(&line); if (!sameString("Date:", w)) errAbort("expected keyword Date: parsing commits.tmp\n"); commit->date = cloneString(line); lineFileNext(lf, &line, &lineSize); if (!sameString("", line)) errAbort("expected blank line parsing commits.tmp\n"); /* collect the comment-lines */ struct dyString *dy = NULL; dy = dyStringNew(0); sep = ""; files = NULL; while (lineFileNext(lf, &line, &lineSize)) { if (sameString("", line)) break; w = skipLeadingSpaces(line); dyStringPrintf(dy, "%s%s", w, sep); sep = "\n"; } commit->comment = cloneString(dy->string); freeDyString(&dy); if (commit->merge) { makeDiffAndSplit(commit, "getFileNamesForMergeCommit", FALSE); // special tricks to get this list (status field will not be applicable). } else { /* collect the files-list */ while (lineFileNext(lf, &line, &lineSize)) { if (sameString("", line)) break; AllocVar(f); w = nextWord(&line); f->type = w[0]; f->path = cloneString(line); slAddHead(&files, f); } slReverse(&files); commit->files = files; } if (!startsWith("Merge branch 'master' of", commit->comment) && !endsWith(commit->comment, "elease log update")) /* filter out automatic release log commits */ slAddHead(&commits, commit); verbose(2, "commitId: %s\n" "author: %s\n" "date: %s\n" "comment: [%s]\n" "file(s): \n" , commit->commitId , commit->author , commit->date , commit->comment); for (f=commit->files; f; f = f->next) { verbose(2, "%c %s\n", f->type, f->path); // anything other than M or A? if (f->type != 'M' && f->type != 'A' ) verbose(2, "special type: %c %s\n", f->type, f->path); } verbose(2, "------------\n"); } lineFileClose(&lf); /* We want to keep them chronological order, so do not need slReverse since the addHead reversed git log's rev chron order already */ unlink("commits.tmp"); return commits; } int makeHtml(char *diffPath, char *htmlPath, char *path, char *commitId) /* Make a color-coded html diff * Return the number of lines changed */ { int linesChanged = 0; FILE *h = mustOpen(htmlPath, "w"); struct lineFile *lf = lineFileOpen(diffPath, TRUE); int lineSize; char *line; char *xline = NULL; char fmtString[256]; boolean inBody = FALSE; boolean inBlock = TRUE; int blockP = 0, blockN = 0; fprintf(h, "\n\n%s %s\n\n\n
\n", path, commitId);
boolean hasMore = TRUE;
boolean combinedDiff = FALSE;
while (hasMore)
    {
    boolean checkEob = FALSE;
    hasMore = lineFileNext(lf, &line, &lineSize);
    if (hasMore)
	{
	char *color = NULL;
	xline = htmlEncode(line);	
	if ((line[0] == '-') || (combinedDiff && (line[1] == '-')))
	    {
	    color = "#FF9999";  /* deleted text light red */
	    if (inBody)
		{
		inBlock = TRUE;
		++blockN;
		}
	    }
	else if ((line[0] == '+') || (combinedDiff && (line[1] == '+')))
	    {
	    color = "#99FF99";  /* added text light green */
	    if (inBody)
		{
		inBlock = TRUE;
		++blockP;
		}
	    }
	else
	    {
	    if (line[0] == '@')
		{
		color = "#FFFF99";  /* diff control text light yellow (red+green) */
		if (!combinedDiff && startsWith("@@@", line))
		    combinedDiff = TRUE;
		}
	    checkEob = TRUE;
	    }
	if (color)
	    safef(fmtString, sizeof(fmtString), "%%s\n", color);
	else
	    safef(fmtString, sizeof(fmtString), "%%s\n");
	fprintf(h, fmtString, xline);
	
	if (line[0] == '@')
	    inBody = TRUE;

	freeMem(xline);
	}
    else
	{
	checkEob = TRUE;
	}

    if (checkEob && inBlock)
	{
	inBlock = FALSE;
	if (blockP >= blockN)
	    linesChanged += blockP;
	else
	    linesChanged += blockN;
	blockP = 0;
	blockN = 0;
	}

    }

lineFileClose(&lf);
fprintf(h, "
\n\n\n"); fclose(h); return linesChanged; } void makeDiffAndSplit(struct commit *c, char *u, boolean full) /* Generate a full diff and then split it up into its parts. * This was motivated because no other way to show deleted files * since they are not in repo and git paths must actually exist * in working repo dir. However leaving off the path produces * a diff with everything we want, we just have to split it up. */ { if (c->merge) { safef(gitCmd,sizeof(gitCmd), "git diff-tree --cc -b -w --no-prefix --unified=%d %s > %s" // -b -w currently ignored for combined-diff , full ? 1000000 : contextSize , c->commitId, tempMakeDiffName); } else { safef(gitCmd,sizeof(gitCmd), "git diff -b -w --no-prefix --unified=%d %s^! > %s" , full ? 1000000 : contextSize , c->commitId, tempMakeDiffName); //git shorthand: x^! is equiv to range x^ x, // i.e. just the one commit and nothing more. // hack until better fix - this is the case where there is no previous commit if (sameString(c->commitId, "dc78303b079985b5a146d093bbb8a5d06489562d")) { safef(gitCmd,sizeof(gitCmd), "git show -b -w --no-prefix --unified=%d %s > %s" // -b -w probably get ignored , full ? 1000000 : contextSize , c->commitId, tempMakeDiffName); } } runShell(gitCmd); // now parse it and split it into separate files with the right path. boolean getNamesOnly = c->merge && sameString(u,"getFileNamesForMergeCommit"); struct lineFile *lf = lineFileOpen(tempMakeDiffName, TRUE); int lineSize; char *line; FILE *h = NULL; char *section = "@@"; if (c->merge) section = "@@@"; while (lineFileNext(lf, &line, &lineSize)) { char *pattern = "diff --git "; if (c->merge) pattern = "diff --cc "; if (startsWith(pattern, line)) { if (h) { fclose(h); h = NULL; } char *fpath = line + strlen(pattern); if (getNamesOnly) // too bad we had not choice but to get the merge names this way. { /* collect the files-list */ struct files *f = NULL; AllocVar(f); f->path = cloneString(fpath); slAddHead(&c->files, f); } else { if (!c->merge) { fpath = strchr(fpath, ' '); ++fpath; // now we should be pointing to the world } char path[1024]; char *r = strrchr(fpath, '/'); if (r) { *r = 0; /* make internal levels of subdirs */ safef(path, sizeof(path), "mkdir -p %s/%s/%s/%s/%s/%s", outDir, outPrefix, "user", u, full ? "full" : "context", fpath); runShell(path); *r = '/'; } safef(path, sizeof(path), "%s/%s/%s/%s/%s/%s.%s.diff" , outDir, outPrefix, "user", u, full ? "full" : "context", fpath, c->commitId); h = mustOpen(path, "w"); fprintf(h, "%s\n", c->commitId); if (c->merge) fprintf(h, "Merge parents %s\n", c->merge); fprintf(h, "%s\n", c->author); fprintf(h, "%s\n", c->date); fprintf(h, "%s\n", c->comment); } } else if (startsWith(section, line)) { char *end = strchr(line+strlen(section), '@'); *(end+strlen(section)) = 0; // chop the weird unwanted context string from here following e.g. //@@ -99,7 +99,9 @@ weird unwanted context string here // converts to //@@ -99,7 +99,9 @@ // saves 17 seconds over the more expensive sed command } if (h) fprintf(h, "%s\n", line); } if (h) { fclose(h); h = NULL; } lineFileClose(&lf); if (getNamesOnly) // too bad we had not choice but to get the merge names this way. slReverse(&c->files); } void doUserCommits(char *u, struct commit *commits, int *saveUlc, int *saveUfc) /* process one user, commit-view */ { char userPath[1024]; safef(userPath, sizeof(userPath), "%s/%s/%s/%s/index.html", outDir, outPrefix, "user", u); FILE *h = mustOpen(userPath, "w"); fprintf(h, "\n\nCommits for %s\n\n\n", u); fprintf(h, "

Commits for %s

\n", u); fprintf(h, "switch to files view, user index\n"); fprintf(h, "

%s to %s (%s to %s) %s

\n", startTag, endTag, startDate, endDate, title); fprintf(h, "\n"); fprintf(h, "switch to files view, user index\n"); fprintf(h, "\n\n"); fclose(h); *saveUlc = userLinesChanged; *saveUfc = userFileCount; } int slComFileCmp(const void *va, const void *vb) /* Compare two comFiles. */ { const struct comFile *a = *((struct comFile **)va); const struct comFile *b = *((struct comFile **)vb); int result = strcmp(a->f->path, b->f->path); if (result == 0) result = b->commit->commitNumber - a->commit->commitNumber; return result; } void doUserFiles(char *u, struct commit *commits) /* process one user's files-view (or all if u is NULL) */ { // http://hgwdev.cse.ucsc.edu/cvs-reports/branch/user/galt/index-by-file.html // if u is NULL // http://hgwdev.cse.ucsc.edu/cvs-reports/branch/file/index.html char userPath[1024]; if (u) safef(userPath, sizeof(userPath), "%s/%s/%s/%s/index-by-file.html", outDir, outPrefix, "user", u); else safef(userPath, sizeof(userPath), "%s/%s/%s/index.html", outDir, outPrefix, "file"); FILE *h = mustOpen(userPath, "w"); if (u) { fprintf(h, "\n\nFile Changes for %s\n\n\n", u); fprintf(h, "

File Changes for %s

\n", u); fprintf(h, "switch to commits view, user index"); } else { fprintf(h, "\n\nAll File Changes\n\n\n"); fprintf(h, "

All File Changes

\n"); } fprintf(h, "

%s to %s (%s to %s) %s

\n", startTag, endTag, startDate, endDate, title); fprintf(h, "\n"); if (u) { fprintf(h, "switch to commits view, user index"); } else { fprintf(h, "\n"); } fprintf(h, "\n\n"); fclose(h); } void doMainIndex() /* Create simple main index page */ { char path[256]; safef(path, sizeof(path), "%s/%s/index.html", outDir, outPrefix); FILE *h = mustOpen(path, "w"); fprintf(h, "\n\nSource Code Changes\n\n\n"); fprintf(h, "

%s %s Changes

\n", title, outPrefix); fprintf(h, "

%s to %s (%s to %s) %s

\n", startTag, endTag, startDate, endDate, title); fprintf(h, "\n\n\n"); fclose(h); } void makeMyDir(char *path) /* Make a single dir if it does not already exit */ { if (!fileExists(path) && mkdir(path, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) != 0) errnoAbort("unable to mkdir %s", path); } void gitReports() /* Generate code-review reports from git repo */ { int totalChangedLines = 0; int totalChangedFiles = 0; int userChangedLines = 0; int userChangedFiles = 0; tempMakeDiffName = cloneString(rTempName("/tmp", "makeDiff", ".tmp")); /* read the commits */ struct commit *commits = getCommits(), *c = NULL; /* make the user list */ for(c = commits; c; c = c->next) { if (!hashLookup(userHash, c->author)) { hashStore(userHash, c->author); struct slName *name = newSlName(c->author); slAddHead(&users, name); } } slNameSort(&users); /* create prefix dir */ char path[256]; safef(path, sizeof(path), "%s/%s", outDir, outPrefix); makeMyDir(path); /* create file dir */ safef(path, sizeof(path), "%s/%s/%s", outDir, outPrefix, "file"); makeMyDir(path); /* create user dir */ safef(path, sizeof(path), "%s/%s/%s", outDir, outPrefix, "user"); makeMyDir(path); char usersPath[1024]; safef(usersPath, sizeof(usersPath), "%s/%s/%s/index.html", outDir, outPrefix, "user"); FILE *h = mustOpen(usersPath, "w"); fprintf(h, "\n\nChanges By User\n\n\n"); fprintf(h, "

Changes By User

\n"); fprintf(h, "

%s to %s (%s to %s) %s

\n", startTag, endTag, startDate, endDate, title); fprintf(h, "\n"); if (u) { fprintf(h, "switch to commits view, user index"); } else { fprintf(h, "\n"); } fprintf(h, "\n\n"); fclose(h); // make index of all files view doUserFiles(NULL, commits); // make main index page doMainIndex(); // tidying up unlink(tempMakeDiffName); freez(&tempMakeDiffName); } int main(int argc, char *argv[]) { optionInit(&argc, argv, options); if (argc != 9) usage("wrong number of args"); if (optionExists("-help")) usage("help"); startTag = argv[1]; endTag = argv[2]; startDate = argv[3]; endDate = argv[4]; title = argv[5]; repoDir = argv[6]; outDir = argv[7]; outPrefix = argv[8]; contextSize = optionInt("-context", 15); userHash = hashNew(5); setCurrentDir(repoDir); gitReports(); hashFree(&userHash); printf("Done.\n"); return 0; }