Merge branch 'fl/cvsserver'
[gitweb.git] / builtin-blame.c
index 3033e9bdad55bdccc560a10996620654fe2c9b7e..8919b028e620d573e30203c5bffafce9b5776406 100644 (file)
 #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;
@@ -39,6 +41,7 @@ static int max_score_digits;
 static int show_root;
 static int blank_boundary;
 static int incremental;
+static int cmd_is_annotate;
 
 #ifndef DEBUG
 #define DEBUG 0
@@ -84,9 +87,9 @@ struct origin {
 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;
        }
@@ -177,16 +180,15 @@ struct scoreboard {
        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 *);
 
 /*
@@ -199,7 +201,7 @@ static void coalesce(struct scoreboard *sb)
        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;
@@ -260,7 +262,6 @@ static struct origin *get_origin(struct scoreboard *sb,
 static int fill_blob_sha1(struct origin *origin)
 {
        unsigned mode;
-       char type[10];
 
        if (!is_null_sha1(origin->blob_sha1))
                return 0;
@@ -268,8 +269,7 @@ static int fill_blob_sha1(struct origin *origin)
                           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:
@@ -333,9 +333,13 @@ static struct origin *find_origin(struct scoreboard *sb,
        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",
@@ -402,9 +406,13 @@ static struct origin *find_rename(struct scoreboard *sb,
        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++) {
@@ -550,7 +558,7 @@ static void free_patch(struct patch *p)
 }
 
 /*
- * 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)
@@ -766,7 +774,7 @@ static int find_last_in_target(struct scoreboard *sb, struct origin *target)
        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;
@@ -786,7 +794,7 @@ static void blame_chunk(struct scoreboard *sb,
        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;
@@ -961,7 +969,7 @@ static int find_move_in_parent(struct scoreboard *sb,
        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 &&
@@ -993,12 +1001,12 @@ static struct blame_list *setup_blame_list(struct scoreboard *sb,
        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;
@@ -1047,9 +1055,12 @@ static int find_copy_in_parent(struct scoreboard *sb,
            (!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);
@@ -1125,7 +1136,7 @@ static void pass_whole_blame(struct scoreboard *sb,
                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);
@@ -1232,26 +1243,26 @@ static void pass_blame(struct scoreboard *sb, struct origin *origin, int opt)
  */
 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;
@@ -1268,7 +1279,7 @@ static void get_ac_line(const char *inbuf, const char *what,
        if (bufsz <= len) {
        error_out:
                /* Ugh */
-               person = *mail = *tz = "(unknown)";
+               *mail = *tz = "(unknown)";
                *time = 0;
                return;
        }
@@ -1308,10 +1319,10 @@ static void get_commit_info(struct commit *commit,
         * 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 ",
@@ -1336,9 +1347,9 @@ static void get_commit_info(struct commit *commit,
        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;
@@ -1392,7 +1403,7 @@ static void found_guilty_entry(struct blame_entry *ent)
 
 /*
  * 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)
@@ -1431,7 +1442,7 @@ 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);
 
@@ -1541,12 +1552,12 @@ static void emit_other(struct scoreboard *sb, struct blame_entry *ent, int opt)
                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);
@@ -1910,6 +1921,137 @@ static int git_blame_config(const char *var, const char *value)
        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;
@@ -1920,10 +2062,14 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
        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;
@@ -1938,6 +2084,8 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
                        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))
@@ -1946,17 +2094,17 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
                        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);
@@ -1968,6 +2116,11 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
                                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))
@@ -2001,7 +2154,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
        /*
         * 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:
         *
@@ -2047,6 +2200,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
                        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]);
@@ -2055,6 +2209,8 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
                }
                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];
@@ -2087,7 +2243,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
        argv[unk] = NULL;
 
        init_revisions(&revs, NULL);
-       setup_revisions(unk, argv, &revs, "HEAD");
+       setup_revisions(unk, argv, &revs, NULL);
        memset(&sb, 0, sizeof(sb));
 
        /*
@@ -2114,16 +2270,14 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
        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
@@ -2132,11 +2286,22 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
         */
        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);
 
@@ -2186,7 +2351,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
                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);