Code

git-blame --incremental
[git.git] / refs.c
diff --git a/refs.c b/refs.c
index 75cbc0e7ef32c384daafb4e8b8c27f8d1fd40538..12e46b8bbefb35e5d402b3c8ef20dd9e4263e81a 100644 (file)
--- a/refs.c
+++ b/refs.c
@@ -1,18 +1,20 @@
-#include "refs.h"
 #include "cache.h"
+#include "refs.h"
 #include "object.h"
 #include "tag.h"
 
-#include <errno.h>
+/* ISSYMREF=01 and ISPACKED=02 are public interfaces */
+#define REF_KNOWS_PEELED 04
 
 struct ref_list {
        struct ref_list *next;
-       unsigned char flag; /* ISSYMREF? ISPACKED? ISPEELED? */
+       unsigned char flag; /* ISSYMREF? ISPACKED? */
        unsigned char sha1[20];
+       unsigned char peeled[20];
        char name[FLEX_ARRAY];
 };
 
-static const char *parse_ref_line(char *line, unsigned char *sha1, int *flag)
+static const char *parse_ref_line(char *line, unsigned char *sha1)
 {
        /*
         * 42: the answer to everything.
@@ -23,7 +25,6 @@ static const char *parse_ref_line(char *line, unsigned char *sha1, int *flag)
         *  +1 (newline at the end of the line)
         */
        int len = strlen(line) - 42;
-       int peeled = 0;
 
        if (len <= 0)
                return NULL;
@@ -32,29 +33,18 @@ static const char *parse_ref_line(char *line, unsigned char *sha1, int *flag)
        if (!isspace(line[40]))
                return NULL;
        line += 41;
-
-       if (isspace(*line)) {
-               /* "SHA-1 SP SP refs/tags/tagname^{} LF"? */
-               line++;
-               len--;
-               peeled = 1;
-       }
+       if (isspace(*line))
+               return NULL;
        if (line[len] != '\n')
                return NULL;
        line[len] = 0;
 
-       if (peeled && (len < 3 || strcmp(line + len - 3, "^{}")))
-               return NULL;
-
-       if (!peeled)
-               *flag &= ~REF_ISPEELED;
-       else
-               *flag |= REF_ISPEELED;
        return line;
 }
 
 static struct ref_list *add_ref(const char *name, const unsigned char *sha1,
-                               int flag, struct ref_list *list)
+                               int flag, struct ref_list *list,
+                               struct ref_list **new_entry)
 {
        int len;
        struct ref_list **p = &list, *entry;
@@ -66,8 +56,11 @@ static struct ref_list *add_ref(const char *name, const unsigned char *sha1,
                        break;
 
                /* Same as existing entry? */
-               if (!cmp)
+               if (!cmp) {
+                       if (new_entry)
+                               *new_entry = entry;
                        return list;
+               }
                p = &entry->next;
        }
 
@@ -75,10 +68,13 @@ static struct ref_list *add_ref(const char *name, const unsigned char *sha1,
        len = strlen(name) + 1;
        entry = xmalloc(sizeof(struct ref_list) + len);
        hashcpy(entry->sha1, sha1);
+       hashclr(entry->peeled);
        memcpy(entry->name, name, len);
        entry->flag = flag;
        entry->next = *p;
        *p = entry;
+       if (new_entry)
+               *new_entry = entry;
        return list;
 }
 
@@ -114,27 +110,50 @@ static void invalidate_cached_refs(void)
        ca->did_loose = ca->did_packed = 0;
 }
 
+static void read_packed_refs(FILE *f, struct cached_refs *cached_refs)
+{
+       struct ref_list *list = NULL;
+       struct ref_list *last = NULL;
+       char refline[PATH_MAX];
+       int flag = REF_ISPACKED;
+
+       while (fgets(refline, sizeof(refline), f)) {
+               unsigned char sha1[20];
+               const char *name;
+               static const char header[] = "# pack-refs with:";
+
+               if (!strncmp(refline, header, sizeof(header)-1)) {
+                       const char *traits = refline + sizeof(header) - 1;
+                       if (strstr(traits, " peeled "))
+                               flag |= REF_KNOWS_PEELED;
+                       /* perhaps other traits later as well */
+                       continue;
+               }
+
+               name = parse_ref_line(refline, sha1);
+               if (name) {
+                       list = add_ref(name, sha1, flag, list, &last);
+                       continue;
+               }
+               if (last &&
+                   refline[0] == '^' &&
+                   strlen(refline) == 42 &&
+                   refline[41] == '\n' &&
+                   !get_sha1_hex(refline + 1, sha1))
+                       hashcpy(last->peeled, sha1);
+       }
+       cached_refs->packed = list;
+}
+
 static struct ref_list *get_packed_refs(void)
 {
        if (!cached_refs.did_packed) {
-               struct ref_list *refs = NULL;
                FILE *f = fopen(git_path("packed-refs"), "r");
+               cached_refs.packed = NULL;
                if (f) {
-                       struct ref_list *list = NULL;
-                       char refline[PATH_MAX];
-                       while (fgets(refline, sizeof(refline), f)) {
-                               unsigned char sha1[20];
-                               int flag = REF_ISPACKED;
-                               const char *name =
-                                       parse_ref_line(refline, sha1, &flag);
-                               if (!name)
-                                       continue;
-                               list = add_ref(name, sha1, flag, list);
-                       }
+                       read_packed_refs(f, &cached_refs);
                        fclose(f);
-                       refs = list;
                }
-               cached_refs.packed = refs;
                cached_refs.did_packed = 1;
        }
        return cached_refs.packed;
@@ -177,7 +196,7 @@ static struct ref_list *get_ref_dir(const char *base, struct ref_list *list)
                                error("%s points nowhere!", ref);
                                continue;
                        }
-                       list = add_ref(ref, sha1, flag, list);
+                       list = add_ref(ref, sha1, flag, list, NULL);
                }
                free(ref);
                closedir(dir);
@@ -225,8 +244,7 @@ const char *resolve_ref(const char *ref, unsigned char *sha1, int reading, int *
                if (lstat(path, &st) < 0) {
                        struct ref_list *list = get_packed_refs();
                        while (list) {
-                               if (!(list->flag & REF_ISPEELED) &&
-                                   !strcmp(ref, list->name)) {
+                               if (!strcmp(ref, list->name)) {
                                        hashcpy(sha1, list->sha1);
                                        if (flag)
                                                *flag |= REF_ISPACKED;
@@ -266,7 +284,7 @@ const char *resolve_ref(const char *ref, unsigned char *sha1, int reading, int *
                fd = open(path, O_RDONLY);
                if (fd < 0)
                        return NULL;
-               len = read(fd, buffer, sizeof(buffer)-1);
+               len = read_in_full(fd, buffer, sizeof(buffer)-1);
                close(fd);
 
                /*
@@ -313,8 +331,12 @@ int create_symref(const char *ref_target, const char *refs_heads_master)
                return -1;
        }
        lockpath = mkpath("%s.lock", git_HEAD);
-       fd = open(lockpath, O_CREAT | O_EXCL | O_WRONLY, 0666); 
-       written = write(fd, ref, len);
+       fd = open(lockpath, O_CREAT | O_EXCL | O_WRONLY, 0666);
+       if (fd < 0) {
+               error("Unable to open %s for writing", lockpath);
+               return -5;
+       }
+       written = write_in_full(fd, ref, len);
        close(fd);
        if (written != len) {
                unlink(lockpath);
@@ -348,8 +370,6 @@ static int do_one_ref(const char *base, each_ref_fn fn, int trim,
                return 0;
        if (is_null_sha1(entry->sha1))
                return 0;
-       if (entry->flag & REF_ISPEELED)
-               return 0;
        if (!has_sha1_file(entry->sha1)) {
                error("%s does not point to a valid object!", entry->name);
                return 0;
@@ -368,22 +388,21 @@ int peel_ref(const char *ref, unsigned char *sha1)
 
        if ((flag & REF_ISPACKED)) {
                struct ref_list *list = get_packed_refs();
-               int len = strlen(ref);
 
                while (list) {
-                       if ((list->flag & REF_ISPEELED) &&
-                           !strncmp(list->name, ref, len) &&
-                           strlen(list->name) == len + 3 &&
-                           !strcmp(list->name + len, "^{}")) {
-                               hashcpy(sha1, list->sha1);
-                               return 0;
+                       if (!strcmp(list->name, ref)) {
+                               if (list->flag & REF_KNOWS_PEELED) {
+                                       hashcpy(sha1, list->peeled);
+                                       return 0;
+                               }
+                               /* older pack-refs did not leave peeled ones */
+                               break;
                        }
                        list = list->next;
                }
-               /* older pack-refs did not leave peeled ones in */
        }
 
-       /* otherwise ... */
+       /* fallback - callers should not call this for unpacked refs */
        o = parse_object(base);
        if (o->type == OBJ_TAG) {
                o = deref_tag(o, ref, 0);
@@ -517,7 +536,7 @@ int check_ref_format(const char *ref)
                level++;
                if (!ch) {
                        if (level < 2)
-                               return -1; /* at least of form "heads/blah" */
+                               return -2; /* at least of form "heads/blah" */
                        return 0;
                }
        }
@@ -593,6 +612,29 @@ static int remove_empty_directories(char *file)
        return remove_empty_dir_recursive(path, len);
 }
 
+static int is_refname_available(const char *ref, const char *oldref,
+                               struct ref_list *list, int quiet)
+{
+       int namlen = strlen(ref); /* e.g. 'foo/bar' */
+       while (list) {
+               /* list->name could be 'foo' or 'foo/bar/baz' */
+               if (!oldref || strcmp(oldref, list->name)) {
+                       int len = strlen(list->name);
+                       int cmplen = (namlen < len) ? namlen : len;
+                       const char *lead = (namlen < len) ? list->name : ref;
+                       if (!strncmp(ref, list->name, cmplen) &&
+                           lead[cmplen] == '/') {
+                               if (!quiet)
+                                       error("'%s' exists; cannot create '%s'",
+                                             list->name, ref);
+                               return 0;
+                       }
+               }
+               list = list->next;
+       }
+       return 1;
+}
+
 static struct ref_lock *lock_ref_sha1_basic(const char *ref, const unsigned char *old_sha1, int *flag)
 {
        char *ref_file;
@@ -626,29 +668,14 @@ static struct ref_lock *lock_ref_sha1_basic(const char *ref, const unsigned char
                        orig_ref, strerror(errno));
                goto error_return;
        }
-       if (is_null_sha1(lock->old_sha1)) {
-               /* The ref did not exist and we are creating it.
-                * Make sure there is no existing ref that is packed
-                * whose name begins with our refname, nor a ref whose
-                * name is a proper prefix of our refname.
-                */
-               int namlen = strlen(ref); /* e.g. 'foo/bar' */
-               struct ref_list *list = get_packed_refs();
-               while (list) {
-                       /* list->name could be 'foo' or 'foo/bar/baz' */
-                       int len = strlen(list->name);
-                       int cmplen = (namlen < len) ? namlen : len;
-                       const char *lead = (namlen < len) ? list->name : ref;
-
-                       if (!strncmp(ref, list->name, cmplen) &&
-                           lead[cmplen] == '/') {
-                               error("'%s' exists; cannot create '%s'",
-                                     list->name, ref);
-                               goto error_return;
-                       }
-                       list = list->next;
-               }
-       }
+       /* When the ref did not exist and we are creating it,
+        * make sure there is no existing ref that is packed
+        * whose name begins with our refname, nor a ref whose
+        * name is a proper prefix of our refname.
+        */
+       if (is_null_sha1(lock->old_sha1) &&
+            !is_refname_available(ref, NULL, get_packed_refs(), 0))
+               goto error_return;
 
        lock->lk = xcalloc(1, sizeof(struct lock_file));
 
@@ -703,7 +730,6 @@ static int repack_without_ref(const char *refname)
        }
        if (!found)
                return 0;
-       memset(&packlock, 0, sizeof(packlock));
        fd = hold_lock_file_for_update(&packlock, git_path("packed-refs"), 0);
        if (fd < 0)
                return error("cannot delete '%s' from packed refs", refname);
@@ -759,6 +785,132 @@ int delete_ref(const char *refname, unsigned char *sha1)
        return ret;
 }
 
+int rename_ref(const char *oldref, const char *newref, const char *logmsg)
+{
+       static const char renamed_ref[] = "RENAMED-REF";
+       unsigned char sha1[20], orig_sha1[20];
+       int flag = 0, logmoved = 0;
+       struct ref_lock *lock;
+       struct stat loginfo;
+       int log = !lstat(git_path("logs/%s", oldref), &loginfo);
+
+       if (S_ISLNK(loginfo.st_mode))
+               return error("reflog for %s is a symlink", oldref);
+
+       if (!resolve_ref(oldref, orig_sha1, 1, &flag))
+               return error("refname %s not found", oldref);
+
+       if (!is_refname_available(newref, oldref, get_packed_refs(), 0))
+               return 1;
+
+       if (!is_refname_available(newref, oldref, get_loose_refs(), 0))
+               return 1;
+
+       lock = lock_ref_sha1_basic(renamed_ref, NULL, NULL);
+       if (!lock)
+               return error("unable to lock %s", renamed_ref);
+       lock->force_write = 1;
+       if (write_ref_sha1(lock, orig_sha1, logmsg))
+               return error("unable to save current sha1 in %s", renamed_ref);
+
+       if (log && rename(git_path("logs/%s", oldref), git_path("tmp-renamed-log")))
+               return error("unable to move logfile logs/%s to tmp-renamed-log: %s",
+                       oldref, strerror(errno));
+
+       if (delete_ref(oldref, orig_sha1)) {
+               error("unable to delete old %s", oldref);
+               goto rollback;
+       }
+
+       if (resolve_ref(newref, sha1, 1, &flag) && delete_ref(newref, sha1)) {
+               if (errno==EISDIR) {
+                       if (remove_empty_directories(git_path("%s", newref))) {
+                               error("Directory not empty: %s", newref);
+                               goto rollback;
+                       }
+               } else {
+                       error("unable to delete existing %s", newref);
+                       goto rollback;
+               }
+       }
+
+       if (log && safe_create_leading_directories(git_path("logs/%s", newref))) {
+               error("unable to create directory for %s", newref);
+               goto rollback;
+       }
+
+ retry:
+       if (log && rename(git_path("tmp-renamed-log"), git_path("logs/%s", newref))) {
+               if (errno==EISDIR || errno==ENOTDIR) {
+                       /*
+                        * rename(a, b) when b is an existing
+                        * directory ought to result in ISDIR, but
+                        * Solaris 5.8 gives ENOTDIR.  Sheesh.
+                        */
+                       if (remove_empty_directories(git_path("logs/%s", newref))) {
+                               error("Directory not empty: logs/%s", newref);
+                               goto rollback;
+                       }
+                       goto retry;
+               } else {
+                       error("unable to move logfile tmp-renamed-log to logs/%s: %s",
+                               newref, strerror(errno));
+                       goto rollback;
+               }
+       }
+       logmoved = log;
+
+       lock = lock_ref_sha1_basic(newref, NULL, NULL);
+       if (!lock) {
+               error("unable to lock %s for update", newref);
+               goto rollback;
+       }
+
+       lock->force_write = 1;
+       hashcpy(lock->old_sha1, orig_sha1);
+       if (write_ref_sha1(lock, orig_sha1, logmsg)) {
+               error("unable to write current sha1 into %s", newref);
+               goto rollback;
+       }
+
+       if (!strncmp(oldref, "refs/heads/", 11) &&
+                       !strncmp(newref, "refs/heads/", 11)) {
+               char oldsection[1024], newsection[1024];
+
+               snprintf(oldsection, 1024, "branch.%s", oldref + 11);
+               snprintf(newsection, 1024, "branch.%s", newref + 11);
+               if (git_config_rename_section(oldsection, newsection) < 0)
+                       return 1;
+       }
+
+       return 0;
+
+ rollback:
+       lock = lock_ref_sha1_basic(oldref, NULL, NULL);
+       if (!lock) {
+               error("unable to lock %s for rollback", oldref);
+               goto rollbacklog;
+       }
+
+       lock->force_write = 1;
+       flag = log_all_ref_updates;
+       log_all_ref_updates = 0;
+       if (write_ref_sha1(lock, orig_sha1, NULL))
+               error("unable to write current sha1 into %s", oldref);
+       log_all_ref_updates = flag;
+
+ rollbacklog:
+       if (logmoved && rename(git_path("logs/%s", newref), git_path("logs/%s", oldref)))
+               error("unable to restore logfile %s from %s: %s",
+                       oldref, newref, strerror(errno));
+       if (!logmoved && log &&
+           rename(git_path("tmp-renamed-log"), git_path("logs/%s", oldref)))
+               error("unable to restore logfile %s from tmp-renamed-log: %s",
+                       oldref, strerror(errno));
+
+       return 1;
+}
+
 void unlock_ref(struct ref_lock *lock)
 {
        if (lock->lock_fd >= 0) {
@@ -777,11 +929,16 @@ static int log_ref_write(struct ref_lock *lock,
 {
        int logfd, written, oflags = O_APPEND | O_WRONLY;
        unsigned maxlen, len;
+       int msglen;
        char *logrec;
        const char *committer;
 
+       if (log_all_ref_updates < 0)
+               log_all_ref_updates = !is_bare_repository();
+
        if (log_all_ref_updates &&
-           !strncmp(lock->ref_name, "refs/heads/", 11)) {
+           (!strncmp(lock->ref_name, "refs/heads/", 11) ||
+            !strncmp(lock->ref_name, "refs/remotes/", 13))) {
                if (safe_create_leading_directories(lock->log_file) < 0)
                        return error("unable to create directory for %s",
                                lock->log_file);
@@ -806,25 +963,31 @@ static int log_ref_write(struct ref_lock *lock,
                                     lock->log_file, strerror(errno));
        }
 
-       committer = git_committer_info(1);
+       msglen = 0;
        if (msg) {
-               maxlen = strlen(committer) + strlen(msg) + 2*40 + 5;
-               logrec = xmalloc(maxlen);
-               len = snprintf(logrec, maxlen, "%s %s %s\t%s\n",
-                       sha1_to_hex(lock->old_sha1),
-                       sha1_to_hex(sha1),
-                       committer,
-                       msg);
-       }
-       else {
-               maxlen = strlen(committer) + 2*40 + 4;
-               logrec = xmalloc(maxlen);
-               len = snprintf(logrec, maxlen, "%s %s %s\n",
-                       sha1_to_hex(lock->old_sha1),
-                       sha1_to_hex(sha1),
-                       committer);
-       }
-       written = len <= maxlen ? write(logfd, logrec, len) : -1;
+               /* clean up the message and make sure it is a single line */
+               for ( ; *msg; msg++)
+                       if (!isspace(*msg))
+                               break;
+               if (*msg) {
+                       const char *ep = strchr(msg, '\n');
+                       if (ep)
+                               msglen = ep - msg;
+                       else
+                               msglen = strlen(msg);
+               }
+       }
+
+       committer = git_committer_info(-1);
+       maxlen = strlen(committer) + msglen + 100;
+       logrec = xmalloc(maxlen);
+       len = sprintf(logrec, "%s %s %s\n",
+                     sha1_to_hex(lock->old_sha1),
+                     sha1_to_hex(sha1),
+                     committer);
+       if (msglen)
+               len += sprintf(logrec + len - 1, "\t%.*s\n", msglen, msg) - 1;
+       written = len <= maxlen ? write_in_full(logfd, logrec, len) : -1;
        free(logrec);
        close(logfd);
        if (written != len)
@@ -843,8 +1006,8 @@ int write_ref_sha1(struct ref_lock *lock,
                unlock_ref(lock);
                return 0;
        }
-       if (write(lock->lock_fd, sha1_to_hex(sha1), 40) != 40 ||
-           write(lock->lock_fd, &term, 1) != 1
+       if (write_in_full(lock->lock_fd, sha1_to_hex(sha1), 40) != 40 ||
+           write_in_full(lock->lock_fd, &term, 1) != 1
                || close(lock->lock_fd) < 0) {
                error("Couldn't write %s", lock->lk->filename);
                unlock_ref(lock);
@@ -865,14 +1028,29 @@ int write_ref_sha1(struct ref_lock *lock,
        return 0;
 }
 
-int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *sha1)
+static char *ref_msg(const char *line, const char *endp)
+{
+       const char *ep;
+       char *msg;
+
+       line += 82;
+       for (ep = line; ep < endp && *ep != '\n'; ep++)
+               ;
+       msg = xmalloc(ep - line + 1);
+       memcpy(msg, line, ep - line);
+       msg[ep - line] = 0;
+       return msg;
+}
+
+int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *sha1, char **msg, unsigned long *cutoff_time, int *cutoff_tz, int *cutoff_cnt)
 {
        const char *logfile, *logdata, *logend, *rec, *lastgt, *lastrec;
        char *tz_c;
-       int logfd, tz;
+       int logfd, tz, reccnt = 0;
        struct stat st;
        unsigned long date;
        unsigned char logged_sha1[20];
+       void *log_mapped;
 
        logfile = git_path("logs/%s", ref);
        logfd = open(logfile, O_RDONLY, 0);
@@ -881,12 +1059,14 @@ int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *
        fstat(logfd, &st);
        if (!st.st_size)
                die("Log %s is empty.", logfile);
-       logdata = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, logfd, 0);
+       log_mapped = xmmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, logfd, 0);
+       logdata = log_mapped;
        close(logfd);
 
        lastrec = NULL;
        rec = logend = logdata + st.st_size;
        while (logdata < rec) {
+               reccnt++;
                if (logdata < rec && *(rec-1) == '\n')
                        rec--;
                lastgt = NULL;
@@ -899,13 +1079,21 @@ int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *
                        die("Log %s is corrupt.", logfile);
                date = strtoul(lastgt + 1, &tz_c, 10);
                if (date <= at_time || cnt == 0) {
+                       tz = strtoul(tz_c, NULL, 10);
+                       if (msg)
+                               *msg = ref_msg(rec, logend);
+                       if (cutoff_time)
+                               *cutoff_time = date;
+                       if (cutoff_tz)
+                               *cutoff_tz = tz;
+                       if (cutoff_cnt)
+                               *cutoff_cnt = reccnt - 1;
                        if (lastrec) {
                                if (get_sha1_hex(lastrec, logged_sha1))
                                        die("Log %s is corrupt.", logfile);
                                if (get_sha1_hex(rec + 41, sha1))
                                        die("Log %s is corrupt.", logfile);
                                if (hashcmp(logged_sha1, sha1)) {
-                                       tz = strtoul(tz_c, NULL, 10);
                                        fprintf(stderr,
                                                "warning: Log %s has gap after %s.\n",
                                                logfile, show_rfc2822_date(date, tz));
@@ -919,13 +1107,12 @@ int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *
                                if (get_sha1_hex(rec + 41, logged_sha1))
                                        die("Log %s is corrupt.", logfile);
                                if (hashcmp(logged_sha1, sha1)) {
-                                       tz = strtoul(tz_c, NULL, 10);
                                        fprintf(stderr,
                                                "warning: Log %s unexpectedly ended on %s.\n",
                                                logfile, show_rfc2822_date(date, tz));
                                }
                        }
-                       munmap((void*)logdata, st.st_size);
+                       munmap(log_mapped, st.st_size);
                        return 0;
                }
                lastrec = rec;
@@ -942,8 +1129,58 @@ int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned char *
        tz = strtoul(tz_c, NULL, 10);
        if (get_sha1_hex(logdata, sha1))
                die("Log %s is corrupt.", logfile);
-       munmap((void*)logdata, st.st_size);
-       fprintf(stderr, "warning: Log %s only goes back to %s.\n",
-               logfile, show_rfc2822_date(date, tz));
-       return 0;
+       if (msg)
+               *msg = ref_msg(logdata, logend);
+       munmap(log_mapped, st.st_size);
+
+       if (cutoff_time)
+               *cutoff_time = date;
+       if (cutoff_tz)
+               *cutoff_tz = tz;
+       if (cutoff_cnt)
+               *cutoff_cnt = reccnt;
+       return 1;
 }
+
+int for_each_reflog_ent(const char *ref, each_reflog_ent_fn fn, void *cb_data)
+{
+       const char *logfile;
+       FILE *logfp;
+       char buf[1024];
+       int ret = 0;
+
+       logfile = git_path("logs/%s", ref);
+       logfp = fopen(logfile, "r");
+       if (!logfp)
+               return -1;
+       while (fgets(buf, sizeof(buf), logfp)) {
+               unsigned char osha1[20], nsha1[20];
+               char *email_end, *message;
+               unsigned long timestamp;
+               int len, tz;
+
+               /* old SP new SP name <email> SP time TAB msg LF */
+               len = strlen(buf);
+               if (len < 83 || buf[len-1] != '\n' ||
+                   get_sha1_hex(buf, osha1) || buf[40] != ' ' ||
+                   get_sha1_hex(buf + 41, nsha1) || buf[81] != ' ' ||
+                   !(email_end = strchr(buf + 82, '>')) ||
+                   email_end[1] != ' ' ||
+                   !(timestamp = strtoul(email_end + 2, &message, 10)) ||
+                   !message || message[0] != ' ' ||
+                   (message[1] != '+' && message[1] != '-') ||
+                   !isdigit(message[2]) || !isdigit(message[3]) ||
+                   !isdigit(message[4]) || !isdigit(message[5]) ||
+                   message[6] != '\t')
+                       continue; /* corrupt? */
+               email_end[1] = '\0';
+               tz = strtol(message + 1, NULL, 10);
+               message += 7;
+               ret = fn(osha1, nsha1, buf+82, timestamp, tz, message, cb_data);
+               if (ret)
+                       break;
+       }
+       fclose(logfp);
+       return ret;
+}
+