diff --git a/.gitignore b/.gitignore index 4da58c6754899e..5f329179c8d730 100644 --- a/.gitignore +++ b/.gitignore @@ -164,6 +164,7 @@ /git-show-branch /git-show-index /git-show-ref +/git-son /git-sparse-checkout /git-stage /git-stash diff --git a/Documentation/git-son.adoc b/Documentation/git-son.adoc new file mode 100644 index 00000000000000..17ec992bfd0cfb --- /dev/null +++ b/Documentation/git-son.adoc @@ -0,0 +1,64 @@ +git-son(1) +========== + +NAME +---- +git-son - Create an independent child repository that knows its parent + +SYNOPSIS +-------- +[verse] +'git son' [--inherit] [--branch ] + +DESCRIPTION +----------- + +Create a new independent Git repository inside the current working +tree as a subdirectory named ``. Unlike a submodule, the child +repository is not tracked by the parent; instead, `/` is added +to the parent's `.gitignore`. + +The child repository is configured with a remote called `parent` +pointing back to the parent repository's origin URL (or local path +if no origin is set), allowing the child to fetch from the parent +at any time. + +OPTIONS +------- +--inherit:: + Fetch the parent's history into the child repository at + creation time. Without this flag, the child starts with a + single initial commit. + +--branch :: + When used with `--inherit`, check out the given branch from + the parent instead of the default branch. This option + requires `--inherit`. + +:: + The name of the subdirectory (and child repository) to create. + Must not already exist. + +EXAMPLES +-------- + +Create a simple child repository: + + git son my-tool + +Create a child that inherits the parent's history: + + git son --inherit my-fork + +Create a child starting from a specific parent branch: + + git son --inherit --branch feature my-experiment + +Later, from within the child, fetch updates from the parent: + + cd my-tool + git fetch parent + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/Documentation/meson.build b/Documentation/meson.build index f4854f802d455f..1ae7e5f6445ac0 100644 --- a/Documentation/meson.build +++ b/Documentation/meson.build @@ -139,6 +139,7 @@ manpages = { 'git-show-ref.adoc' : 1, 'git-show.adoc' : 1, 'git-sh-setup.adoc' : 1, + 'git-son.adoc' : 1, 'git-sparse-checkout.adoc' : 1, 'git-stage.adoc' : 1, 'git-stash.adoc' : 1, diff --git a/Makefile b/Makefile index fb50c57e4f253d..4791f47af1fcd4 100644 --- a/Makefile +++ b/Makefile @@ -728,6 +728,7 @@ SCRIPT_SH += git-merge-resolve.sh SCRIPT_SH += git-mergetool.sh SCRIPT_SH += git-quiltimport.sh SCRIPT_SH += git-request-pull.sh +SCRIPT_SH += git-son.sh SCRIPT_SH += git-submodule.sh SCRIPT_SH += git-web--browse.sh diff --git a/command-list.txt b/command-list.txt index 21b802c42026b3..880177e0fd7df8 100644 --- a/command-list.txt +++ b/command-list.txt @@ -186,6 +186,7 @@ git-show mainporcelain info git-show-branch ancillaryinterrogators complete git-show-index plumbinginterrogators git-show-ref plumbinginterrogators +git-son mainporcelain git-sparse-checkout mainporcelain git-stage complete git-stash mainporcelain diff --git a/git-son.sh b/git-son.sh new file mode 100755 index 00000000000000..a212c7b69f0316 --- /dev/null +++ b/git-son.sh @@ -0,0 +1,97 @@ +#!/bin/sh +# +# git-son: create an independent child repository that knows its parent +# + +SUBDIRECTORY_OK='Yes' +OPTIONS_SPEC='git son [options] +-- +inherit fetch parent history into the son +branch= start the son from a specific parent branch +' + +. git-sh-setup +require_work_tree +cd_to_toplevel + +inherit= +branch= +while test $# -gt 0 +do + case "$1" in + --inherit) + inherit=1 ;; + --branch) + shift + branch="$1" ;; + --) + shift; break ;; + -*) + usage ;; + *) + break ;; + esac + shift +done + +name="$1" +test -n "$name" || usage + +if test -n "$branch" && test -z "$inherit" +then + die "fatal: --branch requires --inherit" +fi + +parent_dir="$(pwd)" +parent_remote="$(git remote get-url origin 2>/dev/null)" || parent_remote= + +if test -e "$name" +then + die "fatal: '$name' already exists" +fi + +mkdir "$name" || die "fatal: could not create directory '$name'" + +if ! echo "$name/" >> "$parent_dir/.gitignore" 2>/dev/null +then + rm -rf "$name" + die "fatal: could not update .gitignore" +fi + +cd "$name" || die "fatal: could not enter directory '$name'" + +if ! git init +then + rm -rf "$parent_dir/$name" + die "fatal: could not initialize repository in '$name'" +fi + +if test -n "$parent_remote" +then + git remote add parent "$parent_remote" +else + git remote add parent "$parent_dir" +fi + +if test -n "$inherit" +then + git fetch parent || die "fatal: could not fetch from parent" + if test -n "$branch" + then + git checkout -b "$branch" "parent/$branch" || + die "fatal: could not checkout branch '$branch'" + else + git checkout -b main parent/HEAD 2>/dev/null || + git checkout -b main "parent/$(git remote show parent | sed -n 's/.*HEAD branch: //p')" 2>/dev/null || + echo "warning: could not determine parent HEAD, starting empty" + fi +else + echo "# $name" > README.md + git add README.md + git commit -q -m "Initial commit" +fi + +echo "" +echo "Created son repository '$name'" +echo " parent: ${parent_remote:-$parent_dir}" +echo " inherit: ${inherit:-no}" diff --git a/meson.build b/meson.build index 052c81f2887bac..538bd4025f59a6 100644 --- a/meson.build +++ b/meson.build @@ -1973,6 +1973,7 @@ scripts_sh = [ 'git-mergetool.sh', 'git-quiltimport.sh', 'git-request-pull.sh', + 'git-son.sh', 'git-sh-i18n.sh', 'git-sh-setup.sh', 'git-submodule.sh', diff --git a/t/meson.build b/t/meson.build index fd955f44efc0be..523062df6627dd 100644 --- a/t/meson.build +++ b/t/meson.build @@ -591,6 +591,7 @@ integration_tests = [ 't5004-archive-corner-cases.sh', 't5100-mailinfo.sh', 't5150-request-pull.sh', + 't5151-son.sh', 't5200-update-server-info.sh', 't5300-pack-object.sh', 't5301-sliding-window.sh', diff --git a/t/t5151-son.sh b/t/t5151-son.sh new file mode 100755 index 00000000000000..826cbbfa66c41b --- /dev/null +++ b/t/t5151-son.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +test_description='Test git son command.' + +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +. ./test-lib.sh + +test_expect_success 'setup parent repository' ' + echo "parent content" >file.txt && + git add file.txt && + git commit -m "Initial parent commit" +' + +test_expect_success 'son creates child repository' ' + git son my-child && + test -d my-child && + test -d my-child/.git +' + +test_expect_success 'son sets parent remote in child' ' + ( + cd my-child && + git remote get-url parent + ) +' + +test_expect_success 'son adds child to parent .gitignore' ' + grep "my-child/" .gitignore +' + +test_expect_success 'son child has initial commit' ' + ( + cd my-child && + test $(git log --oneline | wc -l) -eq 1 + ) +' + +test_expect_success 'son fails if target already exists' ' + test_must_fail git son my-child +' + +test_expect_success 'son with --branch requires --inherit' ' + test_must_fail git son --branch main branch-child +' + +test_expect_success 'son with --branch leaves no directory on failure' ' + ! test -e branch-child +' + +test_expect_success 'son with --inherit fetches parent history' ' + git init --bare "$TRASH_DIRECTORY/parent.git" && + git push "$TRASH_DIRECTORY/parent.git" main && + git remote add origin "file://$TRASH_DIRECTORY/parent.git" && + git son --inherit inherited-child && + ( + cd inherited-child && + git log --oneline parent/main + ) +' + +test_done