Skip to content

Commit 3b48045

Browse files
committed
Merge branch 'sd/branch-copy'
"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
2 parents b2a2c4d + e5435ff commit 3b48045

File tree

10 files changed

+478
-48
lines changed

10 files changed

+478
-48
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: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ static const char * const builtin_branch_usage[] = {
2828
N_("git branch [<options>] [-l] [-f] <branch-name> [<start-point>]"),
2929
N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."),
3030
N_("git branch [<options>] (-m | -M) [<old-branch>] <new-branch>"),
31+
N_("git branch [<options>] (-c | -C) [<old-branch>] <new-branch>"),
3132
N_("git branch [<options>] [-r | -a] [--points-at]"),
3233
N_("git branch [<options>] [-r | -a] [--format]"),
3334
NULL
@@ -456,15 +457,19 @@ static void reject_rebase_or_bisect_branch(const char *target)
456457
free_worktrees(worktrees);
457458
}
458459

459-
static void rename_branch(const char *oldname, const char *newname, int force)
460+
static void copy_or_rename_branch(const char *oldname, const char *newname, int copy, int force)
460461
{
461462
struct strbuf oldref = STRBUF_INIT, newref = STRBUF_INIT, logmsg = STRBUF_INIT;
462463
struct strbuf oldsection = STRBUF_INIT, newsection = STRBUF_INIT;
463464
int recovery = 0;
464465
int clobber_head_ok;
465466

466-
if (!oldname)
467-
die(_("cannot rename the current branch while not on any."));
467+
if (!oldname) {
468+
if (copy)
469+
die(_("cannot copy the current branch while not on any."));
470+
else
471+
die(_("cannot rename the current branch while not on any."));
472+
}
468473

469474
if (strbuf_check_branch_ref(&oldref, oldname)) {
470475
/*
@@ -487,16 +492,29 @@ static void rename_branch(const char *oldname, const char *newname, int force)
487492

488493
reject_rebase_or_bisect_branch(oldref.buf);
489494

490-
strbuf_addf(&logmsg, "Branch: renamed %s to %s",
491-
oldref.buf, newref.buf);
495+
if (copy)
496+
strbuf_addf(&logmsg, "Branch: copied %s to %s",
497+
oldref.buf, newref.buf);
498+
else
499+
strbuf_addf(&logmsg, "Branch: renamed %s to %s",
500+
oldref.buf, newref.buf);
492501

493-
if (rename_ref(oldref.buf, newref.buf, logmsg.buf))
502+
if (!copy && rename_ref(oldref.buf, newref.buf, logmsg.buf))
494503
die(_("Branch rename failed"));
504+
if (copy && copy_existing_ref(oldref.buf, newref.buf, logmsg.buf))
505+
die(_("Branch copy failed"));
495506

496-
if (recovery)
497-
warning(_("Renamed a misnamed branch '%s' away"), oldref.buf + 11);
507+
if (recovery) {
508+
if (copy)
509+
warning(_("Copied a misnamed branch '%s' away"),
510+
oldref.buf + 11);
511+
else
512+
warning(_("Renamed a misnamed branch '%s' away"),
513+
oldref.buf + 11);
514+
}
498515

499-
if (replace_each_worktree_head_symref(oldref.buf, newref.buf, logmsg.buf))
516+
if (!copy &&
517+
replace_each_worktree_head_symref(oldref.buf, newref.buf, logmsg.buf))
500518
die(_("Branch renamed to %s, but HEAD is not updated!"), newname);
501519

502520
strbuf_release(&logmsg);
@@ -505,8 +523,10 @@ static void rename_branch(const char *oldname, const char *newname, int force)
505523
strbuf_release(&oldref);
506524
strbuf_addf(&newsection, "branch.%s", newref.buf + 11);
507525
strbuf_release(&newref);
508-
if (git_config_rename_section(oldsection.buf, newsection.buf) < 0)
526+
if (!copy && git_config_rename_section(oldsection.buf, newsection.buf) < 0)
509527
die(_("Branch is renamed, but update of config-file failed"));
528+
if (copy && strcmp(oldname, newname) && git_config_copy_section(oldsection.buf, newsection.buf) < 0)
529+
die(_("Branch is copied, but update of config-file failed"));
510530
strbuf_release(&oldsection);
511531
strbuf_release(&newsection);
512532
}
@@ -544,7 +564,7 @@ static int edit_branch_description(const char *branch_name)
544564

545565
int cmd_branch(int argc, const char **argv, const char *prefix)
546566
{
547-
int delete = 0, rename = 0, force = 0, list = 0;
567+
int delete = 0, rename = 0, copy = 0, force = 0, list = 0;
548568
int reflog = 0, edit_description = 0;
549569
int quiet = 0, unset_upstream = 0;
550570
const char *new_upstream = NULL;
@@ -581,6 +601,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
581601
OPT_BIT('D', NULL, &delete, N_("delete branch (even if not merged)"), 2),
582602
OPT_BIT('m', "move", &rename, N_("move/rename a branch and its reflog"), 1),
583603
OPT_BIT('M', NULL, &rename, N_("move/rename a branch, even if target exists"), 2),
604+
OPT_BIT('c', "copy", &copy, N_("copy a branch and its reflog"), 1),
605+
OPT_BIT('C', NULL, &copy, N_("copy a branch, even if target exists"), 2),
584606
OPT_BOOL(0, "list", &list, N_("list branch names")),
585607
OPT_BOOL('l', "create-reflog", &reflog, N_("create the branch's reflog")),
586608
OPT_BOOL(0, "edit-description", &edit_description,
@@ -624,14 +646,14 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
624646
argc = parse_options(argc, argv, prefix, options, builtin_branch_usage,
625647
0);
626648

627-
if (!delete && !rename && !edit_description && !new_upstream && !unset_upstream && argc == 0)
649+
if (!delete && !rename && !copy && !edit_description && !new_upstream && !unset_upstream && argc == 0)
628650
list = 1;
629651

630652
if (filter.with_commit || filter.merge != REF_FILTER_MERGED_NONE || filter.points_at.nr ||
631653
filter.no_commit)
632654
list = 1;
633655

634-
if (!!delete + !!rename + !!new_upstream +
656+
if (!!delete + !!rename + !!copy + !!new_upstream +
635657
list + unset_upstream > 1)
636658
usage_with_options(builtin_branch_usage, options);
637659

@@ -649,6 +671,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
649671
if (force) {
650672
delete *= 2;
651673
rename *= 2;
674+
copy *= 2;
652675
}
653676

654677
if (delete) {
@@ -703,13 +726,22 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
703726

704727
if (edit_branch_description(branch_name))
705728
return 1;
729+
} else if (copy) {
730+
if (!argc)
731+
die(_("branch name required"));
732+
else if (argc == 1)
733+
copy_or_rename_branch(head, argv[0], 1, copy > 1);
734+
else if (argc == 2)
735+
copy_or_rename_branch(argv[0], argv[1], 1, copy > 1);
736+
else
737+
die(_("too many branches for a copy operation"));
706738
} else if (rename) {
707739
if (!argc)
708740
die(_("branch name required"));
709741
else if (argc == 1)
710-
rename_branch(head, argv[0], rename > 1);
742+
copy_or_rename_branch(head, argv[0], 0, rename > 1);
711743
else if (argc == 2)
712-
rename_branch(argv[0], argv[1], rename > 1);
744+
copy_or_rename_branch(argv[0], argv[1], 0, rename > 1);
713745
else
714746
die(_("too many branches for a rename operation"));
715747
} else if (new_upstream) {

config.c

Lines changed: 91 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2292,11 +2292,10 @@ static int write_error(const char *filename)
22922292
return 4;
22932293
}
22942294

2295-
static ssize_t write_section(int fd, const char *key)
2295+
static struct strbuf store_create_section(const char *key)
22962296
{
22972297
const char *dot;
22982298
int i;
2299-
ssize_t ret;
23002299
struct strbuf sb = STRBUF_INIT;
23012300

23022301
dot = memchr(key, '.', store.baselen);
@@ -2312,7 +2311,15 @@ static ssize_t write_section(int fd, const char *key)
23122311
strbuf_addf(&sb, "[%.*s]\n", store.baselen, key);
23132312
}
23142313

2315-
ret = write_in_full(fd, sb.buf, sb.len);
2314+
return sb;
2315+
}
2316+
2317+
static ssize_t write_section(int fd, const char *key)
2318+
{
2319+
struct strbuf sb = store_create_section(key);
2320+
ssize_t ret;
2321+
2322+
ret = write_in_full(fd, sb.buf, sb.len) == sb.len;
23162323
strbuf_release(&sb);
23172324

23182325
return ret;
@@ -2743,8 +2750,8 @@ static int section_name_is_ok(const char *name)
27432750
}
27442751

27452752
/* if new_name == NULL, the section is removed instead */
2746-
int git_config_rename_section_in_file(const char *config_filename,
2747-
const char *old_name, const char *new_name)
2753+
static int git_config_copy_or_rename_section_in_file(const char *config_filename,
2754+
const char *old_name, const char *new_name, int copy)
27482755
{
27492756
int ret = 0, remove = 0;
27502757
char *filename_buf = NULL;
@@ -2753,6 +2760,7 @@ int git_config_rename_section_in_file(const char *config_filename,
27532760
char buf[1024];
27542761
FILE *config_file = NULL;
27552762
struct stat st;
2763+
struct strbuf copystr = STRBUF_INIT;
27562764

27572765
if (new_name && !section_name_is_ok(new_name)) {
27582766
ret = error("invalid section name: %s", new_name);
@@ -2791,50 +2799,91 @@ int git_config_rename_section_in_file(const char *config_filename,
27912799
while (fgets(buf, sizeof(buf), config_file)) {
27922800
int i;
27932801
int length;
2802+
int is_section = 0;
27942803
char *output = buf;
27952804
for (i = 0; buf[i] && isspace(buf[i]); i++)
27962805
; /* do nothing */
27972806
if (buf[i] == '[') {
27982807
/* it's a section */
2799-
int offset = section_name_match(&buf[i], old_name);
2808+
int offset;
2809+
is_section = 1;
2810+
2811+
/*
2812+
* When encountering a new section under -c we
2813+
* need to flush out any section we're already
2814+
* coping and begin anew. There might be
2815+
* multiple [branch "$name"] sections.
2816+
*/
2817+
if (copystr.len > 0) {
2818+
if (write_in_full(out_fd, copystr.buf, copystr.len) != copystr.len) {
2819+
ret = write_error(get_lock_file_path(lock));
2820+
goto out;
2821+
}
2822+
strbuf_reset(&copystr);
2823+
}
2824+
2825+
offset = section_name_match(&buf[i], old_name);
28002826
if (offset > 0) {
28012827
ret++;
28022828
if (new_name == NULL) {
28032829
remove = 1;
28042830
continue;
28052831
}
28062832
store.baselen = strlen(new_name);
2807-
if (write_section(out_fd, new_name) < 0) {
2808-
ret = write_error(get_lock_file_path(lock));
2809-
goto out;
2810-
}
2811-
/*
2812-
* We wrote out the new section, with
2813-
* a newline, now skip the old
2814-
* section's length
2815-
*/
2816-
output += offset + i;
2817-
if (strlen(output) > 0) {
2833+
if (!copy) {
2834+
if (write_section(out_fd, new_name) < 0) {
2835+
ret = write_error(get_lock_file_path(lock));
2836+
goto out;
2837+
}
28182838
/*
2819-
* More content means there's
2820-
* a declaration to put on the
2821-
* next line; indent with a
2822-
* tab
2839+
* We wrote out the new section, with
2840+
* a newline, now skip the old
2841+
* section's length
28232842
*/
2824-
output -= 1;
2825-
output[0] = '\t';
2843+
output += offset + i;
2844+
if (strlen(output) > 0) {
2845+
/*
2846+
* More content means there's
2847+
* a declaration to put on the
2848+
* next line; indent with a
2849+
* tab
2850+
*/
2851+
output -= 1;
2852+
output[0] = '\t';
2853+
}
2854+
} else {
2855+
copystr = store_create_section(new_name);
28262856
}
28272857
}
28282858
remove = 0;
28292859
}
28302860
if (remove)
28312861
continue;
28322862
length = strlen(output);
2863+
2864+
if (!is_section && copystr.len > 0) {
2865+
strbuf_add(&copystr, output, length);
2866+
}
2867+
28332868
if (write_in_full(out_fd, output, length) < 0) {
28342869
ret = write_error(get_lock_file_path(lock));
28352870
goto out;
28362871
}
28372872
}
2873+
2874+
/*
2875+
* Copy a trailing section at the end of the config, won't be
2876+
* flushed by the usual "flush because we have a new section
2877+
* logic in the loop above.
2878+
*/
2879+
if (copystr.len > 0) {
2880+
if (write_in_full(out_fd, copystr.buf, copystr.len) != copystr.len) {
2881+
ret = write_error(get_lock_file_path(lock));
2882+
goto out;
2883+
}
2884+
strbuf_reset(&copystr);
2885+
}
2886+
28382887
fclose(config_file);
28392888
config_file = NULL;
28402889
commit_and_out:
@@ -2850,11 +2899,30 @@ int git_config_rename_section_in_file(const char *config_filename,
28502899
return ret;
28512900
}
28522901

2902+
int git_config_rename_section_in_file(const char *config_filename,
2903+
const char *old_name, const char *new_name)
2904+
{
2905+
return git_config_copy_or_rename_section_in_file(config_filename,
2906+
old_name, new_name, 0);
2907+
}
2908+
28532909
int git_config_rename_section(const char *old_name, const char *new_name)
28542910
{
28552911
return git_config_rename_section_in_file(NULL, old_name, new_name);
28562912
}
28572913

2914+
int git_config_copy_section_in_file(const char *config_filename,
2915+
const char *old_name, const char *new_name)
2916+
{
2917+
return git_config_copy_or_rename_section_in_file(config_filename,
2918+
old_name, new_name, 1);
2919+
}
2920+
2921+
int git_config_copy_section(const char *old_name, const char *new_name)
2922+
{
2923+
return git_config_copy_section_in_file(NULL, old_name, new_name);
2924+
}
2925+
28582926
/*
28592927
* Call this to report error for your variable that should not
28602928
* get a boolean value (i.e. "[my] var" means "true").

config.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ extern int git_config_set_multivar_in_file_gently(const char *, const char *, co
7070
extern void git_config_set_multivar_in_file(const char *, const char *, const char *, const char *, int);
7171
extern int git_config_rename_section(const char *, const char *);
7272
extern int git_config_rename_section_in_file(const char *, const char *, const char *);
73+
extern int git_config_copy_section(const char *, const char *);
74+
extern int git_config_copy_section_in_file(const char *, const char *, const char *);
7375
extern const char *git_etc_gitconfig(void);
7476
extern int git_env_bool(const char *, int);
7577
extern unsigned long git_env_ulong(const char *, unsigned long);

0 commit comments

Comments
 (0)