summaryrefslogtreecommitdiff
path: root/src/refs.c
diff options
context:
space:
mode:
authorschu <schu-github@schulog.org>2011-06-14 15:16:43 +0200
committerschu <schu-github@schulog.org>2011-07-06 12:25:27 +0200
commit0ffcf78a30377971d4928439ae387396b3922cbf (patch)
tree45ffc20bdba3bb90dca8ea8c322ef20cd27fcd5f /src/refs.c
parent7ea50f6077d0da28a471e8de2dc32dcdc994c5c6 (diff)
downloadlibgit2-0ffcf78a30377971d4928439ae387396b3922cbf.tar.gz
reference_rename: git compliant reference renaming
So far libgit2 didn't handle the following scenarios: * Rename of reference m -> m/m * Rename of reference n/n -> n Fixed. Since we don't write reflogs, we have to delete any old reflog for the renamed reference. Otherwise git.git will possibly fail when it finds invalid logs. Reported-by: nulltoken <emeric.fermas@gmail.com> Signed-off-by: schu <schu-github@schulog.org>
Diffstat (limited to 'src/refs.c')
-rw-r--r--src/refs.c196
1 files changed, 105 insertions, 91 deletions
diff --git a/src/refs.c b/src/refs.c
index eea96adba..94a7a15c1 100644
--- a/src/refs.c
+++ b/src/refs.c
@@ -1287,137 +1287,151 @@ int git_reference_set_target(git_reference *ref, const char *target)
* Other
*/
-/*
- * Rename a reference
- *
- * If the reference is packed, we need to rewrite the
- * packfile to remove the reference from it and create
- * the reference back as a loose one.
- *
- * If the reference is loose, we just rename it on
- * the filesystem.
- *
- * We also need to re-insert the reference on its corresponding
- * in-memory cache, since the caches are indexed by refname.
- */
int git_reference_rename(git_reference *ref, const char *new_name, int force)
{
int error;
- char *old_name;
- char old_path[GIT_PATH_MAX], new_path[GIT_PATH_MAX], normalized_name[GIT_REFNAME_MAX];
- git_reference *looked_up_ref, *old_ref = NULL;
+ char *old_name = git__strdup(ref->name);
+ char new_path[GIT_PATH_MAX];
+ char old_path[GIT_PATH_MAX];
+ char old_logs[GIT_PATH_MAX];
+ char normalized[GIT_REFNAME_MAX];
+ const char *target_ref = NULL;
+ const char *head_target = NULL;
+ const git_oid *target_oid = NULL;
+ git_reference *new_ref = NULL, *old_ref = NULL, *head = NULL;
assert(ref);
- /* Ensure the name is valid */
- error = normalize_name(normalized_name, sizeof(normalized_name), new_name, ref->type & GIT_REF_OID);
+ error = normalize_name(normalized, sizeof(normalized), new_name, ref->type & GIT_REF_OID);
if (error < GIT_SUCCESS)
- return git__rethrow(error, "Failed to rename reference");
+ return git__rethrow(error, "Failed to rename reference. Invalid name");
- new_name = normalized_name;
+ new_name = normalized;
- /* Ensure we're not going to overwrite an existing reference
- unless the user has allowed us */
- error = git_reference_lookup(&looked_up_ref, ref->owner, new_name);
+ error = git_reference_lookup(&new_ref, ref->owner, new_name);
if (error == GIT_SUCCESS && !force)
return git__throw(GIT_EEXISTS, "Failed to rename reference. Reference already exists");
- if (error < GIT_SUCCESS &&
- error != GIT_ENOTFOUND)
- return git__rethrow(error, "Failed to rename reference");
+ if (error < GIT_SUCCESS && error != GIT_ENOTFOUND)
+ goto cleanup;
if ((error = reference_available(ref->owner, new_name, ref->name)) < GIT_SUCCESS)
- return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to rename reference. Reference already exists");
+ return git__rethrow(error, "Failed to rename reference. Reference already exists");
- old_name = ref->name;
- ref->name = git__strdup(new_name);
+ /*
+ * First, we backup the reference targets. Just keeping the old
+ * reference won't work, since we may have to remove it to create
+ * the new reference, e.g. when renaming foo/bar -> foo.
+ */
- if (ref->name == NULL) {
- ref->name = old_name;
- return GIT_ENOMEM;
+ if (ref->type & GIT_REF_SYMBOLIC) {
+ if ((target_ref = git_reference_target(ref)) == NULL)
+ goto cleanup;
+ } else {
+ if ((target_oid = git_reference_oid(ref)) == NULL)
+ goto cleanup;
}
- if (ref->type & GIT_REF_PACKED) {
- /* write the packfile to disk; note
- * that the state of the in-memory cache is not
- * consistent, because the reference is indexed
- * by its old name but it already has the new one.
- * This doesn't affect writing, though, and allows
- * us to rollback if writing fails
- */
-
- /* Create the loose ref under its new name */
- error = loose_write(ref);
- if (error < GIT_SUCCESS) {
- ref->type |= GIT_REF_PACKED;
- goto cleanup;
- }
+ /*
+ * Now delete the old ref and remove an possibly existing directory
+ * named `new_name`.
+ */
+ if (ref->type & GIT_REF_PACKED) {
ref->type &= ~GIT_REF_PACKED;
- /* Remove from the packfile cache in order to avoid packing it back
- * Note : we do not rely on git_reference_delete() because this would
- * invalidate the reference.
- */
git_hashtable_remove(ref->owner->references.packfile, old_name);
-
- /* Recreate the packed-refs file without the reference */
- error = packed_write(ref->owner);
- if (error < GIT_SUCCESS)
- goto rename_loose_to_old_name;
-
+ if ((error = packed_write(ref->owner)) < GIT_SUCCESS)
+ goto rollback;
} else {
git_path_join(old_path, ref->owner->path_repository, old_name);
- git_path_join(new_path, ref->owner->path_repository, ref->name);
-
- error = git_futils_mv_withpath(old_path, new_path);
- if (error < GIT_SUCCESS)
+ if ((error = p_unlink(old_path)) < GIT_SUCCESS)
goto cleanup;
- /* Once succesfully renamed, remove from the cache the reference known by its old name*/
git_hashtable_remove(ref->owner->references.loose_cache, old_name);
}
- /* Store the renamed reference into the loose ref cache */
- error = git_hashtable_insert2(ref->owner->references.loose_cache, ref->name, ref, (void **) &old_ref);
+ git_path_join(new_path, ref->owner->path_repository, new_name);
- /* If we force-replaced, we need to free the old reference */
- if(old_ref)
- reference_free(old_ref);
+ if (git_futils_exists(new_path) == GIT_SUCCESS) {
+ if (git_futils_isdir(new_path) == GIT_SUCCESS) {
+ if ((error = git_futils_rmdir_recurs(new_path, 0)) < GIT_SUCCESS)
+ goto rollback;
+ } else goto rollback;
+ }
- free(old_name);
- return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to rename reference");
+ /*
+ * Crude hack: delete any logs till we support proper reflogs.
+ * Otherwise git.git will possibly fail and leave a mess. git.git
+ * writes reflogs by default in any repo with a working directory:
+ *
+ * "We only enable reflogs in repositories that have a working directory
+ * associated with them, as shared/bare repositories do not have
+ * an easy means to prune away old log entries, or may fail logging
+ * entirely if the user's gecos information is not valid during a push.
+ * This heuristic was suggested on the mailing list by Junio."
+ *
+ * Shawn O. Pearce - 0bee59186976b1d9e6b2dd77332480c9480131d5
+ *
+ * TODO
+ *
+ */
+
+ git_path_join_n(old_logs, 3, ref->owner->path_repository, "logs", old_name);
+ if (git_futils_exists(old_logs) == GIT_SUCCESS) {
+ if (git_futils_isfile(old_logs) == GIT_SUCCESS)
+ if ((error = p_unlink(old_logs)) < GIT_SUCCESS)
+ goto rollback;
+ }
+
+ /*
+ * Finally we can create the new reference.
+ */
+
+ if (ref->type & GIT_REF_SYMBOLIC) {
+ if ((error = git_reference_create_symbolic(&new_ref, ref->owner, new_name, target_ref, 0)) < GIT_SUCCESS)
+ goto rollback;
+ } else {
+ if ((error = git_reference_create_oid(&new_ref, ref->owner, new_name, target_oid, 0)) < GIT_SUCCESS)
+ goto rollback;
+ }
-cleanup:
- /* restore the old name if this failed */
free(ref->name);
- ref->name = old_name;
- return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to rename reference");
+ ref->name = new_ref->name;
-rename_loose_to_old_name:
- /* If we hit this point. Something *bad* happened! Think "Ghostbusters
- * crossing the streams" definition of bad.
- * Either the packed-refs has been correctly generated and something else
- * has gone wrong, or the writing of the new packed-refs has failed, and
- * we're stuck with the old one. As a loose ref always takes priority over
- * a packed ref, we'll eventually try and rename the generated loose ref to
- * its former name. It even that fails, well... we might have lost the reference
- * for good. :-/
- */
+ if ((error = git_hashtable_insert2(ref->owner->references.loose_cache, new_ref->name, new_ref, (void **)&old_ref)) < GIT_SUCCESS)
+ goto rollback;
- git_path_join(old_path, ref->owner->path_repository, ref->name);
- git_path_join(new_path, ref->owner->path_repository, old_name);
+ /*
+ * Check if we have to update HEAD.
+ */
- /* No error checking. We'll return the initial error */
- git_futils_mv_withpath(old_path, new_path);
+ if ((error = git_reference_lookup(&head, ref->owner, GIT_HEAD_FILE)) < GIT_SUCCESS)
+ goto cleanup;
- /* restore the old name */
- free(ref->name);
- ref->name = old_name;
+ head_target = git_reference_target(head);
+ if (head_target && !strcmp(head_target, old_name))
+ if ((error = git_reference_create_symbolic(&head, new_ref->owner, "HEAD", new_ref->name, 1)) < GIT_SUCCESS)
+ goto rollback;
+
+cleanup:
+ free(old_name);
return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to rename reference");
-}
+rollback:
+ /*
+ * Try to create the old reference again.
+ */
+ if (ref->type & GIT_REF_SYMBOLIC)
+ error = git_reference_create_symbolic(&new_ref, ref->owner, old_name, target_ref, 0);
+ else
+ error = git_reference_create_oid(&new_ref, ref->owner, old_name, target_oid, 0);
+
+ ref->name = old_name;
+
+ return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to rename reference. Failed to rollback");
+}
/*
* Delete a reference.