Skip to content

Commit

Permalink
Merge branch 'sd/branch-copy'
Browse files Browse the repository at this point in the history
"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
  • Loading branch information
gitster committed Oct 3, 2017
2 parents b2a2c4d + e5435ff commit 3b48045
Show file tree
Hide file tree
Showing 10 changed files with 478 additions and 48 deletions.
14 changes: 13 additions & 1 deletion Documentation/git-branch.txt
Original file line number Diff line number Diff line change
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
62 changes: 47 additions & 15 deletions builtin/branch.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)) {
/*
Expand All @@ -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);
Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);

Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
114 changes: 91 additions & 23 deletions config.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -2791,50 +2799,91 @@ 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 (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;
}
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:
Expand All @@ -2850,11 +2899,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
2 changes: 2 additions & 0 deletions config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 3b48045

Please sign in to comment.