"git branch" learned "-c/-C" to create a new branch by copying an
existing one.

* sd/branch-copy:
  branch: fix "copy" to never touch HEAD
  branch: add a --copy (-c) option to go with --move (-m)
  branch: add test for -m renaming multiple config sections
  config: create a function to format section headers
This commit is contained in:
Junio C Hamano 2017-10-03 15:42:48 +09:00
Родитель b2a2c4d809 e5435ff1fc
Коммит 3b48045c6c
10 изменённых файлов: 478 добавлений и 48 удалений

Просмотреть файл

@ -18,6 +18,7 @@ SYNOPSIS
'git branch' (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>]
'git branch' --unset-upstream [<branchname>]
'git branch' (-m | -M) [<oldbranch>] <newbranch>
'git branch' (-c | -C) [<oldbranch>] <newbranch>
'git branch' (-d | -D) [-r] <branchname>...
'git branch' --edit-description [<branchname>]
@ -64,6 +65,10 @@ If <oldbranch> had a corresponding reflog, it is renamed to match
renaming. If <newbranch> exists, -M must be used to force the rename
to happen.
The `-c` and `-C` options have the exact same semantics as `-m` and
`-M`, except instead of the branch being renamed it along with its
config and reflog will be copied to a new name.
With a `-d` or `-D` option, `<branchname>` will be deleted. You may
specify more than one branch for deletion. If the branch currently
has a reflog then the reflog will also be deleted.
@ -104,7 +109,7 @@ OPTIONS
In combination with `-d` (or `--delete`), allow deleting the
branch irrespective of its merged status. In combination with
`-m` (or `--move`), allow renaming the branch even if the new
branch name already exists.
branch name already exists, the same applies for `-c` (or `--copy`).
-m::
--move::
@ -113,6 +118,13 @@ OPTIONS
-M::
Shortcut for `--move --force`.
-c::
--copy::
Copy a branch and the corresponding reflog.
-C::
Shortcut for `--copy --force`.
--color[=<when>]::
Color branches to highlight current, local, and
remote-tracking branches.

Просмотреть файл

@ -28,6 +28,7 @@ static const char * const builtin_branch_usage[] = {
N_("git branch [<options>] [-l] [-f] <branch-name> [<start-point>]"),
N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."),
N_("git branch [<options>] (-m | -M) [<old-branch>] <new-branch>"),
N_("git branch [<options>] (-c | -C) [<old-branch>] <new-branch>"),
N_("git branch [<options>] [-r | -a] [--points-at]"),
N_("git branch [<options>] [-r | -a] [--format]"),
NULL
@ -456,15 +457,19 @@ static void reject_rebase_or_bisect_branch(const char *target)
free_worktrees(worktrees);
}
static void rename_branch(const char *oldname, const char *newname, int force)
static void copy_or_rename_branch(const char *oldname, const char *newname, int copy, int force)
{
struct strbuf oldref = STRBUF_INIT, newref = STRBUF_INIT, logmsg = STRBUF_INIT;
struct strbuf oldsection = STRBUF_INIT, newsection = STRBUF_INIT;
int recovery = 0;
int clobber_head_ok;
if (!oldname)
die(_("cannot rename the current branch while not on any."));
if (!oldname) {
if (copy)
die(_("cannot copy the current branch while not on any."));
else
die(_("cannot rename the current branch while not on any."));
}
if (strbuf_check_branch_ref(&oldref, oldname)) {
/*
@ -487,16 +492,29 @@ static void rename_branch(const char *oldname, const char *newname, int force)
reject_rebase_or_bisect_branch(oldref.buf);
strbuf_addf(&logmsg, "Branch: renamed %s to %s",
oldref.buf, newref.buf);
if (copy)
strbuf_addf(&logmsg, "Branch: copied %s to %s",
oldref.buf, newref.buf);
else
strbuf_addf(&logmsg, "Branch: renamed %s to %s",
oldref.buf, newref.buf);
if (rename_ref(oldref.buf, newref.buf, logmsg.buf))
if (!copy && rename_ref(oldref.buf, newref.buf, logmsg.buf))
die(_("Branch rename failed"));
if (copy && copy_existing_ref(oldref.buf, newref.buf, logmsg.buf))
die(_("Branch copy failed"));
if (recovery)
warning(_("Renamed a misnamed branch '%s' away"), oldref.buf + 11);
if (recovery) {
if (copy)
warning(_("Copied a misnamed branch '%s' away"),
oldref.buf + 11);
else
warning(_("Renamed a misnamed branch '%s' away"),
oldref.buf + 11);
}
if (replace_each_worktree_head_symref(oldref.buf, newref.buf, logmsg.buf))
if (!copy &&
replace_each_worktree_head_symref(oldref.buf, newref.buf, logmsg.buf))
die(_("Branch renamed to %s, but HEAD is not updated!"), newname);
strbuf_release(&logmsg);
@ -505,8 +523,10 @@ static void rename_branch(const char *oldname, const char *newname, int force)
strbuf_release(&oldref);
strbuf_addf(&newsection, "branch.%s", newref.buf + 11);
strbuf_release(&newref);
if (git_config_rename_section(oldsection.buf, newsection.buf) < 0)
if (!copy && git_config_rename_section(oldsection.buf, newsection.buf) < 0)
die(_("Branch is renamed, but update of config-file failed"));
if (copy && strcmp(oldname, newname) && git_config_copy_section(oldsection.buf, newsection.buf) < 0)
die(_("Branch is copied, but update of config-file failed"));
strbuf_release(&oldsection);
strbuf_release(&newsection);
}
@ -544,7 +564,7 @@ static int edit_branch_description(const char *branch_name)
int cmd_branch(int argc, const char **argv, const char *prefix)
{
int delete = 0, rename = 0, force = 0, list = 0;
int delete = 0, rename = 0, copy = 0, force = 0, list = 0;
int reflog = 0, edit_description = 0;
int quiet = 0, unset_upstream = 0;
const char *new_upstream = NULL;
@ -581,6 +601,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
OPT_BIT('D', NULL, &delete, N_("delete branch (even if not merged)"), 2),
OPT_BIT('m', "move", &rename, N_("move/rename a branch and its reflog"), 1),
OPT_BIT('M', NULL, &rename, N_("move/rename a branch, even if target exists"), 2),
OPT_BIT('c', "copy", &copy, N_("copy a branch and its reflog"), 1),
OPT_BIT('C', NULL, &copy, N_("copy a branch, even if target exists"), 2),
OPT_BOOL(0, "list", &list, N_("list branch names")),
OPT_BOOL('l', "create-reflog", &reflog, N_("create the branch's reflog")),
OPT_BOOL(0, "edit-description", &edit_description,
@ -624,14 +646,14 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
argc = parse_options(argc, argv, prefix, options, builtin_branch_usage,
0);
if (!delete && !rename && !edit_description && !new_upstream && !unset_upstream && argc == 0)
if (!delete && !rename && !copy && !edit_description && !new_upstream && !unset_upstream && argc == 0)
list = 1;
if (filter.with_commit || filter.merge != REF_FILTER_MERGED_NONE || filter.points_at.nr ||
filter.no_commit)
list = 1;
if (!!delete + !!rename + !!new_upstream +
if (!!delete + !!rename + !!copy + !!new_upstream +
list + unset_upstream > 1)
usage_with_options(builtin_branch_usage, options);
@ -649,6 +671,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
if (force) {
delete *= 2;
rename *= 2;
copy *= 2;
}
if (delete) {
@ -703,13 +726,22 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
if (edit_branch_description(branch_name))
return 1;
} else if (copy) {
if (!argc)
die(_("branch name required"));
else if (argc == 1)
copy_or_rename_branch(head, argv[0], 1, copy > 1);
else if (argc == 2)
copy_or_rename_branch(argv[0], argv[1], 1, copy > 1);
else
die(_("too many branches for a copy operation"));
} else if (rename) {
if (!argc)
die(_("branch name required"));
else if (argc == 1)
rename_branch(head, argv[0], rename > 1);
copy_or_rename_branch(head, argv[0], 0, rename > 1);
else if (argc == 2)
rename_branch(argv[0], argv[1], rename > 1);
copy_or_rename_branch(argv[0], argv[1], 0, rename > 1);
else
die(_("too many branches for a rename operation"));
} else if (new_upstream) {

114
config.c
Просмотреть файл

@ -2292,11 +2292,10 @@ static int write_error(const char *filename)
return 4;
}
static ssize_t write_section(int fd, const char *key)
static struct strbuf store_create_section(const char *key)
{
const char *dot;
int i;
ssize_t ret;
struct strbuf sb = STRBUF_INIT;
dot = memchr(key, '.', store.baselen);
@ -2312,7 +2311,15 @@ static ssize_t write_section(int fd, const char *key)
strbuf_addf(&sb, "[%.*s]\n", store.baselen, key);
}
ret = write_in_full(fd, sb.buf, sb.len);
return sb;
}
static ssize_t write_section(int fd, const char *key)
{
struct strbuf sb = store_create_section(key);
ssize_t ret;
ret = write_in_full(fd, sb.buf, sb.len) == sb.len;
strbuf_release(&sb);
return ret;
@ -2743,8 +2750,8 @@ static int section_name_is_ok(const char *name)
}
/* if new_name == NULL, the section is removed instead */
int git_config_rename_section_in_file(const char *config_filename,
const char *old_name, const char *new_name)
static int git_config_copy_or_rename_section_in_file(const char *config_filename,
const char *old_name, const char *new_name, int copy)
{
int ret = 0, remove = 0;
char *filename_buf = NULL;
@ -2753,6 +2760,7 @@ int git_config_rename_section_in_file(const char *config_filename,
char buf[1024];
FILE *config_file = NULL;
struct stat st;
struct strbuf copystr = STRBUF_INIT;
if (new_name && !section_name_is_ok(new_name)) {
ret = error("invalid section name: %s", new_name);
@ -2791,12 +2799,30 @@ int git_config_rename_section_in_file(const char *config_filename,
while (fgets(buf, sizeof(buf), config_file)) {
int i;
int length;
int is_section = 0;
char *output = buf;
for (i = 0; buf[i] && isspace(buf[i]); i++)
; /* do nothing */
if (buf[i] == '[') {
/* it's a section */
int offset = section_name_match(&buf[i], old_name);
int offset;
is_section = 1;
/*
* When encountering a new section under -c we
* need to flush out any section we're already
* coping and begin anew. There might be
* multiple [branch "$name"] sections.
*/
if (copystr.len > 0) {
if (write_in_full(out_fd, copystr.buf, copystr.len) != copystr.len) {
ret = write_error(get_lock_file_path(lock));
goto out;
}
strbuf_reset(&copystr);
}
offset = section_name_match(&buf[i], old_name);
if (offset > 0) {
ret++;
if (new_name == NULL) {
@ -2804,25 +2830,29 @@ int git_config_rename_section_in_file(const char *config_filename,
continue;
}
store.baselen = strlen(new_name);
if (write_section(out_fd, new_name) < 0) {
ret = write_error(get_lock_file_path(lock));
goto out;
}
/*
* We wrote out the new section, with
* a newline, now skip the old
* section's length
*/
output += offset + i;
if (strlen(output) > 0) {
if (!copy) {
if (write_section(out_fd, new_name) < 0) {
ret = write_error(get_lock_file_path(lock));
goto out;
}
/*
* More content means there's
* a declaration to put on the
* next line; indent with a
* tab
* We wrote out the new section, with
* a newline, now skip the old
* section's length
*/
output -= 1;
output[0] = '\t';
output += offset + i;
if (strlen(output) > 0) {
/*
* More content means there's
* a declaration to put on the
* next line; indent with a
* tab
*/
output -= 1;
output[0] = '\t';
}
} else {
copystr = store_create_section(new_name);
}
}
remove = 0;
@ -2830,11 +2860,30 @@ int git_config_rename_section_in_file(const char *config_filename,
if (remove)
continue;
length = strlen(output);
if (!is_section && copystr.len > 0) {
strbuf_add(&copystr, output, length);
}
if (write_in_full(out_fd, output, length) < 0) {
ret = write_error(get_lock_file_path(lock));
goto out;
}
}
/*
* Copy a trailing section at the end of the config, won't be
* flushed by the usual "flush because we have a new section
* logic in the loop above.
*/
if (copystr.len > 0) {
if (write_in_full(out_fd, copystr.buf, copystr.len) != copystr.len) {
ret = write_error(get_lock_file_path(lock));
goto out;
}
strbuf_reset(&copystr);
}
fclose(config_file);
config_file = NULL;
commit_and_out:
@ -2850,11 +2899,30 @@ out_no_rollback:
return ret;
}
int git_config_rename_section_in_file(const char *config_filename,
const char *old_name, const char *new_name)
{
return git_config_copy_or_rename_section_in_file(config_filename,
old_name, new_name, 0);
}
int git_config_rename_section(const char *old_name, const char *new_name)
{
return git_config_rename_section_in_file(NULL, old_name, new_name);
}
int git_config_copy_section_in_file(const char *config_filename,
const char *old_name, const char *new_name)
{
return git_config_copy_or_rename_section_in_file(config_filename,
old_name, new_name, 1);
}
int git_config_copy_section(const char *old_name, const char *new_name)
{
return git_config_copy_section_in_file(NULL, old_name, new_name);
}
/*
* Call this to report error for your variable that should not
* get a boolean value (i.e. "[my] var" means "true").

Просмотреть файл

@ -70,6 +70,8 @@ extern int git_config_set_multivar_in_file_gently(const char *, const char *, co
extern void git_config_set_multivar_in_file(const char *, const char *, const char *, const char *, int);
extern int git_config_rename_section(const char *, const char *);
extern int git_config_rename_section_in_file(const char *, const char *, const char *);
extern int git_config_copy_section(const char *, const char *);
extern int git_config_copy_section_in_file(const char *, const char *, const char *);
extern const char *git_etc_gitconfig(void);
extern int git_env_bool(const char *, int);
extern unsigned long git_env_ulong(const char *, unsigned long);

11
refs.c
Просмотреть файл

@ -2036,3 +2036,14 @@ int rename_ref(const char *oldref, const char *newref, const char *logmsg)
{
return refs_rename_ref(get_main_ref_store(), oldref, newref, logmsg);
}
int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
const char *newref, const char *logmsg)
{
return refs->be->copy_ref(refs, oldref, newref, logmsg);
}
int copy_existing_ref(const char *oldref, const char *newref, const char *logmsg)
{
return refs_copy_existing_ref(get_main_ref_store(), oldref, newref, logmsg);
}

9
refs.h
Просмотреть файл

@ -442,7 +442,14 @@ char *shorten_unambiguous_ref(const char *refname, int strict);
/** rename ref, return 0 on success **/
int refs_rename_ref(struct ref_store *refs, const char *oldref,
const char *newref, const char *logmsg);
int rename_ref(const char *oldref, const char *newref, const char *logmsg);
int rename_ref(const char *oldref, const char *newref,
const char *logmsg);
/** copy ref, return 0 on success **/
int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
const char *newref, const char *logmsg);
int copy_existing_ref(const char *oldref, const char *newref,
const char *logmsg);
int refs_create_symref(struct ref_store *refs, const char *refname,
const char *target, const char *logmsg);

Просмотреть файл

@ -1258,9 +1258,9 @@ static int commit_ref_update(struct files_ref_store *refs,
const struct object_id *oid, const char *logmsg,
struct strbuf *err);
static int files_rename_ref(struct ref_store *ref_store,
static int files_copy_or_rename_ref(struct ref_store *ref_store,
const char *oldrefname, const char *newrefname,
const char *logmsg)
const char *logmsg, int copy)
{
struct files_ref_store *refs =
files_downcast(ref_store, REF_STORE_WRITE, "rename_ref");
@ -1292,8 +1292,12 @@ static int files_rename_ref(struct ref_store *ref_store,
}
if (flag & REF_ISSYMREF) {
ret = error("refname %s is a symbolic ref, renaming it is not supported",
oldrefname);
if (copy)
ret = error("refname %s is a symbolic ref, copying it is not supported",
oldrefname);
else
ret = error("refname %s is a symbolic ref, renaming it is not supported",
oldrefname);
goto out;
}
if (!refs_rename_ref_available(&refs->base, oldrefname, newrefname)) {
@ -1301,13 +1305,19 @@ static int files_rename_ref(struct ref_store *ref_store,
goto out;
}
if (log && rename(sb_oldref.buf, tmp_renamed_log.buf)) {
if (!copy && log && rename(sb_oldref.buf, tmp_renamed_log.buf)) {
ret = error("unable to move logfile logs/%s to logs/"TMP_RENAMED_LOG": %s",
oldrefname, strerror(errno));
goto out;
}
if (refs_delete_ref(&refs->base, logmsg, oldrefname,
if (copy && log && copy_file(tmp_renamed_log.buf, sb_oldref.buf, 0644)) {
ret = error("unable to copy logfile logs/%s to logs/"TMP_RENAMED_LOG": %s",
oldrefname, strerror(errno));
goto out;
}
if (!copy && refs_delete_ref(&refs->base, logmsg, oldrefname,
orig_oid.hash, REF_NODEREF)) {
error("unable to delete old %s", oldrefname);
goto rollback;
@ -1320,7 +1330,7 @@ static int files_rename_ref(struct ref_store *ref_store,
* the safety anyway; we want to delete the reference whatever
* its current value.
*/
if (!refs_read_ref_full(&refs->base, newrefname,
if (!copy && !refs_read_ref_full(&refs->base, newrefname,
RESOLVE_REF_READING | RESOLVE_REF_NO_RECURSE,
oid.hash, NULL) &&
refs_delete_ref(&refs->base, NULL, newrefname,
@ -1351,7 +1361,10 @@ static int files_rename_ref(struct ref_store *ref_store,
lock = lock_ref_sha1_basic(refs, newrefname, NULL, NULL, NULL,
REF_NODEREF, NULL, &err);
if (!lock) {
error("unable to rename '%s' to '%s': %s", oldrefname, newrefname, err.buf);
if (copy)
error("unable to copy '%s' to '%s': %s", oldrefname, newrefname, err.buf);
else
error("unable to rename '%s' to '%s': %s", oldrefname, newrefname, err.buf);
strbuf_release(&err);
goto rollback;
}
@ -1402,6 +1415,22 @@ static int files_rename_ref(struct ref_store *ref_store,
return ret;
}
static int files_rename_ref(struct ref_store *ref_store,
const char *oldrefname, const char *newrefname,
const char *logmsg)
{
return files_copy_or_rename_ref(ref_store, oldrefname,
newrefname, logmsg, 0);
}
static int files_copy_ref(struct ref_store *ref_store,
const char *oldrefname, const char *newrefname,
const char *logmsg)
{
return files_copy_or_rename_ref(ref_store, oldrefname,
newrefname, logmsg, 1);
}
static int close_ref_gently(struct ref_lock *lock)
{
if (close_lock_file_gently(&lock->lk))
@ -3064,6 +3093,7 @@ struct ref_storage_be refs_be_files = {
files_create_symref,
files_delete_refs,
files_rename_ref,
files_copy_ref,
files_ref_iterator_begin,
files_read_raw_ref,

Просмотреть файл

@ -966,6 +966,13 @@ static int packed_rename_ref(struct ref_store *ref_store,
die("BUG: packed reference store does not support renaming references");
}
static int packed_copy_ref(struct ref_store *ref_store,
const char *oldrefname, const char *newrefname,
const char *logmsg)
{
die("BUG: packed reference store does not support copying references");
}
static struct ref_iterator *packed_reflog_iterator_begin(struct ref_store *ref_store)
{
return empty_ref_iterator_begin();
@ -1031,6 +1038,7 @@ struct ref_storage_be refs_be_packed = {
packed_create_symref,
packed_delete_refs,
packed_rename_ref,
packed_copy_ref,
packed_ref_iterator_begin,
packed_read_raw_ref,

Просмотреть файл

@ -559,6 +559,9 @@ typedef int delete_refs_fn(struct ref_store *ref_store, const char *msg,
typedef int rename_ref_fn(struct ref_store *ref_store,
const char *oldref, const char *newref,
const char *logmsg);
typedef int copy_ref_fn(struct ref_store *ref_store,
const char *oldref, const char *newref,
const char *logmsg);
/*
* Iterate over the references in `ref_store` whose names start with
@ -657,6 +660,7 @@ struct ref_storage_be {
create_symref_fn *create_symref;
delete_refs_fn *delete_refs;
rename_ref_fn *rename_ref;
copy_ref_fn *copy_ref;
ref_iterator_begin_fn *iterator_begin;
read_raw_ref_fn *read_raw_ref;

Просмотреть файл

@ -381,6 +381,262 @@ test_expect_success 'config information was renamed, too' '
test_must_fail git config branch.s/s.dummy
'
test_expect_success 'git branch -m correctly renames multiple config sections' '
test_when_finished "git checkout master" &&
git checkout -b source master &&
# Assert that a config file with multiple config sections has
# those sections preserved...
cat >expect <<-\EOF &&
branch.dest.key1=value1
some.gar.b=age
branch.dest.key2=value2
EOF
cat >config.branch <<\EOF &&
;; Note the lack of -\EOF above & mixed indenting here. This is
;; intentional, we are also testing that the formatting of copied
;; sections is preserved.
;; Comment for source. Tabs
[branch "source"]
;; Comment for the source value
key1 = value1
;; Comment for some.gar. Spaces
[some "gar"]
;; Comment for the some.gar value
b = age
;; Comment for source, again. Mixed tabs/spaces.
[branch "source"]
;; Comment for the source value, again
key2 = value2
EOF
cat config.branch >>.git/config &&
git branch -m source dest &&
git config -f .git/config -l | grep -F -e source -e dest -e some.gar >actual &&
test_cmp expect actual &&
# ...and that the comments for those sections are also
# preserved.
cat config.branch | sed "s/\"source\"/\"dest\"/" >expect &&
sed -n -e "/Note the lack/,\$p" .git/config >actual &&
test_cmp expect actual
'
test_expect_success 'git branch -c dumps usage' '
test_expect_code 128 git branch -c 2>err &&
test_i18ngrep "branch name required" err
'
test_expect_success 'git branch --copy dumps usage' '
test_expect_code 128 git branch --copy 2>err &&
test_i18ngrep "branch name required" err
'
test_expect_success 'git branch -c d e should work' '
git branch -l d &&
git reflog exists refs/heads/d &&
git config branch.d.dummy Hello &&
git branch -c d e &&
git reflog exists refs/heads/d &&
git reflog exists refs/heads/e &&
echo Hello >expect &&
git config branch.e.dummy >actual &&
test_cmp expect actual &&
echo Hello >expect &&
git config branch.d.dummy >actual &&
test_cmp expect actual
'
test_expect_success 'git branch --copy is a synonym for -c' '
git branch -l copy &&
git reflog exists refs/heads/copy &&
git config branch.copy.dummy Hello &&
git branch --copy copy copy-to &&
git reflog exists refs/heads/copy &&
git reflog exists refs/heads/copy-to &&
echo Hello >expect &&
git config branch.copy.dummy >actual &&
test_cmp expect actual &&
echo Hello >expect &&
git config branch.copy-to.dummy >actual &&
test_cmp expect actual
'
test_expect_success 'git branch -c ee ef should copy ee to create branch ef' '
git checkout -b ee &&
git reflog exists refs/heads/ee &&
git config branch.ee.dummy Hello &&
git branch -c ee ef &&
git reflog exists refs/heads/ee &&
git reflog exists refs/heads/ef &&
test $(git config branch.ee.dummy) = Hello &&
test $(git config branch.ef.dummy) = Hello &&
test $(git rev-parse --abbrev-ref HEAD) = ee
'
test_expect_success 'git branch -c f/f g/g should work' '
git branch -l f/f &&
git reflog exists refs/heads/f/f &&
git config branch.f/f.dummy Hello &&
git branch -c f/f g/g &&
git reflog exists refs/heads/f/f &&
git reflog exists refs/heads/g/g &&
test $(git config branch.f/f.dummy) = Hello &&
test $(git config branch.g/g.dummy) = Hello
'
test_expect_success 'git branch -c m2 m2 should work' '
git branch -l m2 &&
git reflog exists refs/heads/m2 &&
git config branch.m2.dummy Hello &&
git branch -c m2 m2 &&
git reflog exists refs/heads/m2 &&
test $(git config branch.m2.dummy) = Hello
'
test_expect_success 'git branch -c zz zz/zz should fail' '
git branch -l zz &&
git reflog exists refs/heads/zz &&
test_must_fail git branch -c zz zz/zz
'
test_expect_success 'git branch -c b/b b should fail' '
git branch -l b/b &&
test_must_fail git branch -c b/b b
'
test_expect_success 'git branch -C o/q o/p should work when o/p exists' '
git branch -l o/q &&
git reflog exists refs/heads/o/q &&
git reflog exists refs/heads/o/p &&
git branch -C o/q o/p
'
test_expect_success 'git branch -c -f o/q o/p should work when o/p exists' '
git reflog exists refs/heads/o/q &&
git reflog exists refs/heads/o/p &&
git branch -c -f o/q o/p
'
test_expect_success 'git branch -c qq rr/qq should fail when r exists' '
git branch qq &&
git branch rr &&
test_must_fail git branch -c qq rr/qq
'
test_expect_success 'git branch -C b1 b2 should fail when b2 is checked out' '
git branch b1 &&
git checkout -b b2 &&
test_must_fail git branch -C b1 b2
'
test_expect_success 'git branch -C c1 c2 should succeed when c1 is checked out' '
git checkout -b c1 &&
git branch c2 &&
git branch -C c1 c2 &&
test $(git rev-parse --abbrev-ref HEAD) = c1
'
test_expect_success 'git branch -C c1 c2 should never touch HEAD' '
msg="Branch: copied refs/heads/c1 to refs/heads/c2" &&
! grep "$msg$" .git/logs/HEAD
'
test_expect_success 'git branch -C master should work when master is checked out' '
git checkout master &&
git branch -C master
'
test_expect_success 'git branch -C master master should work when master is checked out' '
git checkout master &&
git branch -C master master
'
test_expect_success 'git branch -C master5 master5 should work when master is checked out' '
git checkout master &&
git branch master5 &&
git branch -C master5 master5
'
test_expect_success 'git branch -C ab cd should overwrite existing config for cd' '
git branch -l cd &&
git reflog exists refs/heads/cd &&
git config branch.cd.dummy CD &&
git branch -l ab &&
git reflog exists refs/heads/ab &&
git config branch.ab.dummy AB &&
git branch -C ab cd &&
git reflog exists refs/heads/ab &&
git reflog exists refs/heads/cd &&
test $(git config branch.ab.dummy) = AB &&
test $(git config branch.cd.dummy) = AB
'
test_expect_success 'git branch -c correctly copies multiple config sections' '
FOO=1 &&
export FOO &&
test_when_finished "git checkout master" &&
git checkout -b source2 master &&
# Assert that a config file with multiple config sections has
# those sections preserved...
cat >expect <<-\EOF &&
branch.source2.key1=value1
branch.dest2.key1=value1
more.gar.b=age
branch.source2.key2=value2
branch.dest2.key2=value2
EOF
cat >config.branch <<\EOF &&
;; Note the lack of -\EOF above & mixed indenting here. This is
;; intentional, we are also testing that the formatting of copied
;; sections is preserved.
;; Comment for source2. Tabs
[branch "source2"]
;; Comment for the source2 value
key1 = value1
;; Comment for more.gar. Spaces
[more "gar"]
;; Comment for the more.gar value
b = age
;; Comment for source2, again. Mixed tabs/spaces.
[branch "source2"]
;; Comment for the source2 value, again
key2 = value2
EOF
cat config.branch >>.git/config &&
git branch -c source2 dest2 &&
git config -f .git/config -l | grep -F -e source2 -e dest2 -e more.gar >actual &&
test_cmp expect actual &&
# ...and that the comments and formatting for those sections
# is also preserved.
cat >expect <<\EOF &&
;; Comment for source2. Tabs
[branch "source2"]
;; Comment for the source2 value
key1 = value1
;; Comment for more.gar. Spaces
[branch "dest2"]
;; Comment for the source2 value
key1 = value1
;; Comment for more.gar. Spaces
[more "gar"]
;; Comment for the more.gar value
b = age
;; Comment for source2, again. Mixed tabs/spaces.
[branch "source2"]
;; Comment for the source2 value, again
key2 = value2
[branch "dest2"]
;; Comment for the source2 value, again
key2 = value2
EOF
sed -n -e "/Comment for source2/,\$p" .git/config >actual &&
test_cmp expect actual
'
test_expect_success 'deleting a symref' '
git branch target &&
git symbolic-ref refs/heads/symref refs/heads/target &&