refs: change how packed refs are deleted
[gitweb.git] / refs.c
diff --git a/refs.c b/refs.c
index 002daebf6c213ffe9c7663c12fe105c2706ba3df..f8f295ea6927df502472238268effd70348a0ace 100644 (file)
--- a/refs.c
+++ b/refs.c
@@ -367,18 +367,17 @@ static int ref_entry_cmp_sslice(const void *key_, const void *ent_)
 }
 
 /*
- * Return the entry with the given refname from the ref_dir
- * (non-recursively), sorting dir if necessary.  Return NULL if no
- * such entry is found.  dir must already be complete.
+ * Return the index of the entry with the given refname from the
+ * ref_dir (non-recursively), sorting dir if necessary.  Return -1 if
+ * no such entry is found.  dir must already be complete.
  */
-static struct ref_entry *search_ref_dir(struct ref_dir *dir,
-                                       const char *refname, size_t len)
+static int search_ref_dir(struct ref_dir *dir, const char *refname, size_t len)
 {
        struct ref_entry **r;
        struct string_slice key;
 
        if (refname == NULL || !dir->nr)
-               return NULL;
+               return -1;
 
        sort_ref_dir(dir);
        key.len = len;
@@ -387,9 +386,9 @@ static struct ref_entry *search_ref_dir(struct ref_dir *dir,
                    ref_entry_cmp_sslice);
 
        if (r == NULL)
-               return NULL;
+               return -1;
 
-       return *r;
+       return r - dir->entries;
 }
 
 /*
@@ -403,8 +402,9 @@ static struct ref_dir *search_for_subdir(struct ref_dir *dir,
                                         const char *subdirname, size_t len,
                                         int mkdir)
 {
-       struct ref_entry *entry = search_ref_dir(dir, subdirname, len);
-       if (!entry) {
+       int entry_index = search_ref_dir(dir, subdirname, len);
+       struct ref_entry *entry;
+       if (entry_index == -1) {
                if (!mkdir)
                        return NULL;
                /*
@@ -415,6 +415,8 @@ static struct ref_dir *search_for_subdir(struct ref_dir *dir,
                 */
                entry = create_dir_entry(dir->ref_cache, subdirname, len, 0);
                add_entry_to_dir(dir, entry);
+       } else {
+               entry = dir->entries[entry_index];
        }
        return get_ref_dir(entry);
 }
@@ -453,12 +455,67 @@ static struct ref_dir *find_containing_dir(struct ref_dir *dir,
  */
 static struct ref_entry *find_ref(struct ref_dir *dir, const char *refname)
 {
+       int entry_index;
        struct ref_entry *entry;
        dir = find_containing_dir(dir, refname, 0);
        if (!dir)
                return NULL;
-       entry = search_ref_dir(dir, refname, strlen(refname));
-       return (entry && !(entry->flag & REF_DIR)) ? entry : NULL;
+       entry_index = search_ref_dir(dir, refname, strlen(refname));
+       if (entry_index == -1)
+               return NULL;
+       entry = dir->entries[entry_index];
+       return (entry->flag & REF_DIR) ? NULL : entry;
+}
+
+/*
+ * Remove the entry with the given name from dir, recursing into
+ * subdirectories as necessary.  If refname is the name of a directory
+ * (i.e., ends with '/'), then remove the directory and its contents.
+ * If the removal was successful, return the number of entries
+ * remaining in the directory entry that contained the deleted entry.
+ * If the name was not found, return -1.  Please note that this
+ * function only deletes the entry from the cache; it does not delete
+ * it from the filesystem or ensure that other cache entries (which
+ * might be symbolic references to the removed entry) are updated.
+ * Nor does it remove any containing dir entries that might be made
+ * empty by the removal.  dir must represent the top-level directory
+ * and must already be complete.
+ */
+static int remove_entry(struct ref_dir *dir, const char *refname)
+{
+       int refname_len = strlen(refname);
+       int entry_index;
+       struct ref_entry *entry;
+       int is_dir = refname[refname_len - 1] == '/';
+       if (is_dir) {
+               /*
+                * refname represents a reference directory.  Remove
+                * the trailing slash; otherwise we will get the
+                * directory *representing* refname rather than the
+                * one *containing* it.
+                */
+               char *dirname = xmemdupz(refname, refname_len - 1);
+               dir = find_containing_dir(dir, dirname, 0);
+               free(dirname);
+       } else {
+               dir = find_containing_dir(dir, refname, 0);
+       }
+       if (!dir)
+               return -1;
+       entry_index = search_ref_dir(dir, refname, refname_len);
+       if (entry_index == -1)
+               return -1;
+       entry = dir->entries[entry_index];
+
+       memmove(&dir->entries[entry_index],
+               &dir->entries[entry_index + 1],
+               (dir->nr - entry_index - 1) * sizeof(*dir->entries)
+               );
+       dir->nr--;
+       if (dir->sorted > entry_index)
+               dir->sorted--;
+       free_ref_entry(entry);
+       return dir->nr;
 }
 
 /*
@@ -1889,19 +1946,12 @@ struct ref_lock *lock_any_ref_for_update(const char *refname,
        return lock_ref_sha1_basic(refname, old_sha1, flags, NULL);
 }
 
-struct repack_without_ref_sb {
-       const char *refname;
-       int fd;
-};
-
-static int repack_without_ref_fn(struct ref_entry *entry, void *cb_data)
+static int repack_ref_fn(struct ref_entry *entry, void *cb_data)
 {
-       struct repack_without_ref_sb *data = cb_data;
+       int *fd = cb_data;
        char line[PATH_MAX + 100];
        int len;
 
-       if (!strcmp(data->refname, entry->name))
-               return 0;
        if (entry->flag & REF_ISBROKEN) {
                /* This shouldn't happen to packed refs. */
                error("%s is broken!", entry->name);
@@ -1942,7 +1992,7 @@ static int repack_without_ref_fn(struct ref_entry *entry, void *cb_data)
        /* this should not happen but just being defensive */
        if (len > sizeof(line))
                die("too long a refname '%s'", entry->name);
-       write_or_die(data->fd, line, len);
+       write_or_die(*fd, line, len);
        return 0;
 }
 
@@ -1950,22 +2000,30 @@ static struct lock_file packlock;
 
 static int repack_without_ref(const char *refname)
 {
-       struct repack_without_ref_sb data;
+       int fd;
        struct ref_cache *refs = get_ref_cache(NULL);
        struct ref_dir *packed;
 
        if (!get_packed_ref(refname))
                return 0; /* refname does not exist in packed refs */
 
-       data.refname = refname;
-       data.fd = hold_lock_file_for_update(&packlock, git_path("packed-refs"), 0);
-       if (data.fd < 0) {
+       fd = hold_lock_file_for_update(&packlock, git_path("packed-refs"), 0);
+       if (fd < 0) {
                unable_to_lock_error(git_path("packed-refs"), errno);
                return error("cannot delete '%s' from packed refs", refname);
        }
        clear_packed_ref_cache(refs);
        packed = get_packed_refs(refs);
-       do_for_each_entry_in_dir(packed, 0, repack_without_ref_fn, &data);
+       /* Remove refname from the cache. */
+       if (remove_entry(packed, refname) == -1) {
+               /*
+                * The packed entry disappeared while we were
+                * acquiring the lock.
+                */
+               rollback_lock_file(&packlock);
+               return 0;
+       }
+       do_for_each_entry_in_dir(packed, 0, repack_ref_fn, &fd);
        return commit_lock_file(&packlock);
 }
 
@@ -1994,7 +2052,7 @@ int delete_ref(const char *refname, const unsigned char *sha1, int delopt)
        ret |= repack_without_ref(lock->ref_name);
 
        unlink_or_warn(git_path("logs/%s", lock->ref_name));
-       invalidate_ref_cache(NULL);
+       clear_loose_ref_cache(get_ref_cache(NULL));
        unlock_ref(lock);
        return ret;
 }