1
0
Fork 0
mirror of https://github.com/git/git.git synced 2024-05-27 13:26:12 +02:00

Merge branch 'jc/web-blame'

* jc/web-blame:
  gitweb: spell "blame --porcelain" with -p
  blame: Document and add help text for -f, -n, and -p
  gitweb: blame porcelain: lineno and orig lineno swapped
  Remove git-annotate.perl and create a builtin-alias for git-blame
  gitweb: use blame --porcelain
  git-blame --porcelain
  blame.c: move code to output metainfo into a separate function.
  git-blame: --show-number (and -n)
  git-blame: --show-name (and -f)
  blame.c: whitespace and formatting clean-up.
  Gitweb - provide site headers and footers
  gitweb: blame: Mouse-over commit-8 shows author and date
  gitweb: blame: print commit-8 on the leading row of a commit-block
  Revert 954a618375
  gitweb: prepare for repositories with packed refs.
  gitweb: make leftmost column of blame less cluttered.
This commit is contained in:
Junio C Hamano 2006-10-25 13:18:06 -07:00
commit 8e95026f29
8 changed files with 482 additions and 882 deletions

View File

@ -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]

View File

@ -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 $@+ $@

451
blame.c
View File

@ -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];
char *tmp, *endp;
tmp = strstr(commit->buffer, "\nauthor ") + 8;
len = strchr(tmp, '\n') - tmp;
ret->author = author_buf;
memcpy(ret->author, tmp, len);
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);
}
max_orig_digits = lineno_width(longest_file_lines);
for (i = 0; i < num_blame_lines; i++) {
struct commit *c = blame_lines[i];
struct util_info* u;
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 (!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);
}
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;
}

25
builtin-annotate.c Normal file
View File

@ -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);
}

View File

@ -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);

View File

@ -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}});
}

1
git.c
View File

@ -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 },

View File

@ -41,8 +41,18 @@
# 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 @@ sub feature_pickaxe {
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 @@ sub git_header_html {
}
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";
}