Commit 0ffcf78a by schu

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>
parent 7ea50f60
...@@ -1287,137 +1287,151 @@ int git_reference_set_target(git_reference *ref, const char *target) ...@@ -1287,137 +1287,151 @@ int git_reference_set_target(git_reference *ref, const char *target)
* Other * 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 git_reference_rename(git_reference *ref, const char *new_name, int force)
{ {
int error; int error;
char *old_name; char *old_name = git__strdup(ref->name);
char old_path[GIT_PATH_MAX], new_path[GIT_PATH_MAX], normalized_name[GIT_REFNAME_MAX]; char new_path[GIT_PATH_MAX];
git_reference *looked_up_ref, *old_ref = NULL; 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); assert(ref);
/* Ensure the name is valid */ error = normalize_name(normalized, sizeof(normalized), new_name, ref->type & GIT_REF_OID);
error = normalize_name(normalized_name, sizeof(normalized_name), new_name, ref->type & GIT_REF_OID);
if (error < GIT_SUCCESS) 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 error = git_reference_lookup(&new_ref, ref->owner, new_name);
unless the user has allowed us */
error = git_reference_lookup(&looked_up_ref, ref->owner, new_name);
if (error == GIT_SUCCESS && !force) if (error == GIT_SUCCESS && !force)
return git__throw(GIT_EEXISTS, "Failed to rename reference. Reference already exists"); return git__throw(GIT_EEXISTS, "Failed to rename reference. Reference already exists");
if (error < GIT_SUCCESS && if (error < GIT_SUCCESS && error != GIT_ENOTFOUND)
error != GIT_ENOTFOUND) goto cleanup;
return git__rethrow(error, "Failed to rename reference");
if ((error = reference_available(ref->owner, new_name, ref->name)) < GIT_SUCCESS) 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) { if (ref->type & GIT_REF_SYMBOLIC) {
ref->name = old_name; if ((target_ref = git_reference_target(ref)) == NULL)
return GIT_ENOMEM; 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 * Now delete the old ref and remove an possibly existing directory
* that the state of the in-memory cache is not * named `new_name`.
* 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;
}
if (ref->type & GIT_REF_PACKED) {
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); git_hashtable_remove(ref->owner->references.packfile, old_name);
if ((error = packed_write(ref->owner)) < GIT_SUCCESS)
/* Recreate the packed-refs file without the reference */ goto rollback;
error = packed_write(ref->owner);
if (error < GIT_SUCCESS)
goto rename_loose_to_old_name;
} else { } else {
git_path_join(old_path, ref->owner->path_repository, old_name); git_path_join(old_path, ref->owner->path_repository, old_name);
git_path_join(new_path, ref->owner->path_repository, ref->name); if ((error = p_unlink(old_path)) < GIT_SUCCESS)
error = git_futils_mv_withpath(old_path, new_path);
if (error < GIT_SUCCESS)
goto cleanup; 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); git_hashtable_remove(ref->owner->references.loose_cache, old_name);
} }
/* Store the renamed reference into the loose ref cache */ git_path_join(new_path, ref->owner->path_repository, new_name);
error = git_hashtable_insert2(ref->owner->references.loose_cache, ref->name, ref, (void **) &old_ref);
/* If we force-replaced, we need to free the old reference */ if (git_futils_exists(new_path) == GIT_SUCCESS) {
if(old_ref) if (git_futils_isdir(new_path) == GIT_SUCCESS) {
reference_free(old_ref); 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); free(ref->name);
ref->name = old_name; ref->name = new_ref->name;
return error == GIT_SUCCESS ? GIT_SUCCESS : git__rethrow(error, "Failed to rename reference");
rename_loose_to_old_name: if ((error = git_hashtable_insert2(ref->owner->references.loose_cache, new_ref->name, new_ref, (void **)&old_ref)) < GIT_SUCCESS)
/* If we hit this point. Something *bad* happened! Think "Ghostbusters goto rollback;
* 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. :-/
*/
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 */ if ((error = git_reference_lookup(&head, ref->owner, GIT_HEAD_FILE)) < GIT_SUCCESS)
git_futils_mv_withpath(old_path, new_path); goto cleanup;
/* restore the old name */ head_target = git_reference_target(head);
free(ref->name);
ref->name = old_name;
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"); 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. * Delete a reference.
......
...@@ -11,6 +11,8 @@ ...@@ -11,6 +11,8 @@
#define GIT_REFS_TAGS_DIR GIT_REFS_DIR "tags/" #define GIT_REFS_TAGS_DIR GIT_REFS_DIR "tags/"
#define GIT_REFS_REMOTES_DIR GIT_REFS_DIR "remotes/" #define GIT_REFS_REMOTES_DIR GIT_REFS_DIR "remotes/"
#define GIT_RENAMED_REF_FILE GIT_REFS_DIR "RENAMED-REF"
#define GIT_SYMREF "ref: " #define GIT_SYMREF "ref: "
#define GIT_PACKEDREFS_FILE "packed-refs" #define GIT_PACKEDREFS_FILE "packed-refs"
#define GIT_PACKEDREFS_HEADER "# pack-refs with: peeled " #define GIT_PACKEDREFS_HEADER "# pack-refs with: peeled "
......
...@@ -701,13 +701,13 @@ BEGIN_TEST(rename7, "can be renamed to a new name prefixed with the old name") ...@@ -701,13 +701,13 @@ BEGIN_TEST(rename7, "can be renamed to a new name prefixed with the old name")
git_oid_cpy(&id, git_reference_oid(ref)); git_oid_cpy(&id, git_reference_oid(ref));
/* Create loose references */ /* Create loose references */
must_pass(git_reference_create_oid(&ref_two, repo, ref_two_name, &id)); must_pass(git_reference_create_oid(&ref_two, repo, ref_two_name, &id, 0));
/* An existing reference... */ /* An existing reference... */
must_pass(git_reference_lookup(&looked_up_ref, repo, ref_two_name)); must_pass(git_reference_lookup(&looked_up_ref, repo, ref_two_name));
/* Can be rename to a new name starting with the old name. */ /* Can be rename to a new name starting with the old name. */
must_pass(git_reference_rename(looked_up_ref, ref_two_name_new)); must_pass(git_reference_rename(looked_up_ref, ref_two_name_new, 0));
/* Check we actually renamed it */ /* Check we actually renamed it */
must_pass(git_reference_lookup(&looked_up_ref, repo, ref_two_name_new)); must_pass(git_reference_lookup(&looked_up_ref, repo, ref_two_name_new));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment