1
0
Fork 0
mirror of https://github.com/git/git.git synced 2024-05-23 05:36:09 +02:00
git/blame.c
Shawn Pearce e702496e43 Convert memcpy(a,b,20) to hashcpy(a,b).
This abstracts away the size of the hash values when copying them
from memory location to memory location, much as the introduction
of hashcmp abstracted away hash value comparsion.

A few call sites were using char* rather than unsigned char* so
I added the cast rather than open hashcpy to be void*.  This is a
reasonable tradeoff as most call sites already use unsigned char*
and the existing hashcmp is also declared to be unsigned char*.

[jc: Splitted the patch to "master" part, to be followed by a
 patch for merge-recursive.c which is not in "master" yet.

 Fixed the cast in the latter hunk to combine-diff.c which was
 wrong in the original.

 Also converted ones left-over in combine-diff.c, diff-lib.c and
 upload-pack.c ]

Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
Signed-off-by: Junio C Hamano <junkio@cox.net>
2006-08-23 13:53:10 -07:00

921 lines
20 KiB
C

/*
* Copyright (C) 2006, Fredrik Kuivinen <freku045@student.liu.se>
*/
#include <assert.h>
#include <time.h>
#include <sys/time.h>
#include <math.h>
#include "cache.h"
#include "refs.h"
#include "tag.h"
#include "commit.h"
#include "tree.h"
#include "blob.h"
#include "diff.h"
#include "diffcore.h"
#include "revision.h"
#include "xdiff-interface.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 struct commit **blame_lines;
static int num_blame_lines;
static char* blame_contents;
static int blame_len;
struct util_info {
int *line_map;
unsigned char sha1[20]; /* blob sha, not commit! */
char *buf;
unsigned long size;
int num_lines;
const char* pathname;
void* topo_data;
};
struct chunk {
int off1, len1; /* --- */
int off2, len2; /* +++ */
};
struct patch {
struct chunk *chunks;
int num;
};
static void get_blob(struct commit *commit);
/* Only used for statistics */
static int num_get_patch;
static int num_commits;
static int patch_time;
struct blame_diff_state {
struct xdiff_emit_state xm;
struct patch *ret;
};
static void process_u0_diff(void *state_, char *line, unsigned long len)
{
struct blame_diff_state *state = state_;
struct chunk *chunk;
if (len < 4 || line[0] != '@' || line[1] != '@')
return;
if (DEBUG)
printf("chunk line: %.*s", (int)len, line);
state->ret->num++;
state->ret->chunks = xrealloc(state->ret->chunks,
sizeof(struct chunk) * state->ret->num);
chunk = &state->ret->chunks[state->ret->num - 1];
assert(!strncmp(line, "@@ -", 4));
if (parse_hunk_header(line, len,
&chunk->off1, &chunk->len1,
&chunk->off2, &chunk->len2)) {
state->ret->num--;
return;
}
if (chunk->len1 == 0)
chunk->off1++;
if (chunk->len2 == 0)
chunk->off2++;
if (chunk->off1 > 0)
chunk->off1--;
if (chunk->off2 > 0)
chunk->off2--;
assert(chunk->off1 >= 0);
assert(chunk->off2 >= 0);
}
static struct patch *get_patch(struct commit *commit, struct commit *other)
{
struct blame_diff_state state;
xpparam_t xpp;
xdemitconf_t xecfg;
mmfile_t file_c, file_o;
xdemitcb_t ecb;
struct util_info *info_c = (struct util_info *)commit->util;
struct util_info *info_o = (struct util_info *)other->util;
struct timeval tv_start, tv_end;
get_blob(commit);
file_c.ptr = info_c->buf;
file_c.size = info_c->size;
get_blob(other);
file_o.ptr = info_o->buf;
file_o.size = info_o->size;
gettimeofday(&tv_start, NULL);
xpp.flags = XDF_NEED_MINIMAL;
xecfg.ctxlen = 0;
xecfg.flags = 0;
ecb.outf = xdiff_outf;
ecb.priv = &state;
memset(&state, 0, sizeof(state));
state.xm.consume = process_u0_diff;
state.ret = xmalloc(sizeof(struct patch));
state.ret->chunks = NULL;
state.ret->num = 0;
xdl_diff(&file_c, &file_o, &xpp, &xecfg, &ecb);
gettimeofday(&tv_end, NULL);
patch_time += 1000000 * (tv_end.tv_sec - tv_start.tv_sec) +
tv_end.tv_usec - tv_start.tv_usec;
num_get_patch++;
return state.ret;
}
static void free_patch(struct patch *p)
{
free(p->chunks);
free(p);
}
static int get_blob_sha1_internal(const unsigned char *sha1, const char *base,
int baselen, const char *pathname,
unsigned mode, int stage);
static unsigned char blob_sha1[20];
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;
pathspec[1] = NULL;
memset(blob_sha1, 0, sizeof(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)
return -1;
hashcpy(sha1, blob_sha1);
return 0;
}
static int get_blob_sha1_internal(const unsigned char *sha1, const char *base,
int baselen, const char *pathname,
unsigned mode, int stage)
{
if (S_ISDIR(mode))
return READ_TREE_RECURSIVE;
if (strncmp(blame_file, base, baselen) ||
strcmp(blame_file + baselen, pathname))
return -1;
hashcpy(blob_sha1, sha1);
return -1;
}
static void get_blob(struct commit *commit)
{
struct util_info *info = commit->util;
char type[20];
if (info->buf)
return;
info->buf = read_sha1_file(info->sha1, type, &info->size);
assert(!strcmp(type, blob_type));
}
/* For debugging only */
static void print_patch(struct patch *p)
{
int i;
printf("Num chunks: %d\n", p->num);
for (i = 0; i < p->num; i++) {
printf("%d,%d %d,%d\n", p->chunks[i].off1, p->chunks[i].len1,
p->chunks[i].off2, p->chunks[i].len2);
}
}
#if DEBUG
/* For debugging only */
static void print_map(struct commit *cmit, struct commit *other)
{
struct util_info *util = cmit->util;
struct util_info *util2 = other->util;
int i;
int max =
util->num_lines >
util2->num_lines ? util->num_lines : util2->num_lines;
int num;
for (i = 0; i < max; i++) {
printf("i: %d ", i);
num = -1;
if (i < util->num_lines) {
num = util->line_map[i];
printf("%d\t", num);
} else
printf("\t");
if (i < util2->num_lines) {
int num2 = util2->line_map[i];
printf("%d\t", num2);
if (num != -1 && num2 != num)
printf("---");
} else
printf("\t");
printf("\n");
}
}
#endif
/* p is a patch from commit to other. */
static void fill_line_map(struct commit *commit, struct commit *other,
struct patch *p)
{
struct util_info *util = commit->util;
struct util_info *util2 = other->util;
int *map = util->line_map;
int *map2 = util2->line_map;
int cur_chunk = 0;
int i1, i2;
if (p->num && DEBUG)
print_patch(p);
if (DEBUG)
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;
if (cur_chunk < p->num)
chunk = &p->chunks[cur_chunk];
if (chunk && chunk->off1 == i1) {
if (DEBUG && i2 != chunk->off2)
printf("i2: %d off2: %d\n", i2, chunk->off2);
assert(i2 == chunk->off2);
i1--;
i2--;
if (chunk->len1 > 0)
i1 += chunk->len1;
if (chunk->len2 > 0)
i2 += chunk->len2;
cur_chunk++;
} else {
if (i2 >= util2->num_lines)
break;
if (map[i1] != map2[i2] && map[i1] != -1) {
if (DEBUG)
printf("map: i1: %d %d %p i2: %d %d %p\n",
i1, map[i1],
(void *) (i1 != -1 ? blame_lines[map[i1]] : NULL),
i2, map2[i2],
(void *) (i2 != -1 ? blame_lines[map2[i2]] : NULL));
if (map2[i2] != -1 &&
blame_lines[map[i1]] &&
!blame_lines[map2[i2]])
map[i1] = map2[i2];
}
if (map[i1] == -1 && map2[i2] != -1)
map[i1] = map2[i2];
}
if (DEBUG > 1)
printf("l1: %d l2: %d i1: %d i2: %d\n",
map[i1], map2[i2], i1, i2);
}
}
static int map_line(struct commit *commit, int line)
{
struct util_info *info = commit->util;
assert(line >= 0 && line < info->num_lines);
return info->line_map[line];
}
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->num_lines = -1;
util->pathname = NULL;
commit->util = util;
return util;
}
static int fill_util_info(struct commit *commit)
{
struct util_info *util = commit->util;
assert(util);
assert(util->pathname);
return !!get_blob_sha1(commit->tree, util->pathname, util->sha1);
}
static void alloc_line_map(struct commit *commit)
{
struct util_info *util = commit->util;
int i;
if (util->line_map)
return;
get_blob(commit);
util->num_lines = 0;
for (i = 0; i < util->size; i++) {
if (util->buf[i] == '\n')
util->num_lines++;
}
if(util->buf[util->size - 1] != '\n')
util->num_lines++;
util->line_map = xmalloc(sizeof(int) * util->num_lines);
for (i = 0; i < util->num_lines; i++)
util->line_map[i] = -1;
}
static void init_first_commit(struct commit* commit, const char* filename)
{
struct util_info* util = commit->util;
int i;
util->pathname = filename;
if (fill_util_info(commit))
die("fill_util_info failed");
alloc_line_map(commit);
util = commit->util;
for (i = 0; i < util->num_lines; i++)
util->line_map[i] = i;
}
static void process_commits(struct rev_info *rev, const char *path,
struct commit** initial)
{
int i;
struct util_info* util;
int lines_left;
int *blame_p;
int *new_lines;
int new_lines_len;
struct commit* commit = get_revision(rev);
assert(commit);
init_first_commit(commit, path);
util = commit->util;
num_blame_lines = util->num_lines;
blame_lines = xmalloc(sizeof(struct commit *) * num_blame_lines);
blame_contents = util->buf;
blame_len = util->size;
for (i = 0; i < num_blame_lines; i++)
blame_lines[i] = NULL;
lines_left = num_blame_lines;
blame_p = xmalloc(sizeof(int) * num_blame_lines);
new_lines = xmalloc(sizeof(int) * num_blame_lines);
do {
struct commit_list *parents;
int num_parents;
struct util_info *util;
if (DEBUG)
printf("\nProcessing commit: %d %s\n", num_commits,
sha1_to_hex(commit->object.sha1));
if (lines_left == 0)
return;
num_commits++;
memset(blame_p, 0, sizeof(int) * num_blame_lines);
new_lines_len = 0;
num_parents = 0;
for (parents = commit->parents;
parents != NULL; parents = parents->next)
num_parents++;
if(num_parents == 0)
*initial = commit;
if (fill_util_info(commit))
continue;
alloc_line_map(commit);
util = commit->util;
for (parents = commit->parents;
parents != NULL; parents = parents->next) {
struct commit *parent = parents->item;
struct patch *patch;
if (parse_commit(parent) < 0)
die("parse_commit error");
if (DEBUG)
printf("parent: %s\n",
sha1_to_hex(parent->object.sha1));
if (fill_util_info(parent)) {
num_parents--;
continue;
}
patch = get_patch(parent, commit);
alloc_line_map(parent);
fill_line_map(parent, commit, patch);
for (i = 0; i < patch->num; i++) {
int l;
for (l = 0; l < patch->chunks[i].len2; l++) {
int mapped_line =
map_line(commit, patch->chunks[i].off2 + l);
if (mapped_line != -1) {
blame_p[mapped_line]++;
if (blame_p[mapped_line] == num_parents)
new_lines[new_lines_len++] = mapped_line;
}
}
}
free_patch(patch);
}
if (DEBUG)
printf("parents: %d\n", num_parents);
for (i = 0; i < new_lines_len; i++) {
int mapped_line = new_lines[i];
if (blame_lines[mapped_line] == NULL) {
blame_lines[mapped_line] = commit;
lines_left--;
if (DEBUG)
printf("blame: mapped: %d i: %d\n",
mapped_line, i);
}
}
} while ((commit = get_revision(rev)) != NULL);
}
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;
paths[0] = util->pathname;
paths[1] = NULL;
diff_tree_setup_paths(get_pathspec(revs->prefix, paths),
&revs->pruning);
ret = rev_compare_tree(revs, c1->tree, c2->tree);
diff_tree_release_paths(&revs->pruning);
return ret;
}
static int same_tree_as_empty_path(struct rev_info *revs, struct tree* t1,
const char* path)
{
int ret;
const char* paths[2];
paths[0] = path;
paths[1] = NULL;
diff_tree_setup_paths(get_pathspec(revs->prefix, paths),
&revs->pruning);
ret = rev_same_tree_as_empty(revs, t1);
diff_tree_release_paths(&revs->pruning);
return ret;
}
static const char* find_rename(struct commit* commit, struct commit* parent)
{
struct util_info* cutil = commit->util;
struct diff_options diff_opts;
const char *paths[1];
int i;
if (DEBUG) {
printf("find_rename commit: %s ",
sha1_to_hex(commit->object.sha1));
puts(sha1_to_hex(parent->object.sha1));
}
diff_setup(&diff_opts);
diff_opts.recursive = 1;
diff_opts.detect_rename = DIFF_DETECT_RENAME;
paths[0] = NULL;
diff_tree_setup_paths(paths, &diff_opts);
if (diff_setup_done(&diff_opts) < 0)
die("diff_setup_done failed");
diff_tree_sha1(commit->tree->object.sha1, parent->tree->object.sha1,
"", &diff_opts);
diffcore_std(&diff_opts);
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 (DEBUG)
printf("rename %s -> %s\n", p->one->path, p->two->path);
return p->two->path;
}
}
return 0;
}
static void simplify_commit(struct rev_info *revs, struct commit *commit)
{
struct commit_list **pp, *parent;
if (!commit->tree)
return;
if (!commit->parents) {
struct util_info* util = commit->util;
if (!same_tree_as_empty_path(revs, commit->tree,
util->pathname))
commit->object.flags |= TREECHANGE;
return;
}
pp = &commit->parents;
while ((parent = *pp) != NULL) {
struct commit *p = parent->item;
if (p->object.flags & UNINTERESTING) {
pp = &parent->next;
continue;
}
parse_commit(p);
switch (compare_tree_path(revs, p, commit)) {
case REV_TREE_SAME:
parent->next = NULL;
commit->parents = parent;
get_util(p)->pathname = get_util(commit)->pathname;
return;
case REV_TREE_NEW:
{
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);
if (new_name) {
struct util_info* putil = get_util(p);
if (!putil->pathname)
putil->pathname = strdup(new_name);
} else {
*pp = parent->next;
continue;
}
}
}
/* fallthrough */
case REV_TREE_DIFFERENT:
pp = &parent->next;
if (!get_util(p)->pathname)
get_util(p)->pathname =
get_util(commit)->pathname;
continue;
}
die("bad tree compare for commit %s",
sha1_to_hex(commit->object.sha1));
}
commit->object.flags |= TREECHANGE;
}
struct commit_info
{
char* author;
char* author_mail;
unsigned long author_time;
char* author_tz;
};
static void get_commit_info(struct commit* commit, struct commit_info* ret)
{
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);
tmp = ret->author;
tmp += len;
*tmp = 0;
while(*tmp != ' ')
tmp--;
ret->author_tz = tmp+1;
*tmp = 0;
while(*tmp != ' ')
tmp--;
ret->author_time = strtoul(tmp, NULL, 10);
*tmp = 0;
while(*tmp != ' ')
tmp--;
ret->author_mail = tmp + 1;
*tmp = 0;
}
static const char* format_time(unsigned long time, const char* tz_str,
int show_raw_time)
{
static char time_buf[128];
time_t t = time;
int minutes, tz;
struct tm *tm;
if (show_raw_time) {
sprintf(time_buf, "%lu %s", time, tz_str);
return time_buf;
}
tz = atoi(tz_str);
minutes = tz < 0 ? -tz : tz;
minutes = (minutes / 100)*60 + (minutes % 100);
minutes = tz < 0 ? -minutes : minutes;
t = time + minutes * 60;
tm = gmtime(&t);
strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S ", tm);
strcat(time_buf, tz_str);
return time_buf;
}
static void topo_setter(struct commit* c, void* data)
{
struct util_info* util = c->util;
util->topo_data = data;
}
static void* topo_getter(struct commit* c)
{
struct util_info* util = c->util;
return util->topo_data;
}
static int read_ancestry(const char *graft_file,
unsigned char **start_sha1)
{
FILE *fp = fopen(graft_file, "r");
char buf[1024];
if (!fp)
return -1;
while (fgets(buf, sizeof(buf), fp)) {
/* The format is just "Commit Parent1 Parent2 ...\n" */
int len = strlen(buf);
struct commit_graft *graft = read_graft_line(buf, len);
register_commit_graft(graft, 0);
if (!*start_sha1)
*start_sha1 = graft->sha1;
}
fclose(fp);
return 0;
}
int main(int argc, const char **argv)
{
int i;
struct commit *initial = NULL;
unsigned char sha1[20], *sha1_p = NULL;
const char *filename = NULL, *commit = NULL;
char filename_buf[256];
int sha1_len = 8;
int compatibility = 0;
int show_raw_time = 0;
int options = 1;
struct commit* start_commit;
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;
const char* prefix = setup_git_directory();
git_config(git_default_config);
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")) {
sha1_len = 40;
continue;
} else if(!strcmp(argv[i], "-c") ||
!strcmp(argv[i], "--compatibility")) {
compatibility = 1;
continue;
} else if(!strcmp(argv[i], "-t") ||
!strcmp(argv[i], "--time")) {
show_raw_time = 1;
continue;
} else if(!strcmp(argv[i], "-S")) {
if (i + 1 < argc &&
!read_ancestry(argv[i + 1], &sha1_p)) {
compatibility = 1;
i++;
continue;
}
usage(blame_usage);
} else if(!strcmp(argv[i], "--")) {
options = 0;
continue;
} else if(argv[i][0] == '-')
usage(blame_usage);
else
options = 0;
}
if(!options) {
if(!filename)
filename = argv[i];
else if(!commit)
commit = argv[i];
else
usage(blame_usage);
}
}
if(!filename)
usage(blame_usage);
if (commit && sha1_p)
usage(blame_usage);
else if(!commit)
commit = "HEAD";
if(prefix)
sprintf(filename_buf, "%s%s", prefix, filename);
else
strcpy(filename_buf, filename);
filename = filename_buf;
if (!sha1_p) {
if (get_sha1(commit, sha1))
die("get_sha1 failed, commit '%s' not found", commit);
sha1_p = sha1;
}
start_commit = lookup_commit_reference(sha1_p);
get_util(start_commit)->pathname = filename;
if (fill_util_info(start_commit)) {
printf("%s not found in %s\n", filename, commit);
return 1;
}
init_revisions(&rev, setup_git_directory());
rev.remove_empty_trees = 1;
rev.topo_order = 1;
rev.prune_fn = simplify_commit;
rev.topo_setter = topo_setter;
rev.topo_getter = topo_getter;
rev.parents = 1;
rev.limited = 1;
commit_list_insert(start_commit, &rev.commits);
args[0] = filename;
args[1] = NULL;
diff_tree_setup_paths(args, &rev.pruning);
prepare_revision_walk(&rev);
process_commits(&rev, filename, &initial);
buf = blame_contents;
for (max_digits = 1, i = 10; i <= num_blame_lines + 1; max_digits++)
i *= 10;
longest_file = 0;
longest_author = 0;
found_rename = 0;
for (i = 0; i < num_blame_lines; i++) {
struct commit *c = blame_lines[i];
struct util_info* u;
if (!c)
c = initial;
u = c->util;
if (!found_rename && strcmp(filename, u->pathname))
found_rename = 1;
if (longest_file < strlen(u->pathname))
longest_file = strlen(u->pathname);
get_commit_info(c, &ci);
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;
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) {
fwrite(buf, blame_len - (buf - blame_contents),
1, stdout);
if(blame_contents[blame_len-1] != '\n')
putc('\n', stdout);
} else {
char* next_buf = strchr(buf, '\n') + 1;
fwrite(buf, next_buf - buf, 1, stdout);
buf = next_buf;
}
}
if (DEBUG) {
printf("num get patch: %d\n", num_get_patch);
printf("num commits: %d\n", num_commits);
printf("patch time: %f\n", patch_time / 1000000.0);
printf("initial: %s\n", sha1_to_hex(initial->object.sha1));
}
return 0;
}