use skip_prefix to avoid magic numbers
[gitweb.git] / refs.c
diff --git a/refs.c b/refs.c
index 5e5a3824b992132aaac8d86698feebcb773e13c6..dc457742eade241ec6b0001580d1ce1a880fee31 100644 (file)
--- a/refs.c
+++ b/refs.c
@@ -6,8 +6,29 @@
 #include "string-list.h"
 
 /*
- * Make sure "ref" is something reasonable to have under ".git/refs/";
- * We do not like it if:
+ * How to handle various characters in refnames:
+ * 0: An acceptable character for refs
+ * 1: End-of-component
+ * 2: ., look for a preceding . to reject .. in refs
+ * 3: {, look for a preceding @ to reject @{ in refs
+ * 4: A bad character: ASCII control characters, "~", "^", ":" or SP
+ */
+static unsigned char refname_disposition[256] = {
+       1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+       4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+       4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 2, 1,
+       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 4,
+       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 4, 0,
+       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 4, 4
+};
+
+/*
+ * Try to read one refname component from the front of refname.
+ * Return the length of the component found, or -1 if the component is
+ * not legal.  It is legal if it is something reasonable to have under
+ * ".git/refs/"; We do not like it if:
  *
  * - any path component of it begins with ".", or
  * - it has double dots "..", or
  * - it ends with ".lock"
  * - it contains a "\" (backslash)
  */
-
-/* Return true iff ch is not allowed in reference names. */
-static inline int bad_ref_char(int ch)
-{
-       if (((unsigned) ch) <= ' ' || ch == 0x7f ||
-           ch == '~' || ch == '^' || ch == ':' || ch == '\\')
-               return 1;
-       /* 2.13 Pattern Matching Notation */
-       if (ch == '*' || ch == '?' || ch == '[') /* Unsupported */
-               return 1;
-       return 0;
-}
-
-/*
- * Try to read one refname component from the front of refname.  Return
- * the length of the component found, or -1 if the component is not
- * legal.
- */
 static int check_refname_component(const char *refname, int flags)
 {
        const char *cp;
        char last = '\0';
 
        for (cp = refname; ; cp++) {
-               char ch = *cp;
-               if (ch == '\0' || ch == '/')
+               int ch = *cp & 255;
+               unsigned char disp = refname_disposition[ch];
+               switch (disp) {
+               case 1:
+                       goto out;
+               case 2:
+                       if (last == '.')
+                               return -1; /* Refname contains "..". */
+                       break;
+               case 3:
+                       if (last == '@')
+                               return -1; /* Refname contains "@{". */
                        break;
-               if (bad_ref_char(ch))
-                       return -1; /* Illegal character in refname. */
-               if (last == '.' && ch == '.')
-                       return -1; /* Refname contains "..". */
-               if (last == '@' && ch == '{')
-                       return -1; /* Refname contains "@{". */
+               case 4:
+                       return -1;
+               }
                last = ch;
        }
+out:
        if (cp == refname)
                return 0; /* Component has zero length. */
        if (refname[0] == '.') {
@@ -637,7 +648,7 @@ static int do_one_ref(struct ref_entry *entry, void *cb_data)
        struct ref_entry *old_current_ref;
        int retval;
 
-       if (prefixcmp(entry->name, data->base))
+       if (!starts_with(entry->name, data->base))
                return 0;
 
        if (!(data->flags & DO_FOR_EACH_INCLUDE_BROKEN) &&
@@ -1042,7 +1053,7 @@ static void read_packed_refs(FILE *f, struct ref_dir *dir)
                if (refname) {
                        last = create_ref_entry(refname, sha1, REF_ISPACKED, 1);
                        if (peeled == PEELED_FULLY ||
-                           (peeled == PEELED_TAGS && !prefixcmp(refname, "refs/tags/")))
+                           (peeled == PEELED_TAGS && starts_with(refname, "refs/tags/")))
                                last->flag |= REF_KNOWS_PEELED;
                        add_ref(dir, last);
                        continue;
@@ -1222,7 +1233,7 @@ static int resolve_gitlink_packed_ref(struct ref_cache *refs,
        if (ref == NULL)
                return -1;
 
-       memcpy(sha1, ref->u.value.sha1, 20);
+       hashcpy(sha1, ref->u.value.sha1);
        return 0;
 }
 
@@ -1376,7 +1387,7 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
                                        return NULL;
                        }
                        buffer[len] = 0;
-                       if (!prefixcmp(buffer, "refs/") &&
+                       if (starts_with(buffer, "refs/") &&
                                        !check_refname_format(buffer, 0)) {
                                strcpy(refname_buffer, buffer);
                                refname = refname_buffer;
@@ -1415,7 +1426,7 @@ const char *resolve_ref_unsafe(const char *refname, unsigned char *sha1, int rea
                /*
                 * Is it a symbolic ref?
                 */
-               if (prefixcmp(buffer, "ref:")) {
+               if (!starts_with(buffer, "ref:")) {
                        /*
                         * Please note that FETCH_HEAD has a second
                         * line containing other data.
@@ -1477,7 +1488,7 @@ static int filter_refs(const char *refname, const unsigned char *sha1, int flags
                       void *data)
 {
        struct ref_filter *filter = (struct ref_filter *)data;
-       if (fnmatch(filter->pattern, refname, 0))
+       if (wildmatch(filter->pattern, refname, 0, NULL))
                return 0;
        return filter->fn(refname, sha1, flags, filter->cb_data);
 }
@@ -1611,6 +1622,7 @@ int peel_ref(const char *refname, unsigned char *sha1)
 struct warn_if_dangling_data {
        FILE *fp;
        const char *refname;
+       const struct string_list *refnames;
        const char *msg_fmt;
 };
 
@@ -1625,8 +1637,12 @@ static int warn_if_dangling_symref(const char *refname, const unsigned char *sha
                return 0;
 
        resolves_to = resolve_ref_unsafe(refname, junk, 0, NULL);
-       if (!resolves_to || strcmp(resolves_to, d->refname))
+       if (!resolves_to
+           || (d->refname
+               ? strcmp(resolves_to, d->refname)
+               : !string_list_has_string(d->refnames, resolves_to))) {
                return 0;
+       }
 
        fprintf(d->fp, d->msg_fmt, refname);
        fputc('\n', d->fp);
@@ -1639,6 +1655,18 @@ void warn_dangling_symref(FILE *fp, const char *msg_fmt, const char *refname)
 
        data.fp = fp;
        data.refname = refname;
+       data.refnames = NULL;
+       data.msg_fmt = msg_fmt;
+       for_each_rawref(warn_if_dangling_symref, &data);
+}
+
+void warn_dangling_symrefs(FILE *fp, const char *msg_fmt, const struct string_list *refnames)
+{
+       struct warn_if_dangling_data data;
+
+       data.fp = fp;
+       data.refname = NULL;
+       data.refnames = refnames;
        data.msg_fmt = msg_fmt;
        for_each_rawref(warn_if_dangling_symref, &data);
 }
@@ -1837,7 +1865,7 @@ int for_each_glob_ref_in(each_ref_fn fn, const char *pattern,
        struct ref_filter filter;
        int ret;
 
-       if (!prefix && prefixcmp(pattern, "refs/"))
+       if (!prefix && !starts_with(pattern, "refs/"))
                strbuf_addstr(&real_pattern, "refs/");
        else if (prefix)
                strbuf_addstr(&real_pattern, prefix);
@@ -1874,13 +1902,13 @@ int for_each_rawref(each_ref_fn fn, void *cb_data)
 const char *prettify_refname(const char *name)
 {
        return name + (
-               !prefixcmp(name, "refs/heads/") ? 11 :
-               !prefixcmp(name, "refs/tags/") ? 10 :
-               !prefixcmp(name, "refs/remotes/") ? 13 :
+               starts_with(name, "refs/heads/") ? 11 :
+               starts_with(name, "refs/tags/") ? 10 :
+               starts_with(name, "refs/remotes/") ? 13 :
                0);
 }
 
-const char *ref_rev_parse_rules[] = {
+static const char *ref_rev_parse_rules[] = {
        "%.*s",
        "refs/%.*s",
        "refs/tags/%.*s",
@@ -1890,12 +1918,12 @@ const char *ref_rev_parse_rules[] = {
        NULL
 };
 
-int refname_match(const char *abbrev_name, const char *full_name, const char **rules)
+int refname_match(const char *abbrev_name, const char *full_name)
 {
        const char **p;
        const int abbrev_name_len = strlen(abbrev_name);
 
-       for (p = rules; *p; p++) {
+       for (p = ref_rev_parse_rules; *p; p++) {
                if (!strcmp(full_name, mkpath(*p, abbrev_name_len, abbrev_name))) {
                        return 1;
                }
@@ -1999,7 +2027,6 @@ int dwim_log(const char *str, int len, unsigned char *sha1, char **log)
 
        *log = NULL;
        for (p = ref_rev_parse_rules; *p; p++) {
-               struct stat st;
                unsigned char hash[20];
                char path[PATH_MAX];
                const char *ref, *it;
@@ -2008,12 +2035,9 @@ int dwim_log(const char *str, int len, unsigned char *sha1, char **log)
                ref = resolve_ref_unsafe(path, hash, 1, NULL);
                if (!ref)
                        continue;
-               if (!stat(git_path("logs/%s", path), &st) &&
-                   S_ISREG(st.st_mode))
+               if (reflog_exists(path))
                        it = path;
-               else if (strcmp(ref, path) &&
-                        !stat(git_path("logs/%s", ref), &st) &&
-                        S_ISREG(st.st_mode))
+               else if (strcmp(ref, path) && reflog_exists(ref))
                        it = ref;
                else
                        continue;
@@ -2039,6 +2063,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
        int type, lflags;
        int mustexist = (old_sha1 && !is_null_sha1(old_sha1));
        int missing = 0;
+       int attempts_remaining = 3;
 
        lock = xcalloc(1, sizeof(struct ref_lock));
        lock->lock_fd = -1;
@@ -2080,7 +2105,7 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
 
        lock->lk = xcalloc(1, sizeof(struct lock_file));
 
-       lflags = LOCK_DIE_ON_ERROR;
+       lflags = 0;
        if (flags & REF_NODEREF) {
                refname = orig_refname;
                lflags |= LOCK_NODEREF;
@@ -2093,13 +2118,32 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
        if ((flags & REF_NODEREF) && (type & REF_ISSYMREF))
                lock->force_write = 1;
 
-       if (safe_create_leading_directories(ref_file)) {
+ retry:
+       switch (safe_create_leading_directories(ref_file)) {
+       case SCLD_OK:
+               break; /* success */
+       case SCLD_VANISHED:
+               if (--attempts_remaining > 0)
+                       goto retry;
+               /* fall through */
+       default:
                last_errno = errno;
                error("unable to create directory for %s", ref_file);
                goto error_return;
        }
 
        lock->lock_fd = hold_lock_file_for_update(lock->lk, ref_file, lflags);
+       if (lock->lock_fd < 0) {
+               if (errno == ENOENT && --attempts_remaining > 0)
+                       /*
+                        * Maybe somebody just deleted one of the
+                        * directories leading to ref_file.  Try
+                        * again:
+                        */
+                       goto retry;
+               else
+                       unable_to_lock_index_die(ref_file, errno);
+       }
        return old_sha1 ? verify_lock(lock, old_sha1, mustexist) : lock;
 
  error_return:
@@ -2244,7 +2288,7 @@ static int pack_if_possible_fn(struct ref_entry *entry, void *cb_data)
        struct pack_refs_cb_data *cb = cb_data;
        enum peel_status peel_status;
        struct ref_entry *packed_entry;
-       int is_tag_ref = !prefixcmp(entry->name, "refs/tags/");
+       int is_tag_ref = starts_with(entry->name, "refs/tags/");
 
        /* ALWAYS pack tags */
        if (!(cb->flags & PACK_REFS_ALL) && !is_tag_ref)
@@ -2411,7 +2455,7 @@ static int curate_packed_ref_fn(struct ref_entry *entry, void *cb_data)
        return 0;
 }
 
-static int repack_without_refs(const char **refnames, int n)
+int repack_without_refs(const char **refnames, int n)
 {
        struct ref_dir *packed;
        struct string_list refs_to_delete = STRING_LIST_INIT_DUP;
@@ -2508,6 +2552,51 @@ int delete_ref(const char *refname, const unsigned char *sha1, int delopt)
  */
 #define TMP_RENAMED_LOG  "logs/refs/.tmp-renamed-log"
 
+static int rename_tmp_log(const char *newrefname)
+{
+       int attempts_remaining = 4;
+
+ retry:
+       switch (safe_create_leading_directories(git_path("logs/%s", newrefname))) {
+       case SCLD_OK:
+               break; /* success */
+       case SCLD_VANISHED:
+               if (--attempts_remaining > 0)
+                       goto retry;
+               /* fall through */
+       default:
+               error("unable to create directory for %s", newrefname);
+               return -1;
+       }
+
+       if (rename(git_path(TMP_RENAMED_LOG), git_path("logs/%s", newrefname))) {
+               if ((errno==EISDIR || errno==ENOTDIR) && --attempts_remaining > 0) {
+                       /*
+                        * 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", newrefname))) {
+                               error("Directory not empty: logs/%s", newrefname);
+                               return -1;
+                       }
+                       goto retry;
+               } else if (errno == ENOENT && --attempts_remaining > 0) {
+                       /*
+                        * Maybe another process just deleted one of
+                        * the directories in the path to newrefname.
+                        * Try again from the beginning.
+                        */
+                       goto retry;
+               } else {
+                       error("unable to move logfile "TMP_RENAMED_LOG" to logs/%s: %s",
+                               newrefname, strerror(errno));
+                       return -1;
+               }
+       }
+       return 0;
+}
+
 int rename_ref(const char *oldrefname, const char *newrefname, const char *logmsg)
 {
        unsigned char sha1[20], orig_sha1[20];
@@ -2555,30 +2644,9 @@ int rename_ref(const char *oldrefname, const char *newrefname, const char *logms
                }
        }
 
-       if (log && safe_create_leading_directories(git_path("logs/%s", newrefname))) {
-               error("unable to create directory for %s", newrefname);
+       if (log && rename_tmp_log(newrefname))
                goto rollback;
-       }
 
- retry:
-       if (log && rename(git_path(TMP_RENAMED_LOG), git_path("logs/%s", newrefname))) {
-               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", newrefname))) {
-                               error("Directory not empty: logs/%s", newrefname);
-                               goto rollback;
-                       }
-                       goto retry;
-               } else {
-                       error("unable to move logfile "TMP_RENAMED_LOG" to logs/%s: %s",
-                               newrefname, strerror(errno));
-                       goto rollback;
-               }
-       }
        logmoved = log;
 
        lock = lock_ref_sha1_basic(newrefname, NULL, 0, NULL);
@@ -2679,9 +2747,9 @@ int log_ref_setup(const char *refname, char *logfile, int bufsize)
 
        git_snpath(logfile, bufsize, "logs/%s", refname);
        if (log_all_ref_updates &&
-           (!prefixcmp(refname, "refs/heads/") ||
-            !prefixcmp(refname, "refs/remotes/") ||
-            !prefixcmp(refname, "refs/notes/") ||
+           (starts_with(refname, "refs/heads/") ||
+            starts_with(refname, "refs/remotes/") ||
+            starts_with(refname, "refs/notes/") ||
             !strcmp(refname, "HEAD"))) {
                if (safe_create_leading_directories(logfile) < 0)
                        return error("unable to create directory for %s",
@@ -2751,7 +2819,7 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1,
 
 static int is_branch(const char *refname)
 {
-       return !strcmp(refname, "HEAD") || !prefixcmp(refname, "refs/heads/");
+       return !strcmp(refname, "HEAD") || starts_with(refname, "refs/heads/");
 }
 
 int write_ref_sha1(struct ref_lock *lock,
@@ -2886,122 +2954,133 @@ int create_symref(const char *ref_target, const char *refs_heads_master,
        return 0;
 }
 
-static char *ref_msg(const char *line, const char *endp)
-{
-       const char *ep;
-       line += 82;
-       ep = memchr(line, '\n', endp - line);
-       if (!ep)
-               ep = endp;
-       return xmemdupz(line, ep - line);
+struct read_ref_at_cb {
+       const char *refname;
+       unsigned long at_time;
+       int cnt;
+       int reccnt;
+       unsigned char *sha1;
+       int found_it;
+
+       unsigned char osha1[20];
+       unsigned char nsha1[20];
+       int tz;
+       unsigned long date;
+       char **msg;
+       unsigned long *cutoff_time;
+       int *cutoff_tz;
+       int *cutoff_cnt;
+};
+
+static int read_ref_at_ent(unsigned char *osha1, unsigned char *nsha1,
+               const char *email, unsigned long timestamp, int tz,
+               const char *message, void *cb_data)
+{
+       struct read_ref_at_cb *cb = cb_data;
+
+       cb->reccnt++;
+       cb->tz = tz;
+       cb->date = timestamp;
+
+       if (timestamp <= cb->at_time || cb->cnt == 0) {
+               if (cb->msg)
+                       *cb->msg = xstrdup(message);
+               if (cb->cutoff_time)
+                       *cb->cutoff_time = timestamp;
+               if (cb->cutoff_tz)
+                       *cb->cutoff_tz = tz;
+               if (cb->cutoff_cnt)
+                       *cb->cutoff_cnt = cb->reccnt - 1;
+               /*
+                * we have not yet updated cb->[n|o]sha1 so they still
+                * hold the values for the previous record.
+                */
+               if (!is_null_sha1(cb->osha1)) {
+                       hashcpy(cb->sha1, nsha1);
+                       if (hashcmp(cb->osha1, nsha1))
+                               warning("Log for ref %s has gap after %s.",
+                                       cb->refname, show_date(cb->date, cb->tz, DATE_RFC2822));
+               }
+               else if (cb->date == cb->at_time)
+                       hashcpy(cb->sha1, nsha1);
+               else if (hashcmp(nsha1, cb->sha1))
+                       warning("Log for ref %s unexpectedly ended on %s.",
+                               cb->refname, show_date(cb->date, cb->tz,
+                                                  DATE_RFC2822));
+               hashcpy(cb->osha1, osha1);
+               hashcpy(cb->nsha1, nsha1);
+               cb->found_it = 1;
+               return 1;
+       }
+       hashcpy(cb->osha1, osha1);
+       hashcpy(cb->nsha1, nsha1);
+       if (cb->cnt > 0)
+               cb->cnt--;
+       return 0;
+}
+
+static int read_ref_at_ent_oldest(unsigned char *osha1, unsigned char *nsha1,
+                                 const char *email, unsigned long timestamp,
+                                 int tz, const char *message, void *cb_data)
+{
+       struct read_ref_at_cb *cb = cb_data;
+
+       if (cb->msg)
+               *cb->msg = xstrdup(message);
+       if (cb->cutoff_time)
+               *cb->cutoff_time = timestamp;
+       if (cb->cutoff_tz)
+               *cb->cutoff_tz = tz;
+       if (cb->cutoff_cnt)
+               *cb->cutoff_cnt = cb->reccnt;
+       hashcpy(cb->sha1, osha1);
+       if (is_null_sha1(cb->sha1))
+               hashcpy(cb->sha1, nsha1);
+       /* We just want the first entry */
+       return 1;
 }
 
 int read_ref_at(const char *refname, 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, reccnt = 0;
-       struct stat st;
-       unsigned long date;
-       unsigned char logged_sha1[20];
-       void *log_mapped;
-       size_t mapsz;
+       struct read_ref_at_cb cb;
 
-       logfile = git_path("logs/%s", refname);
-       logfd = open(logfile, O_RDONLY, 0);
-       if (logfd < 0)
-               die_errno("Unable to read log '%s'", logfile);
-       fstat(logfd, &st);
-       if (!st.st_size)
-               die("Log %s is empty.", logfile);
-       mapsz = xsize_t(st.st_size);
-       log_mapped = xmmap(NULL, mapsz, PROT_READ, MAP_PRIVATE, logfd, 0);
-       logdata = log_mapped;
-       close(logfd);
+       memset(&cb, 0, sizeof(cb));
+       cb.refname = refname;
+       cb.at_time = at_time;
+       cb.cnt = cnt;
+       cb.msg = msg;
+       cb.cutoff_time = cutoff_time;
+       cb.cutoff_tz = cutoff_tz;
+       cb.cutoff_cnt = cutoff_cnt;
+       cb.sha1 = sha1;
+
+       for_each_reflog_ent_reverse(refname, read_ref_at_ent, &cb);
+
+       if (!cb.reccnt)
+               die("Log for %s is empty.", refname);
+       if (cb.found_it)
+               return 0;
+
+       for_each_reflog_ent(refname, read_ref_at_ent_oldest, &cb);
 
-       lastrec = NULL;
-       rec = logend = logdata + st.st_size;
-       while (logdata < rec) {
-               reccnt++;
-               if (logdata < rec && *(rec-1) == '\n')
-                       rec--;
-               lastgt = NULL;
-               while (logdata < rec && *(rec-1) != '\n') {
-                       rec--;
-                       if (*rec == '>')
-                               lastgt = rec;
-               }
-               if (!lastgt)
-                       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)) {
-                                       warning("Log %s has gap after %s.",
-                                               logfile, show_date(date, tz, DATE_RFC2822));
-                               }
-                       }
-                       else if (date == at_time) {
-                               if (get_sha1_hex(rec + 41, sha1))
-                                       die("Log %s is corrupt.", logfile);
-                       }
-                       else {
-                               if (get_sha1_hex(rec + 41, logged_sha1))
-                                       die("Log %s is corrupt.", logfile);
-                               if (hashcmp(logged_sha1, sha1)) {
-                                       warning("Log %s unexpectedly ended on %s.",
-                                               logfile, show_date(date, tz, DATE_RFC2822));
-                               }
-                       }
-                       munmap(log_mapped, mapsz);
-                       return 0;
-               }
-               lastrec = rec;
-               if (cnt > 0)
-                       cnt--;
-       }
-
-       rec = logdata;
-       while (rec < logend && *rec != '>' && *rec != '\n')
-               rec++;
-       if (rec == logend || *rec == '\n')
-               die("Log %s is corrupt.", logfile);
-       date = strtoul(rec + 1, &tz_c, 10);
-       tz = strtoul(tz_c, NULL, 10);
-       if (get_sha1_hex(logdata, sha1))
-               die("Log %s is corrupt.", logfile);
-       if (is_null_sha1(sha1)) {
-               if (get_sha1_hex(logdata + 41, sha1))
-                       die("Log %s is corrupt.", logfile);
-       }
-       if (msg)
-               *msg = ref_msg(logdata, logend);
-       munmap(log_mapped, mapsz);
-
-       if (cutoff_time)
-               *cutoff_time = date;
-       if (cutoff_tz)
-               *cutoff_tz = tz;
-       if (cutoff_cnt)
-               *cutoff_cnt = reccnt;
        return 1;
 }
 
+int reflog_exists(const char *refname)
+{
+       struct stat st;
+
+       return !lstat(git_path("logs/%s", refname), &st) &&
+               S_ISREG(st.st_mode);
+}
+
+int delete_reflog(const char *refname)
+{
+       return remove_path(git_path("logs/%s", refname));
+}
+
 static int show_one_reflog_ent(struct strbuf *sb, each_reflog_ent_fn fn, void *cb_data)
 {
        unsigned char osha1[20], nsha1[20];
@@ -3199,9 +3278,9 @@ static struct ref_lock *update_ref_lock(const char *refname,
        if (!lock) {
                const char *str = "Cannot lock the ref '%s'.";
                switch (onerr) {
-               case MSG_ON_ERR: error(str, refname); break;
-               case DIE_ON_ERR: die(str, refname); break;
-               case QUIET_ON_ERR: break;
+               case UPDATE_REFS_MSG_ON_ERR: error(str, refname); break;
+               case UPDATE_REFS_DIE_ON_ERR: die(str, refname); break;
+               case UPDATE_REFS_QUIET_ON_ERR: break;
                }
        }
        return lock;
@@ -3214,15 +3293,118 @@ static int update_ref_write(const char *action, const char *refname,
        if (write_ref_sha1(lock, sha1, action) < 0) {
                const char *str = "Cannot update the ref '%s'.";
                switch (onerr) {
-               case MSG_ON_ERR: error(str, refname); break;
-               case DIE_ON_ERR: die(str, refname); break;
-               case QUIET_ON_ERR: break;
+               case UPDATE_REFS_MSG_ON_ERR: error(str, refname); break;
+               case UPDATE_REFS_DIE_ON_ERR: die(str, refname); break;
+               case UPDATE_REFS_QUIET_ON_ERR: break;
                }
                return 1;
        }
        return 0;
 }
 
+/**
+ * Information needed for a single ref update.  Set new_sha1 to the
+ * new value or to zero to delete the ref.  To check the old value
+ * while locking the ref, set have_old to 1 and set old_sha1 to the
+ * value or to zero to ensure the ref does not exist before update.
+ */
+struct ref_update {
+       unsigned char new_sha1[20];
+       unsigned char old_sha1[20];
+       int flags; /* REF_NODEREF? */
+       int have_old; /* 1 if old_sha1 is valid, 0 otherwise */
+       struct ref_lock *lock;
+       int type;
+       const char refname[FLEX_ARRAY];
+};
+
+/*
+ * Data structure for holding a reference transaction, which can
+ * consist of checks and updates to multiple references, carried out
+ * as atomically as possible.  This structure is opaque to callers.
+ */
+struct ref_transaction {
+       struct ref_update **updates;
+       size_t alloc;
+       size_t nr;
+};
+
+struct ref_transaction *ref_transaction_begin(void)
+{
+       return xcalloc(1, sizeof(struct ref_transaction));
+}
+
+static void ref_transaction_free(struct ref_transaction *transaction)
+{
+       int i;
+
+       for (i = 0; i < transaction->nr; i++)
+               free(transaction->updates[i]);
+
+       free(transaction->updates);
+       free(transaction);
+}
+
+void ref_transaction_rollback(struct ref_transaction *transaction)
+{
+       ref_transaction_free(transaction);
+}
+
+static struct ref_update *add_update(struct ref_transaction *transaction,
+                                    const char *refname)
+{
+       size_t len = strlen(refname);
+       struct ref_update *update = xcalloc(1, sizeof(*update) + len + 1);
+
+       strcpy((char *)update->refname, refname);
+       ALLOC_GROW(transaction->updates, transaction->nr + 1, transaction->alloc);
+       transaction->updates[transaction->nr++] = update;
+       return update;
+}
+
+void ref_transaction_update(struct ref_transaction *transaction,
+                           const char *refname,
+                           unsigned char *new_sha1, unsigned char *old_sha1,
+                           int flags, int have_old)
+{
+       struct ref_update *update = add_update(transaction, refname);
+
+       hashcpy(update->new_sha1, new_sha1);
+       update->flags = flags;
+       update->have_old = have_old;
+       if (have_old)
+               hashcpy(update->old_sha1, old_sha1);
+}
+
+void ref_transaction_create(struct ref_transaction *transaction,
+                           const char *refname,
+                           unsigned char *new_sha1,
+                           int flags)
+{
+       struct ref_update *update = add_update(transaction, refname);
+
+       assert(!is_null_sha1(new_sha1));
+       hashcpy(update->new_sha1, new_sha1);
+       hashclr(update->old_sha1);
+       update->flags = flags;
+       update->have_old = 1;
+}
+
+void ref_transaction_delete(struct ref_transaction *transaction,
+                           const char *refname,
+                           unsigned char *old_sha1,
+                           int flags, int have_old)
+{
+       struct ref_update *update = add_update(transaction, refname);
+
+       update->flags = flags;
+       update->have_old = have_old;
+       if (have_old) {
+               assert(!is_null_sha1(old_sha1));
+               hashcpy(update->old_sha1, old_sha1);
+       }
+}
+
 int update_ref(const char *action, const char *refname,
               const unsigned char *sha1, const unsigned char *oldval,
               int flags, enum action_on_err onerr)
@@ -3238,7 +3420,7 @@ static int ref_update_compare(const void *r1, const void *r2)
 {
        const struct ref_update * const *u1 = r1;
        const struct ref_update * const *u2 = r2;
-       return strcmp((*u1)->ref_name, (*u2)->ref_name);
+       return strcmp((*u1)->refname, (*u2)->refname);
 }
 
 static int ref_update_reject_duplicates(struct ref_update **updates, int n,
@@ -3246,15 +3428,15 @@ static int ref_update_reject_duplicates(struct ref_update **updates, int n,
 {
        int i;
        for (i = 1; i < n; i++)
-               if (!strcmp(updates[i - 1]->ref_name, updates[i]->ref_name)) {
+               if (!strcmp(updates[i - 1]->refname, updates[i]->refname)) {
                        const char *str =
                                "Multiple updates for ref '%s' not allowed.";
                        switch (onerr) {
-                       case MSG_ON_ERR:
-                               error(str, updates[i]->ref_name); break;
-                       case DIE_ON_ERR:
-                               die(str, updates[i]->ref_name); break;
-                       case QUIET_ON_ERR:
+                       case UPDATE_REFS_MSG_ON_ERR:
+                               error(str, updates[i]->refname); break;
+                       case UPDATE_REFS_DIE_ON_ERR:
+                               die(str, updates[i]->refname); break;
+                       case UPDATE_REFS_QUIET_ON_ERR:
                                break;
                        }
                        return 1;
@@ -3262,26 +3444,21 @@ static int ref_update_reject_duplicates(struct ref_update **updates, int n,
        return 0;
 }
 
-int update_refs(const char *action, const struct ref_update **updates_orig,
-               int n, enum action_on_err onerr)
+int ref_transaction_commit(struct ref_transaction *transaction,
+                          const char *msg, enum action_on_err onerr)
 {
        int ret = 0, delnum = 0, i;
-       struct ref_update **updates;
-       int *types;
-       struct ref_lock **locks;
        const char **delnames;
+       int n = transaction->nr;
+       struct ref_update **updates = transaction->updates;
 
-       if (!updates_orig || !n)
+       if (!n)
                return 0;
 
        /* Allocate work space */
-       updates = xmalloc(sizeof(*updates) * n);
-       types = xmalloc(sizeof(*types) * n);
-       locks = xcalloc(n, sizeof(*locks));
        delnames = xmalloc(sizeof(*delnames) * n);
 
        /* Copy, sort, and reject duplicate refs */
-       memcpy(updates, updates_orig, sizeof(*updates) * n);
        qsort(updates, n, sizeof(*updates), ref_update_compare);
        ret = ref_update_reject_duplicates(updates, n, onerr);
        if (ret)
@@ -3289,35 +3466,44 @@ int update_refs(const char *action, const struct ref_update **updates_orig,
 
        /* Acquire all locks while verifying old values */
        for (i = 0; i < n; i++) {
-               locks[i] = update_ref_lock(updates[i]->ref_name,
-                                          (updates[i]->have_old ?
-                                           updates[i]->old_sha1 : NULL),
-                                          updates[i]->flags,
-                                          &types[i], onerr);
-               if (!locks[i]) {
+               struct ref_update *update = updates[i];
+
+               update->lock = update_ref_lock(update->refname,
+                                              (update->have_old ?
+                                               update->old_sha1 : NULL),
+                                              update->flags,
+                                              &update->type, onerr);
+               if (!update->lock) {
                        ret = 1;
                        goto cleanup;
                }
        }
 
        /* Perform updates first so live commits remain referenced */
-       for (i = 0; i < n; i++)
-               if (!is_null_sha1(updates[i]->new_sha1)) {
-                       ret = update_ref_write(action,
-                                              updates[i]->ref_name,
-                                              updates[i]->new_sha1,
-                                              locks[i], onerr);
-                       locks[i] = NULL; /* freed by update_ref_write */
+       for (i = 0; i < n; i++) {
+               struct ref_update *update = updates[i];
+
+               if (!is_null_sha1(update->new_sha1)) {
+                       ret = update_ref_write(msg,
+                                              update->refname,
+                                              update->new_sha1,
+                                              update->lock, onerr);
+                       update->lock = NULL; /* freed by update_ref_write */
                        if (ret)
                                goto cleanup;
                }
+       }
 
        /* Perform deletes now that updates are safely completed */
-       for (i = 0; i < n; i++)
-               if (locks[i]) {
-                       delnames[delnum++] = locks[i]->ref_name;
-                       ret |= delete_ref_loose(locks[i], types[i]);
+       for (i = 0; i < n; i++) {
+               struct ref_update *update = updates[i];
+
+               if (update->lock) {
+                       delnames[delnum++] = update->lock->ref_name;
+                       ret |= delete_ref_loose(update->lock, update->type);
                }
+       }
+
        ret |= repack_without_refs(delnames, delnum);
        for (i = 0; i < delnum; i++)
                unlink_or_warn(git_path("logs/%s", delnames[i]));
@@ -3325,38 +3511,13 @@ int update_refs(const char *action, const struct ref_update **updates_orig,
 
 cleanup:
        for (i = 0; i < n; i++)
-               if (locks[i])
-                       unlock_ref(locks[i]);
-       free(updates);
-       free(types);
-       free(locks);
+               if (updates[i]->lock)
+                       unlock_ref(updates[i]->lock);
        free(delnames);
+       ref_transaction_free(transaction);
        return ret;
 }
 
-/*
- * generate a format suitable for scanf from a ref_rev_parse_rules
- * rule, that is replace the "%.*s" spec with a "%s" spec
- */
-static void gen_scanf_fmt(char *scanf_fmt, const char *rule)
-{
-       char *spec;
-
-       spec = strstr(rule, "%.*s");
-       if (!spec || strstr(spec + 4, "%.*s"))
-               die("invalid rule in ref_rev_parse_rules: %s", rule);
-
-       /* copy all until spec */
-       strncpy(scanf_fmt, rule, spec - rule);
-       scanf_fmt[spec - rule] = '\0';
-       /* copy new spec */
-       strcat(scanf_fmt, "%s");
-       /* copy remaining rule */
-       strcat(scanf_fmt, spec + 4);
-
-       return;
-}
-
 char *shorten_unambiguous_ref(const char *refname, int strict)
 {
        int i;
@@ -3364,23 +3525,29 @@ char *shorten_unambiguous_ref(const char *refname, int strict)
        static int nr_rules;
        char *short_name;
 
-       /* pre generate scanf formats from ref_rev_parse_rules[] */
        if (!nr_rules) {
+               /*
+                * Pre-generate scanf formats from ref_rev_parse_rules[].
+                * Generate a format suitable for scanf from a
+                * ref_rev_parse_rules rule by interpolating "%s" at the
+                * location of the "%.*s".
+                */
                size_t total_len = 0;
+               size_t offset = 0;
 
                /* the rule list is NULL terminated, count them first */
                for (nr_rules = 0; ref_rev_parse_rules[nr_rules]; nr_rules++)
-                       /* no +1 because strlen("%s") < strlen("%.*s") */
-                       total_len += strlen(ref_rev_parse_rules[nr_rules]);
+                       /* -2 for strlen("%.*s") - strlen("%s"); +1 for NUL */
+                       total_len += strlen(ref_rev_parse_rules[nr_rules]) - 2 + 1;
 
                scanf_fmts = xmalloc(nr_rules * sizeof(char *) + total_len);
 
-               total_len = 0;
+               offset = 0;
                for (i = 0; i < nr_rules; i++) {
-                       scanf_fmts[i] = (char *)&scanf_fmts[nr_rules]
-                                       + total_len;
-                       gen_scanf_fmt(scanf_fmts[i], ref_rev_parse_rules[i]);
-                       total_len += strlen(ref_rev_parse_rules[i]);
+                       assert(offset < total_len);
+                       scanf_fmts[i] = (char *)&scanf_fmts[nr_rules] + offset;
+                       offset += snprintf(scanf_fmts[i], total_len - offset,
+                                          ref_rev_parse_rules[i], 2, "%s") + 1;
                }
        }
 
@@ -3450,7 +3617,7 @@ int parse_hide_refs_config(const char *var, const char *value, const char *secti
 {
        if (!strcmp("transfer.hiderefs", var) ||
            /* NEEDSWORK: use parse_config_key() once both are merged */
-           (!prefixcmp(var, section) && var[strlen(section)] == '.' &&
+           (starts_with(var, section) && var[strlen(section)] == '.' &&
             !strcmp(var + strlen(section), ".hiderefs"))) {
                char *ref;
                int len;
@@ -3478,7 +3645,7 @@ int ref_is_hidden(const char *refname)
                return 0;
        for_each_string_list_item(item, hide_refs) {
                int len;
-               if (prefixcmp(refname, item->string))
+               if (!starts_with(refname, item->string))
                        continue;
                len = strlen(item->string);
                if (!refname[len] || refname[len] == '/')