#include "revision.h"
#include "quote.h"
#include "xdiff-interface.h"
+#include "cache-tree.h"
static char blame_usage[] =
-"git-blame [-c] [-l] [-t] [-f] [-n] [-p] [-L n,m] [-S <revs-file>] [-M] [-C] [-C] [commit] [--] file\n"
-" -c, --compatibility Use the same output mode as git-annotate (Default: off)\n"
+"git-blame [-c] [-l] [-t] [-f] [-n] [-p] [-L n,m] [-S <revs-file>] [-M] [-C] [-C] [--contents <filename>] [--incremental] [commit] [--] file\n"
+" -c Use the same output mode as git-annotate (Default: off)\n"
" -b Show blank SHA-1 for boundary commits (Default: off)\n"
-" -l, --long Show long commit SHA1 (Default: off)\n"
+" -l Show long commit SHA1 (Default: off)\n"
" --root Do not treat root commits as boundaries (Default: off)\n"
-" -t, --time Show raw timestamp (Default: off)\n"
+" -t 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"
" -L n,m Process only line range n,m, counting from 1\n"
" -M, -C Find line movements within and across files\n"
" --incremental Show blame entries as we find them, incrementally\n"
+" --contents file Use <file>'s contents as the final image\n"
" -S revs-file Use revisions from revs-file instead of calling git-rev-list\n";
static int longest_file;
static int show_root;
static int blank_boundary;
static int incremental;
+static int cmd_is_annotate;
#ifndef DEBUG
#define DEBUG 0
static char *fill_origin_blob(struct origin *o, mmfile_t *file)
{
if (!o->file.ptr) {
- char type[10];
+ enum object_type type;
num_read_blob++;
- file->ptr = read_sha1_file(o->blob_sha1, type,
+ file->ptr = read_sha1_file(o->blob_sha1, &type,
(unsigned long *)(&(file->size)));
o->file = *file;
}
int *lineno;
};
-static int cmp_suspect(struct origin *a, struct origin *b)
+static inline int same_suspect(struct origin *a, struct origin *b)
{
- int cmp = hashcmp(a->commit->object.sha1, b->commit->object.sha1);
- if (cmp)
- return cmp;
- return strcmp(a->path, b->path);
+ if (a == b)
+ return 1;
+ if (a->commit != b->commit)
+ return 0;
+ return !strcmp(a->path, b->path);
}
-#define cmp_suspect(a, b) ( ((a)==(b)) ? 0 : cmp_suspect(a,b) )
-
static void sanity_check_refcnt(struct scoreboard *);
/*
struct blame_entry *ent, *next;
for (ent = sb->ent; ent && (next = ent->next); ent = next) {
- if (!cmp_suspect(ent->suspect, next->suspect) &&
+ if (same_suspect(ent->suspect, next->suspect) &&
ent->guilty == next->guilty &&
ent->s_lno + ent->num_lines == next->s_lno) {
ent->num_lines += next->num_lines;
static int fill_blob_sha1(struct origin *origin)
{
unsigned mode;
- char type[10];
if (!is_null_sha1(origin->blob_sha1))
return 0;
origin->path,
origin->blob_sha1, &mode))
goto error_out;
- if (sha1_object_info(origin->blob_sha1, type, NULL) ||
- strcmp(type, blob_type))
+ if (sha1_object_info(origin->blob_sha1, NULL) != OBJ_BLOB)
goto error_out;
return 0;
error_out:
diff_tree_setup_paths(paths, &diff_opts);
if (diff_setup_done(&diff_opts) < 0)
die("diff-setup");
- diff_tree_sha1(parent->tree->object.sha1,
- origin->commit->tree->object.sha1,
- "", &diff_opts);
+
+ if (is_null_sha1(origin->commit->object.sha1))
+ do_diff_cache(parent->tree->object.sha1, &diff_opts);
+ else
+ diff_tree_sha1(parent->tree->object.sha1,
+ origin->commit->tree->object.sha1,
+ "", &diff_opts);
diffcore_std(&diff_opts);
/* It is either one entry that says "modified", or "created",
diff_tree_setup_paths(paths, &diff_opts);
if (diff_setup_done(&diff_opts) < 0)
die("diff-setup");
- diff_tree_sha1(parent->tree->object.sha1,
- origin->commit->tree->object.sha1,
- "", &diff_opts);
+
+ if (is_null_sha1(origin->commit->object.sha1))
+ do_diff_cache(parent->tree->object.sha1, &diff_opts);
+ else
+ diff_tree_sha1(parent->tree->object.sha1,
+ origin->commit->tree->object.sha1,
+ "", &diff_opts);
diffcore_std(&diff_opts);
for (i = 0; i < diff_queued_diff.nr; i++) {
}
/*
- * Link in a new blame entry to the scorebord. Entries that cover the
+ * Link in a new blame entry to the scoreboard. Entries that cover the
* same line range have been removed from the scoreboard previously.
*/
static void add_blame_entry(struct scoreboard *sb, struct blame_entry *e)
int last_in_target = -1;
for (e = sb->ent; e; e = e->next) {
- if (e->guilty || cmp_suspect(e->suspect, target))
+ if (e->guilty || !same_suspect(e->suspect, target))
continue;
if (last_in_target < e->s_lno + e->num_lines)
last_in_target = e->s_lno + e->num_lines;
struct blame_entry *e;
for (e = sb->ent; e; e = e->next) {
- if (e->guilty || cmp_suspect(e->suspect, target))
+ if (e->guilty || !same_suspect(e->suspect, target))
continue;
if (same <= e->s_lno)
continue;
while (made_progress) {
made_progress = 0;
for (e = sb->ent; e; e = e->next) {
- if (e->guilty || cmp_suspect(e->suspect, target))
+ if (e->guilty || !same_suspect(e->suspect, target))
continue;
find_copy_in_blob(sb, e, parent, split, &file_p);
if (split[1].suspect &&
struct blame_list *blame_list = NULL;
for (e = sb->ent, num_ents = 0; e; e = e->next)
- if (!e->guilty && !cmp_suspect(e->suspect, target))
+ if (!e->guilty && same_suspect(e->suspect, target))
num_ents++;
if (num_ents) {
blame_list = xcalloc(num_ents, sizeof(struct blame_list));
for (e = sb->ent, i = 0; e; e = e->next)
- if (!e->guilty && !cmp_suspect(e->suspect, target))
+ if (!e->guilty && same_suspect(e->suspect, target))
blame_list[i++].ent = e;
}
*num_ents_p = num_ents;
(!porigin || strcmp(target->path, porigin->path)))
diff_opts.find_copies_harder = 1;
- diff_tree_sha1(parent->tree->object.sha1,
- target->commit->tree->object.sha1,
- "", &diff_opts);
+ if (is_null_sha1(target->commit->object.sha1))
+ do_diff_cache(parent->tree->object.sha1, &diff_opts);
+ else
+ diff_tree_sha1(parent->tree->object.sha1,
+ target->commit->tree->object.sha1,
+ "", &diff_opts);
if (!diff_opts.find_copies_harder)
diffcore_std(&diff_opts);
origin->file.ptr = NULL;
}
for (e = sb->ent; e; e = e->next) {
- if (cmp_suspect(e->suspect, origin))
+ if (!same_suspect(e->suspect, origin))
continue;
origin_incref(porigin);
origin_decref(e->suspect);
*/
struct commit_info
{
- char *author;
- char *author_mail;
+ const char *author;
+ const char *author_mail;
unsigned long author_time;
- char *author_tz;
+ const char *author_tz;
/* filled only when asked for details */
- char *committer;
- char *committer_mail;
+ const char *committer;
+ const char *committer_mail;
unsigned long committer_time;
- char *committer_tz;
+ const char *committer_tz;
- char *summary;
+ const char *summary;
};
/*
* Parse author/committer line in the commit object buffer
*/
static void get_ac_line(const char *inbuf, const char *what,
- int bufsz, char *person, char **mail,
- unsigned long *time, char **tz)
+ int bufsz, char *person, const char **mail,
+ unsigned long *time, const char **tz)
{
int len;
char *tmp, *endp;
if (bufsz <= len) {
error_out:
/* Ugh */
- person = *mail = *tz = "(unknown)";
+ *mail = *tz = "(unknown)";
*time = 0;
return;
}
* we now need to populate them for output.
*/
if (!commit->buffer) {
- char type[20];
+ enum object_type type;
unsigned long size;
commit->buffer =
- read_sha1_file(commit->object.sha1, type, &size);
+ read_sha1_file(commit->object.sha1, &type, &size);
}
ret->author = author_buf;
get_ac_line(commit->buffer, "\nauthor ",
tmp += 2;
endp = strchr(tmp, '\n');
if (!endp)
- goto error_out;
+ endp = tmp + strlen(tmp);
len = endp - tmp;
- if (len >= sizeof(summary_buf))
+ if (len >= sizeof(summary_buf) || len == 0)
goto error_out;
memcpy(summary_buf, tmp, len);
summary_buf[len] = 0;
/*
* The main loop -- while the scoreboard has lines whose true origin
- * is still unknown, pick one brame_entry, and allow its current
+ * is still unknown, pick one blame_entry, and allow its current
* suspect to pass blames to its parents.
*/
static void assign_blame(struct scoreboard *sb, struct rev_info *revs, int opt)
/* Take responsibility for the remaining entries */
for (ent = sb->ent; ent; ent = ent->next)
- if (!cmp_suspect(ent->suspect, suspect))
+ if (same_suspect(ent->suspect, suspect))
found_guilty_entry(ent);
origin_decref(suspect);
int length = (opt & OUTPUT_LONG_OBJECT_NAME) ? 40 : 8;
if (suspect->commit->object.flags & UNINTERESTING) {
- if (!blank_boundary) {
+ if (blank_boundary)
+ memset(hex, ' ', length);
+ else if (!cmd_is_annotate) {
length--;
putchar('^');
}
- else
- memset(hex, ' ', length);
}
printf("%.*s", length, hex);
return git_default_config(var, value);
}
+static struct commit *fake_working_tree_commit(const char *path, const char *contents_from)
+{
+ struct commit *commit;
+ struct origin *origin;
+ unsigned char head_sha1[20];
+ char *buf;
+ const char *ident;
+ int fd;
+ time_t now;
+ unsigned long fin_size;
+ int size, len;
+ struct cache_entry *ce;
+ unsigned mode;
+
+ if (get_sha1("HEAD", head_sha1))
+ die("No such ref: HEAD");
+
+ time(&now);
+ commit = xcalloc(1, sizeof(*commit));
+ commit->parents = xcalloc(1, sizeof(*commit->parents));
+ commit->parents->item = lookup_commit_reference(head_sha1);
+ commit->object.parsed = 1;
+ commit->date = now;
+ commit->object.type = OBJ_COMMIT;
+
+ origin = make_origin(commit, path);
+
+ if (!contents_from || strcmp("-", contents_from)) {
+ struct stat st;
+ const char *read_from;
+
+ if (contents_from) {
+ if (stat(contents_from, &st) < 0)
+ die("Cannot stat %s", contents_from);
+ read_from = contents_from;
+ }
+ else {
+ if (lstat(path, &st) < 0)
+ die("Cannot lstat %s", path);
+ read_from = path;
+ }
+ fin_size = xsize_t(st.st_size);
+ buf = xmalloc(fin_size+1);
+ mode = canon_mode(st.st_mode);
+ switch (st.st_mode & S_IFMT) {
+ case S_IFREG:
+ fd = open(read_from, O_RDONLY);
+ if (fd < 0)
+ die("cannot open %s", read_from);
+ if (read_in_full(fd, buf, fin_size) != fin_size)
+ die("cannot read %s", read_from);
+ break;
+ case S_IFLNK:
+ if (readlink(read_from, buf, fin_size+1) != fin_size)
+ die("cannot readlink %s", read_from);
+ break;
+ default:
+ die("unsupported file type %s", read_from);
+ }
+ }
+ else {
+ /* Reading from stdin */
+ contents_from = "standard input";
+ buf = NULL;
+ fin_size = 0;
+ mode = 0;
+ while (1) {
+ ssize_t cnt = 8192;
+ buf = xrealloc(buf, fin_size + cnt);
+ cnt = xread(0, buf + fin_size, cnt);
+ if (cnt < 0)
+ die("read error %s from stdin",
+ strerror(errno));
+ if (!cnt)
+ break;
+ fin_size += cnt;
+ }
+ buf = xrealloc(buf, fin_size + 1);
+ }
+ buf[fin_size] = 0;
+ origin->file.ptr = buf;
+ origin->file.size = fin_size;
+ pretend_sha1_file(buf, fin_size, OBJ_BLOB, origin->blob_sha1);
+ commit->util = origin;
+
+ /*
+ * Read the current index, replace the path entry with
+ * origin->blob_sha1 without mucking with its mode or type
+ * bits; we are not going to write this index out -- we just
+ * want to run "diff-index --cached".
+ */
+ discard_cache();
+ read_cache();
+
+ len = strlen(path);
+ if (!mode) {
+ int pos = cache_name_pos(path, len);
+ if (0 <= pos)
+ mode = ntohl(active_cache[pos]->ce_mode);
+ else
+ /* Let's not bother reading from HEAD tree */
+ mode = S_IFREG | 0644;
+ }
+ size = cache_entry_size(len);
+ ce = xcalloc(1, size);
+ hashcpy(ce->sha1, origin->blob_sha1);
+ memcpy(ce->name, path, len);
+ ce->ce_flags = create_ce_flags(len, 0);
+ ce->ce_mode = create_ce_mode(mode);
+ add_cache_entry(ce, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE);
+
+ /*
+ * We are not going to write this out, so this does not matter
+ * right now, but someday we might optimize diff-index --cached
+ * with cache-tree information.
+ */
+ cache_tree_invalidate_path(active_cache_tree, path);
+
+ commit->buffer = xmalloc(400);
+ ident = fmt_ident("Not Committed Yet", "not.committed.yet", NULL, 0);
+ snprintf(commit->buffer, 400,
+ "tree 0000000000000000000000000000000000000000\n"
+ "parent %s\n"
+ "author %s\n"
+ "committer %s\n\n"
+ "Version of %s from %s\n",
+ sha1_to_hex(head_sha1),
+ ident, ident, path, contents_from ? contents_from : path);
+ return commit;
+}
+
int cmd_blame(int argc, const char **argv, const char *prefix)
{
struct rev_info revs;
int i, seen_dashdash, unk, opt;
long bottom, top, lno;
int output_option = 0;
+ int show_stats = 0;
const char *revs_file = NULL;
const char *final_commit_name = NULL;
- char type[10];
+ enum object_type type;
const char *bottomtop = NULL;
+ const char *contents_from = NULL;
+
+ cmd_is_annotate = !strcmp(argv[0], "annotate");
git_config(git_blame_config);
save_commit_buffer = 0;
blank_boundary = 1;
else if (!strcmp("--root", arg))
show_root = 1;
+ else if (!strcmp(arg, "--show-stats"))
+ show_stats = 1;
else if (!strcmp("-c", arg))
output_option |= OUTPUT_ANNOTATE_COMPAT;
else if (!strcmp("-t", arg))
output_option |= OUTPUT_LONG_OBJECT_NAME;
else if (!strcmp("-S", arg) && ++i < argc)
revs_file = argv[i];
- else if (!strncmp("-M", arg, 2)) {
+ else if (!prefixcmp(arg, "-M")) {
opt |= PICKAXE_BLAME_MOVE;
blame_move_score = parse_score(arg+2);
}
- else if (!strncmp("-C", arg, 2)) {
+ else if (!prefixcmp(arg, "-C")) {
if (opt & PICKAXE_BLAME_COPY)
opt |= PICKAXE_BLAME_COPY_HARDER;
opt |= PICKAXE_BLAME_COPY | PICKAXE_BLAME_MOVE;
blame_copy_score = parse_score(arg+2);
}
- else if (!strncmp("-L", arg, 2)) {
+ else if (!prefixcmp(arg, "-L")) {
if (!arg[2]) {
if (++i >= argc)
usage(blame_usage);
die("More than one '-L n,m' option given");
bottomtop = arg;
}
+ else if (!strcmp("--contents", arg)) {
+ if (++i >= argc)
+ usage(blame_usage);
+ contents_from = argv[i];
+ }
else if (!strcmp("--incremental", arg))
incremental = 1;
else if (!strcmp("--score-debug", arg))
/*
* We have collected options unknown to us in argv[1..unk]
* which are to be passed to revision machinery if we are
- * going to do the "bottom" procesing.
+ * going to do the "bottom" processing.
*
* The remaining are:
*
if (!strcmp(argv[j], "--"))
seen_dashdash = j;
if (seen_dashdash) {
+ /* (2) */
if (seen_dashdash + 1 != argc - 1)
usage(blame_usage);
path = add_prefix(prefix, argv[seen_dashdash + 1]);
}
else {
/* (3) */
+ if (argc <= i)
+ usage(blame_usage);
path = add_prefix(prefix, argv[i]);
if (i + 1 == argc - 1) {
final_commit_name = argv[i + 1];
argv[unk] = NULL;
init_revisions(&revs, NULL);
- setup_revisions(unk, argv, &revs, "HEAD");
+ setup_revisions(unk, argv, &revs, NULL);
memset(&sb, 0, sizeof(sb));
/*
if (!sb.final) {
/*
* "--not A B -- path" without anything positive;
- * default to HEAD.
+ * do not default to HEAD, but use the working tree
+ * or "--contents".
*/
- unsigned char head_sha1[20];
-
- final_commit_name = "HEAD";
- if (get_sha1(final_commit_name, head_sha1))
- die("No such ref: HEAD");
- sb.final = lookup_commit_reference(head_sha1);
- add_pending_object(&revs, &(sb.final->object), "HEAD");
+ sb.final = fake_working_tree_commit(path, contents_from);
+ add_pending_object(&revs, &(sb.final->object), ":");
}
+ else if (contents_from)
+ die("Cannot use --contents with final commit object name");
/*
* If we have bottom, this will mark the ancestors of the
*/
prepare_revision_walk(&revs);
- o = get_origin(&sb, sb.final, path);
- if (fill_blob_sha1(o))
- die("no such path %s in %s", path, final_commit_name);
+ if (is_null_sha1(sb.final->object.sha1)) {
+ char *buf;
+ o = sb.final->util;
+ buf = xmalloc(o->file.size + 1);
+ memcpy(buf, o->file.ptr, o->file.size + 1);
+ sb.final_buf = buf;
+ sb.final_buf_size = o->file.size;
+ }
+ else {
+ o = get_origin(&sb, sb.final, path);
+ if (fill_blob_sha1(o))
+ die("no such path %s in %s", path, final_commit_name);
- sb.final_buf = read_sha1_file(o->blob_sha1, type, &sb.final_buf_size);
+ sb.final_buf = read_sha1_file(o->blob_sha1, &type,
+ &sb.final_buf_size);
+ }
num_read_blob++;
lno = prepare_lines(&sb);
ent = e;
}
- if (DEBUG) {
+ if (show_stats) {
printf("num read blob: %d\n", num_read_blob);
printf("num get patch: %d\n", num_get_patch);
printf("num commits: %d\n", num_commits);