4966ea67f91dd60e9ac4125e84a11888eef516df
chmalee
  Thu Mar 14 16:27:33 2024 -0700
Special case Jairo's username for the git reports because of the hgwdev username/email mismatch, refs #33244

diff --git src/utils/git-reports/git-reports.c src/utils/git-reports/git-reports.c
index e0b8067..1d79fe4 100644
--- src/utils/git-reports/git-reports.c
+++ src/utils/git-reports/git-reports.c
@@ -1,1041 +1,1041 @@
 /* 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;
 
 // command to filter out commits from non-GB members:
 char *getUsersCmd = "getent group kentcommit | cut -d':' -f4 | sed -e 's/^\\|,/ --author=/g'";
 char gitCmd[4096];
 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 *fullAuthor;
     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), ""
-"validUsers=$(%s); git log %s..%s --name-status --color=never ${validUsers} > commits.tmp"
+"validUsers=$(%s); git log %s..%s --name-status --color=never ${validUsers} --author=jnavarr5> commits.tmp"
 , getUsersCmd, 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;
 while (lineFileNext(lf, &line, &lineSize))
     {
     
     char *w = nextWord(&line);
     AllocVar(commit);
     if (!sameString("commit", w))
 	lineFileAbort(lf, "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))
 	lineFileAbort(lf, "expected keyword Author: parsing commits.tmp\n");
 
     /* Full author information */
     commit->fullAuthor = cloneString(line);
 
     /* by request, keep just the email account name */
     char *lc = strchr(line, '<');
     if (!lc)
 	lineFileAbort(lf, "expected '<' char in email address in Author: parsing commits.tmp\n");
     ++lc;
     char *rc = strchr(lc, '>');
     if (!rc)
 	lineFileAbort(lf, "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))
 	lineFileAbort(lf, "expected keyword Date: parsing commits.tmp\n");
     commit->date = cloneString(line);
 
     lineFileNext(lf, &line, &lineSize);
     if (!sameString("", line))
 	lineFileAbort(lf, "expected blank line parsing commits.tmp\n");
 
     /* collect the comment-lines */
     struct dyString *dy = NULL;
     dy = dyStringNew(0);
     files = NULL;
     while (lineFileNext(lf, &line, &lineSize))
 	{
 	if (sameString("", line))
 	    break;
 	w = skipLeadingSpaces(line);
 	dyStringPrintf(dy, "%s\n", w);
 	}
     commit->comment = cloneString(dy->string);
     dyStringFree(&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 (startsWith("commit ", line))
 		{
 		lineFileReuse(lf); // extremely rare case of commit with no file lines
 		break;
 		}
 	    if (sameString("", line))
 		break;
 	    w = nextWord(&line);
             if (startsWith("src/hg/js/react/bundle/", line))
                 continue;
 	    AllocVar(f);
 	    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;
 }
 
 void makeParentPath(char *path)
 /* make a parent path if it does not exist already */
 {
 // create parent path
 char cmd[1024];
 char *r = strrchr(path, '/');
 if (r)
     {
     *r = 0;
     /* make internal levels of subdirs */
     if (!fileExists(path))
 	{
 	safef(cmd, sizeof(cmd), "mkdir -p %s", path);
 	runShell(cmd);
 	}
     *r = '/';
     }
 }
 
 
 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;
 
 makeParentPath(htmlPath);  // may be needed with white-space only diffs
 FILE *h = mustOpen(htmlPath, "w");
 if (!fileExists(diffPath))  // this may happen with white-space only diffs 
     {
     makeParentPath(diffPath);
     FILE *f = mustOpen(diffPath, "w"); // create an empty diff file
     fprintf(f, "white space only change\n");
     carefulClose(&f);
     }
 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, "<html>\n<head>\n<title>%s %s</title>\n</head>\n</body>\n<pre>\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), "<span style=\"background-color:%s\">%%s</span>\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, "</pre>\n</body>\n</html>\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.txt"
 		, 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 insertDetailsCssAndJs(FILE *h)
 /* insert needed CSS and JS helper functions to support showing and hiding details */
 {
 fprintf(h, 
 "<style>"
 ".details { display: none; }"  // default to hidden
 "</style>\n");
 
 fprintf(h, 
 "<script language='JavaScript'>"
 "function getCSSRule(search) {"
 "  return [].map.call(document.styleSheets, function(item) {"
 "    return [].slice.call(item.cssRules);"
 "  }).reduce(function(a, b) {"
 "    return b.concat(a);"
 "  }).filter(function(rule) {"
 "    return rule.selectorText.lastIndexOf(search) === rule.selectorText.length - search.length;"
 "  })[0];"
 "}"
 "</script>\n");
 }
 
 void insertShowDetailsLink(FILE *h)
 /* Insert Show Details link with onclick. */
 {
 fprintf(h, "<a href='#' onclick=\""
 "if (this.innerText === 'Hide details') {"
 "  getCSSRule('.details').style.display = 'none'; "
 "  this.innerText = 'Show details'; "
 "} else {"
 "  getCSSRule('.details').style.display = 'inline'; "
 "  this.innerText = 'Hide details'; "
 "}"
 "return false;"
 "\">Show details</a>\n"
 );
 /* Comments found with the code:
 It wasn't working in some cases. 
 I changed the third return rule... into return 
  return rule.selectorText === search;
 and now it seems to work always.
 	
 There are rule objects that do not have the selectorText property, e.g. @font-face{..}. 
 Therefore I altered the return rules.selectorText... line to
  return rule && rule.selectorText ? (rule.selectorText.lastIndexOf(search) === rule.selectorText.length - search.length) : -1;
 */
 }
 
 
 
 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, "<html>\n<head>\n<title>Commits for %s</title>\n</head>\n</body>\n", u);
 
 insertDetailsCssAndJs(h);
 
 fprintf(h, "<h1>Commits for %s</h1>\n", u);
 
 fprintf(h, "switch to <A href=\"index-by-file.html\">files view</A>, <A href=\"../index.html\">user index</A>\n");
 fprintf(h, "<h3>%s to %s (%s to %s) %s</h3>\n", startTag, endTag, startDate, endDate, title);
 
 insertShowDetailsLink(h);
 
 fprintf(h, "<ul>\n");
 
 
 int userLinesChanged = 0;
 int userFileCount = 0;
 
 char *cDiff = NULL, *cHtml = NULL, *fDiff = NULL, *fHtml = NULL;
 char *relativePath = NULL;
 char *commonPath = NULL;
 
 struct commit *c = NULL;
 struct files *f = NULL;
 
 for(c = commits; c; c = c->next)
     {
     if (sameString(c->author, u))
 	{
         fprintf(h, "<span class='details' >"); // toggle details none, inline //style='display:inline;'
 	fprintf(h, "%s ", c->commitId);
 	fprintf(h, "%s ", c->date);
 	fprintf(h, "<br>\n");
 	fprintf(h, "</span>\n");
 
 	char *cc = htmlEncode(c->comment);
 	char *ccc = replaceChars(cc, "&#x0A;", "<br>\n");
 	fprintf(h, "<li>%s\n", ccc);
 	freeMem(cc);
 	freeMem(ccc);
 
 	makeDiffAndSplit(c, u, FALSE);
 	makeDiffAndSplit(c, u, TRUE);
 	for(f = c->files; f; f = f->next)
 	    {
 	    char path[1024];
 
             // context unified
 	    safef(path, sizeof(path), "%s/%s.%s", "context", f->path, c->commitId);
 	    relativePath = cloneString(path);
 
 	    safef(path, sizeof(path), "%s/%s/%s/%s/%s", outDir, outPrefix, "user", u, relativePath);
 	    commonPath = cloneString(path);
 
 	    safef(path, sizeof(path), "%s.html", commonPath);
 	    cHtml = cloneString(path);
 
 	    safef(path, sizeof(path), "%s.diff.txt", commonPath);
 	    cDiff = cloneString(path);
 
 	    // make context html page
 	    f->linesChanged = makeHtml(cDiff, cHtml, f->path, c->commitId);
 
 	    userLinesChanged += f->linesChanged;
 	    ++userFileCount;
 
 	    freeMem(cDiff);
 	    freeMem(cHtml);
 	    safef(path, sizeof(path), "%s.html", relativePath);
 	    cHtml = cloneString(path);
 	    safef(path, sizeof(path), "%s.diff.txt", relativePath);
 	    cDiff = cloneString(path);
 
 
 
             // full text (up to 10,000 lines)
 	    freeMem(relativePath);
 	    safef(path, sizeof(path), "%s/%s.%s", "full", f->path, c->commitId);
 	    relativePath = cloneString(path);
 
 	    safef(path, sizeof(path), "%s/%s/%s/%s/%s", outDir, outPrefix, "user", u, relativePath);
 	    freeMem(commonPath);
 	    commonPath = cloneString(path);
 
 	    safef(path, sizeof(path), "%s.html", commonPath);
 	    fHtml = cloneString(path);
 
 	    safef(path, sizeof(path), "%s.diff.txt", commonPath);
 	    fDiff = cloneString(path);
 
 
 	    //git show --unified=10000 11a20b6cd113d75d84549eb642b7f2ac7a2594fe src/utils/qa/weeklybld/buildEnv.csh
 
 	    // make full html page
 	    makeHtml(fDiff, fHtml, f->path, c->commitId);
 
 	    freeMem(fDiff);
 	    freeMem(fHtml);
 	    safef(path, sizeof(path), "%s.html", relativePath);
 	    fHtml = cloneString(path);
 	    safef(path, sizeof(path), "%s.diff.txt", relativePath);
 	    fDiff = cloneString(path);
 
 	    // make file diff links
 	    fprintf(h, "<ul><li> %s - lines changed %d, "
 		"context: <A href=\"%s\">html</A>, <A href=\"%s\">text</A>, "
 		"full: <A href=\"%s\">html</A>, <A href=\"%s\">text</A>%s</li></ul>\n"
 		, f->path, f->linesChanged
 		, cHtml, cDiff, fHtml, fDiff, f->linesChanged == 0 ? " (a binary file or whitespace-only change or file-permission change shows no diff)" : "");
 
 	    freeMem(relativePath);
 	    freeMem(commonPath);
 	    freeMem(cDiff);
 	    freeMem(cHtml);
 	    freeMem(fDiff);
 	    freeMem(fHtml);
 
 	    }
 	fprintf(h, "</li>\n");
 	fprintf(h, "<br>\n");
 	}
     }
 fprintf(h, "</ul>\n");
 fprintf(h, "switch to <A href=\"index-by-file.html\">files view</A>, <A href=\"../index.html\">user index</A>\n");
 fprintf(h, "</body>\n</html>\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.gi.ucsc.edu/cvs-reports/branch/user/galt/index-by-file.html
 // if u is NULL
 // http://hgwdev.gi.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, "<html>\n<head>\n<title>File Changes for %s</title>\n</head>\n</body>\n", u);
     insertDetailsCssAndJs(h);
     fprintf(h, "<h1>File Changes for %s</h1>\n", u);
     fprintf(h, "switch to <A href=\"index.html\">commits view</A>, <A href=\"../index.html\">user index</A>");
     }
 else
     {
     fprintf(h, "<html>\n<head>\n<title>All File Changes</title>\n</head>\n</body>\n");
     insertDetailsCssAndJs(h);
     fprintf(h, "<h1>All File Changes</h1>\n");
     }
 
 fprintf(h, "<h3>%s to %s (%s to %s) %s</h3>\n", startTag, endTag, startDate, endDate, title);
 
 insertShowDetailsLink(h);
 
 fprintf(h, "<ul>\n");
 
 
 int totalLinesChanged = 0;
 int totalFileCount = 0;
 
 char *cDiff = NULL, *cHtml = NULL, *fDiff = NULL, *fHtml = NULL;
 char *relativePath = NULL;
 
 struct commit *c = NULL;
 struct files *f = NULL;
 
 struct comFile *comFiles = NULL, *cf = NULL;
 
 // pre-filter for u if u is not NULL  
 for(c = commits; c; c = c->next)
     {
     if (!u || (u && sameString(c->author, u)))
 	{
 	for(f = c->files; f; f = f->next)
 	    {
 	    AllocVar(cf);
 	    cf->f = f;
 	    cf->commit = c;
 	    slAddHead(&comFiles, cf);
 	    }
 	}
     }
 // sort by file path, and then by commitNumber
 //  so that newest commit is on the bottom.
 slSort(&comFiles, slComFileCmp);
 
 char *lastPath = "";
 char *closure = "";
 
 for(cf = comFiles; cf; cf = cf->next)
     {
     c = cf->commit;
     f = cf->f;
  
     if (!sameString(f->path, lastPath))
 	{
 	fprintf(h, "%s", closure);
 	lastPath = f->path;
 	fprintf(h, "<li>%s\n", f->path);
     	closure = "</li>\n";
 	}
 
 
     char path[1024];
 
     // context unified
     if (u)
 	safef(path, sizeof(path), "%s/%s.%s", "context", f->path, c->commitId);
     else
 	safef(path, sizeof(path), "../user/%s/%s/%s.%s", c->author, "context", f->path, c->commitId);
     relativePath = cloneString(path);
     safef(path, sizeof(path), "%s.html", relativePath);
     cHtml = cloneString(path);
     safef(path, sizeof(path), "%s.diff.txt", relativePath);
     cDiff = cloneString(path);
 
 
 
     // full text (up to 10,000 lines)
     freeMem(relativePath);
     if (u)
 	safef(path, sizeof(path), "%s/%s.%s", "full", f->path, c->commitId);
     else
 	safef(path, sizeof(path), "../user/%s/%s/%s.%s", c->author, "full", f->path, c->commitId);
     relativePath = cloneString(path);
     safef(path, sizeof(path), "%s.html", relativePath);
     fHtml = cloneString(path);
     safef(path, sizeof(path), "%s.diff.txt", relativePath);
     fDiff = cloneString(path);
 
     // make file view links
     fprintf(h, "<ul><li>");
     fprintf(h, " lines changed %d, "
 	"context: <A href=\"%s\">html</A>, <A href=\"%s\">text</A>, "
 	"full: <A href=\"%s\">html</A>, <A href=\"%s\">text</A><br>\n"
 	, f->linesChanged
 	, cHtml, cDiff, fHtml, fDiff);
 
     fprintf(h, "<span class='details' >"); // toggle details none, inline //style='display:inline;'
     fprintf(h, "%s", c->commitId);
     fprintf(h, " %s", c->date);
     fprintf(h, "<br>\n");
     fprintf(h, "</span>\n");
 
     char *cc = htmlEncode(c->comment);
     char *ccc = replaceChars(cc, "&#x0A;", "<br>\n");
     fprintf(h, "    %s\n", ccc);
     freeMem(cc);
     freeMem(ccc);
     fprintf(h, "</li></ul>\n");
 
     freeMem(relativePath);
     freeMem(cDiff);
     freeMem(cHtml);
     freeMem(fDiff);
     freeMem(fHtml);
 
     totalLinesChanged += f->linesChanged;
     ++totalFileCount;
 
     fprintf(h, "\n");
     }
 fprintf(h, "%s", closure);
 fprintf(h, "</ul>\n");
 if (u)
     {
     fprintf(h, "switch to <A href=\"index.html\">commits view</A>, <A href=\"../index.html\">user index</A>");
     }
 else
     {
     fprintf(h, "<ul>\n");
     fprintf(h, "<li> lines changed: %d</li>\n", totalLinesChanged);
     fprintf(h, "<li> files changed: %d</li>\n", totalFileCount);
     fprintf(h, "</ul>\n");
     }
 fprintf(h, "</body>\n</html>\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, "<html>\n<head>\n<title>Source Code Changes</title>\n</head>\n</body>\n");
 fprintf(h, "<h1>%s %s Changes</h1>\n", title, outPrefix);
 
 fprintf(h, "<h2>%s to %s (%s to %s) %s</h2>\n", startTag, endTag, startDate, endDate, title);
 
 fprintf(h, "<ul>\n");
 
 fprintf(h, "<li> <A href=\"user/index.html\">Changes by User</A></li>\n");
 fprintf(h, "\n");
 fprintf(h, "<li> <A href=\"file/index.html\">All File Changes</A></li>\n");
 
 fprintf(h, "</ul>\n</body>\n</html>\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))
 	{
 	hashAdd(userHash, c->author, cloneString(c->fullAuthor));
 	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, "<html>\n<head>\n<title>Changes By User</title>\n</head>\n</body>\n");
 fprintf(h, "<h1>Changes By User</h1>\n");
 
 fprintf(h, "<h2>%s to %s (%s to %s) %s</h2>\n", startTag, endTag, startDate, endDate, title);
 
 fprintf(h, "<ul>\n");
 
 struct slName*u;
 for(u = users; u; u = u->next)
     {
     printf("user: %s\n", u->name);
 
     /* create user/name dir */
     safef(path, sizeof(path), "%s/%s/%s/%s", outDir, outPrefix, "user", u->name);
     makeMyDir(path);
 
     /* create user/name/context dir */
     safef(path, sizeof(path), "%s/%s/%s/%s/%s", outDir, outPrefix, "user", u->name, "context");
     makeMyDir(path);
 
     /* create user/name/full dir */
     safef(path, sizeof(path), "%s/%s/%s/%s/%s", outDir, outPrefix, "user", u->name, "full");
     makeMyDir(path);
 
     userChangedLines = 0;
     userChangedFiles = 0;
 
 
     /* make user's reports */
     doUserCommits(u->name, commits, &userChangedLines, &userChangedFiles);
 
     doUserFiles(u->name, commits);
 
     char relPath[1024];
     safef(relPath, sizeof(relPath), "%s/index.html", u->name);
     fprintf(h, "<li> <A href=\"%s\">%s</A> - changed lines: %d, files: %d</li>\n", relPath, u->name, userChangedLines, userChangedFiles);
 
     totalChangedLines += userChangedLines;
     totalChangedFiles += userChangedFiles;  
 
     }
 
 fprintf(h, "</ul>\n");
 if (u)
     {
     fprintf(h, "switch to <A href=\"index.html\">commits view</A>, <A href=\"../index.html\">user index</A>");
     }
 else
     {
     fprintf(h, "<ul>\n");
     fprintf(h, "<li>  lines changed: %d</li>\n", totalChangedLines);
     fprintf(h, "<li>  files changed: %d</li>\n", totalChangedFiles);
     fprintf(h, "</ul>\n");
     }
     fprintf(h, "<br><A href=\"authors.html\">View Full Authors Details</A><br>");
 fprintf(h, "</body>\n</html>\n");
 
 fclose(h);
 
 
 
 // Authors Details page
 char authorsPath[1024];
 safef(authorsPath, sizeof(authorsPath), "%s/%s/%s/authors.html", outDir, outPrefix, "user");
 
 h = mustOpen(authorsPath, "w");
 fprintf(h, "<html>\n<head>\n<title>Full Authors Details</title>\n</head>\n</body>\n");
 fprintf(h, "<h1>Full Authors Details</h1>\n");
 fprintf(h, "<h2>%s to %s (%s to %s) %s</h2>\n", startTag, endTag, startDate, endDate, title);
 fprintf(h, "<ul>\n");
 for(u = users; u; u = u->next)
     {
     struct hashEl *hel = hashLookup(userHash, u->name);
     char *fullAuthor = hel->val;
     fprintf(h, "%s<br>\n", htmlEncode(fullAuthor));
     }
 fprintf(h, "</ul>\n");
 fprintf(h, "</body>\n</html>\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;
 }