{"thread":{"id":"43551","subject":"[RFC] git-branch: add options and tests for branch renaming","startedAt":"2006-11-28T14:47:40Z","lastAt":"2006-11-28T16:10:30Z","messageCount":4,"participants":["Lars Hjemli","Andy Whitcroft","Shawn Pearce"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"295426","messageId":"11647252603530-git-send-email-hjemli@gmail.com","threadId":"43551","inReplyTo":null,"subject":"[RFC] git-branch: add options and tests for branch renaming","fromName":"Lars Hjemli","fromEmail":"hjemli@gmail.com","sentAt":"2006-11-28T14:47:40Z","receivedAt":"2006-11-28T14:47:40Z","isPatch":false,"sender":{"key":"hjemli@gmail.com","avatar":null},"body":"Extend git-branch with the following options:\n\n  git-branch -m|-M [<oldbranch>] newbranch\n\nThe -M variation is required to force renaming over an exsisting\nbranchname.\n\nThis also indroduces $GIT_DIR/RENAME_REF which is a \"metabranch\"\nused when renaming branches. It will always hold the original sha1\nfor the latest renamed branch.\n\nAdditionally, if $GIT_DIR/logs/RENAME_REF exists, all branch rename\nevents are logged there.\n\nFinally, some testcases are added to verify the new options.\n\nSigned-off-by: Lars Hjemli <hjemli@gmail.com>\n---\n Documentation/git-branch.txt |   23 +++++-\n builtin-branch.c             |   49 ++++++++++++-\n refs.c                       |  169 ++++++++++++++++++++++++++++++++++++------\n refs.h                       |    3 +\n t/t3200-branch.sh            |   34 +++++++++\n 5 files changed, 251 insertions(+), 27 deletions(-)\n\ndiff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt\nindex 4f5b5d5..71417fe 100644\n--- a/Documentation/git-branch.txt\n+++ b/Documentation/git-branch.txt\n@@ -8,8 +8,9 @@ git-branch - List, create, or delete branches.\n SYNOPSIS\n --------\n [verse]\n-'git-branch' [-r] [-a] [-v] [--abbrev=<length>]\n+'git-branch' [-r | -a] [-v [--abbrev=<length>]]\n 'git-branch' [-l] [-f] <branchname> [<start-point>]\n+'git-branch' (-m | -M) [<oldbranch>] <newbranch>\n 'git-branch' (-d | -D) <branchname>...\n \n DESCRIPTION\n@@ -24,6 +25,12 @@ It will start out with a head equal to the one given as <start-point>.\n If no <start-point> is given, the branch will be created with a head\n equal to that of the currently checked out branch.\n \n+With a '-m' or '-M' option, <oldbranch> will be renamed to <newbranch>.\n+If <oldbranch> had a corresponding reflog, it is renamed to match\n+<newbranch>, and a reflog entry is created to remember the branch\n+renaming. If <newbranch> exists, -M must be used to force the rename\n+to happen.\n+\n With a `-d` or `-D` option, `<branchname>` will be deleted.  You may\n specify more than one branch for deletion.  If the branch currently\n has a ref log then the ref log will also be deleted.\n@@ -46,6 +53,12 @@ OPTIONS\n \tForce the creation of a new branch even if it means deleting\n \ta branch that already exists with the same name.\n \n+-m::\n+\tMove/rename a branch and the corresponding reflog.\n+\n+-M::\n+\tMove/rename a branch even if the new branchname already exists.\n+\n -r::\n \tList the remote-tracking branches.\n \n@@ -53,7 +66,7 @@ OPTIONS\n \tList both remote-tracking branches and local branches.\n \n -v::\n-\tShow sha1 and subject message for each head.\n+\tShow sha1 and commit subjectline for each head.\n \n --abbrev=<length>::\n \tAlter minimum display length for sha1 in output listing,\n@@ -70,6 +83,12 @@ OPTIONS\n \tbe given as a branch name, a commit-id, or a tag.  If this option\n \tis omitted, the current branch is assumed.\n \n+<oldbranch>::\n+\tThe name of an existing branch to rename.\n+\n+<newbranch>::\n+\tThe new name for an existing branch. The same restrictions as for\n+\t<branchname> applies.\n \n \n Examples\ndiff --git a/builtin-branch.c b/builtin-branch.c\nindex 3d5cb0e..3e206b9 100644\n--- a/builtin-branch.c\n+++ b/builtin-branch.c\n@@ -10,8 +10,8 @@\n #include \"commit.h\"\n #include \"builtin.h\"\n \n-static const char builtin_branch_usage[] =\n-\"git-branch (-d | -D) <branchname> | [-l] [-f] <branchname> [<start-point>] | [-r | -a] [-v] [--abbrev=<length>] \";\n+static const char builtin_branch_usage[] =\n+  \"git-branch (-d | -D) <branchname> | [-l] [-f] <branchname> [<start-point>] | (-m | -M) [<oldbranch>] <newbranch> | [-r | -a] [-v [--abbrev=<length>]]\";\n \n \n static const char *head;\n@@ -245,9 +245,37 @@ static void create_branch(const char *name, const char *start,\n \t\tdie(\"Failed to write ref: %s.\", strerror(errno));\n }\n \n+static void rename_branch(const char *oldname, const char *newname, int force)\n+{\n+\tchar oldref[PATH_MAX], newref[PATH_MAX];\n+\tunsigned char sha1[20];\n+\n+\tif (snprintf(oldref, sizeof(oldref), \"refs/heads/%s\", oldname) > sizeof(oldref))\n+\t\tdie(\"Old branchname too long\");\n+\n+\tif (check_ref_format(oldref))\n+\t\tdie(\"Invalid branch name: %s\", oldref);\n+\n+\tif (snprintf(newref, sizeof(newref), \"refs/heads/%s\", newname) > sizeof(newref))\n+\t\tdie(\"New branchname too long\");\n+\n+\tif (check_ref_format(newref))\n+\t\tdie(\"Invalid branch name: %s\", newref);\n+\n+\tif (resolve_ref(newref, sha1, 1, NULL) && !force)\n+\t\tdie(\"A branch named '%s' already exists.\", newname);\n+\n+\tif (rename_ref(oldref, newref))\n+\t\tdie(\"Branch rename failed\");\n+\n+\tif (!strcmp(oldname, head) && create_symref(\"HEAD\", newref))\n+\t\tdie(\"Branch renamed to %s, but HEAD is not updated!\", newname);\n+}\n+\n int cmd_branch(int argc, const char **argv, const char *prefix)\n {\n \tint delete = 0, force_delete = 0, force_create = 0;\n+\tint rename = 0, force_rename = 0;\n \tint verbose = 0, abbrev = DEFAULT_ABBREV;\n \tint reflog = 0;\n \tint kinds = REF_LOCAL_BRANCH;\n@@ -277,6 +305,15 @@ int cmd_branch(int argc, const char **argv, const char *prefix)\n \t\t\tforce_create = 1;\n \t\t\tcontinue;\n \t\t}\n+\t\tif (!strcmp(arg, \"-m\")) {\n+\t\t\trename = 1;\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif (!strcmp(arg, \"-M\")) {\n+\t\t\trename = 1;\n+\t\t\tforce_rename = 1;\n+\t\t\tcontinue;\n+\t\t}\n \t\tif (!strcmp(arg, \"-r\")) {\n \t\t\tkinds = REF_REMOTE_BRANCH;\n \t\t\tcontinue;\n@@ -300,6 +337,10 @@ int cmd_branch(int argc, const char **argv, const char *prefix)\n \t\tusage(builtin_branch_usage);\n \t}\n \n+\tif ((delete && rename) || (delete && force_create) ||\n+\t    (rename && force_create))\n+\t\tusage(builtin_branch_usage);\n+\n \thead = xstrdup(resolve_ref(\"HEAD\", head_sha1, 0, NULL));\n \tif (!head)\n \t\tdie(\"Failed to resolve HEAD as a valid ref.\");\n@@ -311,6 +352,10 @@ int cmd_branch(int argc, const char **argv, const char *prefix)\n \t\tdelete_branches(argc - i, argv + i, force_delete);\n \telse if (i == argc)\n \t\tprint_ref_list(kinds, verbose, abbrev);\n+\telse if (rename && (i == argc - 1))\n+\t\trename_branch(head, argv[i], force_rename);\n+\telse if (rename && (i == argc - 2))\n+\t\trename_branch(argv[i], argv[i + 1], force_rename);\n \telse if (i == argc - 1)\n \t\tcreate_branch(argv[i], head, force_create, reflog);\n \telse if (i == argc - 2)\ndiff --git a/refs.c b/refs.c\nindex 96ea8b6..479f18c 100644\n--- a/refs.c\n+++ b/refs.c\n@@ -610,6 +610,29 @@ static int remove_empty_directories(char *file)\n \treturn remove_empty_dir_recursive(path, len);\n }\n \n+static int is_refname_available(const char *ref, const char *oldref,\n+\t\t\t\tstruct ref_list *list, int quiet)\n+{\n+\tint namlen = strlen(ref); /* e.g. 'foo/bar' */\n+\twhile (list) {\n+\t\t/* list->name could be 'foo' or 'foo/bar/baz' */\n+\t\tif (!oldref || strcmp(oldref, list->name)) {\n+\t\t\tint len = strlen(list->name);\n+\t\t\tint cmplen = (namlen < len) ? namlen : len;\n+\t\t\tconst char *lead = (namlen < len) ? list->name : ref;\n+\t\t\tif (!strncmp(ref, list->name, cmplen) &&\n+\t\t\t    lead[cmplen] == '/') {\n+\t\t\t\tif (!quiet)\n+\t\t\t\t\terror(\"'%s' exists; cannot create '%s'\",\n+\t\t\t\t\t      list->name, ref);\n+\t\t\t\treturn 0;\n+\t\t\t}\n+\t\t}\n+\t\tlist = list->next;\n+\t}\n+\treturn 1;\n+}\n+\n static struct ref_lock *lock_ref_sha1_basic(const char *ref, const unsigned char *old_sha1, int *flag)\n {\n \tchar *ref_file;\n@@ -643,29 +666,14 @@ static struct ref_lock *lock_ref_sha1_basic(const char *ref, const unsigned char\n \t\t\torig_ref, strerror(errno));\n \t\tgoto error_return;\n \t}\n-\tif (is_null_sha1(lock->old_sha1)) {\n-\t\t/* The ref did not exist and we are creating it.\n-\t\t * Make sure there is no existing ref that is packed\n-\t\t * whose name begins with our refname, nor a ref whose\n-\t\t * name is a proper prefix of our refname.\n-\t\t */\n-\t\tint namlen = strlen(ref); /* e.g. 'foo/bar' */\n-\t\tstruct ref_list *list = get_packed_refs();\n-\t\twhile (list) {\n-\t\t\t/* list->name could be 'foo' or 'foo/bar/baz' */\n-\t\t\tint len = strlen(list->name);\n-\t\t\tint cmplen = (namlen < len) ? namlen : len;\n-\t\t\tconst char *lead = (namlen < len) ? list->name : ref;\n-\n-\t\t\tif (!strncmp(ref, list->name, cmplen) &&\n-\t\t\t    lead[cmplen] == '/') {\n-\t\t\t\terror(\"'%s' exists; cannot create '%s'\",\n-\t\t\t\t      list->name, ref);\n-\t\t\t\tgoto error_return;\n-\t\t\t}\n-\t\t\tlist = list->next;\n-\t\t}\n-\t}\n+\t/* When the ref did not exist and we are creating it,\n+\t * make sure there is no existing ref that is packed\n+\t * whose name begins with our refname, nor a ref whose\n+\t * name is a proper prefix of our refname.\n+\t */\n+\tif (is_null_sha1(lock->old_sha1) &&\n+            !is_refname_available(ref, NULL, get_packed_refs(), 0))\n+\t\tgoto error_return;\n \n \tlock->lk = xcalloc(1, sizeof(struct lock_file));\n \n@@ -776,6 +784,121 @@ int delete_ref(const char *refname, unsigned char *sha1)\n \treturn ret;\n }\n \n+int rename_ref(const char *oldref, const char *newref)\n+{\n+\tstatic const char renamed_ref[] = \"RENAMED-REF\";\n+\tunsigned char sha1[20], orig_sha1[20];\n+\tint flag = 0, logmoved = 0;\n+\tstruct ref_lock *lock;\n+\tchar msg[PATH_MAX*2 + 100];\n+\tstruct stat loginfo;\n+\tint log = !stat(git_path(\"logs/%s\", oldref), &loginfo);\n+\n+\tif (S_ISLNK(loginfo.st_mode))\n+\t\treturn error(\"reflog for %s is a symlink\", oldref);\n+\n+\tif (!resolve_ref(oldref, orig_sha1, 1, &flag))\n+\t\treturn error(\"refname %s not found\", oldref);\n+\n+\tif (!is_refname_available(newref, oldref, get_packed_refs(), 0))\n+\t\treturn 1;\n+\n+\tif (!is_refname_available(newref, oldref, get_loose_refs(), 0))\n+\t\treturn 1;\n+\n+\tif (snprintf(msg, sizeof(msg), \"renamed %s to %s\", oldref, newref) > sizeof(msg))\n+\t\treturn error(\"Refnames to long\");\n+\n+\tlock = lock_ref_sha1_basic(renamed_ref, NULL, NULL);\n+\tif (!lock)\n+\t\treturn error(\"unable to lock %s\", renamed_ref);\n+\tlock->force_write = 1;\n+\tif (write_ref_sha1(lock, orig_sha1, msg))\n+\t\treturn error(\"unable to save current sha1 in %s\", renamed_ref);\n+\n+\tif (log && rename(git_path(\"logs/%s\", oldref), git_path(\"tmp-renamed-log\")))\n+\t\treturn error(\"unable to move logfile logs/%s to tmp-renamed-log: %s\",\n+\t\t\toldref, strerror(errno));\n+\n+\tif (delete_ref(oldref, orig_sha1)) {\n+\t\terror(\"unable to delete old %s\", oldref);\n+\t\tgoto rollback;\n+\t}\n+\n+\tif (resolve_ref(newref, sha1, 1, &flag) && delete_ref(newref, sha1)) {\n+\t\tif (errno==EISDIR) {\n+\t\t\tif (remove_empty_directories(git_path(\"%s\", newref))) {\n+\t\t\t\terror(\"Directory not empty: %s\", newref);\n+\t\t\t\tgoto rollback;\n+\t\t\t}\n+\t\t} else {\n+\t\t\terror(\"unable to delete existing %s\", newref);\n+\t\t\tgoto rollback;\n+\t\t}\n+\t}\n+\n+\tif (log && safe_create_leading_directories(git_path(\"logs/%s\", newref))) {\n+\t\terror(\"unable to create directory for %s\", newref);\n+\t\tgoto rollback;\n+\t}\n+\n+ retry:\n+\tif (log && rename(git_path(\"tmp-renamed-log\"), git_path(\"logs/%s\", newref))) {\n+\t\tif (errno==EISDIR) {\n+\t\t\tif (remove_empty_directories(git_path(\"logs/%s\", newref))) {\n+\t\t\t\terror(\"Directory not empty: logs/%s\", newref);\n+\t\t\t\tgoto rollback;\n+\t\t\t}\n+\t\t\tgoto retry;\n+\t\t} else {\n+\t\t\terror(\"unable to move logfile tmp-renamed-log to logs/%s: %s\",\n+\t\t\t\tnewref, strerror(errno));\n+\t\t\tgoto rollback;\n+\t\t}\n+\t}\n+\tlogmoved = log;\n+\n+\tlock = lock_ref_sha1_basic(newref, NULL, NULL);\n+\tif (!lock) {\n+\t\terror(\"unable to lock %s for update\", newref);\n+\t\tgoto rollback;\n+\t}\n+\n+\tlock->force_write = 1;\n+\thashcpy(lock->old_sha1, orig_sha1);\n+\tif (write_ref_sha1(lock, orig_sha1, msg)) {\n+\t\terror(\"unable to write current sha1 into %s\", newref);\n+\t\tgoto rollback;\n+\t}\n+\n+\treturn 0;\n+\n+ rollback:\n+\tlock = lock_ref_sha1_basic(oldref, NULL, NULL);\n+\tif (!lock) {\n+\t\terror(\"unable to lock %s for rollback\", oldref);\n+\t\tgoto rollbacklog;\n+\t}\n+\n+\tlock->force_write = 1;\n+\tflag = log_all_ref_updates;\n+\tlog_all_ref_updates = 0;\n+\tif (write_ref_sha1(lock, orig_sha1, NULL))\n+\t\terror(\"unable to write current sha1 into %s\", oldref);\n+\tlog_all_ref_updates = flag;\n+\n+ rollbacklog:\n+\tif (logmoved && rename(git_path(\"logs/%s\", newref), git_path(\"logs/%s\", oldref)))\n+\t\terror(\"unable to restore logfile %s from %s: %s\",\n+\t\t\toldref, newref, strerror(errno));\n+\tif (!logmoved && log &&\n+\t    rename(git_path(\"tmp-renamed-log\"), git_path(\"logs/%s\", oldref)))\n+\t\terror(\"unable to restore logfile %s from tmp-renamed-log: %s\",\n+\t\t\toldref, strerror(errno));\n+\n+\treturn 1;\n+}\n+\n void unlock_ref(struct ref_lock *lock)\n {\n \tif (lock->lock_fd >= 0) {\ndiff --git a/refs.h b/refs.h\nindex cd1e1d6..ce73d5c 100644\n--- a/refs.h\n+++ b/refs.h\n@@ -47,4 +47,7 @@ extern int read_ref_at(const char *ref, unsigned long at_time, int cnt, unsigned\n /** Returns 0 if target has the right format for a ref. **/\n extern int check_ref_format(const char *target);\n \n+/** rename ref, return 0 on success **/\n+extern int rename_ref(const char *oldref, const char *newref);\n+\n #endif /* REFS_H */\ndiff --git a/t/t3200-branch.sh b/t/t3200-branch.sh\nindex acb54b6..afaa853 100755\n--- a/t/t3200-branch.sh\n+++ b/t/t3200-branch.sh\n@@ -70,4 +70,38 @@ test_expect_success \\\n         git-branch -d l/m &&\n         git-branch l'\n \n+test_expect_success \\\n+    'git branch -m m m/m should work' \\\n+       'git-branch -l m &&\n+        git-branch -m m m/m &&\n+        test -f .git/logs/refs/heads/m/m'\n+\n+test_expect_success \\\n+    'git branch -m n/n n should work' \\\n+       'git-branch -l n/n &&\n+        git-branch -m n/n n\n+        test -f .git/logs/refs/heads/n'\n+\n+test_expect_failure \\\n+    'git branch -m o/o o should fail when o/p exists' \\\n+       'git-branch o/o &&\n+        git-branch o/p &&\n+        git-branch -m o/o o'\n+\n+test_expect_failure \\\n+    'git branch -m q r/q should fail when r exists' \\\n+       'git-branch q &&\n+         git-branch r &&\n+         git-branch -m q r/q'\n+\n+test_expect_success \\\n+    'git branch -m s/s s should work when s/t is deleted' \\\n+       'git-branch -l s/s &&\n+        test -f .git/logs/refs/heads/s/s &&\n+        git-branch -l s/t &&\n+        test -f .git/logs/refs/heads/s/t &&\n+        git-branch -d s/t &&\n+        git-branch -m s/s s &&\n+        test -f .git/logs/refs/heads/s'\n+\n test_done\n-- \n1.4.4.1.gf0df\n"},{"id":"295684","messageId":"456C4FCE.6090306@shadowen.org","threadId":"43551","inReplyTo":"11647252603530-git-send-email-hjemli@gmail.com","subject":"Re: [RFC] git-branch: add options and tests for branch renaming","fromName":"Andy Whitcroft","fromEmail":"apw@shadowen.org","sentAt":"2006-11-28T15:03:42Z","receivedAt":"2006-11-28T15:03:42Z","isPatch":false,"sender":{"key":"apw@shadowen.org","avatar":"https://gravatar.com/avatar/d3088262854661a913ef35cc40fedcc270142d4461791142bc1ea0b2a4e2e147?d=mp&s=160"},"body":"Lars Hjemli wrote:\n> Extend git-branch with the following options:\n> \n>   git-branch -m|-M [<oldbranch>] newbranch\n> \n> The -M variation is required to force renaming over an exsisting\n> branchname.\n> \n> This also indroduces $GIT_DIR/RENAME_REF which is a \"metabranch\"\n> used when renaming branches. It will always hold the original sha1\n> for the latest renamed branch.\n> \n> Additionally, if $GIT_DIR/logs/RENAME_REF exists, all branch rename\n> events are logged there.\n> \n> Finally, some testcases are added to verify the new options.\n\nOk, I have been feeling uneasy about rename and reflogs for a while now.\n About removing reflogs too for that matter.\n\nIn my mind the ref log is about tracking what a ref points to.  So I\nwant to be able to say \"what was next, yesterday\".  Do I care if its not\nthere now?  Perhaps I want a rename to just put a rename from in the top\nof the new reflog and leave the old there.\n\nYep, no idea how we would clean them up with this model.  But ...\n\n"},{"id":"296731","messageId":"20061128153904.GC28337@spearce.org","threadId":"43551","inReplyTo":"456C4FCE.6090306@shadowen.org","subject":"Re: [RFC] git-branch: add options and tests for branch renaming","fromName":"Shawn Pearce","fromEmail":"spearce@spearce.org","sentAt":"2006-11-28T15:39:04Z","receivedAt":"2006-11-28T15:39:04Z","isPatch":false,"sender":{"key":"spearce@spearce.org","avatar":"https://avatars.githubusercontent.com/u/34844?v=4"},"body":"Andy Whitcroft <apw@shadowen.org> wrote:\n> Ok, I have been feeling uneasy about rename and reflogs for a while now.\n>  About removing reflogs too for that matter.\n> \n> In my mind the ref log is about tracking what a ref points to.  So I\n> want to be able to say \"what was next, yesterday\".  Do I care if its not\n> there now?  Perhaps I want a rename to just put a rename from in the top\n> of the new reflog and leave the old there.\n> \n> Yep, no idea how we would clean them up with this model.  But ...\n\nYou use a global reflog instead of a ref specific log.  There are\nof course huge problems with that idea, but it is probably the\nsimplest way to solve what you want.\n\nThough I'm not sure why you care that much.  To me a ref is just\nas transient as the name of the working directory the repository is\nhoused in.  I could care less what I called that directory last week,\njust that it exists and has the content I expect.  On the other\nhand, once I'm in that directory I *do* want the history of it.\nThat's where git comes in.  Likewise for a ref once I am speaking\nabout a given ref, I want the history of it, and that's where git's\nreflog comes in.\n\n\nAlthough I just realized this paticular case:\n\n\tgit branch -D next\n\tgit fetch origin next:next\n\t...\n\tgit fetch origin next:next\n\tgit branch -M next bob\n\t...\n\t... 2 days go by  ...\n\t...\n\tgit fetch origin next:next\n\t... arrgh next is no good! ...\n\tgit log next@{2.days.ago}\n\nand there's nothing there, as the log for next just started in\nthe last fetch.  Yet you had a next that you renamed to bob, and\nthat next has the log.\n\nThough this can be easily dealt with by reading the RENAME_LOG.\nIf you scan both this ref's log and the RENAME_LOG and switch to\nscanning another ref's log when you find that this ref was renamed\nto another ref, then you get what you expect above.  Except if you\ndeleted the rename destination branch.\n\nI can't say I would use that feature though, despite how handy I\nfind reflogs to be.\n\n-- \n"},{"id":"296058","messageId":"8c5c35580611280810g2214ec8cs7406653c319be6fc@mail.gmail.com","threadId":"43551","inReplyTo":"456C4FCE.6090306@shadowen.org","subject":"Re: [RFC] git-branch: add options and tests for branch renaming","fromName":"Lars Hjemli","fromEmail":"hjemli@gmail.com","sentAt":"2006-11-28T16:10:30Z","receivedAt":"2006-11-28T16:10:30Z","isPatch":false,"sender":{"key":"hjemli@gmail.com","avatar":null},"body":"On 11/28/06, Andy Whitcroft <apw@shadowen.org> wrote:\n> Perhaps I want a rename to just put a rename from in the top\n> of the new reflog and leave the old there.\n\nThat is what happens when you do\n\n  git branch -l topic master\n\nThe first entry in reflog for 'topic' will say:\n\n  Branch: created from master\n\n--\n"}]}