diff options
-rw-r--r-- | Documentation/git-blame.txt | 39 | ||||
-rw-r--r-- | Makefile | 7 | ||||
-rw-r--r-- | blame.c | 453 | ||||
-rw-r--r-- | builtin-annotate.c | 25 | ||||
-rw-r--r-- | builtin.h | 1 | ||||
-rwxr-xr-x | git-annotate.perl | 708 | ||||
-rw-r--r-- | git.c | 1 | ||||
-rwxr-xr-x | gitweb/gitweb.perl | 132 |
8 files changed, 483 insertions, 883 deletions
diff --git a/Documentation/git-blame.txt b/Documentation/git-blame.txt index e1f89444a..9891c1d37 100644 --- a/Documentation/git-blame.txt +++ b/Documentation/git-blame.txt @@ -7,7 +7,7 @@ git-blame - Show what revision and author last modified each line of a file SYNOPSIS -------- -'git-blame' [-c] [-l] [-t] [-S <revs-file>] [--] <file> [<rev>] +'git-blame' [-c] [-l] [-t] [-f] [-n] [-p] [-S <revs-file>] [--] <file> [<rev>] DESCRIPTION ----------- @@ -45,10 +45,47 @@ OPTIONS -S, --rev-file <revs-file>:: Use revs from revs-file instead of calling gitlink:git-rev-list[1]. +-f, --show-name:: + Show filename in the original commit. By default + filename is shown if there is any line that came from a + file with different name, due to rename detection. + +-n, --show-number:: + Show line number in the original commit (Default: off). + +-p, --porcelain:: + Show in a format designed for machine consumption. + -h, --help:: Show help message. +THE PORCELAIN FORMAT +-------------------- + +In this format, each line is output after a header; the +header at the minumum has the first line which has: + +- 40-byte SHA-1 of the commit the line is attributed to; +- the line number of the line in the original file; +- the line number of the line in the final file; +- on a line that starts a group of line from a different + commit than the previous one, the number of lines in this + group. On subsequent lines this field is absent. + +This header line is followed by the following information +at least once for each commit: + +- author name ("author"), email ("author-mail"), time + ("author-time"), and timezone ("author-tz"); similarly + for committer. +- filename in the commit the line is attributed to. +- the first line of the commit log message ("summary"). + +The contents of the actual line is output after the above +header, prefixed by a TAB. This is to allow adding more +header elements later. + SEE ALSO -------- gitlink:git-annotate[1] @@ -132,6 +132,8 @@ GITWEB_HOMETEXT = indextext.html GITWEB_CSS = gitweb.css GITWEB_LOGO = git-logo.png GITWEB_FAVICON = git-favicon.png +GITWEB_SITE_HEADER = +GITWEB_SITE_FOOTER = export prefix bindir gitexecdir template_dir GIT_PYTHON_DIR @@ -173,7 +175,7 @@ SCRIPT_SH = \ SCRIPT_PERL = \ git-archimport.perl git-cvsimport.perl git-relink.perl \ git-shortlog.perl git-rerere.perl \ - git-annotate.perl git-cvsserver.perl \ + git-cvsserver.perl \ git-svnimport.perl git-cvsexportcommit.perl \ git-send-email.perl git-svn.perl @@ -265,6 +267,7 @@ LIB_OBJS = \ BUILTIN_OBJS = \ builtin-add.o \ + builtin-annotate.o \ builtin-apply.o \ builtin-archive.o \ builtin-cat-file.o \ @@ -675,6 +678,8 @@ gitweb/gitweb.cgi: gitweb/gitweb.perl -e 's|++GITWEB_CSS++|$(GITWEB_CSS)|g' \ -e 's|++GITWEB_LOGO++|$(GITWEB_LOGO)|g' \ -e 's|++GITWEB_FAVICON++|$(GITWEB_FAVICON)|g' \ + -e 's|++GITWEB_SITE_HEADER++|$(GITWEB_SITE_HEADER)|g' \ + -e 's|++GITWEB_SITE_FOOTER++|$(GITWEB_SITE_FOOTER)|g' \ $< >$@+ chmod +x $@+ mv $@+ $@ @@ -17,19 +17,24 @@ #include "diffcore.h" #include "revision.h" #include "xdiff-interface.h" +#include "quote.h" #define DEBUG 0 -static const char blame_usage[] = "git-blame [-c] [-l] [-t] [-S <revs-file>] [--] file [commit]\n" - " -c, --compatibility Use the same output mode as git-annotate (Default: off)\n" - " -l, --long Show long commit SHA1 (Default: off)\n" - " -t, --time Show raw timestamp (Default: off)\n" - " -S, --revs-file Use revisions from revs-file instead of calling git-rev-list\n" - " -h, --help This message"; +static const char blame_usage[] = +"git-blame [-c] [-l] [-t] [-f] [-n] [-p] [-S <revs-file>] [--] file [commit]\n" +" -c, --compatibility Use the same output mode as git-annotate (Default: off)\n" +" -l, --long Show long commit SHA1 (Default: off)\n" +" -t, --time Show raw timestamp (Default: off)\n" +" -f, --show-name Show original filename (Default: auto)\n" +" -n, --show-number Show original linenumber (Default: off)\n" +" -p, --porcelain Show in a format designed for machine consumption\n" +" -S revs-file Use revisions from revs-file instead of calling git-rev-list\n" +" -h, --help This message"; static struct commit **blame_lines; static int num_blame_lines; -static char* blame_contents; +static char *blame_contents; static int blame_len; struct util_info { @@ -38,9 +43,10 @@ struct util_info { char *buf; unsigned long size; int num_lines; - const char* pathname; + const char *pathname; + unsigned meta_given:1; - void* topo_data; + void *topo_data; }; struct chunk { @@ -156,11 +162,10 @@ static int get_blob_sha1_internal(const unsigned char *sha1, const char *base, unsigned mode, int stage); static unsigned char blob_sha1[20]; -static const char* blame_file; +static const char *blame_file; static int get_blob_sha1(struct tree *t, const char *pathname, unsigned char *sha1) { - int i; const char *pathspec[2]; blame_file = pathname; pathspec[0] = pathname; @@ -168,12 +173,7 @@ static int get_blob_sha1(struct tree *t, const char *pathname, hashclr(blob_sha1); read_tree_recursive(t, "", 0, 0, pathspec, get_blob_sha1_internal); - for (i = 0; i < 20; i++) { - if (blob_sha1[i] != 0) - break; - } - - if (i == 20) + if (is_null_sha1(blob_sha1)) return -1; hashcpy(sha1, blob_sha1); @@ -239,7 +239,8 @@ static void print_map(struct commit *cmit, struct commit *other) if (i < util->num_lines) { num = util->line_map[i]; printf("%d\t", num); - } else + } + else printf("\t"); if (i < util2->num_lines) { @@ -247,7 +248,8 @@ static void print_map(struct commit *cmit, struct commit *other) printf("%d\t", num2); if (num != -1 && num2 != num) printf("---"); - } else + } + else printf("\t"); printf("\n"); @@ -266,12 +268,12 @@ static void fill_line_map(struct commit *commit, struct commit *other, int cur_chunk = 0; int i1, i2; - if (p->num && DEBUG) - print_patch(p); - - if (DEBUG) + if (DEBUG) { + if (p->num) + print_patch(p); printf("num lines 1: %d num lines 2: %d\n", util->num_lines, util2->num_lines); + } for (i1 = 0, i2 = 0; i1 < util->num_lines; i1++, i2++) { struct chunk *chunk = NULL; @@ -293,7 +295,8 @@ static void fill_line_map(struct commit *commit, struct commit *other, i2 += chunk->len2; cur_chunk++; - } else { + } + else { if (i2 >= util2->num_lines) break; @@ -327,19 +330,15 @@ static int map_line(struct commit *commit, int line) return info->line_map[line]; } -static struct util_info* get_util(struct commit *commit) +static struct util_info *get_util(struct commit *commit) { struct util_info *util = commit->util; if (util) return util; - util = xmalloc(sizeof(struct util_info)); - util->buf = NULL; - util->size = 0; - util->line_map = NULL; + util = xcalloc(1, sizeof(struct util_info)); util->num_lines = -1; - util->pathname = NULL; commit->util = util; return util; } @@ -369,7 +368,7 @@ static void alloc_line_map(struct commit *commit) if (util->buf[i] == '\n') util->num_lines++; } - if(util->buf[util->size - 1] != '\n') + if (util->buf[util->size - 1] != '\n') util->num_lines++; util->line_map = xmalloc(sizeof(int) * util->num_lines); @@ -378,9 +377,9 @@ static void alloc_line_map(struct commit *commit) util->line_map[i] = -1; } -static void init_first_commit(struct commit* commit, const char* filename) +static void init_first_commit(struct commit *commit, const char *filename) { - struct util_info* util = commit->util; + struct util_info *util = commit->util; int i; util->pathname = filename; @@ -395,18 +394,17 @@ static void init_first_commit(struct commit* commit, const char* filename) util->line_map[i] = i; } - static void process_commits(struct rev_info *rev, const char *path, - struct commit** initial) + struct commit **initial) { int i; - struct util_info* util; + struct util_info *util; int lines_left; int *blame_p; int *new_lines; int new_lines_len; - struct commit* commit = get_revision(rev); + struct commit *commit = get_revision(rev); assert(commit); init_first_commit(commit, path); @@ -442,7 +440,7 @@ static void process_commits(struct rev_info *rev, const char *path, parents != NULL; parents = parents->next) num_parents++; - if(num_parents == 0) + if (num_parents == 0) *initial = commit; if (fill_util_info(commit)) @@ -503,13 +501,12 @@ static void process_commits(struct rev_info *rev, const char *path, } while ((commit = get_revision(rev)) != NULL); } - -static int compare_tree_path(struct rev_info* revs, - struct commit* c1, struct commit* c2) +static int compare_tree_path(struct rev_info *revs, + struct commit *c1, struct commit *c2) { int ret; - const char* paths[2]; - struct util_info* util = c2->util; + const char *paths[2]; + struct util_info *util = c2->util; paths[0] = util->pathname; paths[1] = NULL; @@ -520,12 +517,11 @@ static int compare_tree_path(struct rev_info* revs, return ret; } - -static int same_tree_as_empty_path(struct rev_info *revs, struct tree* t1, - const char* path) +static int same_tree_as_empty_path(struct rev_info *revs, struct tree *t1, + const char *path) { int ret; - const char* paths[2]; + const char *paths[2]; paths[0] = path; paths[1] = NULL; @@ -536,9 +532,9 @@ static int same_tree_as_empty_path(struct rev_info *revs, struct tree* t1, return ret; } -static const char* find_rename(struct commit* commit, struct commit* parent) +static const char *find_rename(struct commit *commit, struct commit *parent) { - struct util_info* cutil = commit->util; + struct util_info *cutil = commit->util; struct diff_options diff_opts; const char *paths[1]; int i; @@ -564,9 +560,11 @@ static const char* find_rename(struct commit* commit, struct commit* parent) for (i = 0; i < diff_queued_diff.nr; i++) { struct diff_filepair *p = diff_queued_diff.queue[i]; - if (p->status == 'R' && !strcmp(p->one->path, cutil->pathname)) { + if (p->status == 'R' && + !strcmp(p->one->path, cutil->pathname)) { if (DEBUG) - printf("rename %s -> %s\n", p->one->path, p->two->path); + printf("rename %s -> %s\n", + p->one->path, p->two->path); return p->two->path; } } @@ -582,7 +580,7 @@ static void simplify_commit(struct rev_info *revs, struct commit *commit) return; if (!commit->parents) { - struct util_info* util = commit->util; + struct util_info *util = commit->util; if (!same_tree_as_empty_path(revs, commit->tree, util->pathname)) commit->object.flags |= TREECHANGE; @@ -608,17 +606,17 @@ static void simplify_commit(struct rev_info *revs, struct commit *commit) case REV_TREE_NEW: { - - struct util_info* util = commit->util; + struct util_info *util = commit->util; if (revs->remove_empty_trees && same_tree_as_empty_path(revs, p->tree, util->pathname)) { - const char* new_name = find_rename(commit, p); + const char *new_name = find_rename(commit, p); if (new_name) { - struct util_info* putil = get_util(p); + struct util_info *putil = get_util(p); if (!putil->pathname) putil->pathname = xstrdup(new_name); - } else { + } + else { *pp = parent->next; continue; } @@ -639,47 +637,106 @@ static void simplify_commit(struct rev_info *revs, struct commit *commit) commit->object.flags |= TREECHANGE; } - struct commit_info { - char* author; - char* author_mail; + char *author; + char *author_mail; unsigned long author_time; - char* author_tz; + char *author_tz; + + /* filled only when asked for details */ + char *committer; + char *committer_mail; + unsigned long committer_time; + char *committer_tz; + + char *summary; }; -static void get_commit_info(struct commit* commit, struct commit_info* ret) +static void get_ac_line(const char *inbuf, const char *what, + int bufsz, char *person, char **mail, + unsigned long *time, char **tz) { int len; - char* tmp; - static char author_buf[1024]; - - tmp = strstr(commit->buffer, "\nauthor ") + 8; - len = strchr(tmp, '\n') - tmp; - ret->author = author_buf; - memcpy(ret->author, tmp, len); + char *tmp, *endp; + + tmp = strstr(inbuf, what); + if (!tmp) + goto error_out; + tmp += strlen(what); + endp = strchr(tmp, '\n'); + if (!endp) + len = strlen(tmp); + else + len = endp - tmp; + if (bufsz <= len) { + error_out: + /* Ugh */ + person = *mail = *tz = "(unknown)"; + *time = 0; + return; + } + memcpy(person, tmp, len); - tmp = ret->author; + tmp = person; tmp += len; *tmp = 0; - while(*tmp != ' ') + while (*tmp != ' ') tmp--; - ret->author_tz = tmp+1; + *tz = tmp+1; *tmp = 0; - while(*tmp != ' ') + while (*tmp != ' ') tmp--; - ret->author_time = strtoul(tmp, NULL, 10); + *time = strtoul(tmp, NULL, 10); *tmp = 0; - while(*tmp != ' ') + while (*tmp != ' ') tmp--; - ret->author_mail = tmp + 1; - + *mail = tmp + 1; *tmp = 0; } -static const char* format_time(unsigned long time, const char* tz_str, +static void get_commit_info(struct commit *commit, struct commit_info *ret, int detailed) +{ + int len; + char *tmp, *endp; + static char author_buf[1024]; + static char committer_buf[1024]; + static char summary_buf[1024]; + + ret->author = author_buf; + get_ac_line(commit->buffer, "\nauthor ", + sizeof(author_buf), author_buf, &ret->author_mail, + &ret->author_time, &ret->author_tz); + + if (!detailed) + return; + + ret->committer = committer_buf; + get_ac_line(commit->buffer, "\ncommitter ", + sizeof(committer_buf), committer_buf, &ret->committer_mail, + &ret->committer_time, &ret->committer_tz); + + ret->summary = summary_buf; + tmp = strstr(commit->buffer, "\n\n"); + if (!tmp) { + error_out: + sprintf(summary_buf, "(%s)", sha1_to_hex(commit->object.sha1)); + return; + } + tmp += 2; + endp = strchr(tmp, '\n'); + if (!endp) + goto error_out; + len = endp - tmp; + if (len >= sizeof(summary_buf)) + goto error_out; + memcpy(summary_buf, tmp, len); + summary_buf[len] = 0; +} + +static const char *format_time(unsigned long time, const char *tz_str, int show_raw_time) { static char time_buf[128]; @@ -704,15 +761,15 @@ static const char* format_time(unsigned long time, const char* tz_str, return time_buf; } -static void topo_setter(struct commit* c, void* data) +static void topo_setter(struct commit *c, void *data) { - struct util_info* util = c->util; + struct util_info *util = c->util; util->topo_data = data; } -static void* topo_getter(struct commit* c) +static void *topo_getter(struct commit *c) { - struct util_info* util = c->util; + struct util_info *util = c->util; return util->topo_data; } @@ -735,6 +792,101 @@ static int read_ancestry(const char *graft_file, return 0; } +static int lineno_width(int lines) +{ + int i, width; + + for (width = 1, i = 10; i <= lines + 1; width++) + i *= 10; + return width; +} + +static int find_orig_linenum(struct util_info *u, int lineno) +{ + int i; + + for (i = 0; i < u->num_lines; i++) + if (lineno == u->line_map[i]) + return i + 1; + return 0; +} + +static void emit_meta(struct commit *c, int lno, + int sha1_len, int compatibility, int porcelain, + int show_name, int show_number, int show_raw_time, + int longest_file, int longest_author, + int max_digits, int max_orig_digits) +{ + struct util_info *u; + int lineno; + struct commit_info ci; + + u = c->util; + lineno = find_orig_linenum(u, lno); + + if (porcelain) { + int group_size = -1; + struct commit *cc = (lno == 0) ? NULL : blame_lines[lno-1]; + if (cc != c) { + /* This is the beginning of this group */ + int i; + for (i = lno + 1; i < num_blame_lines; i++) + if (blame_lines[i] != c) + break; + group_size = i - lno; + } + if (0 < group_size) + printf("%s %d %d %d\n", sha1_to_hex(c->object.sha1), + lineno, lno + 1, group_size); + else + printf("%s %d %d\n", sha1_to_hex(c->object.sha1), + lineno, lno + 1); + if (!u->meta_given) { + get_commit_info(c, &ci, 1); + printf("author %s\n", ci.author); + printf("author-mail %s\n", ci.author_mail); + printf("author-time %lu\n", ci.author_time); + printf("author-tz %s\n", ci.author_tz); + printf("committer %s\n", ci.committer); + printf("committer-mail %s\n", ci.committer_mail); + printf("committer-time %lu\n", ci.committer_time); + printf("committer-tz %s\n", ci.committer_tz); + printf("filename "); + if (quote_c_style(u->pathname, NULL, NULL, 0)) + quote_c_style(u->pathname, NULL, stdout, 0); + else + fputs(u->pathname, stdout); + printf("\nsummary %s\n", ci.summary); + + u->meta_given = 1; + } + putchar('\t'); + return; + } + + get_commit_info(c, &ci, 0); + fwrite(sha1_to_hex(c->object.sha1), sha1_len, 1, stdout); + if (compatibility) { + printf("\t(%10s\t%10s\t%d)", ci.author, + format_time(ci.author_time, ci.author_tz, + show_raw_time), + lno + 1); + } + else { + if (show_name) + printf(" %-*.*s", longest_file, longest_file, + u->pathname); + if (show_number) + printf(" %*d", max_orig_digits, + lineno); + printf(" (%-*.*s %10s %*d) ", + longest_author, longest_author, ci.author, + format_time(ci.author_time, ci.author_tz, + show_raw_time), + max_digits, lno + 1); + } +} + int main(int argc, const char **argv) { int i; @@ -747,38 +899,43 @@ int main(int argc, const char **argv) int compatibility = 0; int show_raw_time = 0; int options = 1; - struct commit* start_commit; + struct commit *start_commit; - const char* args[10]; + const char *args[10]; struct rev_info rev; struct commit_info ci; const char *buf; - int max_digits; - int longest_file, longest_author; - int found_rename; + int max_digits, max_orig_digits; + int longest_file, longest_author, longest_file_lines; + int show_name = 0; + int show_number = 0; + int porcelain = 0; - const char* prefix = setup_git_directory(); + const char *prefix = setup_git_directory(); git_config(git_default_config); - for(i = 1; i < argc; i++) { - if(options) { - if(!strcmp(argv[i], "-h") || + for (i = 1; i < argc; i++) { + if (options) { + if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) usage(blame_usage); - else if(!strcmp(argv[i], "-l") || - !strcmp(argv[i], "--long")) { + if (!strcmp(argv[i], "-l") || + !strcmp(argv[i], "--long")) { sha1_len = 40; continue; - } else if(!strcmp(argv[i], "-c") || - !strcmp(argv[i], "--compatibility")) { + } + if (!strcmp(argv[i], "-c") || + !strcmp(argv[i], "--compatibility")) { compatibility = 1; continue; - } else if(!strcmp(argv[i], "-t") || - !strcmp(argv[i], "--time")) { + } + if (!strcmp(argv[i], "-t") || + !strcmp(argv[i], "--time")) { show_raw_time = 1; continue; - } else if(!strcmp(argv[i], "-S")) { + } + if (!strcmp(argv[i], "-S")) { if (i + 1 < argc && !read_ancestry(argv[i + 1], &sha1_p)) { compatibility = 1; @@ -786,33 +943,51 @@ int main(int argc, const char **argv) continue; } usage(blame_usage); - } else if(!strcmp(argv[i], "--")) { + } + if (!strcmp(argv[i], "-f") || + !strcmp(argv[i], "--show-name")) { + show_name = 1; + continue; + } + if (!strcmp(argv[i], "-n") || + !strcmp(argv[i], "--show-number")) { + show_number = 1; + continue; + } + if (!strcmp(argv[i], "-p") || + !strcmp(argv[i], "--porcelain")) { + porcelain = 1; + sha1_len = 40; + show_raw_time = 1; + continue; + } + if (!strcmp(argv[i], "--")) { options = 0; continue; - } else if(argv[i][0] == '-') + } + if (argv[i][0] == '-') usage(blame_usage); - else - options = 0; + options = 0; } - if(!options) { - if(!filename) + if (!options) { + if (!filename) filename = argv[i]; - else if(!commit) + else if (!commit) commit = argv[i]; else usage(blame_usage); } } - if(!filename) + if (!filename) usage(blame_usage); if (commit && sha1_p) usage(blame_usage); - else if(!commit) + else if (!commit) commit = "HEAD"; - if(prefix) + if (prefix) sprintf(filename_buf, "%s%s", prefix, filename); else strcpy(filename_buf, filename); @@ -830,7 +1005,6 @@ int main(int argc, const char **argv) return 1; } - init_revisions(&rev, setup_git_directory()); rev.remove_empty_trees = 1; rev.topo_order = 1; @@ -848,62 +1022,49 @@ int main(int argc, const char **argv) prepare_revision_walk(&rev); process_commits(&rev, filename, &initial); + for (i = 0; i < num_blame_lines; i++) + if (!blame_lines[i]) + blame_lines[i] = initial; + buf = blame_contents; - for (max_digits = 1, i = 10; i <= num_blame_lines + 1; max_digits++) - i *= 10; + max_digits = lineno_width(num_blame_lines); longest_file = 0; longest_author = 0; - found_rename = 0; + longest_file_lines = 0; for (i = 0; i < num_blame_lines; i++) { struct commit *c = blame_lines[i]; - struct util_info* u; - if (!c) - c = initial; + struct util_info *u; u = c->util; - if (!found_rename && strcmp(filename, u->pathname)) - found_rename = 1; + if (!show_name && strcmp(filename, u->pathname)) + show_name = 1; if (longest_file < strlen(u->pathname)) longest_file = strlen(u->pathname); - get_commit_info(c, &ci); + if (longest_file_lines < u->num_lines) + longest_file_lines = u->num_lines; + get_commit_info(c, &ci, 0); if (longest_author < strlen(ci.author)) longest_author = strlen(ci.author); } - for (i = 0; i < num_blame_lines; i++) { - struct commit *c = blame_lines[i]; - struct util_info* u; + max_orig_digits = lineno_width(longest_file_lines); - if (!c) - c = initial; - - u = c->util; - get_commit_info(c, &ci); - fwrite(sha1_to_hex(c->object.sha1), sha1_len, 1, stdout); - if(compatibility) { - printf("\t(%10s\t%10s\t%d)", ci.author, - format_time(ci.author_time, ci.author_tz, - show_raw_time), - i+1); - } else { - if (found_rename) - printf(" %-*.*s", longest_file, longest_file, - u->pathname); - printf(" (%-*.*s %10s %*d) ", - longest_author, longest_author, ci.author, - format_time(ci.author_time, ci.author_tz, - show_raw_time), - max_digits, i+1); - } + for (i = 0; i < num_blame_lines; i++) { + emit_meta(blame_lines[i], i, + sha1_len, compatibility, porcelain, + show_name, show_number, show_raw_time, + longest_file, longest_author, + max_digits, max_orig_digits); - if(i == num_blame_lines - 1) { + if (i == num_blame_lines - 1) { fwrite(buf, blame_len - (buf - blame_contents), 1, stdout); - if(blame_contents[blame_len-1] != '\n') + if (blame_contents[blame_len-1] != '\n') putc('\n', stdout); - } else { - char* next_buf = strchr(buf, '\n') + 1; + } + else { + char *next_buf = strchr(buf, '\n') + 1; fwrite(buf, next_buf - buf, 1, stdout); buf = next_buf; } diff --git a/builtin-annotate.c b/builtin-annotate.c new file mode 100644 index 000000000..25ad47371 --- /dev/null +++ b/builtin-annotate.c @@ -0,0 +1,25 @@ +/* + * "git annotate" builtin alias + * + * Copyright (C) 2006 Ryan Anderson + */ +#include "git-compat-util.h" +#include "exec_cmd.h" + +int cmd_annotate(int argc, const char **argv, const char *prefix) +{ + const char **nargv; + int i; + nargv = xmalloc(sizeof(char *) * (argc + 2)); + + nargv[0] = "blame"; + nargv[1] = "-c"; + + for (i = 1; i < argc; i++) { + nargv[i+1] = argv[i]; + } + nargv[argc + 1] = NULL; + + return execv_git_cmd(nargv); +} + @@ -14,6 +14,7 @@ extern int write_tree(unsigned char *sha1, int missing_ok, const char *prefix); extern void prune_packed_objects(int); extern int cmd_add(int argc, const char **argv, const char *prefix); +extern int cmd_annotate(int argc, const char **argv, const char *prefix); extern int cmd_apply(int argc, const char **argv, const char *prefix); extern int cmd_archive(int argc, const char **argv, const char *prefix); extern int cmd_cat_file(int argc, const char **argv, const char *prefix); diff --git a/git-annotate.perl b/git-annotate.perl deleted file mode 100755 index 215ed26f3..000000000 --- a/git-annotate.perl +++ /dev/null @@ -1,708 +0,0 @@ -#!/usr/bin/perl -# Copyright 2006, Ryan Anderson <ryan@michonline.com> -# -# GPL v2 (See COPYING) -# -# This file is licensed under the GPL v2, or a later version -# at the discretion of Linus Torvalds. - -use warnings; -use strict; -use Getopt::Long; -use POSIX qw(strftime gmtime); -use File::Basename qw(basename dirname); - -sub usage() { - print STDERR "Usage: ${\basename $0} [-s] [-S revs-file] file [ revision ] - -l, --long - Show long rev (Defaults off) - -t, --time - Show raw timestamp (Defaults off) - -r, --rename - Follow renames (Defaults on). - -S, --rev-file revs-file - Use revs from revs-file instead of calling git-rev-list - -h, --help - This message. -"; - - exit(1); -} - -our ($help, $longrev, $rename, $rawtime, $starting_rev, $rev_file) = (0, 0, 1); - -my $rc = GetOptions( "long|l" => \$longrev, - "time|t" => \$rawtime, - "help|h" => \$help, - "rename|r" => \$rename, - "rev-file|S=s" => \$rev_file); -if (!$rc or $help or !@ARGV) { - usage(); -} - -my $filename = shift @ARGV; -if (@ARGV) { - $starting_rev = shift @ARGV; -} - -my @stack = ( - { - 'rev' => defined $starting_rev ? $starting_rev : "HEAD", - 'filename' => $filename, - }, -); - -our @filelines = (); - -if (defined $starting_rev) { - @filelines = git_cat_file($starting_rev, $filename); -} else { - open(F,"<",$filename) - or die "Failed to open filename: $!"; - - while(<F>) { - chomp; - push @filelines, $_; - } - close(F); - -} - -our %revs; -our @revqueue; -our $head; - -my $revsprocessed = 0; -while (my $bound = pop @stack) { - my @revisions = git_rev_list($bound->{'rev'}, $bound->{'filename'}); - foreach my $revinst (@revisions) { - my ($rev, @parents) = @$revinst; - $head ||= $rev; - - if (!defined($rev)) { - $rev = ""; - } - $revs{$rev}{'filename'} = $bound->{'filename'}; - if (scalar @parents > 0) { - $revs{$rev}{'parents'} = \@parents; - next; - } - - if (!$rename) { - next; - } - - my $newbound = find_parent_renames($rev, $bound->{'filename'}); - if ( exists $newbound->{'filename'} && $newbound->{'filename'} ne $bound->{'filename'}) { - push @stack, $newbound; - $revs{$rev}{'parents'} = [$newbound->{'rev'}]; - } - } -} -push @revqueue, $head; -init_claim( defined $starting_rev ? $head : 'dirty'); -unless (defined $starting_rev) { - my $diff = open_pipe("git","diff","HEAD", "--",$filename) - or die "Failed to call git diff to check for dirty state: $!"; - - _git_diff_parse($diff, [$head], "dirty", ( - 'author' => gitvar_name("GIT_AUTHOR_IDENT"), - 'author_date' => sprintf("%s +0000",time()), - ) - ); - close($diff); -} -handle_rev(); - - -my $i = 0; -foreach my $l (@filelines) { - my ($output, $rev, $committer, $date); - if (ref $l eq 'ARRAY') { - ($output, $rev, $committer, $date) = @$l; - if (!$longrev && length($rev) > 8) { - $rev = substr($rev,0,8); - } - } else { - $output = $l; - ($rev, $committer, $date) = ('unknown', 'unknown', 'unknown'); - } - - printf("%s\t(%10s\t%10s\t%d)%s\n", $rev, $committer, - format_date($date), ++$i, $output); -} - -sub init_claim { - my ($rev) = @_; - for (my $i = 0; $i < @filelines; $i++) { - $filelines[$i] = [ $filelines[$i], '', '', '', 1]; - # line, - # rev, - # author, - # date, - # 1 <-- belongs to the original file. - } - $revs{$rev}{'lines'} = \@filelines; -} - - -sub handle_rev { - my $revseen = 0; - my %seen; - while (my $rev = shift @revqueue) { - next if $seen{$rev}++; - - my %revinfo = git_commit_info($rev); - - if (exists $revs{$rev}{parents} && - scalar @{$revs{$rev}{parents}} != 0) { - - git_diff_parse($revs{$rev}{'parents'}, $rev, %revinfo); - push @revqueue, @{$revs{$rev}{'parents'}}; - - } else { - # We must be at the initial rev here, so claim everything that is left. - for (my $i = 0; $i < @{$revs{$rev}{lines}}; $i++) { - if (ref ${$revs{$rev}{lines}}[$i] eq '' || ${$revs{$rev}{lines}}[$i][1] eq '') { - claim_line($i, $rev, $revs{$rev}{lines}, %revinfo); - } - } - } - } -} - - -sub git_rev_list { - my ($rev, $file) = @_; - - my $revlist; - if ($rev_file) { - open($revlist, '<' . $rev_file) - or die "Failed to open $rev_file : $!"; - } else { - $revlist = open_pipe("git-rev-list","--parents","--remove-empty",$rev,"--",$file) - or die "Failed to exec git-rev-list: $!"; - } - - my @revs; - while(my $line = <$revlist>) { - chomp $line; - my ($rev, @parents) = split /\s+/, $line; - push @revs, [ $rev, @parents ]; - } - close($revlist); - - printf("0 revs found for rev %s (%s)\n", $rev, $file) if (@revs == 0); - return @revs; -} - -sub find_parent_renames { - my ($rev, $file) = @_; - - my $patch = open_pipe("git-diff-tree", "-M50", "-r","--name-status", "-z","$rev") - or die "Failed to exec git-diff: $!"; - - local $/ = "\0"; - my %bound; - my $junk = <$patch>; - while (my $change = <$patch>) { - chomp $change; - my $filename = <$patch>; - if (!defined $filename) { - next; - } - chomp $filename; - - if ($change =~ m/^[AMD]$/ ) { - next; - } elsif ($change =~ m/^R/ ) { - my $oldfilename = $filename; - $filename = <$patch>; - chomp $filename; - if ( $file eq $filename ) { - my $parent = git_find_parent($rev, $oldfilename); - @bound{'rev','filename'} = ($parent, $oldfilename); - last; - } - } - } - close($patch); - - return \%bound; -} - - -sub git_find_parent { - my ($rev, $filename) = @_; - - my $revparent = open_pipe("git-rev-list","--remove-empty", "--parents","--max-count=1","$rev","--",$filename) - or die "Failed to open git-rev-list to find a single parent: $!"; - - my $parentline = <$revparent>; - chomp $parentline; - my ($revfound,$parent) = split m/\s+/, $parentline; - - close($revparent); - - return $parent; -} - -sub git_find_all_parents { - my ($rev) = @_; - - my $revparent = open_pipe("git-rev-list","--remove-empty", "--parents","--max-count=1","$rev") - or die "Failed to open git-rev-list to find a single parent: $!"; - - my $parentline = <$revparent>; - chomp $parentline; - my ($origrev, @parents) = split m/\s+/, $parentline; - - close($revparent); - - return @parents; -} - -sub git_merge_base { - my ($rev1, $rev2) = @_; - - my $mb = open_pipe("git-merge-base", $rev1, $rev2) - or die "Failed to open git-merge-base: $!"; - - my $base = <$mb>; - chomp $base; - - close($mb); - - return $base; -} - -# Construct a set of pseudo parents that are in the same order, -# and the same quantity as the real parents, -# but whose SHA1s are as similar to the logical parents -# as possible. -sub get_pseudo_parents { - my ($all, $fake) = @_; - - my @all = @$all; - my @fake = @$fake; - - my @pseudo; - - my %fake = map {$_ => 1} @fake; - my %seenfake; - - my $fakeidx = 0; - foreach my $p (@all) { - if (exists $fake{$p}) { - if ($fake[$fakeidx] ne $p) { - die sprintf("parent mismatch: %s != %s\nall:%s\nfake:%s\n", - $fake[$fakeidx], $p, - join(", ", @all), - join(", ", @fake), - ); - } - - push @pseudo, $p; - $fakeidx++; - $seenfake{$p}++; - - } else { - my $base = git_merge_base($fake[$fakeidx], $p); - if ($base ne $fake[$fakeidx]) { - die sprintf("Result of merge-base doesn't match fake: %s,%s != %s\n", - $fake[$fakeidx], $p, $base); - } - - # The details of how we parse the diffs - # mean that we cannot have a duplicate - # revision in the list, so if we've already - # seen the revision we would normally add, just use - # the actual revision. - if ($seenfake{$base}) { - push @pseudo, $p; - } else { - push @pseudo, $base; - $seenfake{$base}++; - } - } - } - - return @pseudo; -} - - -# Get a diff between the current revision and a parent. -# Record the commit information that results. -sub git_diff_parse { - my ($parents, $rev, %revinfo) = @_; - - my @pseudo_parents; - my @command = ("git-diff-tree"); - my $revision_spec; - - if (scalar @$parents == 1) { - - $revision_spec = join("..", $parents->[0], $rev); - @pseudo_parents = @$parents; - } else { - my @all_parents = git_find_all_parents($rev); - - if (@all_parents != @$parents) { - @pseudo_parents = get_pseudo_parents(\@all_parents, $parents); - } else { - @pseudo_parents = @$parents; - } - - $revision_spec = $rev; - push @command, "-c"; - } - - my @filenames = ( $revs{$rev}{'filename'} ); - - foreach my $parent (@$parents) { - push @filenames, $revs{$parent}{'filename'}; - } - - push @command, "-p", "-M", $revision_spec, "--", @filenames; - - - my $diff = open_pipe( @command ) - or die "Failed to call git-diff for annotation: $!"; - - _git_diff_parse($diff, \@pseudo_parents, $rev, %revinfo); - - close($diff); -} - -sub _git_diff_parse { - my ($diff, $parents, $rev, %revinfo) = @_; - - my $ri = 0; - - my $slines = $revs{$rev}{'lines'}; - my (%plines, %pi); - - my $gotheader = 0; - my ($remstart); - my $parent_count = @$parents; - - my $diff_header_regexp = "^@"; - $diff_header_regexp .= "@" x @$parents; - $diff_header_regexp .= ' -\d+,\d+' x @$parents; - $diff_header_regexp .= ' \+(\d+),\d+'; - $diff_header_regexp .= " " . ("@" x @$parents); - - my %claim_regexps; - my $allparentplus = '^' . '\\+' x @$parents . '(.*)$'; - - { - my $i = 0; - foreach my $parent (@$parents) { - - $pi{$parent} = 0; - my $r = '^' . '.' x @$parents . '(.*)$'; - my $p = $r; - substr($p,$i+1, 1) = '\\+'; - - my $m = $r; - substr($m,$i+1, 1) = '-'; - - $claim_regexps{$parent}{plus} = $p; - $claim_regexps{$parent}{minus} = $m; - - $plines{$parent} = []; - - $i++; - } - } - - DIFF: - while(<$diff>) { - chomp; - #printf("%d:%s:\n", $gotheader, $_); - if (m/$diff_header_regexp/) { - $remstart = $1 - 1; - # (0-based arrays) - - $gotheader = 1; - - foreach my $parent (@$parents) { - for (my $i = $ri; $i < $remstart; $i++) { - $plines{$parent}[$pi{$parent}++] = $slines->[$i]; - } - } - $ri = $remstart; - - next DIFF; - - } elsif (!$gotheader) { - # Skip over the leadin. - next DIFF; - } - - if (m/^\\/) { - ; - # Skip \No newline at end of file. - # But this can be internationalized, so only look - # for an initial \ - - } else { - my %claims = (); - my $negclaim = 0; - my $allclaimed = 0; - my $line; - - if (m/$allparentplus/) { - claim_line($ri, $rev, $slines, %revinfo); - $allclaimed = 1; - - } - - PARENT: - foreach my $parent (keys %claim_regexps) { - my $m = $claim_regexps{$parent}{minus}; - my $p = $claim_regexps{$parent}{plus}; - - if (m/$m/) { - $line = $1; - $plines{$parent}[$pi{$parent}++] = [ $line, '', '', '', 0 ]; - $negclaim++; - - } elsif (m/$p/) { - $line = $1; - if (get_line($slines, $ri) eq $line) { - # Found a match, claim - $claims{$parent}++; - - } else { - die sprintf("Sync error: %d\n|%s\n|%s\n%s => %s\n", - $ri, $line, - get_line($slines, $ri), - $rev, $parent); - } - } - } - - if (%claims) { - foreach my $parent (@$parents) { - next if $claims{$parent} || $allclaimed; - $plines{$parent}[$pi{$parent}++] = $slines->[$ri]; - #[ $line, '', '', '', 0 ]; - } - $ri++; - - } elsif ($negclaim) { - next DIFF; - - } else { - if (substr($_,scalar @$parents) ne get_line($slines,$ri) ) { - foreach my $parent (@$parents) { - printf("parent %s is on line %d\n", $parent, $pi{$parent}); - } - - my @context; - for (my $i = -2; $i < 2; $i++) { - push @context, get_line($slines, $ri + $i); - } - my $context = join("\n", @context); - - my $justline = substr($_, scalar @$parents); - die sprintf("Line %d, does not match:\n|%s|\n|%s|\n%s\n", - $ri, - $justline, - $context); - } - foreach my $parent (@$parents) { - $plines{$parent}[$pi{$parent}++] = $slines->[$ri]; - } - $ri++; - } - } - } - - for (my $i = $ri; $i < @{$slines} ; $i++) { - foreach my $parent (@$parents) { - push @{$plines{$parent}}, $slines->[$ri]; - } - $ri++; - } - - foreach my $parent (@$parents) { - $revs{$parent}{lines} = $plines{$parent}; - } - - return; -} - -sub get_line { - my ($lines, $index) = @_; - - return ref $lines->[$index] ne '' ? $lines->[$index][0] : $lines->[$index]; -} - -sub git_cat_file { - my ($rev, $filename) = @_; - return () unless defined $rev && defined $filename; - - my $blob = git_ls_tree($rev, $filename); - die "Failed to find a blob for $filename in rev $rev\n" if !defined $blob; - - my $catfile = open_pipe("git","cat-file", "blob", $blob) - or die "Failed to git-cat-file blob $blob (rev $rev, file $filename): " . $!; - - my @lines; - while(<$catfile>) { - chomp; - push @lines, $_; - } - close($catfile); - - return @lines; -} - -sub git_ls_tree { - my ($rev, $filename) = @_; - - my $lstree = open_pipe("git","ls-tree",$rev,$filename) - or die "Failed to call git ls-tree: $!"; - - my ($mode, $type, $blob, $tfilename); - while(<$lstree>) { - chomp; - ($mode, $type, $blob, $tfilename) = split(/\s+/, $_, 4); - last if ($tfilename eq $filename); - } - close($lstree); - - return $blob if ($tfilename eq $filename); - die "git-ls-tree failed to find blob for $filename"; - -} - - - -sub claim_line { - my ($floffset, $rev, $lines, %revinfo) = @_; - my $oline = get_line($lines, $floffset); - @{$lines->[$floffset]} = ( $oline, $rev, - $revinfo{'author'}, $revinfo{'author_date'} ); - #printf("Claiming line %d with rev %s: '%s'\n", - # $floffset, $rev, $oline) if 1; -} - -sub git_commit_info { - my ($rev) = @_; - my $commit = open_pipe("git-cat-file", "commit", $rev) - or die "Failed to call git-cat-file: $!"; - - my %info; - while(<$commit>) { - chomp; - last if (length $_ == 0); - - if (m/^author (.*) <(.*)> (.*)$/) { - $info{'author'} = $1; - $info{'author_email'} = $2; - $info{'author_date'} = $3; - } elsif (m/^committer (.*) <(.*)> (.*)$/) { - $info{'committer'} = $1; - $info{'committer_email'} = $2; - $info{'committer_date'} = $3; - } - } - close($commit); - - return %info; -} - -sub format_date { - if ($rawtime) { - return $_[0]; - } - my ($timestamp, $timezone) = split(' ', $_[0]); - my $minutes = abs($timezone); - $minutes = int($minutes / 100) * 60 + ($minutes % 100); - if ($timezone < 0) { - $minutes = -$minutes; - } - my $t = $timestamp + $minutes * 60; - return strftime("%Y-%m-%d %H:%M:%S " . $timezone, gmtime($t)); -} - -# Copied from git-send-email.perl - We need a Git.pm module.. -sub gitvar { - my ($var) = @_; - my $fh; - my $pid = open($fh, '-|'); - die "$!" unless defined $pid; - if (!$pid) { - exec('git-var', $var) or die "$!"; - } - my ($val) = <$fh>; - close $fh or die "$!"; - chomp($val); - return $val; -} - -sub gitvar_name { - my ($name) = @_; - my $val = gitvar($name); - my @field = split(/\s+/, $val); - return join(' ', @field[0...(@field-4)]); -} - -sub open_pipe { - if ($^O eq '##INSERT_ACTIVESTATE_STRING_HERE##') { - return open_pipe_activestate(@_); - } else { - return open_pipe_normal(@_); - } -} - -sub open_pipe_activestate { - tie *fh, "Git::ActiveStatePipe", @_; - return *fh; -} - -sub open_pipe_normal { - my (@execlist) = @_; - - my $pid = open my $kid, "-|"; - defined $pid or die "Cannot fork: $!"; - - unless ($pid) { - exec @execlist; - die "Cannot exec @execlist: $!"; - } - - return $kid; -} - -package Git::ActiveStatePipe; -use strict; - -sub TIEHANDLE { - my ($class, @params) = @_; - my $cmdline = join " ", @params; - my @data = qx{$cmdline}; - bless { i => 0, data => \@data }, $class; -} - -sub READLINE { - my $self = shift; - if ($self->{i} >= scalar @{$self->{data}}) { - return undef; - } - return $self->{'data'}->[ $self->{i}++ ]; -} - -sub CLOSE { - my $self = shift; - delete $self->{data}; - delete $self->{i}; -} - -sub EOF { - my $self = shift; - return ($self->{i} >= scalar @{$self->{data}}); -} @@ -219,6 +219,7 @@ static void handle_internal_command(int argc, const char **argv, char **envp) int option; } commands[] = { { "add", cmd_add, RUN_SETUP }, + { "annotate", cmd_annotate, }, { "apply", cmd_apply }, { "archive", cmd_archive }, { "cat-file", cmd_cat_file, RUN_SETUP }, diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl index bc8d8eb23..9eb19bb82 100755 --- a/gitweb/gitweb.perl +++ b/gitweb/gitweb.perl @@ -41,8 +41,18 @@ our $home_link_str = "++GITWEB_HOME_LINK_STR++"; # replace this with something more descriptive for clearer bookmarks our $site_name = "++GITWEB_SITENAME++" || $ENV{'SERVER_NAME'} || "Untitled"; +# filename of html text to include at top of each page +our $site_header = "++GITWEB_SITE_HEADER++"; # html text to include at home page our $home_text = "++GITWEB_HOMETEXT++"; +# filename of html text to include at bottom of each page +our $site_footer = "++GITWEB_SITE_FOOTER++"; + +# URI of stylesheets +our @stylesheets = ("++GITWEB_CSS++"); +our $stylesheet; +# default is not to define style sheet, but it can be overwritten later +undef $stylesheet; # URI of default stylesheet our $stylesheet = "++GITWEB_CSS++"; @@ -217,6 +227,22 @@ sub feature_pickaxe { return ($_[0]); } +# checking HEAD file with -e is fragile if the repository was +# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed +# and then pruned. +sub check_head_link { + my ($dir) = @_; + my $headfile = "$dir/HEAD"; + return ((-e $headfile) || + (-l $headfile && readlink($headfile) =~ /^refs\/heads\//)); +} + +sub check_export_ok { + my ($dir) = @_; + return (check_head_link($dir) && + (!$export_ok || -e "$dir/$export_ok")); +} + # rename detection options for git-diff and git-diff-tree # - default is '-M', with the cost proportional to # (number of removed files) * (number of new files). @@ -249,7 +275,7 @@ our $project = $cgi->param('p'); if (defined $project) { if (!validate_pathname($project) || !(-d "$projectroot/$project") || - !(-e "$projectroot/$project/HEAD") || + !check_head_link("$projectroot/$project") || ($export_ok && !(-e "$projectroot/$project/$export_ok")) || ($strict_export && !project_in_list($project))) { undef $project; @@ -326,7 +352,7 @@ sub evaluate_path_info { # find which part of PATH_INFO is project $project = $path_info; $project =~ s,/+$,,; - while ($project && !-e "$projectroot/$project/HEAD") { + while ($project && !check_head_link("$projectroot/$project")) { $project =~ s,/*[^/]*$,,; } # validate project @@ -878,8 +904,7 @@ sub git_get_projects_list { my $subdir = substr($File::Find::name, $pfxlen + 1); # we check related file in $projectroot - if (-e "$projectroot/$subdir/HEAD" && (!$export_ok || - -e "$projectroot/$subdir/$export_ok")) { + if (check_export_ok("$projectroot/$subdir")) { push @list, { path => $subdir }; $File::Find::prune = 1; } @@ -900,8 +925,7 @@ sub git_get_projects_list { if (!defined $path) { next; } - if (-e "$projectroot/$path/HEAD" && (!$export_ok || - -e "$projectroot/$path/$export_ok")) { + if (check_export_ok("$projectroot/$path")) { my $pr = { path => $path, owner => to_utf8($owner), @@ -1011,6 +1035,9 @@ sub parse_date { $date{'hour_local'} = $hour; $date{'minute_local'} = $min; $date{'tz_local'} = $tz; + $date{'iso-tz'} = sprintf ("%04d-%02d-%02d %02d:%02d:%02d %s", + 1900+$year, $mon+1, $mday, + $hour, $min, $sec, $tz); return %date; } @@ -1416,8 +1443,17 @@ sub git_header_html { <meta name="generator" content="gitweb/$version git/$git_version"/> <meta name="robots" content="index, nofollow"/> <title>$title</title> -<link rel="stylesheet" type="text/css" href="$stylesheet"/> EOF +# print out each stylesheet that exist + if (defined $stylesheet) { +#provides backwards capability for those people who define style sheet in a config file + print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n"; + } else { + foreach my $stylesheet (@stylesheets) { + next unless $stylesheet; + print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n"; + } + } if (defined $project) { printf('<link rel="alternate" title="%s log" '. 'href="%s" type="application/rss+xml"/>'."\n", @@ -1435,8 +1471,15 @@ EOF } print "</head>\n" . - "<body>\n" . - "<div class=\"page_header\">\n" . + "<body>\n"; + + if (-f $site_header) { + open (my $fd, $site_header); + print <$fd>; + close $fd; + } + + print "<div class=\"page_header\">\n" . $cgi->a({-href => esc_url($logo_url), -title => $logo_label}, qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>)); @@ -1488,8 +1531,15 @@ sub git_footer_html { print $cgi->a({-href => href(project=>undef, action=>"project_index"), -class => "rss_logo"}, "TXT") . "\n"; } - print "</div>\n" . - "</body>\n" . + print "</div>\n" ; + + if (-f $site_footer) { + open (my $fd, $site_footer); + print <$fd>; + close $fd; + } + + print "</body>\n" . "</html>"; } @@ -2518,7 +2568,8 @@ sub git_blame2 { if ($ftype !~ "blob") { die_error("400 Bad Request", "Object is not a blob"); } - open ($fd, "-|", git_cmd(), "blame", '-l', '--', $file_name, $hash_base) + open ($fd, "-|", git_cmd(), "blame", '-p', '--', + $file_name, $hash_base) or die_error(undef, "Open git-blame failed"); git_header_html(); my $formats_nav = @@ -2542,25 +2593,52 @@ sub git_blame2 { <table class="blame"> <tr><th>Commit</th><th>Line</th><th>Data</th></tr> HTML - while (<$fd>) { - /^([0-9a-fA-F]{40}).*?(\d+)\)\s{1}(\s*.*)/; - my $full_rev = $1; + my %metainfo = (); + while (1) { + $_ = <$fd>; + last unless defined $_; + my ($full_rev, $orig_lineno, $lineno, $group_size) = + /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/; + if (!exists $metainfo{$full_rev}) { + $metainfo{$full_rev} = {}; + } + my $meta = $metainfo{$full_rev}; + while (<$fd>) { + last if (s/^\t//); + if (/^(\S+) (.*)$/) { + $meta->{$1} = $2; + } + } + my $data = $_; my $rev = substr($full_rev, 0, 8); - my $lineno = $2; - my $data = $3; - - if (!defined $last_rev) { - $last_rev = $full_rev; - } elsif ($last_rev ne $full_rev) { - $last_rev = $full_rev; + my $author = $meta->{'author'}; + my %date = parse_date($meta->{'author-time'}, + $meta->{'author-tz'}); + my $date = $date{'iso-tz'}; + if ($group_size) { $current_color = ++$current_color % $num_colors; } print "<tr class=\"$rev_color[$current_color]\">\n"; - print "<td class=\"sha1\">" . - $cgi->a({-href => href(action=>"commit", hash=>$full_rev, file_name=>$file_name)}, - esc_html($rev)) . "</td>\n"; - print "<td class=\"linenr\"><a id=\"l$lineno\" href=\"#l$lineno\" class=\"linenr\">" . - esc_html($lineno) . "</a></td>\n"; + if ($group_size) { + print "<td class=\"sha1\""; + print " title=\"$author, $date\""; + print " rowspan=\"$group_size\"" if ($group_size > 1); + print ">"; + print $cgi->a({-href => href(action=>"commit", + hash=>$full_rev, + file_name=>$file_name)}, + esc_html($rev)); + print "</td>\n"; + } + my $blamed = href(action => 'blame', + file_name => $meta->{'filename'}, + hash_base => $full_rev); + print "<td class=\"linenr\">"; + print $cgi->a({ -href => "$blamed#l$orig_lineno", + -id => "l$lineno", + -class => "linenr" }, + esc_html($lineno)); + print "</td>"; print "<td class=\"pre\">" . esc_html($data) . "</td>\n"; print "</tr>\n"; } |