Skip to content

Commit

Permalink
branch: add a --copy (-c) option to go with --move (-m)
Browse files Browse the repository at this point in the history
Add the ability to --copy a branch and its reflog and configuration,
this uses the same underlying machinery as the --move (-m) option
except the reflog and configuration is copied instead of being moved.

This is useful for e.g. copying a topic branch to a new version,
e.g. work to work-2 after submitting the work topic to the list, while
preserving all the tracking info and other configuration that goes
with the branch, and unlike --move keeping the other already-submitted
branch around for reference.

Like --move, when the source branch is the currently checked out
branch the HEAD is moved to the destination branch. In the case of
--move we don't really have a choice (other than remaining on a
detached HEAD) and in order to keep the functionality consistent, we
are doing it in similar way for --copy too.

The most common usage of this feature is expected to be moving to a
new topic branch which is a copy of the current one, in that case
moving to the target branch is what the user wants, and doesn't
unexpectedly behave differently than --move would.

One outstanding caveat of this implementation is that:

    git checkout maint &&
    git checkout master &&
    git branch -c topic &&
    git checkout -

Will check out 'maint' instead of 'master'. This is because the @{-N}
feature (or its -1 shorthand "-") relies on HEAD reflogs created by
the checkout command, so in this case we'll checkout maint instead of
master, as the user might expect. What to do about that is left to a
future change.

Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Signed-off-by: Sahil Dua <sahildua2305@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
  • Loading branch information
sahildua2305 authored and gitster committed Jun 19, 2017
1 parent c8b2cec commit 52d59cc
Show file tree
Hide file tree
Showing 9 changed files with 424 additions and 46 deletions.
14 changes: 13 additions & 1 deletion Documentation/git-branch.txt
Expand Up @@ -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>]

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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::
Expand All @@ -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.
Expand Down
67 changes: 51 additions & 16 deletions builtin/branch.c
Expand Up @@ -27,6 +27,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
Expand Down Expand Up @@ -449,15 +450,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)) {
/*
Expand All @@ -480,26 +485,44 @@ 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))
die(_("Branch renamed to %s, but HEAD is not updated!"), newname);
if (replace_each_worktree_head_symref(oldref.buf, newref.buf, logmsg.buf)) {
if (copy)
die(_("Branch copied to %s, but HEAD is not updated!"), newname);
else
die(_("Branch renamed to %s, but HEAD is not updated!"), newname);
}

strbuf_release(&logmsg);

strbuf_addf(&oldsection, "branch.%s", oldref.buf + 11);
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);
}
Expand Down Expand Up @@ -537,7 +560,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;
Expand Down Expand Up @@ -574,6 +597,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,
Expand Down Expand Up @@ -617,14 +642,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);

Expand All @@ -642,6 +667,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
if (force) {
delete *= 2;
rename *= 2;
copy *= 2;
}

if (delete) {
Expand Down Expand Up @@ -696,13 +722,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) {
Expand Down
2 changes: 2 additions & 0 deletions cache.h
Expand Up @@ -1941,6 +1941,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);
Expand Down
102 changes: 82 additions & 20 deletions config.c
Expand Up @@ -2638,8 +2638,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;
Expand All @@ -2648,6 +2648,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);
Expand Down Expand Up @@ -2683,50 +2684,92 @@ 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) {
remove = 1;
continue;
}
store.baselen = strlen(new_name);
if (!store_write_section(out_fd, new_name)) {
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 (!store_write_section(out_fd, new_name)) {
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;
}
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) != length) {
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:
Expand All @@ -2742,11 +2785,30 @@ int git_config_rename_section_in_file(const char *config_filename,
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").
Expand Down
11 changes: 11 additions & 0 deletions refs.c
Expand Up @@ -2032,3 +2032,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 changes: 8 additions & 1 deletion refs.h
Expand Up @@ -440,7 +440,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);
Expand Down

0 comments on commit 52d59cc

Please sign in to comment.