From patchwork Mon Nov 22 22:32:49 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Glen Choo X-Patchwork-Id: 12633017 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 5DD3FC433F5 for ; Mon, 22 Nov 2021 22:33:09 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S232955AbhKVWgP (ORCPT ); Mon, 22 Nov 2021 17:36:15 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:43748 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S229502AbhKVWgP (ORCPT ); Mon, 22 Nov 2021 17:36:15 -0500 Received: from mail-pf1-x44a.google.com (mail-pf1-x44a.google.com [IPv6:2607:f8b0:4864:20::44a]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 2E8DFC061574 for ; Mon, 22 Nov 2021 14:33:08 -0800 (PST) Received: by mail-pf1-x44a.google.com with SMTP id c6-20020aa781c6000000b004a4fcdf1d6dso768086pfn.4 for ; Mon, 22 Nov 2021 14:33:08 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20210112; h=date:in-reply-to:message-id:mime-version:references:subject:from:to :cc; bh=1ZBnoYlNLuCrJ0s5Z5CcSRYjFcGti2Z5TgzLzFmgVgc=; b=qXMDmhkt4yic+NhpJ3pIsauRi3kCpDeiZmpboEwUYDaEh6MiX6D5rXqUm7l+lZFS0X mNnLwTPLmwxmD8KO76H1L4c6EW9V0uvUk5YQiEaXrIwg+jFMl0HiITWaE3FZCWdqz/pG axwImTMavMQTbGp+SNYRDJTs7ULT6hLyBJC9y6GlrCwYC29wPB4p72VXT4RcwP6NFd59 Kt3Q+fo4NCMfoss4/lzIbB02cqoBcsrg+NQtoXu1rchjGwaW+E9IB0M8NtVu+KcgnkLU nVVnFnwYR/fD8sfgdM0YIy1EHADJKsHoQnrMQZFiLQVMHpcOrVVA6IyFKfZifolS7MWd Bygg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:date:in-reply-to:message-id:mime-version :references:subject:from:to:cc; bh=1ZBnoYlNLuCrJ0s5Z5CcSRYjFcGti2Z5TgzLzFmgVgc=; b=wT87roo8xNmlNdfhVDd+idxWZBYbHdKbUu3qqtCLYgH3jOCOALUqCwzidM1vM+Agru MLyG7+NlAw3FoximGZfLHMkiGc1D/LgjxchpdoUWvZLooeFeJbviT7k0b8Ni7kNjXfVD GnWpyDulPN9wsPdBuBRxanEY5V6sN8zJztbukPCpqSdOexZ80hh/SUL5JaBp1kFe97bf Nbfgkcg3+cv0vJYKTBsSDCWnQ4AeCcfY2sqgiA7DuKTCC0uRaxRLW37mzaZt917iZGfk d+3IRZQ+QXV3baheaOiabh+1PEYLpfI0GFy/gWZXGl8gcPpLSJFqOLYHxDd0PvNEGHV7 iubA== X-Gm-Message-State: AOAM531vdsonf9V//0/BCH+xYZKXOR7Ymv/YyhCeBFSmKmYuo8ayRygp 1T3YUn1TODwCGcz46k4vSZfxfyUA79uLa89VmXgmu8xs1KT9Zj/3nNs1gR9WxyDBDGvlWqruRK1 4XNwHvvb6xCNMb17v/fx2Zs1W9SP/pjxKyxy8ABdKpKNETAsAXq6F3W52GZqlewk= X-Google-Smtp-Source: ABdhPJw3GAa1batCefvGDLELn9PpHIE0BXTneofHJtkdx5mTZ+65CeH/P09IzvrPnh/BMwEBxuy/c14Ctwu7kA== X-Received: from chooglen.c.googlers.com ([fda3:e722:ac3:cc00:24:72f4:c0a8:26d9]) (user=chooglen job=sendgmr) by 2002:a17:90a:cf85:: with SMTP id i5mr37422449pju.101.1637620387566; Mon, 22 Nov 2021 14:33:07 -0800 (PST) Date: Mon, 22 Nov 2021 14:32:49 -0800 In-Reply-To: <20211122223252.19922-1-chooglen@google.com> Message-Id: <20211122223252.19922-2-chooglen@google.com> Mime-Version: 1.0 References: <20211122223252.19922-1-chooglen@google.com> X-Mailer: git-send-email 2.34.0.rc2.393.gf8c9666880-goog Subject: [PATCH 1/4] submodule-config: add submodules_of_tree() helper From: Glen Choo To: git@vger.kernel.org Cc: Jonathan Tan , Josh Steadmon , Emily Shaffer , Glen Choo Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org As we introduce a submodule UX with branches, we would like to be able to get the submodule commit ids in a superproject tree because those ids are the source of truth e.g. "git branch --recurse-submodules topic start-point" should create branches based off the commit ids recorded in the superproject's 'start-point' tree. To make this easy, introduce a submodules_of_tree() helper function that iterates through a tree and returns the tree's gitlink entries as a list. Signed-off-by: Glen Choo --- submodule-config.c | 19 +++++++++++++++++++ submodule-config.h | 13 +++++++++++++ 2 files changed, 32 insertions(+) diff --git a/submodule-config.c b/submodule-config.c index f95344028b..97da373301 100644 --- a/submodule-config.c +++ b/submodule-config.c @@ -7,6 +7,7 @@ #include "strbuf.h" #include "object-store.h" #include "parse-options.h" +#include "tree-walk.h" /* * submodule cache lookup structure @@ -726,6 +727,24 @@ const struct submodule *submodule_from_path(struct repository *r, return config_from(r->submodule_cache, treeish_name, path, lookup_path); } +struct submodule_entry_list * +submodules_of_tree(struct repository *r, const struct object_id *treeish_name) +{ + struct tree_desc tree; + struct name_entry entry; + struct submodule_entry_list *ret; + + CALLOC_ARRAY(ret, 1); + fill_tree_descriptor(r, &tree, treeish_name); + while (tree_entry(&tree, &entry)) { + if (!S_ISGITLINK(entry.mode)) + continue; + ALLOC_GROW(ret->name_entries, ret->entry_nr + 1, ret->entry_alloc); + ret->name_entries[ret->entry_nr++] = entry; + } + return ret; +} + void submodule_free(struct repository *r) { if (r->submodule_cache) diff --git a/submodule-config.h b/submodule-config.h index 65875b94ea..4379ec77e3 100644 --- a/submodule-config.h +++ b/submodule-config.h @@ -6,6 +6,7 @@ #include "hashmap.h" #include "submodule.h" #include "strbuf.h" +#include "tree-walk.h" /** * The submodule config cache API allows to read submodule @@ -67,6 +68,18 @@ const struct submodule *submodule_from_name(struct repository *r, const struct object_id *commit_or_tree, const char *name); +struct submodule_entry_list { + struct name_entry *name_entries; + int entry_nr; + int entry_alloc; +}; + +/** + * Given a tree-ish, return all submodules in the tree. + */ +struct submodule_entry_list * +submodules_of_tree(struct repository *r, const struct object_id *treeish_name); + /** * Given a tree-ish in the superproject and a path, return the submodule that * is bound at the path in the named tree. From patchwork Mon Nov 22 22:32:50 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Glen Choo X-Patchwork-Id: 12633019 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 8BAB6C433F5 for ; Mon, 22 Nov 2021 22:33:11 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S232145AbhKVWgR (ORCPT ); Mon, 22 Nov 2021 17:36:17 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:43764 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S233148AbhKVWgR (ORCPT ); Mon, 22 Nov 2021 17:36:17 -0500 Received: from mail-pj1-x104a.google.com (mail-pj1-x104a.google.com [IPv6:2607:f8b0:4864:20::104a]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 44E1AC061746 for ; Mon, 22 Nov 2021 14:33:10 -0800 (PST) Received: by mail-pj1-x104a.google.com with SMTP id o4-20020a17090a3d4400b001a66f10df6cso6533992pjf.0 for ; Mon, 22 Nov 2021 14:33:10 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20210112; h=date:in-reply-to:message-id:mime-version:references:subject:from:to :cc; bh=wno8FRWOoVSdU20IAC+7f3H30uVr5WbyvVEdvgNyFLY=; b=E9sxUfeQO1p69F3fjGi3cTLSIKy+gfp2V8sl24eZ4OAdxuUjpy/yxlRsAwgJpsz4wb J8J4O4g/D4u7qwDzs26PmgUrVmSgSoEa3R8yTZCNYB58DXq35syXQj5MxTxPBtn5YxSH WKKnzW6bAbaMCSiEjKSFn4fOvWDQhcwFBPaYrLilVA5hJvcgIxE5ZOT1AZYL2d+0hRzF lkyMpArOEHOxUF5G2Wp3L6Xan/+1LqblAYqZh5HLQxxhiE7KZPf1DFBoscg+0+b8cllQ fsU71xMyBchCD5o5D9gAfUJHC60OIdWznkmaK7D/Jfs/UPMTASuxijLjNciVK6WsYKbe FK7A== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:date:in-reply-to:message-id:mime-version :references:subject:from:to:cc; bh=wno8FRWOoVSdU20IAC+7f3H30uVr5WbyvVEdvgNyFLY=; b=XSiFBOGEabv5+J0tyUSppcWdYzVXARPc+a0fC25K5ON/inzEVbmr0txWvGEooO/B66 ZGLswvcPrOvuWUiDSwlTaZU6WCO89Oc85QyiNjKXJeX87j4+gq3zNU8TvOHzgbt7aV3c vN/aAfgluTd+BOWZauuQxnhSeGmzd1tlZWqCLxvlWfXq+vybfPGeVuURZiYuJBBTtWMY dEe1wlpTwLbYZMYsrDfc/QUPyc6qYhO1DQlOK4YJZJDsBMDR57ie67Xs67dg2KwT9xyM XCh7bqqrNfhd/tJJ8EzyoCQ6AuTvXuG+wwLs8femUnIKwQfkrMzae0Jx2C95hP1EO/jn hC8w== X-Gm-Message-State: AOAM530LIM01JeeG5GKSQrXUp0Fh/O+zPlX37zp2+ZFo9n6YEEOJxAjp uFOXLlD4K4ZIOCd4/O9ECqCq+7HkObeUX35zbeE0xX2mz6++Od979NClm86Jfqq08dklBdwYlO9 sN2vmIKnkBxtY/2OOPTpWxiF+KHiUGv0FwR9ciBe9zrs918K/uO2d68DxGEC8ZNw= X-Google-Smtp-Source: ABdhPJxlniWIn1ykH1CAwk1TSRTOc+T2Hp7c5TW8M0bXQ37wQn4fs2HB2+PKc7DOV8WK7qbOyiZup3J1qwJOSQ== X-Received: from chooglen.c.googlers.com ([fda3:e722:ac3:cc00:24:72f4:c0a8:26d9]) (user=chooglen job=sendgmr) by 2002:a17:90a:bb84:: with SMTP id v4mr36716760pjr.4.1637620389464; Mon, 22 Nov 2021 14:33:09 -0800 (PST) Date: Mon, 22 Nov 2021 14:32:50 -0800 In-Reply-To: <20211122223252.19922-1-chooglen@google.com> Message-Id: <20211122223252.19922-3-chooglen@google.com> Mime-Version: 1.0 References: <20211122223252.19922-1-chooglen@google.com> X-Mailer: git-send-email 2.34.0.rc2.393.gf8c9666880-goog Subject: [PATCH 2/4] branch: refactor out branch validation from create_branch() From: Glen Choo To: git@vger.kernel.org Cc: Jonathan Tan , Josh Steadmon , Emily Shaffer , Glen Choo Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org In a subsequent commit, we would like to be able to validate whether or not a branch name is valid before we create it (--dry-run). This is useful for `git branch --recurse-submodules topic` because it allows Git to determine if the branch 'topic' can be created in all submodules without creating the branch 'topic'. A good starting point would be to refactor out the start point validation and dwim logic in create_branch() in a validate_branch_start() helper function. Once we do so, it becomes clear that create_branch() is more complex than it needs to be - create_branch() is also used to set tracking information when performing `git branch --set-upstream-to`. This made more sense when (the now unsupported) --set-upstream was first introduced in 4fc5006676 (Add branch --set-upstream, 2010-01-18), because it would sometimes create a branch and sometimes update tracking information without creating a branch. Refactor out the branch validation and dwim logic from create_branch() into validate_branch_start(), make it so that create_branch() always tries to create a branch, and replace the now-incorrect create_branch() call with setup_tracking(). Since there were none, add tests for creating a branch with `--force`. Signed-off-by: Glen Choo --- In this refactor, I preserved the existing behavior by making setup_tracking() call validate_branch_start(). setup_tracking() needs the dwim behavior e.g. to expand 'origin/main' into 'refs/remotes/origin/main' but I'm doubtful that it needs the exact same set of validation behavior as creating a new branch e.g. validating that the object_id is a commit. branch.c | 177 ++++++++++++++++++++++++---------------------- branch.h | 13 +++- builtin/branch.c | 7 +- t/t3200-branch.sh | 17 +++++ 4 files changed, 121 insertions(+), 93 deletions(-) diff --git a/branch.c b/branch.c index 07a46430b3..f8b755513f 100644 --- a/branch.c +++ b/branch.c @@ -126,43 +126,6 @@ int install_branch_config(int flag, const char *local, const char *origin, const return -1; } -/* - * This is called when new_ref is branched off of orig_ref, and tries - * to infer the settings for branch..{remote,merge} from the - * config. - */ -static void setup_tracking(const char *new_ref, const char *orig_ref, - enum branch_track track, int quiet) -{ - struct tracking tracking; - int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE; - - memset(&tracking, 0, sizeof(tracking)); - tracking.spec.dst = (char *)orig_ref; - if (for_each_remote(find_tracked_branch, &tracking)) - return; - - if (!tracking.matches) - switch (track) { - case BRANCH_TRACK_ALWAYS: - case BRANCH_TRACK_EXPLICIT: - case BRANCH_TRACK_OVERRIDE: - break; - default: - return; - } - - if (tracking.matches > 1) - die(_("Not tracking: ambiguous information for ref %s"), - orig_ref); - - if (install_branch_config(config_flags, new_ref, tracking.remote, - tracking.src ? tracking.src : orig_ref) < 0) - exit(-1); - - free(tracking.src); -} - int read_branch_desc(struct strbuf *buf, const char *branch_name) { char *v = NULL; @@ -243,33 +206,17 @@ N_("\n" "will track its remote counterpart, you may want to use\n" "\"git push -u\" to set the upstream config as you push."); -void create_branch(struct repository *r, - const char *name, const char *start_name, - int force, int clobber_head_ok, int reflog, - int quiet, enum branch_track track) +static void validate_branch_start(struct repository *r, const char *start_name, + enum branch_track track, + struct object_id *oid, char **full_ref) { struct commit *commit; - struct object_id oid; - char *real_ref; - struct strbuf ref = STRBUF_INIT; - int forcing = 0; - int dont_change_ref = 0; int explicit_tracking = 0; if (track == BRANCH_TRACK_EXPLICIT || track == BRANCH_TRACK_OVERRIDE) explicit_tracking = 1; - if ((track == BRANCH_TRACK_OVERRIDE || clobber_head_ok) - ? validate_branchname(name, &ref) - : validate_new_branchname(name, &ref, force)) { - if (!force) - dont_change_ref = 1; - else - forcing = 1; - } - - real_ref = NULL; - if (get_oid_mb(start_name, &oid)) { + if (repo_get_oid_mb(r, start_name, oid)) { if (explicit_tracking) { if (advice_enabled(ADVICE_SET_UPSTREAM_FAILURE)) { error(_(upstream_missing), start_name); @@ -281,7 +228,8 @@ void create_branch(struct repository *r, die(_("Not a valid object name: '%s'."), start_name); } - switch (dwim_ref(start_name, strlen(start_name), &oid, &real_ref, 0)) { + switch (repo_dwim_ref(r, start_name, strlen(start_name), oid, full_ref, + 0)) { case 0: /* Not branching from any existing branch */ if (explicit_tracking) @@ -289,12 +237,12 @@ void create_branch(struct repository *r, break; case 1: /* Unique completion -- good, only if it is a real branch */ - if (!starts_with(real_ref, "refs/heads/") && - validate_remote_tracking_branch(real_ref)) { + if (!starts_with(*full_ref, "refs/heads/") && + validate_remote_tracking_branch(*full_ref)) { if (explicit_tracking) die(_(upstream_not_branch), start_name); else - FREE_AND_NULL(real_ref); + FREE_AND_NULL(*full_ref); } break; default: @@ -302,37 +250,96 @@ void create_branch(struct repository *r, break; } - if ((commit = lookup_commit_reference(r, &oid)) == NULL) + if ((commit = lookup_commit_reference(r, oid)) == NULL) die(_("Not a valid branch point: '%s'."), start_name); - oidcpy(&oid, &commit->object.oid); + oidcpy(oid, &commit->object.oid); +} + +void setup_tracking(const char *new_ref, const char *orig_ref, + enum branch_track track, int quiet, int expand_orig) +{ + struct tracking tracking; + int config_flags = quiet ? 0 : BRANCH_CONFIG_VERBOSE; + char *full_orig_ref; + struct object_id unused_oid; + + memset(&tracking, 0, sizeof(tracking)); + if (expand_orig) + validate_branch_start(the_repository, orig_ref, track, &unused_oid, &full_orig_ref); + else + full_orig_ref = xstrdup(orig_ref); + + tracking.spec.dst = full_orig_ref; + if (for_each_remote(find_tracked_branch, &tracking)) + goto cleanup; + + if (!tracking.matches) + switch (track) { + case BRANCH_TRACK_ALWAYS: + case BRANCH_TRACK_EXPLICIT: + case BRANCH_TRACK_OVERRIDE: + break; + default: + goto cleanup; + } + + if (tracking.matches > 1) + die(_("Not tracking: ambiguous information for ref %s"), + full_orig_ref); + + if (install_branch_config(config_flags, new_ref, tracking.remote, + tracking.src ? tracking.src : full_orig_ref) < 0) + exit(-1); + +cleanup: + free(tracking.src); + free(full_orig_ref); +} + +void create_branch(struct repository *r, const char *name, + const char *start_name, int force, int clobber_head_ok, + int reflog, int quiet, enum branch_track track) +{ + struct object_id oid; + char *real_ref; + struct strbuf ref = STRBUF_INIT; + int forcing = 0; + struct ref_transaction *transaction; + struct strbuf err = STRBUF_INIT; + char *msg; + + if (clobber_head_ok && !force) + BUG("'clobber_head_ok' can only be used with 'force'"); + + if (clobber_head_ok ? + validate_branchname(name, &ref) : + validate_new_branchname(name, &ref, force)) { + forcing = 1; + } + + validate_branch_start(r, start_name, track, &oid, &real_ref); if (reflog) log_all_ref_updates = LOG_REFS_NORMAL; - if (!dont_change_ref) { - struct ref_transaction *transaction; - struct strbuf err = STRBUF_INIT; - char *msg; - - if (forcing) - msg = xstrfmt("branch: Reset to %s", start_name); - else - msg = xstrfmt("branch: Created from %s", start_name); - - transaction = ref_transaction_begin(&err); - if (!transaction || - ref_transaction_update(transaction, ref.buf, - &oid, forcing ? NULL : null_oid(), - 0, msg, &err) || - ref_transaction_commit(transaction, &err)) - die("%s", err.buf); - ref_transaction_free(transaction); - strbuf_release(&err); - free(msg); - } + if (forcing) + msg = xstrfmt("branch: Reset to %s", start_name); + else + msg = xstrfmt("branch: Created from %s", start_name); + + transaction = ref_transaction_begin(&err); + if (!transaction || + ref_transaction_update(transaction, ref.buf, + &oid, forcing ? NULL : null_oid(), + 0, msg, &err) || + ref_transaction_commit(transaction, &err)) + die("%s", err.buf); + ref_transaction_free(transaction); + strbuf_release(&err); + free(msg); if (real_ref && track) - setup_tracking(ref.buf + 11, real_ref, track, quiet); + setup_tracking(ref.buf + 11, real_ref, track, quiet, 0); strbuf_release(&ref); free(real_ref); diff --git a/branch.h b/branch.h index df0be61506..75cefcdcbd 100644 --- a/branch.h +++ b/branch.h @@ -17,6 +17,15 @@ extern enum branch_track git_branch_track; /* Functions for acting on the information about branches. */ +/* + * This sets the branch..{remote,merge} config settings so that + * branch 'new_ref' tracks 'orig_ref'. This is called when branches are + * created, or when branch configs are updated (e.g. with + * git branch --set-upstream-to). + */ +void setup_tracking(const char *new_ref, const char *orig_ref, + enum branch_track track, int quiet, int expand_orig); + /* * Creates a new branch, where: * @@ -29,8 +38,8 @@ extern enum branch_track git_branch_track; * * - force enables overwriting an existing (non-head) branch * - * - clobber_head_ok allows the currently checked out (hence existing) - * branch to be overwritten; without 'force', it has no effect. + * - clobber_head_ok, when enabled with 'force', allows the currently + * checked out (head) branch to be overwritten * * - reflog creates a reflog for the branch * diff --git a/builtin/branch.c b/builtin/branch.c index 0b7ed82654..eb5c117a6e 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -820,12 +820,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix) if (!ref_exists(branch->refname)) die(_("branch '%s' does not exist"), branch->name); - /* - * create_branch takes care of setting up the tracking - * info and making sure new_upstream is correct - */ - create_branch(the_repository, branch->name, new_upstream, - 0, 0, 0, quiet, BRANCH_TRACK_OVERRIDE); + setup_tracking(branch->name, new_upstream, BRANCH_TRACK_OVERRIDE, quiet, 1); } else if (unset_upstream) { struct branch *branch = branch_get(argv[0]); struct strbuf buf = STRBUF_INIT; diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index e575ffb4ff..6bf95a1707 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -42,6 +42,23 @@ test_expect_success 'git branch abc should create a branch' ' git branch abc && test_path_is_file .git/refs/heads/abc ' +test_expect_success 'git branch abc should fail when abc exists' ' + test_must_fail git branch abc +' + +test_expect_success 'git branch --force abc should fail when abc is checked out' ' + test_when_finished git switch main && + git switch abc && + test_must_fail git branch --force abc HEAD~1 +' + +test_expect_success 'git branch --force abc should succeed when abc exists' ' + git rev-parse HEAD~1 >expect && + git branch --force abc HEAD~1 && + git rev-parse abc >actual && + test_cmp expect actual +' + test_expect_success 'git branch a/b/c should create a branch' ' git branch a/b/c && test_path_is_file .git/refs/heads/a/b/c ' From patchwork Mon Nov 22 22:32:51 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Glen Choo X-Patchwork-Id: 12633021 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id 8BBC2C433F5 for ; Mon, 22 Nov 2021 22:33:19 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S235025AbhKVWgZ (ORCPT ); Mon, 22 Nov 2021 17:36:25 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:43778 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S233472AbhKVWgS (ORCPT ); Mon, 22 Nov 2021 17:36:18 -0500 Received: from mail-pl1-x64a.google.com (mail-pl1-x64a.google.com [IPv6:2607:f8b0:4864:20::64a]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id D6771C061748 for ; Mon, 22 Nov 2021 14:33:11 -0800 (PST) Received: by mail-pl1-x64a.google.com with SMTP id h1-20020a170902f54100b00143c6409dbcso525182plf.5 for ; Mon, 22 Nov 2021 14:33:11 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20210112; h=date:in-reply-to:message-id:mime-version:references:subject:from:to :cc; bh=GrmFINVUdKN4GO230pAsYoAhNxEwlrap+M0B4LV6Vrk=; b=tQABEP5COMkfMwvB66fjvfgrvrxkuGaNtFzAxJPo4nmvpizO5bP1+QSViKMY3t/DXz LYznPwnvzhKI43gu3EOaCmNn2zDUq1lMlGT2YCXAXzxyrI0S6t8nDt/t1Gm1nk6ebWwN JjmxIh1ESdC94ZWy6hMaukZwtlhLN1dtMdO3CJOwAkvvLQGOoyUkCkAtr2wmI0rSDXJR BPbF6McKjppOPXcTtmrwG5uqrcao/XBq3coxGD/AUCZ+LSsEpsrBIqELp8WQZC4L7S2V CzJR1c7RFMxowXs6+b9t/EGLN5w+8DHijcPfBmiyZj9yo3qFVIFxUt+8qQpSkkBhpbMi k8Cg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:date:in-reply-to:message-id:mime-version :references:subject:from:to:cc; bh=GrmFINVUdKN4GO230pAsYoAhNxEwlrap+M0B4LV6Vrk=; b=bMZFq6JYgoLH+eKrTYOMfoYVLQ7sUSf5bGUTIujxf3boN56EPGqJs8H5LJgw+So0tm KegNi9oiKYdXfDUGDHptrRihpcrQXzgKJZXXx8QQh1lwkBQ9X2vrGDdyS9vXY45yDIh4 66txrGQ79NeLLHIbVHT6MYnQPoFyYIw9k9faDwpni05wblDK3+XIfQBee6nCvXSAbUqa +DiEIzokYR2bmIeIzuV8Iu4r1/ymtOQL84ND6JPB18Z8SIThx5lcoG6K9fmQabuTp3J3 ulgN0mW1wB/eHfd1wg7sa3w3NYt2fZLLTNfHoCRJ4P0YyatUqzrZJhIF7+91aUqLY4PW hD/g== X-Gm-Message-State: AOAM531y4o0r1jTTwL+kh7Fk/SbgmhoCKRFonae8CPWqyXuYMFcvF5Xz EtZOcylPXZe964vj4lo4qkY3nh+EZBBUy4l61Pb3c+jAoIwaRYtSjXvDi+vMUmgAidFzwCQsG2u P5gjt3TJC1KJJVdljAZsfc83RRKbRYm5PrSphlp5kgk4RIs6klpYjuXSsuB2L+A8= X-Google-Smtp-Source: ABdhPJzlL7qTTywSoLcZBZLjgIdbN/ZntvdQnGnITyipPd3ULEUkz+MLPCUQqLHw/tAYXLiXlOEykWlLKLQLaA== X-Received: from chooglen.c.googlers.com ([fda3:e722:ac3:cc00:24:72f4:c0a8:26d9]) (user=chooglen job=sendgmr) by 2002:a17:90b:314e:: with SMTP id ip14mr448299pjb.130.1637620391306; Mon, 22 Nov 2021 14:33:11 -0800 (PST) Date: Mon, 22 Nov 2021 14:32:51 -0800 In-Reply-To: <20211122223252.19922-1-chooglen@google.com> Message-Id: <20211122223252.19922-4-chooglen@google.com> Mime-Version: 1.0 References: <20211122223252.19922-1-chooglen@google.com> X-Mailer: git-send-email 2.34.0.rc2.393.gf8c9666880-goog Subject: [PATCH 3/4] branch: add --dry-run option to branch From: Glen Choo To: git@vger.kernel.org Cc: Jonathan Tan , Josh Steadmon , Emily Shaffer , Glen Choo Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org When running "git branch --recurse-submodules topic", it would be useful to know whether or not 'topic' is a valid branch for all repositories. Currently there is no way to test this without actually creating the branch. Add a --dry-run option to branch creation that can check whether or not a branch name and start point would be valid for a repository without creating a branch. Refactor cmd_branch() to make the chosen action more obvious. Incidentally, fix an incorrect usage string that combined the 'list' usage of git branch (-l) with the 'create' usage; this string has been incorrect since its inception, a8dfd5eac4 (Make builtin-branch.c use parse_options., 2007-10-07). Signed-off-by: Glen Choo --- The --dry-run option is motivated mainly by --recurse-submodules. To my knowledge, there isn't a strong existing demand, but this might be mildly useful to some users. Documentation/git-branch.txt | 8 ++++++- branch.c | 6 ++--- branch.h | 22 ++++++++++++++++++ builtin/branch.c | 44 ++++++++++++++++++++++++++---------- t/t3200-branch.sh | 13 +++++++++++ 5 files changed, 77 insertions(+), 16 deletions(-) diff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt index 5449767121..8cdc33c097 100644 --- a/Documentation/git-branch.txt +++ b/Documentation/git-branch.txt @@ -16,7 +16,7 @@ SYNOPSIS [--points-at ] [--format=] [(-r | --remotes) | (-a | --all)] [--list] [...] -'git branch' [--track | --no-track] [-f] [] +'git branch' [--track | --no-track] [-f] [--dry-run | -n] [] 'git branch' (--set-upstream-to= | -u ) [] 'git branch' --unset-upstream [] 'git branch' (-m | -M) [] @@ -205,6 +205,12 @@ This option is only applicable in non-verbose mode. --no-abbrev:: Display the full sha1s in the output listing rather than abbreviating them. +-n:: +--dry-run:: + Can only be used when creating a branch. If the branch creation + would fail, show the relevant error message. If the branch + creation would succeed, show nothing. + -t:: --track:: When creating a new branch, set up `branch..remote` and diff --git a/branch.c b/branch.c index f8b755513f..528cb2d639 100644 --- a/branch.c +++ b/branch.c @@ -206,9 +206,9 @@ N_("\n" "will track its remote counterpart, you may want to use\n" "\"git push -u\" to set the upstream config as you push."); -static void validate_branch_start(struct repository *r, const char *start_name, - enum branch_track track, - struct object_id *oid, char **full_ref) +void validate_branch_start(struct repository *r, const char *start_name, + enum branch_track track, struct object_id *oid, + char **full_ref) { struct commit *commit; int explicit_tracking = 0; diff --git a/branch.h b/branch.h index 75cefcdcbd..d8e5ff4e28 100644 --- a/branch.h +++ b/branch.h @@ -3,6 +3,7 @@ struct repository; struct strbuf; +struct object_id; enum branch_track { BRANCH_TRACK_UNSPECIFIED = -1, @@ -17,6 +18,27 @@ extern enum branch_track git_branch_track; /* Functions for acting on the information about branches. */ +/* + * Validates whether a ref is a valid starting point for a branch, where: + * + * - r is the repository to validate the branch for + * + * - start_name is the ref that we would like to test + * + * - track is the tracking mode of the new branch. If tracking is + * explicitly requested, start_name must be a branch (because + * otherwise start_name cannot be tracked) + * + * - oid is an out parameter containing the object_id of start_name + * + * - full_ref is an out parameter containing the 'full' form of + * start_name e.g. refs/heads/main instead of main + * + */ +void validate_branch_start(struct repository *r, const char *start_name, + enum branch_track track, struct object_id *oid, + char **full_ref); + /* * This sets the branch..{remote,merge} config settings so that * branch 'new_ref' tracks 'orig_ref'. This is called when branches are diff --git a/builtin/branch.c b/builtin/branch.c index eb5c117a6e..5d4b9c82b4 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -27,7 +27,8 @@ static const char * const builtin_branch_usage[] = { N_("git branch [] [-r | -a] [--merged] [--no-merged]"), - N_("git branch [] [-l] [-f] []"), + N_("git branch [] [-l] [...]"), + N_("git branch [] [-f] [--dry-run | -n] []"), N_("git branch [] [-r] (-d | -D) ..."), N_("git branch [] (-m | -M) [] "), N_("git branch [] (-c | -C) [] "), @@ -616,14 +617,14 @@ 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, copy = 0, force = 0, list = 0; - int show_current = 0; - int reflog = 0, edit_description = 0; - int quiet = 0, unset_upstream = 0; + /* possible actions */ + int delete = 0, rename = 0, copy = 0, force = 0, list = 0, create = 0, + unset_upstream = 0, show_current = 0, edit_description = 0; + /* possible options */ + int reflog = 0, quiet = 0, dry_run = 0, icase = 0; const char *new_upstream = NULL; enum branch_track track; struct ref_filter filter; - int icase = 0; static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting; struct ref_format format = REF_FORMAT_INIT; @@ -670,6 +671,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix) N_("print only branches of the object"), parse_opt_object_name), OPT_BOOL('i', "ignore-case", &icase, N_("sorting and filtering are case insensitive")), OPT_STRING( 0 , "format", &format.format, N_("format"), N_("format to use for the output")), + OPT__DRY_RUN(&dry_run, N_("show whether the branch would be created")), OPT_END(), }; @@ -705,10 +707,15 @@ int cmd_branch(int argc, const char **argv, const char *prefix) filter.reachable_from || filter.unreachable_from || filter.points_at.nr) list = 1; - if (!!delete + !!rename + !!copy + !!new_upstream + !!show_current + - list + edit_description + unset_upstream > 1) + create = 1 - (!!delete + !!rename + !!copy + !!new_upstream + + !!show_current + !!list + !!edit_description + + !!unset_upstream); + if (create < 0) usage_with_options(builtin_branch_usage, options); + if (dry_run && !create) + die(_("--dry-run can only be used when creating branches")); + if (filter.abbrev == -1) filter.abbrev = DEFAULT_ABBREV; filter.ignore_case = icase; @@ -844,7 +851,10 @@ int cmd_branch(int argc, const char **argv, const char *prefix) strbuf_addf(&buf, "branch.%s.merge", branch->name); git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE); strbuf_release(&buf); - } else if (argc > 0 && argc <= 2) { + } else if (create && argc > 0 && argc <= 2) { + const char *branch_name = argv[0]; + const char *start_name = (argc == 2) ? argv[1] : head; + if (filter.kind != FILTER_REFS_BRANCHES) die(_("The -a, and -r, options to 'git branch' do not take a branch name.\n" "Did you mean to use: -a|-r --list ?")); @@ -852,10 +862,20 @@ int cmd_branch(int argc, const char **argv, const char *prefix) if (track == BRANCH_TRACK_OVERRIDE) die(_("the '--set-upstream' option is no longer supported. Please use '--track' or '--set-upstream-to' instead.")); - create_branch(the_repository, - argv[0], (argc == 2) ? argv[1] : head, - force, 0, reflog, quiet, track); + if (dry_run) { + struct strbuf buf = STRBUF_INIT; + char *unused_full_ref; + struct object_id unused_oid; + validate_new_branchname(branch_name, &buf, force); + validate_branch_start(the_repository, start_name, track, + &unused_oid, &unused_full_ref); + strbuf_release(&buf); + FREE_AND_NULL(unused_full_ref); + return 0; + } + create_branch(the_repository, branch_name, start_name, force, 0, + reflog, quiet, track); } else usage_with_options(builtin_branch_usage, options); diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index 6bf95a1707..653891736a 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -59,6 +59,19 @@ test_expect_success 'git branch --force abc should succeed when abc exists' ' test_cmp expect actual ' +test_expect_success 'git branch --dry-run abc should fail when abc exists' ' + test_must_fail git branch --dry-run abc +' + +test_expect_success 'git branch --dry-run --force abc should succeed when abc exists' ' + git branch --dry-run --force abc +' + +test_expect_success 'git branch --dry-run def should not create a branch' ' + git branch --dry-run def && + test_must_fail git rev-parse def +' + test_expect_success 'git branch a/b/c should create a branch' ' git branch a/b/c && test_path_is_file .git/refs/heads/a/b/c ' From patchwork Mon Nov 22 22:32:52 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Glen Choo X-Patchwork-Id: 12633023 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by smtp.lore.kernel.org (Postfix) with ESMTP id F2191C433FE for ; Mon, 22 Nov 2021 22:33:20 +0000 (UTC) Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S233639AbhKVWg1 (ORCPT ); Mon, 22 Nov 2021 17:36:27 -0500 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:43796 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S233768AbhKVWgW (ORCPT ); Mon, 22 Nov 2021 17:36:22 -0500 Received: from mail-pl1-x64a.google.com (mail-pl1-x64a.google.com [IPv6:2607:f8b0:4864:20::64a]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 9E794C061756 for ; Mon, 22 Nov 2021 14:33:13 -0800 (PST) Received: by mail-pl1-x64a.google.com with SMTP id p24-20020a170902a41800b001438d6c7d71so8206968plq.7 for ; Mon, 22 Nov 2021 14:33:13 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20210112; h=date:in-reply-to:message-id:mime-version:references:subject:from:to :cc; bh=ykyo9fXTA8SPwq7tZ4PwRLchOOHBpHekhTtdKcDZNaI=; b=M9icJTJRaS3kvuqsd11QXwr68n8IGi4q+WoER5L91NNpN6E/T5ueCxHhoS1j32+AyX cjJtRPTEad6E8jiQmYmoxH6Bd1W/YoEls4pUoBdSLr9JceN04/1pKluS+oJ9n516BksW oNyjXpnKsyfDV7Utz3HMsiDgHaInHI5XQxk8LYsIussnsCQO/5chfbsO5S2yr2xnJRl3 FeugDuRm9rHDKoXA2hAYaF2Ik5uyyCB3ew8kCzLWR1YvYy2JkzIS2e8XC6O1nGh9CoTs 7SraHh8zDSERqG7Iqwm0okbiWu+J/MpfM3Plp3cwWrkmh6A2mi6AHES9z+SQf45Ytnxm zx6w== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; h=x-gm-message-state:date:in-reply-to:message-id:mime-version :references:subject:from:to:cc; bh=ykyo9fXTA8SPwq7tZ4PwRLchOOHBpHekhTtdKcDZNaI=; b=hV6/CUOWLwVSUnCmlku0Y2U5exb1vmS+ksSGkkuFfL6zkj6fQEpF7+TBLl9qpkru6S EBA99o5WyozdphWU2SkinXlDx/KfTsG2SHPv4250MstnvuIbuf1lNj+Gi9fBvr5AJK67 w9X/GCBhTD8D+U1mrpWGBWF4dtVI+adKH3/K3+XQrrQx6qQZYaJOCxJIZU00AKx9JGTM /0eZOT35Adl/lo+N5KIRA6Mvf0gvqLmk2Du0ZhopYCtbAV27aHG7LRbHQ2obwwDjqjx2 rEd6z2MpCdKxWlspi7TcGiQvpDV182HPz0Uzdio7iDNed/vLoMRylPKXrCTZDGb+ITHp VEQw== X-Gm-Message-State: AOAM533bM4L9JuvVT1plj0CuOpvbMlRZE+6bHyjsld/Kq2q0EE/1zesi FyfQaBEs+1S/atc+AVuAz3+MAEU7b40oT+ngqx7ZVR9ZyI75+JzPEIJHyY/ZgNygfeQiYkevxV5 B57xDSC3/rKoDg3vhR/DANpZfBnaJ6Rjvq2XZ5s7rVJtLazXEFtuv7aoYrGG+Xgg= X-Google-Smtp-Source: ABdhPJxP2SqJBP5tLzfFigQIgYPCvB6mxfQaTfwZ16gvrGG7l3yUa73rjP1Hil5WikDuS1TNqOF3vO570rW9uA== X-Received: from chooglen.c.googlers.com ([fda3:e722:ac3:cc00:24:72f4:c0a8:26d9]) (user=chooglen job=sendgmr) by 2002:aa7:84d6:0:b0:49f:a996:b714 with SMTP id x22-20020aa784d6000000b0049fa996b714mr433222pfn.10.1637620392905; Mon, 22 Nov 2021 14:33:12 -0800 (PST) Date: Mon, 22 Nov 2021 14:32:52 -0800 In-Reply-To: <20211122223252.19922-1-chooglen@google.com> Message-Id: <20211122223252.19922-5-chooglen@google.com> Mime-Version: 1.0 References: <20211122223252.19922-1-chooglen@google.com> X-Mailer: git-send-email 2.34.0.rc2.393.gf8c9666880-goog Subject: [PATCH 4/4] branch: add --recurse-submodules option for branch creation From: Glen Choo To: git@vger.kernel.org Cc: Jonathan Tan , Josh Steadmon , Emily Shaffer , Glen Choo Precedence: bulk List-ID: X-Mailing-List: git@vger.kernel.org Add a --recurse-submodules option when creating branches so that `git branch --recurse-submodules topic` will create the "topic" branch in the superproject and all submodules. Guard this (and future submodule branching) behavior behind a new configuration value 'submodule.propagateBranches'. Signed-off-by: Glen Choo --- Documentation/config/advice.txt | 3 + Documentation/config/submodule.txt | 9 ++ advice.c | 1 + advice.h | 1 + branch.c | 123 ++++++++++++++ branch.h | 6 + builtin/branch.c | 28 +++- builtin/submodule--helper.c | 33 ++++ t/t3207-branch-submodule.sh | 249 +++++++++++++++++++++++++++++ 9 files changed, 452 insertions(+), 1 deletion(-) create mode 100755 t/t3207-branch-submodule.sh diff --git a/Documentation/config/advice.txt b/Documentation/config/advice.txt index 063eec2511..e52262dc69 100644 --- a/Documentation/config/advice.txt +++ b/Documentation/config/advice.txt @@ -116,6 +116,9 @@ advice.*:: submoduleAlternateErrorStrategyDie:: Advice shown when a submodule.alternateErrorStrategy option configured to "die" causes a fatal error. + submodulesNotUpdated:: + Advice shown when a user runs a submodule command that fails + because `git submodule update` was not run. addIgnoredFile:: Advice shown if a user attempts to add an ignored file to the index. diff --git a/Documentation/config/submodule.txt b/Documentation/config/submodule.txt index ee454f8126..c318b849aa 100644 --- a/Documentation/config/submodule.txt +++ b/Documentation/config/submodule.txt @@ -72,6 +72,15 @@ submodule.recurse:: For these commands a workaround is to temporarily change the configuration value by using `git -c submodule.recurse=0`. +submodule.propagateBranches:: + [EXPERIMENTAL] A boolean that enables branching support with + submodules. This allows certain commands to accept + `--recurse-submodules` (`git branch --recurse-submodules` will + create branches recursively), and certain commands that already + accept `--recurse-submodules` will now consider branches (`git + switch --recurse-submodules` will switch to the correct branch + in all submodules). + submodule.fetchJobs:: Specifies how many submodules are fetched/cloned at the same time. A positive integer allows up to that number of submodules fetched diff --git a/advice.c b/advice.c index 1dfc91d176..e00d30254c 100644 --- a/advice.c +++ b/advice.c @@ -70,6 +70,7 @@ static struct { [ADVICE_STATUS_HINTS] = { "statusHints", 1 }, [ADVICE_STATUS_U_OPTION] = { "statusUoption", 1 }, [ADVICE_SUBMODULE_ALTERNATE_ERROR_STRATEGY_DIE] = { "submoduleAlternateErrorStrategyDie", 1 }, + [ADVICE_SUBMODULES_NOT_UPDATED] = { "submodulesNotUpdated", 1 }, [ADVICE_UPDATE_SPARSE_PATH] = { "updateSparsePath", 1 }, [ADVICE_WAITING_FOR_EDITOR] = { "waitingForEditor", 1 }, }; diff --git a/advice.h b/advice.h index 601265fd10..a7521d6087 100644 --- a/advice.h +++ b/advice.h @@ -44,6 +44,7 @@ struct string_list; ADVICE_STATUS_HINTS, ADVICE_STATUS_U_OPTION, ADVICE_SUBMODULE_ALTERNATE_ERROR_STRATEGY_DIE, + ADVICE_SUBMODULES_NOT_UPDATED, ADVICE_UPDATE_SPARSE_PATH, ADVICE_WAITING_FOR_EDITOR, ADVICE_SKIPPED_CHERRY_PICKS, diff --git a/branch.c b/branch.c index 528cb2d639..404766d01d 100644 --- a/branch.c +++ b/branch.c @@ -8,6 +8,8 @@ #include "sequencer.h" #include "commit.h" #include "worktree.h" +#include "submodule-config.h" +#include "run-command.h" struct tracking { struct refspec_item spec; @@ -345,6 +347,127 @@ void create_branch(struct repository *r, const char *name, free(real_ref); } +static int submodule_validate_branchname(struct repository *r, const char *name, + const char *start_name, int force, + int quiet, char **err_msg) +{ + int ret = 0; + struct child_process child = CHILD_PROCESS_INIT; + struct strbuf child_err = STRBUF_INIT; + child.git_cmd = 1; + child.err = -1; + + prepare_other_repo_env(&child.env_array, r->gitdir); + strvec_pushl(&child.args, "branch", "--dry-run", NULL); + if (force) + strvec_push(&child.args, "--force"); + if (quiet) + strvec_push(&child.args, "--quiet"); + strvec_pushl(&child.args, name, start_name, NULL); + + if ((ret = start_command(&child))) + return ret; + ret = finish_command(&child); + strbuf_read(&child_err, child.err, 0); + *err_msg = strbuf_detach(&child_err, NULL); + return ret; +} + +static int submodule_create_branch(struct repository *r, const char *name, + const char *start_oid, + const char *start_name, int force, + int reflog, int quiet, + enum branch_track track, char **err_msg) +{ + int ret = 0; + struct child_process child = CHILD_PROCESS_INIT; + struct strbuf child_err = STRBUF_INIT; + child.git_cmd = 1; + child.err = -1; + + prepare_other_repo_env(&child.env_array, r->gitdir); + strvec_pushl(&child.args, "submodule--helper", "create-branch", NULL); + if (force) + strvec_push(&child.args, "--force"); + if (quiet) + strvec_push(&child.args, "--quiet"); + if (reflog) + strvec_push(&child.args, "--create-reflog"); + if (track == BRANCH_TRACK_ALWAYS || track == BRANCH_TRACK_EXPLICIT) + strvec_push(&child.args, "--track"); + + strvec_pushl(&child.args, name, start_oid, start_name, NULL); + + if ((ret = start_command(&child))) + return ret; + ret = finish_command(&child); + strbuf_read(&child_err, child.err, 0); + *err_msg = strbuf_detach(&child_err, NULL); + return ret; +} + +void create_submodule_branches(struct repository *r, const char *name, + const char *start_name, int force, int reflog, + int quiet, enum branch_track track) +{ + int i = 0; + char *branch_point = NULL; + struct repository *subrepos; + struct submodule *submodules; + struct object_id super_oid; + struct submodule_entry_list *submodule_entry_list; + char *err_msg = NULL; + + validate_branch_start(r, start_name, track, &super_oid, &branch_point); + + submodule_entry_list = submodules_of_tree(r, &super_oid); + CALLOC_ARRAY(subrepos, submodule_entry_list->entry_nr); + CALLOC_ARRAY(submodules, submodule_entry_list->entry_nr); + + for (i = 0; i < submodule_entry_list->entry_nr; i++) { + submodules[i] = *submodule_from_path( + r, &super_oid, + submodule_entry_list->name_entries[i].path); + + if (repo_submodule_init( + &subrepos[i], r, + submodule_entry_list->name_entries[i].path, + &super_oid)) { + die(_("submodule %s: unable to find submodule"), + submodules[i].name); + if (advice_enabled(ADVICE_SUBMODULES_NOT_UPDATED)) + advise(_("You may try initializing the submodules using 'git checkout %s && git submodule update'"), + start_name); + } + + if (submodule_validate_branchname( + &subrepos[i], name, + oid_to_hex( + &submodule_entry_list->name_entries[i].oid), + force, quiet, &err_msg)) + die(_("submodule %s: could not create branch '%s'\n\t%s"), + submodules[i].name, name, err_msg); + } + + create_branch(the_repository, name, start_name, force, 0, reflog, quiet, + track); + + for (i = 0; i < submodule_entry_list->entry_nr; i++) { + printf_ln(_("submodule %s: creating branch '%s'"), + submodules[i].name, name); + if (submodule_create_branch( + &subrepos[i], name, + oid_to_hex( + &submodule_entry_list->name_entries[i].oid), + branch_point, force, reflog, quiet, track, + &err_msg)) + die(_("submodule %s: could not create branch '%s'\n\t%s"), + submodules[i].name, name, err_msg); + + repo_clear(&subrepos[i]); + } +} + void remove_merge_branch_state(struct repository *r) { unlink(git_path_merge_head(r)); diff --git a/branch.h b/branch.h index d8e5ff4e28..1b4a635a2f 100644 --- a/branch.h +++ b/branch.h @@ -76,6 +76,12 @@ void create_branch(struct repository *r, int force, int clobber_head_ok, int reflog, int quiet, enum branch_track track); +/* + * Creates a new branch in repository and its submodules. + */ +void create_submodule_branches(struct repository *r, const char *name, + const char *start_name, int force, int reflog, + int quiet, enum branch_track track); /* * Check if 'name' can be a valid name for a branch; die otherwise. * Return 1 if the named branch already exists; return 0 otherwise. diff --git a/builtin/branch.c b/builtin/branch.c index 5d4b9c82b4..6a16bdb1a3 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -39,6 +39,8 @@ static const char * const builtin_branch_usage[] = { static const char *head; static struct object_id head_oid; +static int recurse_submodules = 0; +static int submodule_propagate_branches = 0; static int branch_use_color = -1; static char branch_colors[][COLOR_MAXLEN] = { @@ -101,6 +103,15 @@ static int git_branch_config(const char *var, const char *value, void *cb) return config_error_nonbool(var); return color_parse(value, branch_colors[slot]); } + if (!strcmp(var, "submodule.recurse")) { + recurse_submodules = git_config_bool(var, value); + return 0; + } + if (!strcasecmp(var, "submodule.propagateBranches")) { + submodule_propagate_branches = git_config_bool(var, value); + return 0; + } + return git_color_default_config(var, value, cb); } @@ -621,7 +632,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix) int delete = 0, rename = 0, copy = 0, force = 0, list = 0, create = 0, unset_upstream = 0, show_current = 0, edit_description = 0; /* possible options */ - int reflog = 0, quiet = 0, dry_run = 0, icase = 0; + int reflog = 0, quiet = 0, dry_run = 0, icase = 0, + recurse_submodules_explicit = 0; const char *new_upstream = NULL; enum branch_track track; struct ref_filter filter; @@ -670,6 +682,7 @@ int cmd_branch(int argc, const char **argv, const char *prefix) OPT_CALLBACK(0, "points-at", &filter.points_at, N_("object"), N_("print only branches of the object"), parse_opt_object_name), OPT_BOOL('i', "ignore-case", &icase, N_("sorting and filtering are case insensitive")), + OPT_BOOL(0, "recurse-submodules", &recurse_submodules_explicit, N_("recurse through submodules")), OPT_STRING( 0 , "format", &format.format, N_("format"), N_("format to use for the output")), OPT__DRY_RUN(&dry_run, N_("show whether the branch would be created")), OPT_END(), @@ -713,9 +726,16 @@ int cmd_branch(int argc, const char **argv, const char *prefix) if (create < 0) usage_with_options(builtin_branch_usage, options); + if (recurse_submodules_explicit && submodule_propagate_branches && + !create) + die(_("--recurse-submodules can only be used to create branches")); if (dry_run && !create) die(_("--dry-run can only be used when creating branches")); + recurse_submodules = + (recurse_submodules || recurse_submodules_explicit) && + submodule_propagate_branches; + if (filter.abbrev == -1) filter.abbrev = DEFAULT_ABBREV; filter.ignore_case = icase; @@ -874,6 +894,12 @@ int cmd_branch(int argc, const char **argv, const char *prefix) FREE_AND_NULL(unused_full_ref); return 0; } + if (recurse_submodules) { + create_submodule_branches(the_repository, branch_name, + start_name, force, reflog, + quiet, track); + return 0; + } create_branch(the_repository, branch_name, start_name, force, 0, reflog, quiet, track); } else diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index 6298cbdd4e..3ea1e8cc96 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -20,6 +20,7 @@ #include "diff.h" #include "object-store.h" #include "advice.h" +#include "branch.h" #define OPT_QUIET (1 << 0) #define OPT_CACHED (1 << 1) @@ -2983,6 +2984,37 @@ static int module_set_branch(int argc, const char **argv, const char *prefix) return !!ret; } +static int module_create_branch(int argc, const char **argv, const char *prefix) +{ + enum branch_track track; + int quiet = 0, force = 0, reflog = 0; + + struct option options[] = { + OPT__QUIET(&quiet, N_("print only error messages")), + OPT__FORCE(&force, N_("force creation"), 0), + OPT_BOOL(0, "create-reflog", &reflog, + N_("create the branch's reflog")), + OPT_SET_INT('t', "track", &track, + N_("set up tracking mode (see git-pull(1))"), + BRANCH_TRACK_EXPLICIT), + OPT_END() + }; + const char *const usage[] = { + N_("git submodule--helper create-branch [-f|--force] [--create-reflog] [-q|--quiet] [-t|--track] "), + NULL + }; + + argc = parse_options(argc, argv, prefix, options, usage, 0); + + if (argc != 3) + usage_with_options(usage, options); + + create_branch(the_repository, argv[0], argv[1], force, 0, reflog, quiet, + BRANCH_TRACK_NEVER); + setup_tracking(argv[0], argv[2], track, quiet, 0); + + return 0; +} struct add_data { const char *prefix; const char *branch; @@ -3379,6 +3411,7 @@ static struct cmd_struct commands[] = { {"config", module_config, 0}, {"set-url", module_set_url, 0}, {"set-branch", module_set_branch, 0}, + {"create-branch", module_create_branch, 0}, }; int cmd_submodule__helper(int argc, const char **argv, const char *prefix) diff --git a/t/t3207-branch-submodule.sh b/t/t3207-branch-submodule.sh new file mode 100755 index 0000000000..14ff066e91 --- /dev/null +++ b/t/t3207-branch-submodule.sh @@ -0,0 +1,249 @@ +#!/bin/sh + +test_description='git branch submodule tests' + +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +. ./test-lib.sh +. "$TEST_DIRECTORY"/lib-rebase.sh + +test_expect_success 'setup superproject and submodule' ' + git init super && + test_commit foo && + git init sub-upstream && + test_commit -C sub-upstream foo && + git -C super submodule add ../sub-upstream sub && + git -C super commit -m "add submodule" && + git -C super config submodule.propagateBranches true +' + +cleanup_branches() { + super_dir="$1" + shift + ( + cd "$super_dir" && + git checkout main && + for branch_name in "$@"; do + git branch -D "$branch_name" + git submodule foreach "(git checkout main && git branch -D $branch_name) || true" + done + ) +} >/dev/null 2>/dev/null + +# Test the argument parsing +test_expect_success '--recurse-submodules should create branches' ' + test_when_finished "cleanup_branches super branch-a" && + ( + cd super && + git branch --recurse-submodules branch-a && + git rev-parse --abbrev-ref branch-a && + git -C sub rev-parse --abbrev-ref branch-a + ) +' + +test_expect_success '--recurse-submodules should be ignored if submodule.propagateBranches is false' ' + test_when_finished "cleanup_branches super branch-a" && + ( + cd super && + git -c submodule.propagateBranches=false branch --recurse-submodules branch-a && + git rev-parse branch-a && + test_must_fail git -C sub rev-parse branch-a + ) +' + +test_expect_success '--recurse-submodules should fail when not creating branches' ' + test_when_finished "cleanup_branches super branch-a" && + ( + cd super && + git branch --recurse-submodules branch-a && + test_must_fail git branch --recurse-submodules -D branch-a && + # Assert that the branches were not deleted + git rev-parse --abbrev-ref branch-a && + git -C sub rev-parse --abbrev-ref branch-a + ) +' + +test_expect_success 'should respect submodule.recurse when creating branches' ' + test_when_finished "cleanup_branches super branch-a" && + ( + cd super && + git -c submodule.recurse=true branch branch-a && + git rev-parse --abbrev-ref branch-a && + git -C sub rev-parse --abbrev-ref branch-a + ) +' + +test_expect_success 'should ignore submodule.recurse when not creating branches' ' + test_when_finished "cleanup_branches super branch-a" && + ( + cd super && + git branch --recurse-submodules branch-a && + git -c submodule.recurse=true branch -D branch-a && + test_must_fail git rev-parse --abbrev-ref branch-a && + git -C sub rev-parse --abbrev-ref branch-a + ) +' + +# Test branch creation behavior +test_expect_success 'should create branches based off commit id in superproject' ' + test_when_finished "cleanup_branches super branch-a branch-b" && + ( + cd super && + git branch --recurse-submodules branch-a && + git checkout --recurse-submodules branch-a && + git -C sub rev-parse HEAD >expected && + # Move the tip of sub:branch-a so that it no longer matches the commit in super:branch-a + git -C sub checkout branch-a && + test_commit -C sub bar && + # Create a new branch-b branch with start-point=branch-a + git branch --recurse-submodules branch-b branch-a && + git rev-parse branch-b && + git -C sub rev-parse branch-b >actual && + # Assert that the commit id of sub:second-branch matches super:branch-a and not sub:branch-a + test_cmp expected actual + ) +' + +test_expect_success 'should not create any branches if branch is not valid for all repos' ' + test_when_finished "cleanup_branches super branch-a" && + ( + cd super && + git -C sub branch branch-a && + test_must_fail git branch --recurse-submodules branch-a 2>actual && + test_must_fail git rev-parse branch-a && + + cat >expected <expected && + test_commit -C sub baz && + git -C sub branch branch-a HEAD~1 && + git branch --recurse-submodules --force branch-a && + git rev-parse branch-a && + # assert that sub:branch-a was moved + git -C sub rev-parse branch-a >actual && + test_cmp expected actual + ) +' + +test_expect_success 'should create branch when submodule is in .git/modules but not .gitmodules' ' + test_when_finished "cleanup_branches super branch-a branch-b branch-c" && + ( + cd super && + git branch branch-a && + git checkout -b branch-b && + git submodule add ../sub-upstream sub2 && + # branch-b now has a committed submodule not in branch-a + git commit -m "add second submodule" && + git checkout branch-a && + git branch --recurse-submodules branch-c branch-b && + git rev-parse branch-c && + git -C sub rev-parse branch-c && + git checkout --recurse-submodules branch-c && + git -C sub2 rev-parse branch-c + ) +' + +test_expect_success 'should set up tracking of local branches with track=always' ' + test_when_finished "cleanup_branches super branch-a" && + ( + cd super && + git -c branch.autoSetupMerge=always branch --recurse-submodules branch-a main && + git -C sub rev-parse main && + test "$(git -C sub config branch.branch-a.remote)" = . && + test "$(git -C sub config branch.branch-a.merge)" = refs/heads/main + ) +' + +test_expect_success 'should set up tracking of local branches with explicit track' ' + test_when_finished "cleanup_branches super branch-a" && + ( + cd super && + git branch --track --recurse-submodules branch-a main && + git -C sub rev-parse main && + test "$(git -C sub config branch.branch-a.remote)" = . && + test "$(git -C sub config branch.branch-a.merge)" = refs/heads/main + ) +' + +test_expect_success 'should not set up unnecessary tracking of local branches' ' + test_when_finished "cleanup_branches super branch-a" && + ( + cd super && + git branch --recurse-submodules branch-a main && + git -C sub rev-parse main && + test "$(git -C sub config branch.branch-a.remote)" = "" && + test "$(git -C sub config branch.branch-a.merge)" = "" + ) +' + +test_expect_success 'setup remote-tracking tests' ' + ( + cd super && + git branch branch-a && + git checkout -b branch-b && + git submodule add ../sub-upstream sub2 && + # branch-b now has a committed submodule not in branch-a + git commit -m "add second submodule" + ) && + ( + cd sub-upstream && + git branch branch-a + ) && + git clone --branch main --recurse-submodules super super-clone && + git -C super-clone config submodule.propagateBranches true +' + +test_expect_success 'should not create branch when submodule is not in .git/modules' ' + # The cleanup needs to delete sub2:branch-b in particular because main does not have sub2 + test_when_finished "git -C super-clone/sub2 branch -D branch-b && \ + cleanup_branches super-clone branch-a branch-b" && + ( + cd super-clone && + # This should succeed because super-clone has sub. + git branch --recurse-submodules branch-a origin/branch-a && + # This should fail because super-clone does not have sub2. + test_must_fail git branch --recurse-submodules branch-b origin/branch-b 2>actual && + cat >expected <<-EOF && + fatal: submodule sub: unable to find submodule + You may reinitialize the submodules using ${SQ}git checkout origin/branch-b && git submodule update${SQ} + EOF + test_must_fail git rev-parse branch-b && + test_must_fail git -C sub rev-parse branch-b && + # User can fix themselves by initializing the submodule + git checkout origin/branch-b && + git submodule update && + git branch --recurse-submodules branch-b origin/branch-b + ) +' + +test_expect_success 'should set up tracking of remote-tracking branches' ' + test_when_finished "cleanup_branches super-clone branch-a" && + ( + cd super-clone && + git branch --recurse-submodules branch-a origin/branch-a && + test "$(git -C sub config branch.branch-a.remote)" = origin && + test "$(git -C sub config branch.branch-a.merge)" = refs/heads/branch-a + ) +' + +test_expect_success 'should not fail when unable to set up tracking in submodule' ' + test_when_finished "cleanup_branches super-clone branch-b" && + ( + cd super-clone && + git branch --recurse-submodules branch-b origin/branch-b + ) +' + +test_done