refs.c: don't expose the internal struct ref_lock in the header file
[gitweb.git] / refs.c
diff --git a/refs.c b/refs.c
index fe1352a344e68b7c9ef63f4626314e6ba3e15f62..048fbbf50f6dab1742f6b82b71ae6fa33f0134e1 100644 (file)
--- a/refs.c
+++ b/refs.c
@@ -6,6 +6,15 @@
 #include "dir.h"
 #include "string-list.h"
 
+struct ref_lock {
+       char *ref_name;
+       char *orig_ref_name;
+       struct lock_file *lk;
+       unsigned char old_sha1[20];
+       int lock_fd;
+       int force_write;
+};
+
 /*
  * How to handle various characters in refnames:
  * 0: An acceptable character for refs
@@ -187,8 +196,8 @@ struct ref_dir {
 
 /*
  * Bit values for ref_entry::flag.  REF_ISSYMREF=0x01,
- * REF_ISPACKED=0x02, and REF_ISBROKEN=0x04 are public values; see
- * refs.h.
+ * REF_ISPACKED=0x02, REF_ISBROKEN=0x04 and REF_BAD_NAME=0x08 are
+ * public values; see refs.h.
  */
 
 /*
@@ -196,16 +205,16 @@ struct ref_dir {
  * the correct peeled value for the reference, which might be
  * null_sha1 if the reference is not a tag or if it is broken.
  */
-#define REF_KNOWS_PEELED 0x08
+#define REF_KNOWS_PEELED 0x10
 
 /* ref_entry represents a directory of references */
-#define REF_DIR 0x10
+#define REF_DIR 0x20
 
 /*
  * Entry has not yet been read from disk (used only for REF_DIR
  * entries representing loose references)
  */
-#define REF_INCOMPLETE 0x20
+#define REF_INCOMPLETE 0x40
 
 /*
  * A ref_entry represents either a reference or a "subdirectory" of
@@ -274,6 +283,39 @@ static struct ref_dir *get_ref_dir(struct ref_entry *entry)
        return dir;
 }
 
+/*
+ * Check if a refname is safe.
+ * For refs that start with "refs/" we consider it safe as long they do
+ * not try to resolve to outside of refs/.
+ *
+ * For all other refs we only consider them safe iff they only contain
+ * upper case characters and '_' (like "HEAD" AND "MERGE_HEAD", and not like
+ * "config").
+ */
+static int refname_is_safe(const char *refname)
+{
+       if (starts_with(refname, "refs/")) {
+               char *buf;
+               int result;
+
+               buf = xmalloc(strlen(refname) + 1);
+               /*
+                * Does the refname try to escape refs/?
+                * For example: refs/foo/../bar is safe but refs/foo/../../bar
+                * is not.
+                */
+               result = !normalize_path_copy(buf, refname + strlen("refs/"));
+               free(buf);
+               return result;
+       }
+       while (*refname) {
+               if (!isupper(*refname) && *refname != '_')
+                       return 0;
+               refname++;
+       }
+       return 1;
+}
+
 static struct ref_entry *create_ref_entry(const char *refname,
                                          const unsigned char *sha1, int flag,
                                          int check_name)
@@ -284,6 +326,8 @@ static struct ref_entry *create_ref_entry(const char *refname,
        if (check_name &&
            check_refname_format(refname, REFNAME_ALLOW_ONELEVEL))
                die("Reference has invalid format: '%s'", refname);
+       if (!check_name && !refname_is_safe(refname))
+               die("Reference has invalid name: '%s'", refname);
        len = strlen(refname) + 1;
        ref = xmalloc(sizeof(struct ref_entry) + len);
        hashcpy(ref->u.value.sha1, sha1);
@@ -1111,7 +1155,13 @@ static void read_packed_refs(FILE *f, struct ref_dir *dir)
 
                refname = parse_ref_line(refline, sha1);
                if (refname) {
-                       last = create_ref_entry(refname, sha1, REF_ISPACKED, 1);
+                       int flag = REF_ISPACKED;
+
+                       if (check_refname_format(refname, REFNAME_ALLOW_ONELEVEL)) {
+                               hashclr(sha1);
+                               flag |= REF_BAD_NAME | REF_ISBROKEN;
+                       }
+                       last = create_ref_entry(refname, sha1, flag, 0);
                        if (peeled == PEELED_FULLY ||
                            (peeled == PEELED_TAGS && starts_with(refname, "refs/tags/")))
                                last->flag |= REF_KNOWS_PEELED;
@@ -1249,8 +1299,13 @@ static void read_loose_refs(const char *dirname, struct ref_dir *dir)
                                hashclr(sha1);
                                flag |= REF_ISBROKEN;
                        }
+                       if (check_refname_format(refname.buf,
+                                                REFNAME_ALLOW_ONELEVEL)) {
+                               hashclr(sha1);
+                               flag |= REF_BAD_NAME | REF_ISBROKEN;
+                       }
                        add_entry_to_dir(dir,
-                                        create_ref_entry(refname.buf, sha1, flag, 1));
+                                        create_ref_entry(refname.buf, sha1, flag, 0));
                }
                strbuf_setlen(&refname, dirnamelen);
        }
@@ -1369,10 +1424,10 @@ static struct ref_entry *get_packed_ref(const char *refname)
  * A loose ref file doesn't exist; check for a packed ref.  The
  * options are forwarded from resolve_safe_unsafe().
  */
-static const char *handle_missing_loose_ref(const char *refname,
-                                           int resolve_flags,
-                                           unsigned char *sha1,
-                                           int *flags)
+static int resolve_missing_loose_ref(const char *refname,
+                                    int resolve_flags,
+                                    unsigned char *sha1,
+                                    int *flags)
 {
        struct ref_entry *entry;
 
@@ -1385,14 +1440,15 @@ static const char *handle_missing_loose_ref(const char *refname,
                hashcpy(sha1, entry->u.value.sha1);
                if (flags)
                        *flags |= REF_ISPACKED;
-               return refname;
+               return 0;
        }
        /* The reference is not a packed reference, either. */
        if (resolve_flags & RESOLVE_REF_READING) {
-               return NULL;
+               errno = ENOENT;
+               return -1;
        } else {
                hashclr(sha1);
-               return refname;
+               return 0;
        }
 }
 
@@ -1403,13 +1459,29 @@ const char *resolve_ref_unsafe(const char *refname, int resolve_flags, unsigned
        ssize_t len;
        char buffer[256];
        static char refname_buffer[256];
+       int bad_name = 0;
 
        if (flags)
                *flags = 0;
 
        if (check_refname_format(refname, REFNAME_ALLOW_ONELEVEL)) {
-               errno = EINVAL;
-               return NULL;
+               if (flags)
+                       *flags |= REF_BAD_NAME;
+
+               if (!(resolve_flags & RESOLVE_REF_ALLOW_BAD_NAME) ||
+                   !refname_is_safe(refname)) {
+                       errno = EINVAL;
+                       return NULL;
+               }
+               /*
+                * dwim_ref() uses REF_ISBROKEN to distinguish between
+                * missing refs and refs that were present but invalid,
+                * to complain about the latter to stderr.
+                *
+                * We don't know whether the ref exists, so don't set
+                * REF_ISBROKEN yet.
+                */
+               bad_name = 1;
        }
        for (;;) {
                char path[PATH_MAX];
@@ -1435,11 +1507,17 @@ const char *resolve_ref_unsafe(const char *refname, int resolve_flags, unsigned
                 */
        stat_ref:
                if (lstat(path, &st) < 0) {
-                       if (errno == ENOENT)
-                               return handle_missing_loose_ref(refname,
-                                               resolve_flags, sha1, flags);
-                       else
+                       if (errno != ENOENT)
+                               return NULL;
+                       if (resolve_missing_loose_ref(refname, resolve_flags,
+                                                     sha1, flags))
                                return NULL;
+                       if (bad_name) {
+                               hashclr(sha1);
+                               if (flags)
+                                       *flags |= REF_ISBROKEN;
+                       }
+                       return refname;
                }
 
                /* Follow "normalized" - ie "refs/.." symlinks by hand */
@@ -1512,6 +1590,11 @@ const char *resolve_ref_unsafe(const char *refname, int resolve_flags, unsigned
                                errno = EINVAL;
                                return NULL;
                        }
+                       if (bad_name) {
+                               hashclr(sha1);
+                               if (flags)
+                                       *flags |= REF_ISBROKEN;
+                       }
                        return refname;
                }
                if (flags)
@@ -1527,8 +1610,13 @@ const char *resolve_ref_unsafe(const char *refname, int resolve_flags, unsigned
                if (check_refname_format(buf, REFNAME_ALLOW_ONELEVEL)) {
                        if (flags)
                                *flags |= REF_ISBROKEN;
-                       errno = EINVAL;
-                       return NULL;
+
+                       if (!(resolve_flags & RESOLVE_REF_ALLOW_BAD_NAME) ||
+                           !refname_is_safe(buf)) {
+                               errno = EINVAL;
+                               return NULL;
+                       }
+                       bad_name = 1;
                }
        }
 }
@@ -2011,6 +2099,16 @@ int refname_match(const char *abbrev_name, const char *full_name)
        return 0;
 }
 
+static void unlock_ref(struct ref_lock *lock)
+{
+       /* Do not free lock->lk -- atexit() still looks at them */
+       if (lock->lk)
+               rollback_lock_file(lock->lk);
+       free(lock->ref_name);
+       free(lock->orig_ref_name);
+       free(lock);
+}
+
 /* This function should make sure errno is meaningful on error */
 static struct ref_lock *verify_lock(struct ref_lock *lock,
        const unsigned char *old_sha1, int mustexist)
@@ -2160,18 +2258,16 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
        int missing = 0;
        int attempts_remaining = 3;
 
-       if (check_refname_format(refname, REFNAME_ALLOW_ONELEVEL)) {
-               errno = EINVAL;
-               return NULL;
-       }
-
        lock = xcalloc(1, sizeof(struct ref_lock));
        lock->lock_fd = -1;
 
        if (mustexist)
                resolve_flags |= RESOLVE_REF_READING;
-       if (flags & REF_NODEREF && flags & REF_DELETING)
-               resolve_flags |= RESOLVE_REF_NO_RECURSE;
+       if (flags & REF_DELETING) {
+               resolve_flags |= RESOLVE_REF_ALLOW_BAD_NAME;
+               if (flags & REF_NODEREF)
+                       resolve_flags |= RESOLVE_REF_NO_RECURSE;
+       }
 
        refname = resolve_ref_unsafe(refname, resolve_flags,
                                     lock->old_sha1, &type);
@@ -2259,13 +2355,6 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
        return NULL;
 }
 
-struct ref_lock *lock_any_ref_for_update(const char *refname,
-                                        const unsigned char *old_sha1,
-                                        int flags, int *type_p)
-{
-       return lock_ref_sha1_basic(refname, old_sha1, NULL, flags, type_p);
-}
-
 /*
  * Write an entry to the packed-refs file for the specified refname.
  * If peeled is non-NULL, write it as the entry's peeled value.
@@ -2569,6 +2658,8 @@ int repack_without_refs(const char **refnames, int n, struct strbuf *err)
        struct string_list_item *ref_to_delete;
        int i, ret, removed = 0;
 
+       assert(err);
+
        /* Look for a packed ref */
        for (i = 0; i < n; i++)
                if (get_packed_ref(refnames[i]))
@@ -2579,13 +2670,8 @@ int repack_without_refs(const char **refnames, int n, struct strbuf *err)
                return 0; /* no refname exists in packed refs */
 
        if (lock_packed_refs(0)) {
-               if (err) {
-                       unable_to_lock_message(git_path("packed-refs"), errno,
-                                              err);
-                       return -1;
-               }
-               unable_to_lock_error(git_path("packed-refs"), errno);
-               return error("cannot delete '%s' from packed refs", refnames[i]);
+               unable_to_lock_message(git_path("packed-refs"), errno, err);
+               return -1;
        }
        packed = get_packed_refs(&ref_cache);
 
@@ -2611,7 +2697,7 @@ int repack_without_refs(const char **refnames, int n, struct strbuf *err)
 
        /* Write what remains */
        ret = commit_packed_refs();
-       if (ret && err)
+       if (ret)
                strbuf_addf(err, "unable to overwrite old ref-pack file: %s",
                            strerror(errno));
        return ret;
@@ -2619,6 +2705,8 @@ int repack_without_refs(const char **refnames, int n, struct strbuf *err)
 
 static int delete_ref_loose(struct ref_lock *lock, int flag, struct strbuf *err)
 {
+       assert(err);
+
        if (!(flag & REF_ISPACKED) || flag & REF_ISSYMREF) {
                /*
                 * loose.  The loose file name is the same as the
@@ -2812,7 +2900,7 @@ int rename_ref(const char *oldrefname, const char *newrefname, const char *logms
        return 1;
 }
 
-int close_ref(struct ref_lock *lock)
+static int close_ref(struct ref_lock *lock)
 {
        if (close_lock_file(lock->lk))
                return -1;
@@ -2820,7 +2908,7 @@ int close_ref(struct ref_lock *lock)
        return 0;
 }
 
-int commit_ref(struct ref_lock *lock)
+static int commit_ref(struct ref_lock *lock)
 {
        if (commit_lock_file(lock->lk))
                return -1;
@@ -2828,16 +2916,6 @@ int commit_ref(struct ref_lock *lock)
        return 0;
 }
 
-void unlock_ref(struct ref_lock *lock)
-{
-       /* Do not free lock->lk -- atexit() still looks at them */
-       if (lock->lk)
-               rollback_lock_file(lock->lk);
-       free(lock->ref_name);
-       free(lock->orig_ref_name);
-       free(lock);
-}
-
 /*
  * copy the reflog message msg to buf, which has been allocated sufficiently
  * large, while cleaning up the whitespaces.  Especially, convert LF to space,
@@ -2886,10 +2964,10 @@ int log_ref_setup(const char *refname, char *logfile, int bufsize)
 
        logfd = open(logfile, oflags, 0666);
        if (logfd < 0) {
-               if (!(oflags & O_CREAT) && errno == ENOENT)
+               if (!(oflags & O_CREAT) && (errno == ENOENT || errno == EISDIR))
                        return 0;
 
-               if ((oflags & O_CREAT) && errno == EISDIR) {
+               if (errno == EISDIR) {
                        if (remove_empty_directories(logfile)) {
                                int save_errno = errno;
                                error("There are still logs under '%s'",
@@ -2914,15 +2992,37 @@ int log_ref_setup(const char *refname, char *logfile, int bufsize)
        return 0;
 }
 
+static int log_ref_write_fd(int fd, const unsigned char *old_sha1,
+                           const unsigned char *new_sha1,
+                           const char *committer, const char *msg)
+{
+       int msglen, written;
+       unsigned maxlen, len;
+       char *logrec;
+
+       msglen = msg ? strlen(msg) : 0;
+       maxlen = strlen(committer) + msglen + 100;
+       logrec = xmalloc(maxlen);
+       len = sprintf(logrec, "%s %s %s\n",
+                     sha1_to_hex(old_sha1),
+                     sha1_to_hex(new_sha1),
+                     committer);
+       if (msglen)
+               len += copy_msg(logrec + len - 1, msg) - 1;
+
+       written = len <= maxlen ? write_in_full(fd, logrec, len) : -1;
+       free(logrec);
+       if (written != len)
+               return -1;
+
+       return 0;
+}
+
 static int log_ref_write(const char *refname, const unsigned char *old_sha1,
                         const unsigned char *new_sha1, const char *msg)
 {
-       int logfd, result, written, oflags = O_APPEND | O_WRONLY;
-       unsigned maxlen, len;
-       int msglen;
+       int logfd, result, oflags = O_APPEND | O_WRONLY;
        char log_file[PATH_MAX];
-       char *logrec;
-       const char *committer;
 
        if (log_all_ref_updates < 0)
                log_all_ref_updates = !is_bare_repository();
@@ -2934,19 +3034,9 @@ static int log_ref_write(const char *refname, const unsigned char *old_sha1,
        logfd = open(log_file, oflags);
        if (logfd < 0)
                return 0;
-       msglen = msg ? strlen(msg) : 0;
-       committer = git_committer_info(0);
-       maxlen = strlen(committer) + msglen + 100;
-       logrec = xmalloc(maxlen);
-       len = sprintf(logrec, "%s %s %s\n",
-                     sha1_to_hex(old_sha1),
-                     sha1_to_hex(new_sha1),
-                     committer);
-       if (msglen)
-               len += copy_msg(logrec + len - 1, msg) - 1;
-       written = len <= maxlen ? write_in_full(logfd, logrec, len) : -1;
-       free(logrec);
-       if (written != len) {
+       result = log_ref_write_fd(logfd, old_sha1, new_sha1,
+                                 git_committer_info(0), msg);
+       if (result) {
                int save_errno = errno;
                close(logfd);
                error("Unable to append to %s", log_file);
@@ -3474,6 +3564,8 @@ struct ref_transaction {
 
 struct ref_transaction *ref_transaction_begin(struct strbuf *err)
 {
+       assert(err);
+
        return xcalloc(1, sizeof(struct ref_transaction));
 }
 
@@ -3513,12 +3605,21 @@ int ref_transaction_update(struct ref_transaction *transaction,
 {
        struct ref_update *update;
 
+       assert(err);
+
        if (transaction->state != REF_TRANSACTION_OPEN)
                die("BUG: update called for transaction that is not open");
 
        if (have_old && !old_sha1)
                die("BUG: have_old is true but old_sha1 is NULL");
 
+       if (!is_null_sha1(new_sha1) &&
+           check_refname_format(refname, REFNAME_ALLOW_ONELEVEL)) {
+               strbuf_addf(err, "refusing to update ref with bad name %s",
+                           refname);
+               return -1;
+       }
+
        update = add_update(transaction, refname);
        hashcpy(update->new_sha1, new_sha1);
        update->flags = flags;
@@ -3536,23 +3637,8 @@ int ref_transaction_create(struct ref_transaction *transaction,
                           int flags, const char *msg,
                           struct strbuf *err)
 {
-       struct ref_update *update;
-
-       if (transaction->state != REF_TRANSACTION_OPEN)
-               die("BUG: create called for transaction that is not open");
-
-       if (!new_sha1 || is_null_sha1(new_sha1))
-               die("BUG: create ref with null new_sha1");
-
-       update = add_update(transaction, refname);
-
-       hashcpy(update->new_sha1, new_sha1);
-       hashclr(update->old_sha1);
-       update->flags = flags;
-       update->have_old = 1;
-       if (msg)
-               update->msg = xstrdup(msg);
-       return 0;
+       return ref_transaction_update(transaction, refname, new_sha1,
+                                     null_sha1, flags, 1, msg, err);
 }
 
 int ref_transaction_delete(struct ref_transaction *transaction,
@@ -3561,24 +3647,8 @@ int ref_transaction_delete(struct ref_transaction *transaction,
                           int flags, int have_old, const char *msg,
                           struct strbuf *err)
 {
-       struct ref_update *update;
-
-       if (transaction->state != REF_TRANSACTION_OPEN)
-               die("BUG: delete called for transaction that is not open");
-
-       if (have_old && !old_sha1)
-               die("BUG: have_old is true but old_sha1 is NULL");
-
-       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);
-       }
-       if (msg)
-               update->msg = xstrdup(msg);
-       return 0;
+       return ref_transaction_update(transaction, refname, null_sha1,
+                                     old_sha1, flags, have_old, msg, err);
 }
 
 int update_ref(const char *action, const char *refname,
@@ -3625,13 +3695,14 @@ static int ref_update_reject_duplicates(struct ref_update **updates, int n,
                                        struct strbuf *err)
 {
        int i;
+
+       assert(err);
+
        for (i = 1; i < n; i++)
                if (!strcmp(updates[i - 1]->refname, updates[i]->refname)) {
-                       const char *str =
-                               "Multiple updates for ref '%s' not allowed.";
-                       if (err)
-                               strbuf_addf(err, str, updates[i]->refname);
-
+                       strbuf_addf(err,
+                                   "Multiple updates for ref '%s' not allowed.",
+                                   updates[i]->refname);
                        return 1;
                }
        return 0;
@@ -3645,6 +3716,8 @@ int ref_transaction_commit(struct ref_transaction *transaction,
        int n = transaction->nr;
        struct ref_update **updates = transaction->updates;
 
+       assert(err);
+
        if (transaction->state != REF_TRANSACTION_OPEN)
                die("BUG: commit called for transaction that is not open");
 
@@ -3681,9 +3754,8 @@ int ref_transaction_commit(struct ref_transaction *transaction,
                        ret = (errno == ENOTDIR)
                                ? TRANSACTION_NAME_CONFLICT
                                : TRANSACTION_GENERIC_ERROR;
-                       if (err)
-                               strbuf_addf(err, "Cannot lock the ref '%s'.",
-                                           update->refname);
+                       strbuf_addf(err, "Cannot lock the ref '%s'.",
+                                   update->refname);
                        goto cleanup;
                }
        }
@@ -3696,9 +3768,8 @@ int ref_transaction_commit(struct ref_transaction *transaction,
                        if (write_ref_sha1(update->lock, update->new_sha1,
                                           update->msg)) {
                                update->lock = NULL; /* freed by write_ref_sha1 */
-                               if (err)
-                                       strbuf_addf(err, "Cannot update the ref '%s'.",
-                                                   update->refname);
+                               strbuf_addf(err, "Cannot update the ref '%s'.",
+                                           update->refname);
                                ret = TRANSACTION_GENERIC_ERROR;
                                goto cleanup;
                        }
@@ -3711,16 +3782,20 @@ int ref_transaction_commit(struct ref_transaction *transaction,
                struct ref_update *update = updates[i];
 
                if (update->lock) {
-                       if (delete_ref_loose(update->lock, update->type, err))
+                       if (delete_ref_loose(update->lock, update->type, err)) {
                                ret = TRANSACTION_GENERIC_ERROR;
+                               goto cleanup;
+                       }
 
                        if (!(update->flags & REF_ISPRUNING))
                                delnames[delnum++] = update->lock->ref_name;
                }
        }
 
-       if (repack_without_refs(delnames, delnum, err))
+       if (repack_without_refs(delnames, delnum, err)) {
                ret = TRANSACTION_GENERIC_ERROR;
+               goto cleanup;
+       }
        for (i = 0; i < delnum; i++)
                unlink_or_warn(git_path("logs/%s", delnames[i]));
        clear_loose_ref_cache(&ref_cache);
@@ -3870,3 +3945,132 @@ int ref_is_hidden(const char *refname)
        }
        return 0;
 }
+
+struct expire_reflog_cb {
+       unsigned int flags;
+       reflog_expiry_should_prune_fn *should_prune_fn;
+       void *policy_cb;
+       FILE *newlog;
+       unsigned char last_kept_sha1[20];
+};
+
+static int expire_reflog_ent(unsigned char *osha1, unsigned char *nsha1,
+                            const char *email, unsigned long timestamp, int tz,
+                            const char *message, void *cb_data)
+{
+       struct expire_reflog_cb *cb = cb_data;
+       struct expire_reflog_policy_cb *policy_cb = cb->policy_cb;
+
+       if (cb->flags & EXPIRE_REFLOGS_REWRITE)
+               osha1 = cb->last_kept_sha1;
+
+       if ((*cb->should_prune_fn)(osha1, nsha1, email, timestamp, tz,
+                                  message, policy_cb)) {
+               if (!cb->newlog)
+                       printf("would prune %s", message);
+               else if (cb->flags & EXPIRE_REFLOGS_VERBOSE)
+                       printf("prune %s", message);
+       } else {
+               if (cb->newlog) {
+                       char sign = (tz < 0) ? '-' : '+';
+                       int zone = (tz < 0) ? (-tz) : tz;
+                       fprintf(cb->newlog, "%s %s %s %lu %c%04d\t%s",
+                               sha1_to_hex(osha1), sha1_to_hex(nsha1),
+                               email, timestamp, sign, zone,
+                               message);
+                       hashcpy(cb->last_kept_sha1, nsha1);
+               }
+               if (cb->flags & EXPIRE_REFLOGS_VERBOSE)
+                       printf("keep %s", message);
+       }
+       return 0;
+}
+
+int reflog_expire(const char *refname, const unsigned char *sha1,
+                unsigned int flags,
+                reflog_expiry_prepare_fn prepare_fn,
+                reflog_expiry_should_prune_fn should_prune_fn,
+                reflog_expiry_cleanup_fn cleanup_fn,
+                void *policy_cb_data)
+{
+       static struct lock_file reflog_lock;
+       struct expire_reflog_cb cb;
+       struct ref_lock *lock;
+       char *log_file;
+       int status = 0;
+
+       memset(&cb, 0, sizeof(cb));
+       cb.flags = flags;
+       cb.policy_cb = policy_cb_data;
+       cb.should_prune_fn = should_prune_fn;
+
+       /*
+        * The reflog file is locked by holding the lock on the
+        * reference itself, plus we might need to update the
+        * reference if --updateref was specified:
+        */
+       lock = lock_ref_sha1_basic(refname, sha1, NULL, 0, NULL);
+       if (!lock)
+               return error("cannot lock ref '%s'", refname);
+       if (!reflog_exists(refname)) {
+               unlock_ref(lock);
+               return 0;
+       }
+
+       log_file = git_pathdup("logs/%s", refname);
+       if (!(flags & EXPIRE_REFLOGS_DRY_RUN)) {
+               /*
+                * Even though holding $GIT_DIR/logs/$reflog.lock has
+                * no locking implications, we use the lock_file
+                * machinery here anyway because it does a lot of the
+                * work we need, including cleaning up if the program
+                * exits unexpectedly.
+                */
+               if (hold_lock_file_for_update(&reflog_lock, log_file, 0) < 0) {
+                       struct strbuf err = STRBUF_INIT;
+                       unable_to_lock_message(log_file, errno, &err);
+                       error("%s", err.buf);
+                       strbuf_release(&err);
+                       goto failure;
+               }
+               cb.newlog = fdopen_lock_file(&reflog_lock, "w");
+               if (!cb.newlog) {
+                       error("cannot fdopen %s (%s)",
+                             reflog_lock.filename.buf, strerror(errno));
+                       goto failure;
+               }
+       }
+
+       (*prepare_fn)(refname, sha1, cb.policy_cb);
+       for_each_reflog_ent(refname, expire_reflog_ent, &cb);
+       (*cleanup_fn)(cb.policy_cb);
+
+       if (!(flags & EXPIRE_REFLOGS_DRY_RUN)) {
+               if (close_lock_file(&reflog_lock)) {
+                       status |= error("couldn't write %s: %s", log_file,
+                                       strerror(errno));
+               } else if ((flags & EXPIRE_REFLOGS_UPDATE_REF) &&
+                       (write_in_full(lock->lock_fd,
+                               sha1_to_hex(cb.last_kept_sha1), 40) != 40 ||
+                        write_str_in_full(lock->lock_fd, "\n") != 1 ||
+                        close_ref(lock) < 0)) {
+                       status |= error("couldn't write %s",
+                                       lock->lk->filename.buf);
+                       rollback_lock_file(&reflog_lock);
+               } else if (commit_lock_file(&reflog_lock)) {
+                       status |= error("unable to commit reflog '%s' (%s)",
+                                       log_file, strerror(errno));
+               } else if ((flags & EXPIRE_REFLOGS_UPDATE_REF) && commit_ref(lock)) {
+                       status |= error("couldn't set %s", lock->ref_name);
+               }
+       }
+       free(log_file);
+       unlock_ref(lock);
+       return status;
+
+ failure:
+       rollback_lock_file(&reflog_lock);
+       free(log_file);
+       unlock_ref(lock);
+       return -1;
+}