Skip to content

Commit 52d59cc

Browse files
sahildua2305gitster
authored andcommitted
branch: add a --copy (-c) option to go with --move (-m)
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 <[email protected]> Signed-off-by: Ævar Arnfjörð Bjarmason <[email protected]> Signed-off-by: Sahil Dua <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent c8b2cec commit 52d59cc

File tree

9 files changed

+424
-46
lines changed

9 files changed

+424
-46
lines changed

Documentation/git-branch.txt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ SYNOPSIS
1818
'git branch' (--set-upstream-to=<upstream> | -u <upstream>) [<branchname>]
1919
'git branch' --unset-upstream [<branchname>]
2020
'git branch' (-m | -M) [<oldbranch>] <newbranch>
21+
'git branch' (-c | -C) [<oldbranch>] <newbranch>
2122
'git branch' (-d | -D) [-r] <branchname>...
2223
'git branch' --edit-description [<branchname>]
2324

@@ -64,6 +65,10 @@ If <oldbranch> had a corresponding reflog, it is renamed to match
6465
renaming. If <newbranch> exists, -M must be used to force the rename
6566
to happen.
6667

68+
The `-c` and `-C` options have the exact same semantics as `-m` and
69+
`-M`, except instead of the branch being renamed it along with its
70+
config and reflog will be copied to a new name.
71+
6772
With a `-d` or `-D` option, `<branchname>` will be deleted. You may
6873
specify more than one branch for deletion. If the branch currently
6974
has a reflog then the reflog will also be deleted.
@@ -104,7 +109,7 @@ OPTIONS
104109
In combination with `-d` (or `--delete`), allow deleting the
105110
branch irrespective of its merged status. In combination with
106111
`-m` (or `--move`), allow renaming the branch even if the new
107-
branch name already exists.
112+
branch name already exists, the same applies for `-c` (or `--copy`).
108113

109114
-m::
110115
--move::
@@ -113,6 +118,13 @@ OPTIONS
113118
-M::
114119
Shortcut for `--move --force`.
115120

121+
-c::
122+
--copy::
123+
Copy a branch and the corresponding reflog.
124+
125+
-C::
126+
Shortcut for `--copy --force`.
127+
116128
--color[=<when>]::
117129
Color branches to highlight current, local, and
118130
remote-tracking branches.

builtin/branch.c

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ static const char * const builtin_branch_usage[] = {
2727
N_("git branch [<options>] [-l] [-f] <branch-name> [<start-point>]"),
2828
N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."),
2929
N_("git branch [<options>] (-m | -M) [<old-branch>] <new-branch>"),
30+
N_("git branch [<options>] (-c | -C) [<old-branch>] <new-branch>"),
3031
N_("git branch [<options>] [-r | -a] [--points-at]"),
3132
N_("git branch [<options>] [-r | -a] [--format]"),
3233
NULL
@@ -449,15 +450,19 @@ static void reject_rebase_or_bisect_branch(const char *target)
449450
free_worktrees(worktrees);
450451
}
451452

452-
static void rename_branch(const char *oldname, const char *newname, int force)
453+
static void copy_or_rename_branch(const char *oldname, const char *newname, int copy, int force)
453454
{
454455
struct strbuf oldref = STRBUF_INIT, newref = STRBUF_INIT, logmsg = STRBUF_INIT;
455456
struct strbuf oldsection = STRBUF_INIT, newsection = STRBUF_INIT;
456457
int recovery = 0;
457458
int clobber_head_ok;
458459

459-
if (!oldname)
460-
die(_("cannot rename the current branch while not on any."));
460+
if (!oldname) {
461+
if (copy)
462+
die(_("cannot copy the current branch while not on any."));
463+
else
464+
die(_("cannot rename the current branch while not on any."));
465+
}
461466

462467
if (strbuf_check_branch_ref(&oldref, oldname)) {
463468
/*
@@ -480,26 +485,44 @@ static void rename_branch(const char *oldname, const char *newname, int force)
480485

481486
reject_rebase_or_bisect_branch(oldref.buf);
482487

483-
strbuf_addf(&logmsg, "Branch: renamed %s to %s",
484-
oldref.buf, newref.buf);
488+
if (copy)
489+
strbuf_addf(&logmsg, "Branch: copied %s to %s",
490+
oldref.buf, newref.buf);
491+
else
492+
strbuf_addf(&logmsg, "Branch: renamed %s to %s",
493+
oldref.buf, newref.buf);
485494

486-
if (rename_ref(oldref.buf, newref.buf, logmsg.buf))
495+
if (!copy && rename_ref(oldref.buf, newref.buf, logmsg.buf))
487496
die(_("Branch rename failed"));
497+
if (copy && copy_existing_ref(oldref.buf, newref.buf, logmsg.buf))
498+
die(_("Branch copy failed"));
488499

489-
if (recovery)
490-
warning(_("Renamed a misnamed branch '%s' away"), oldref.buf + 11);
500+
if (recovery) {
501+
if (copy)
502+
warning(_("Copied a misnamed branch '%s' away"),
503+
oldref.buf + 11);
504+
else
505+
warning(_("Renamed a misnamed branch '%s' away"),
506+
oldref.buf + 11);
507+
}
491508

492-
if (replace_each_worktree_head_symref(oldref.buf, newref.buf, logmsg.buf))
493-
die(_("Branch renamed to %s, but HEAD is not updated!"), newname);
509+
if (replace_each_worktree_head_symref(oldref.buf, newref.buf, logmsg.buf)) {
510+
if (copy)
511+
die(_("Branch copied to %s, but HEAD is not updated!"), newname);
512+
else
513+
die(_("Branch renamed to %s, but HEAD is not updated!"), newname);
514+
}
494515

495516
strbuf_release(&logmsg);
496517

497518
strbuf_addf(&oldsection, "branch.%s", oldref.buf + 11);
498519
strbuf_release(&oldref);
499520
strbuf_addf(&newsection, "branch.%s", newref.buf + 11);
500521
strbuf_release(&newref);
501-
if (git_config_rename_section(oldsection.buf, newsection.buf) < 0)
522+
if (!copy && git_config_rename_section(oldsection.buf, newsection.buf) < 0)
502523
die(_("Branch is renamed, but update of config-file failed"));
524+
if (copy && strcmp(oldname, newname) && git_config_copy_section(oldsection.buf, newsection.buf) < 0)
525+
die(_("Branch is copied, but update of config-file failed"));
503526
strbuf_release(&oldsection);
504527
strbuf_release(&newsection);
505528
}
@@ -537,7 +560,7 @@ static int edit_branch_description(const char *branch_name)
537560

538561
int cmd_branch(int argc, const char **argv, const char *prefix)
539562
{
540-
int delete = 0, rename = 0, force = 0, list = 0;
563+
int delete = 0, rename = 0, copy = 0, force = 0, list = 0;
541564
int reflog = 0, edit_description = 0;
542565
int quiet = 0, unset_upstream = 0;
543566
const char *new_upstream = NULL;
@@ -574,6 +597,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
574597
OPT_BIT('D', NULL, &delete, N_("delete branch (even if not merged)"), 2),
575598
OPT_BIT('m', "move", &rename, N_("move/rename a branch and its reflog"), 1),
576599
OPT_BIT('M', NULL, &rename, N_("move/rename a branch, even if target exists"), 2),
600+
OPT_BIT('c', "copy", &copy, N_("copy a branch and its reflog"), 1),
601+
OPT_BIT('C', NULL, &copy, N_("copy a branch, even if target exists"), 2),
577602
OPT_BOOL(0, "list", &list, N_("list branch names")),
578603
OPT_BOOL('l', "create-reflog", &reflog, N_("create the branch's reflog")),
579604
OPT_BOOL(0, "edit-description", &edit_description,
@@ -617,14 +642,14 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
617642
argc = parse_options(argc, argv, prefix, options, builtin_branch_usage,
618643
0);
619644

620-
if (!delete && !rename && !edit_description && !new_upstream && !unset_upstream && argc == 0)
645+
if (!delete && !rename && !copy && !edit_description && !new_upstream && !unset_upstream && argc == 0)
621646
list = 1;
622647

623648
if (filter.with_commit || filter.merge != REF_FILTER_MERGED_NONE || filter.points_at.nr ||
624649
filter.no_commit)
625650
list = 1;
626651

627-
if (!!delete + !!rename + !!new_upstream +
652+
if (!!delete + !!rename + !!copy + !!new_upstream +
628653
list + unset_upstream > 1)
629654
usage_with_options(builtin_branch_usage, options);
630655

@@ -642,6 +667,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
642667
if (force) {
643668
delete *= 2;
644669
rename *= 2;
670+
copy *= 2;
645671
}
646672

647673
if (delete) {
@@ -696,13 +722,22 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
696722

697723
if (edit_branch_description(branch_name))
698724
return 1;
725+
} else if (copy) {
726+
if (!argc)
727+
die(_("branch name required"));
728+
else if (argc == 1)
729+
copy_or_rename_branch(head, argv[0], 1, copy > 1);
730+
else if (argc == 2)
731+
copy_or_rename_branch(argv[0], argv[1], 1, copy > 1);
732+
else
733+
die(_("too many branches for a copy operation"));
699734
} else if (rename) {
700735
if (!argc)
701736
die(_("branch name required"));
702737
else if (argc == 1)
703-
rename_branch(head, argv[0], rename > 1);
738+
copy_or_rename_branch(head, argv[0], 0, rename > 1);
704739
else if (argc == 2)
705-
rename_branch(argv[0], argv[1], rename > 1);
740+
copy_or_rename_branch(argv[0], argv[1], 0, rename > 1);
706741
else
707742
die(_("too many branches for a rename operation"));
708743
} else if (new_upstream) {

cache.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1941,6 +1941,8 @@ extern int git_config_set_multivar_in_file_gently(const char *, const char *, co
19411941
extern void git_config_set_multivar_in_file(const char *, const char *, const char *, const char *, int);
19421942
extern int git_config_rename_section(const char *, const char *);
19431943
extern int git_config_rename_section_in_file(const char *, const char *, const char *);
1944+
extern int git_config_copy_section(const char *, const char *);
1945+
extern int git_config_copy_section_in_file(const char *, const char *, const char *);
19441946
extern const char *git_etc_gitconfig(void);
19451947
extern int git_env_bool(const char *, int);
19461948
extern unsigned long git_env_ulong(const char *, unsigned long);

config.c

Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2638,8 +2638,8 @@ static int section_name_is_ok(const char *name)
26382638
}
26392639

26402640
/* if new_name == NULL, the section is removed instead */
2641-
int git_config_rename_section_in_file(const char *config_filename,
2642-
const char *old_name, const char *new_name)
2641+
static int git_config_copy_or_rename_section_in_file(const char *config_filename,
2642+
const char *old_name, const char *new_name, int copy)
26432643
{
26442644
int ret = 0, remove = 0;
26452645
char *filename_buf = NULL;
@@ -2648,6 +2648,7 @@ int git_config_rename_section_in_file(const char *config_filename,
26482648
char buf[1024];
26492649
FILE *config_file = NULL;
26502650
struct stat st;
2651+
struct strbuf copystr = STRBUF_INIT;
26512652

26522653
if (new_name && !section_name_is_ok(new_name)) {
26532654
ret = error("invalid section name: %s", new_name);
@@ -2683,50 +2684,92 @@ int git_config_rename_section_in_file(const char *config_filename,
26832684
while (fgets(buf, sizeof(buf), config_file)) {
26842685
int i;
26852686
int length;
2687+
int is_section = 0;
26862688
char *output = buf;
26872689
for (i = 0; buf[i] && isspace(buf[i]); i++)
26882690
; /* do nothing */
26892691
if (buf[i] == '[') {
26902692
/* it's a section */
2691-
int offset = section_name_match(&buf[i], old_name);
2693+
int offset;
2694+
is_section = 1;
2695+
2696+
/*
2697+
* When encountering a new section under -c we
2698+
* need to flush out any section we're already
2699+
* coping and begin anew. There might be
2700+
* multiple [branch "$name"] sections.
2701+
*/
2702+
if (copystr.len > 0) {
2703+
if (write_in_full(out_fd, copystr.buf, copystr.len) != copystr.len) {
2704+
ret = write_error(get_lock_file_path(lock));
2705+
goto out;
2706+
}
2707+
strbuf_reset(&copystr);
2708+
}
2709+
2710+
offset = section_name_match(&buf[i], old_name);
26922711
if (offset > 0) {
26932712
ret++;
26942713
if (new_name == NULL) {
26952714
remove = 1;
26962715
continue;
26972716
}
26982717
store.baselen = strlen(new_name);
2699-
if (!store_write_section(out_fd, new_name)) {
2700-
ret = write_error(get_lock_file_path(lock));
2701-
goto out;
2702-
}
2703-
/*
2704-
* We wrote out the new section, with
2705-
* a newline, now skip the old
2706-
* section's length
2707-
*/
2708-
output += offset + i;
2709-
if (strlen(output) > 0) {
2718+
if (!copy) {
2719+
if (!store_write_section(out_fd, new_name)) {
2720+
ret = write_error(get_lock_file_path(lock));
2721+
goto out;
2722+
}
2723+
27102724
/*
2711-
* More content means there's
2712-
* a declaration to put on the
2713-
* next line; indent with a
2714-
* tab
2725+
* We wrote out the new section, with
2726+
* a newline, now skip the old
2727+
* section's length
27152728
*/
2716-
output -= 1;
2717-
output[0] = '\t';
2729+
output += offset + i;
2730+
if (strlen(output) > 0) {
2731+
/*
2732+
* More content means there's
2733+
* a declaration to put on the
2734+
* next line; indent with a
2735+
* tab
2736+
*/
2737+
output -= 1;
2738+
output[0] = '\t';
2739+
}
2740+
} else {
2741+
copystr = store_create_section(new_name);
27182742
}
27192743
}
27202744
remove = 0;
27212745
}
27222746
if (remove)
27232747
continue;
27242748
length = strlen(output);
2749+
2750+
if (!is_section && copystr.len > 0) {
2751+
strbuf_add(&copystr, output, length);
2752+
}
2753+
27252754
if (write_in_full(out_fd, output, length) != length) {
27262755
ret = write_error(get_lock_file_path(lock));
27272756
goto out;
27282757
}
27292758
}
2759+
2760+
/*
2761+
* Copy a trailing section at the end of the config, won't be
2762+
* flushed by the usual "flush because we have a new section
2763+
* logic in the loop above.
2764+
*/
2765+
if (copystr.len > 0) {
2766+
if (write_in_full(out_fd, copystr.buf, copystr.len) != copystr.len) {
2767+
ret = write_error(get_lock_file_path(lock));
2768+
goto out;
2769+
}
2770+
strbuf_reset(&copystr);
2771+
}
2772+
27302773
fclose(config_file);
27312774
config_file = NULL;
27322775
commit_and_out:
@@ -2742,11 +2785,30 @@ int git_config_rename_section_in_file(const char *config_filename,
27422785
return ret;
27432786
}
27442787

2788+
int git_config_rename_section_in_file(const char *config_filename,
2789+
const char *old_name, const char *new_name)
2790+
{
2791+
return git_config_copy_or_rename_section_in_file(config_filename,
2792+
old_name, new_name, 0);
2793+
}
2794+
27452795
int git_config_rename_section(const char *old_name, const char *new_name)
27462796
{
27472797
return git_config_rename_section_in_file(NULL, old_name, new_name);
27482798
}
27492799

2800+
int git_config_copy_section_in_file(const char *config_filename,
2801+
const char *old_name, const char *new_name)
2802+
{
2803+
return git_config_copy_or_rename_section_in_file(config_filename,
2804+
old_name, new_name, 1);
2805+
}
2806+
2807+
int git_config_copy_section(const char *old_name, const char *new_name)
2808+
{
2809+
return git_config_copy_section_in_file(NULL, old_name, new_name);
2810+
}
2811+
27502812
/*
27512813
* Call this to report error for your variable that should not
27522814
* get a boolean value (i.e. "[my] var" means "true").

refs.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2032,3 +2032,14 @@ int rename_ref(const char *oldref, const char *newref, const char *logmsg)
20322032
{
20332033
return refs_rename_ref(get_main_ref_store(), oldref, newref, logmsg);
20342034
}
2035+
2036+
int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
2037+
const char *newref, const char *logmsg)
2038+
{
2039+
return refs->be->copy_ref(refs, oldref, newref, logmsg);
2040+
}
2041+
2042+
int copy_existing_ref(const char *oldref, const char *newref, const char *logmsg)
2043+
{
2044+
return refs_copy_existing_ref(get_main_ref_store(), oldref, newref, logmsg);
2045+
}

refs.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,14 @@ char *shorten_unambiguous_ref(const char *refname, int strict);
440440
/** rename ref, return 0 on success **/
441441
int refs_rename_ref(struct ref_store *refs, const char *oldref,
442442
const char *newref, const char *logmsg);
443-
int rename_ref(const char *oldref, const char *newref, const char *logmsg);
443+
int rename_ref(const char *oldref, const char *newref,
444+
const char *logmsg);
445+
446+
/** copy ref, return 0 on success **/
447+
int refs_copy_existing_ref(struct ref_store *refs, const char *oldref,
448+
const char *newref, const char *logmsg);
449+
int copy_existing_ref(const char *oldref, const char *newref,
450+
const char *logmsg);
444451

445452
int refs_create_symref(struct ref_store *refs, const char *refname,
446453
const char *target, const char *logmsg);

0 commit comments

Comments
 (0)