Makefile: turn on USE_ST_TIMESPEC for FreeBSD
[gitweb.git] / builtin-blame.c
index aae14ef8bb63abc598e2e3ce99bf64d5c07e0067..2aedd17c39fdfa12b72551d61af559f534dd65a7 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Pickaxe
+ * Blame
  *
  * Copyright (c) 2006, Junio C Hamano
  */
@@ -19,6 +19,7 @@
 #include "string-list.h"
 #include "mailmap.h"
 #include "parse-options.h"
+#include "utf8.h"
 
 static char blame_usage[] = "git blame [options] [rev-opts] [rev] [--] file";
 
@@ -39,6 +40,10 @@ static int reverse;
 static int blank_boundary;
 static int incremental;
 static int xdl_opts = XDF_NEED_MINIMAL;
+
+static enum date_mode blame_date_mode = DATE_ISO8601;
+static size_t blame_date_width;
+
 static struct string_list mailmap;
 
 #ifndef DEBUG
@@ -73,6 +78,7 @@ static unsigned blame_copy_score;
  */
 struct origin {
        int refcnt;
+       struct origin *previous;
        struct commit *commit;
        mmfile_t file;
        unsigned char blob_sha1[20];
@@ -114,6 +120,8 @@ static inline struct origin *origin_incref(struct origin *o)
 static void origin_decref(struct origin *o)
 {
        if (o && --o->refcnt <= 0) {
+               if (o->previous)
+                       origin_decref(o->previous);
                free(o->file.ptr);
                free(o);
        }
@@ -1197,6 +1205,10 @@ static void pass_blame(struct scoreboard *sb, struct origin *origin, int opt)
                struct origin *porigin = sg_origin[i];
                if (!porigin)
                        continue;
+               if (!origin->previous) {
+                       origin_incref(porigin);
+                       origin->previous = porigin;
+               }
                if (pass_blame_to_parent(sb, origin, porigin))
                        goto finish;
        }
@@ -1263,11 +1275,12 @@ struct commit_info
  * Parse author/committer line in the commit object buffer
  */
 static void get_ac_line(const char *inbuf, const char *what,
-                       int bufsz, char *person, const char **mail,
+                       int person_len, char *person,
+                       int mail_len, char *mail,
                        unsigned long *time, const char **tz)
 {
        int len, tzlen, maillen;
-       char *tmp, *endp, *timepos;
+       char *tmp, *endp, *timepos, *mailpos;
 
        tmp = strstr(inbuf, what);
        if (!tmp)
@@ -1278,10 +1291,11 @@ static void get_ac_line(const char *inbuf, const char *what,
                len = strlen(tmp);
        else
                len = endp - tmp;
-       if (bufsz <= len) {
+       if (person_len <= len) {
        error_out:
                /* Ugh */
-               *mail = *tz = "(unknown)";
+               *tz = "(unknown)";
+               strcpy(mail, *tz);
                *time = 0;
                return;
        }
@@ -1304,9 +1318,10 @@ static void get_ac_line(const char *inbuf, const char *what,
        *tmp = 0;
        while (*tmp != ' ')
                tmp--;
-       *mail = tmp + 1;
+       mailpos = tmp + 1;
        *tmp = 0;
        maillen = timepos - tmp;
+       memcpy(mail, mailpos, maillen);
 
        if (!mailmap.nr)
                return;
@@ -1315,20 +1330,23 @@ static void get_ac_line(const char *inbuf, const char *what,
         * mailmap expansion may make the name longer.
         * make room by pushing stuff down.
         */
-       tmp = person + bufsz - (tzlen + 1);
+       tmp = person + person_len - (tzlen + 1);
        memmove(tmp, *tz, tzlen);
        tmp[tzlen] = 0;
        *tz = tmp;
 
-       tmp = tmp - (maillen + 1);
-       memmove(tmp, *mail, maillen);
-       tmp[maillen] = 0;
-       *mail = tmp;
-
        /*
-        * Now, convert e-mail using mailmap
+        * Now, convert both name and e-mail using mailmap
         */
-       map_email(&mailmap, tmp + 1, person, tmp-person-1);
+       if(map_user(&mailmap, mail+1, mail_len-1, person, tmp-person-1)) {
+               /* Add a trailing '>' to email, since map_user returns plain emails
+                  Note: It already has '<', since we replace from mail+1 */
+               mailpos = memchr(mail, '\0', mail_len);
+               if (mailpos && mailpos-mail < mail_len - 1) {
+                       *mailpos = '>';
+                       *(mailpos+1) = '\0';
+               }
+       }
 }
 
 static void get_commit_info(struct commit *commit,
@@ -1337,8 +1355,10 @@ static void get_commit_info(struct commit *commit,
 {
        int len;
        char *tmp, *endp, *reencoded, *message;
-       static char author_buf[1024];
-       static char committer_buf[1024];
+       static char author_name[1024];
+       static char author_mail[1024];
+       static char committer_name[1024];
+       static char committer_mail[1024];
        static char summary_buf[1024];
 
        /*
@@ -1356,9 +1376,11 @@ static void get_commit_info(struct commit *commit,
        }
        reencoded = reencode_commit_message(commit, NULL);
        message   = reencoded ? reencoded : commit->buffer;
-       ret->author = author_buf;
+       ret->author = author_name;
+       ret->author_mail = author_mail;
        get_ac_line(message, "\nauthor ",
-                   sizeof(author_buf), author_buf, &ret->author_mail,
+                   sizeof(author_name), author_name,
+                   sizeof(author_mail), author_mail,
                    &ret->author_time, &ret->author_tz);
 
        if (!detailed) {
@@ -1366,9 +1388,11 @@ static void get_commit_info(struct commit *commit,
                return;
        }
 
-       ret->committer = committer_buf;
+       ret->committer = committer_name;
+       ret->committer_mail = committer_mail;
        get_ac_line(message, "\ncommitter ",
-                   sizeof(committer_buf), committer_buf, &ret->committer_mail,
+                   sizeof(committer_name), committer_name,
+                   sizeof(committer_mail), committer_mail,
                    &ret->committer_time, &ret->committer_tz);
 
        ret->summary = summary_buf;
@@ -1401,6 +1425,39 @@ static void write_filename_info(const char *path)
        write_name_quoted(path, stdout, '\n');
 }
 
+/*
+ * Porcelain/Incremental format wants to show a lot of details per
+ * commit.  Instead of repeating this every line, emit it only once,
+ * the first time each commit appears in the output.
+ */
+static int emit_one_suspect_detail(struct origin *suspect)
+{
+       struct commit_info ci;
+
+       if (suspect->commit->object.flags & METAINFO_SHOWN)
+               return 0;
+
+       suspect->commit->object.flags |= METAINFO_SHOWN;
+       get_commit_info(suspect->commit, &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("summary %s\n", ci.summary);
+       if (suspect->commit->object.flags & UNINTERESTING)
+               printf("boundary\n");
+       if (suspect->previous) {
+               struct origin *prev = suspect->previous;
+               printf("previous %s ", sha1_to_hex(prev->commit->object.sha1));
+               write_name_quoted(prev->path, stdout, '\n');
+       }
+       return 1;
+}
+
 /*
  * The blame_entry is found to be guilty for the range.  Mark it
  * as such, and show it in incremental output.
@@ -1416,22 +1473,7 @@ static void found_guilty_entry(struct blame_entry *ent)
                printf("%s %d %d %d\n",
                       sha1_to_hex(suspect->commit->object.sha1),
                       ent->s_lno + 1, ent->lno + 1, ent->num_lines);
-               if (!(suspect->commit->object.flags & METAINFO_SHOWN)) {
-                       struct commit_info ci;
-                       suspect->commit->object.flags |= METAINFO_SHOWN;
-                       get_commit_info(suspect->commit, &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("summary %s\n", ci.summary);
-                       if (suspect->commit->object.flags & UNINTERESTING)
-                               printf("boundary\n");
-               }
+               emit_one_suspect_detail(suspect);
                write_filename_info(suspect->path);
                maybe_flush_or_die(stdout, "stdout");
        }
@@ -1494,24 +1536,20 @@ 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;
+       const char *time_str;
+       int time_len;
+       int tz;
 
        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);
+       else {
+               tz = atoi(tz_str);
+               time_str = show_date(time, tz, blame_date_mode);
+               time_len = strlen(time_str);
+               memcpy(time_buf, time_str, time_len);
+               memset(time_buf + time_len, ' ', blame_date_width - time_len);
+       }
        return time_buf;
 }
 
@@ -1538,24 +1576,8 @@ static void emit_porcelain(struct scoreboard *sb, struct blame_entry *ent)
               ent->s_lno + 1,
               ent->lno + 1,
               ent->num_lines);
-       if (!(suspect->commit->object.flags & METAINFO_SHOWN)) {
-               struct commit_info ci;
-               suspect->commit->object.flags |= METAINFO_SHOWN;
-               get_commit_info(suspect->commit, &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);
-               write_filename_info(suspect->path);
-               printf("summary %s\n", ci.summary);
-               if (suspect->commit->object.flags & UNINTERESTING)
-                       printf("boundary\n");
-       }
-       else if (suspect->commit->object.flags & MORE_THAN_ONE_PATH)
+       if (emit_one_suspect_detail(suspect) ||
+           (suspect->commit->object.flags & MORE_THAN_ONE_PATH))
                write_filename_info(suspect->path);
 
        cp = nth_line(sb, ent->lno);
@@ -1618,13 +1640,14 @@ static void emit_other(struct scoreboard *sb, struct blame_entry *ent, int opt)
                                printf(" %*d", max_orig_digits,
                                       ent->s_lno + 1 + cnt);
 
-                       if (!(opt & OUTPUT_NO_AUTHOR))
-                               printf(" (%-*.*s %10s",
-                                      longest_author, longest_author,
-                                      ci.author,
+                       if (!(opt & OUTPUT_NO_AUTHOR)) {
+                               int pad = longest_author - utf8_strwidth(ci.author);
+                               printf(" (%s%*s %10s",
+                                      ci.author, pad, "",
                                       format_time(ci.author_time,
                                                   ci.author_tz,
                                                   show_raw_time));
+                       }
                        printf(" %*d) ",
                               max_digits, ent->lno + 1 + cnt);
                }
@@ -1755,7 +1778,7 @@ static void find_alignment(struct scoreboard *sb, int *option)
                if (!(suspect->commit->object.flags & METAINFO_SHOWN)) {
                        suspect->commit->object.flags |= METAINFO_SHOWN;
                        get_commit_info(suspect->commit, &ci, 1);
-                       num = strlen(ci.author);
+                       num = utf8_strwidth(ci.author);
                        if (longest_author < num)
                                longest_author = num;
                }
@@ -1792,36 +1815,6 @@ static void sanity_check_refcnt(struct scoreboard *sb)
                        baa = 1;
                }
        }
-       for (ent = sb->ent; ent; ent = ent->next) {
-               /* Mark the ones that haven't been checked */
-               if (0 < ent->suspect->refcnt)
-                       ent->suspect->refcnt = -ent->suspect->refcnt;
-       }
-       for (ent = sb->ent; ent; ent = ent->next) {
-               /*
-                * ... then pick each and see if they have the the
-                * correct refcnt.
-                */
-               int found;
-               struct blame_entry *e;
-               struct origin *suspect = ent->suspect;
-
-               if (0 < suspect->refcnt)
-                       continue;
-               suspect->refcnt = -suspect->refcnt; /* Unmark */
-               for (found = 0, e = sb->ent; e; e = e->next) {
-                       if (e->suspect != suspect)
-                               continue;
-                       found++;
-               }
-               if (suspect->refcnt != found) {
-                       fprintf(stderr, "%s in %s has refcnt %d, not %d\n",
-                               ent->suspect->path,
-                               sha1_to_hex(ent->suspect->commit->object.sha1),
-                               ent->suspect->refcnt, found);
-                       baa = 2;
-               }
-       }
        if (baa) {
                int opt = 0160;
                find_alignment(sb, &opt);
@@ -1961,6 +1954,12 @@ static int git_blame_config(const char *var, const char *value, void *cb)
                blank_boundary = git_config_bool(var, value);
                return 0;
        }
+       if (!strcmp(var, "blame.date")) {
+               if (!value)
+                       return config_error_nonbool(var);
+               blame_date_mode = parse_date_format(value);
+               return 0;
+       }
        return git_default_config(var, value, cb);
 }
 
@@ -2225,6 +2224,8 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
 
        git_config(git_blame_config, NULL);
        init_revisions(&revs, NULL);
+       revs.date_mode = blame_date_mode;
+
        save_commit_buffer = 0;
        dashdash_pos = 0;
 
@@ -2249,8 +2250,35 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
 parse_done:
        argc = parse_options_end(&ctx);
 
-       if (cmd_is_annotate)
+       if (cmd_is_annotate) {
                output_option |= OUTPUT_ANNOTATE_COMPAT;
+               blame_date_mode = DATE_ISO8601;
+       } else {
+               blame_date_mode = revs.date_mode;
+       }
+
+       /* The maximum width used to show the dates */
+       switch (blame_date_mode) {
+       case DATE_RFC2822:
+               blame_date_width = sizeof("Thu, 19 Oct 2006 16:00:04 -0700");
+               break;
+       case DATE_ISO8601:
+               blame_date_width = sizeof("2006-10-19 16:00:04 -0700");
+               break;
+       case DATE_RAW:
+               blame_date_width = sizeof("1161298804 -0700");
+               break;
+       case DATE_SHORT:
+               blame_date_width = sizeof("2006-10-19");
+               break;
+       case DATE_RELATIVE:
+               /* "normal" is used as the fallback for "relative" */
+       case DATE_LOCAL:
+       case DATE_NORMAL:
+               blame_date_width = sizeof("Thu Oct 19 16:00:04 2006 -0700");
+               break;
+       }
+       blame_date_width -= 1; /* strip the null */
 
        if (DIFF_OPT_TST(&revs.diffopt, FIND_COPIES_HARDER))
                opt |= (PICKAXE_BLAME_COPY | PICKAXE_BLAME_MOVE |
@@ -2394,7 +2422,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
                die("reading graft file %s failed: %s",
                    revs_file, strerror(errno));
 
-       read_mailmap(&mailmap, ".mailmap", NULL);
+       read_mailmap(&mailmap, NULL);
 
        if (!incremental)
                setup_pager();