Loading git-supertree +195 −110 Original line number Diff line number Diff line #!/bin/bash set -eu set -eux shopt -s nullglob shopt -s dotglob Loading @@ -19,40 +19,41 @@ USAGE="Usage: $SCRIPT [-h] {init|clone|convert|add} [OPTION [OPTION ...]] \`git worktree add\` command. $SCRIPT init [REPOPATH] [[MAIN-TREE] BRANCH-NAME] Create a new super working tree at NAME (default: current directory) $SCRIPT init [WORKTREE-BASE] [-b BRANCH[:PATH]] Create a new super working tree at WORKTREE-BASE (default: current directory) $SCRIPT clone URL [REPOPATH] [[MAIN-TREE] BRANCH-NAME] Clone a remote repository to NAME (default: current directory) as a super working tree -b --branch Create an initial branch named BRANCH (default: 'master') checked out at PATH (default: BRANCH) relative to WORKTREE-BASE. $SCRIPT convert [REPOPATH] [MAIN-TREE] [NOT IMPLEMENTED YET] Convert a regular git working tree + repository into a super working tree. $SCRIPT clone URL [WORKTREE-BASE] [-b BRANCH[:PATH]] Clone a remote repository at URL in a super working tree at WORKTREE-BASE (default: base name of URL) $SCRIPT add [--from FROM] NAME Add a new worktree named NAME. If a matching branch does not exist it is created from FROM. -b --branch Create or check-out an initial branch named BRANCH (default: origin/HEAD) at PATH (default: BRANCH) relative to WORKTREE-BASE. $SCRIPT rm NAME Remove a worktree name NAME. $SCRIPT convert [REPOPATH] [MAIN-TREE] [-m MASTER] Convert a regular git working tree + repository at WORKTREE (default: PWD) into a super working tree. -m --master Point the base HEAD symbolic ref to MASTER (default: current HEAD) Options: $SCRIPT add [--from SOURCE] BRANCH[:PATH] Add a new worktree at PATH (default: BRANCH) with BRANCH checked-out. If a matching branch does not exist it is created from SOURCE (default: base 'HEAD'). -h --help Show this message and exit. $SCRIPT rm [--branch] NAME Remove a worktree named NAME. --branch Delete the local branch as well. Arguments: Global Options: REPOPATH The path to the top-level directory where a super-tree is to be created. URL A URL to a repository using one of the schemes known to git. BRANCH-NAME The initial branch to check out in the main working tree. MAIN-TREE The name of the main working tree, defaults to the initial branch name. NAME The name of a worktree. FROM A commit/branch to create a branch at. -h --help Show this message and exit. " Loading Loading @@ -81,7 +82,7 @@ arg_error() termcodes=( "\e[31;1m" "\e[m" ) fi echo "$USAGE" echo "Error:" echo "Error${COMMAND+ in $SCRIPT $COMMAND}:" printf " ${termcodes[0]}%s${termcodes[1]}\n" "$*" exit 2 } >&2 Loading @@ -98,7 +99,6 @@ is_empty() [[ ${#files[*]} -eq 0 ]] } is_subdirectory() { local CDIR=$PWD Loading Loading @@ -132,113 +132,123 @@ mkcommit() } prepare_init() new_repo() { case $# in 1) REPOPATH=$1 ;; 2) REPOPATH=$2 ;; 3) REPOPATH=$2 BRANCH_NAME=$3 ;; 4) REPOPATH=$2 MAIN_TREE=$3 BRANCH_NAME=$4 ;; *) arg_error "Too many arguments: $*" ;; esac REPOPATH=${REPOPATH:-.} BRANCH_NAME=${BRANCH_NAME:-master} MAIN_TREE=${MAIN_TREE:-$BRANCH_NAME} if [[ -d $REPOPATH ]]; then is_empty "$REPOPATH" || die "Cannot initialise a repository as the" \ "directory is not empty: $REPOPATH" else mkdir -p -- "$REPOPATH" if [[ -d "$1" ]] && ! is_empty "$1"; then die "Cannot initialise a repository as the directory is not empty: $1" fi mkdir -p -- "$1/.git" git init --bare "$1"/.git } action_init() set_head() ( cd "$1" local HEAD="${2-master}" REPO="${GIT_DIR-.git}" git symbolic-ref HEAD refs/heads/$HEAD ) get_remote_head() { local REPOPATH MAIN_TREE BRANCH_NAME prepare_init . "$@" # sets REPOPATH, MAIN_TREE, BRANCH_NAME mkdir -p "$REPOPATH/.git" git init --bare "$REPOPATH"/.git # -- "$REPOPATH"/"$MAIN_TREE" cd "$REPOPATH" mkcommit "Supertree commit" | tee .git/refs/heads/supertree__ > .git/refs/heads/$BRANCH_NAME echo "ref: refs/heads/supertree__" > .git/HEAD git symbolic-ref SUPERTREE_DEFAULT refs/heads/$BRANCH_NAME action_add $MAIN_TREE mv .git/refs/heads/supertree__ .git/HEAD rm .git/refs/heads/$BRANCH_NAME git ls-remote --symref ${1-origin} HEAD | sed -n 's!\tHEAD$!!;s!^ref: refs/heads/!!p' } action_clone() new_worktree() { local URL=$1; shift local BASE_NAME=$(basename "$URL") local REPOPATH MAIN_TREE BRANCH_NAME prepare_init ${BASE_NAME%.git} "$@" # sets REPOPATH, MAIN_TREE, BRANCH_NAME mkdir "$REPOPATH/.git" git clone --bare -- "$URL" "$REPOPATH/.git" cd "$REPOPATH" mkcommit "Supertree commit" > .git/HEAD git symbolic-ref SUPERTREE_DEFAULT $BRANCH_NAME action_add $MAIN_TREE --from $BRANCH_NAME local WORKTREE=$1 NAME=$2 # minimal manual worktree creation WT_GIT="$WORKTREE/.git" WT_REPO=".git/worktrees/$WORKTREE" mkdir -p -- "$WORKTREE" "$WT_REPO" PREFIX=gitdir gitlink $WT_GIT $WT_REPO gitlink "$WT_REPO/commondir" .git gitlink "$WT_REPO/gitdir" "$WT_GIT" echo "ref: refs/heads/$NAME" > "$WT_REPO/HEAD" } action_add() gitlink() { local BRANCH= FROM= printf "${PREFIX+$PREFIX: }" realpath --relative-to=$(dirname $1) $2 } > $1 while [ $# -gt 0 ]; do action_init() { local REPOPATH BRANCH_SPEC=master:master while [[ $# -gt 0 ]]; do case $1 in --from) FROM=$2; shift ;; -b|--branch) BRANCH_SPEC=$2; shift ;; *) case '' in $BRANCH) BRANCH=$1 ;; *) arg_error "Unknown argument $1 to add action" ;; esac "${REPOPATH-}") REPOPATH=$1 ;; *) arg_error "Unknown argument: $1" ;; esac ;; esac shift done test -n "$BRANCH" || arg_error "A branch name is required" if branch_exists $BRANCH; then git worktree add $BRANCH $BRANCH elif branch_exists ${FROM:-SUPERTREE_DEFAULT}; then git worktree add -b $BRANCH $BRANCH ${FROM:-SUPERTREE_DEFAULT} elif [ -z "$FROM" ]; then warn <<-MSG You cannot create new worktrees without a valid branch. Either use '--from BRANCH', commit something to the default branch (`git symbolic-ref --short SUPERTREE_DEFAULT`) or change the default branch with 'git symbolic-ref SUPERTREE_DEFAULT <BRANCH-NAME>' MSG else warn <<-MSG The branch $FROM does not exist. MSG fi new_repo "${REPOPATH:=.}" set_head "${REPOPATH}" "${BRANCH_SPEC%:*}" (cd "${REPOPATH}"; action_add "${BRANCH_SPEC}") } action_rm() action_clone() { local BRANCH="$1" test -n "$BRANCH" || arg_error "A worktree name is required" local URL REPOPATH BRANCH_SPEC while [[ $# -gt 0 ]]; do case $1 in -b|--branch) BRANCH_SPEC="$2"; shift ;; *) case '' in "${URL-}") URL=$1 ;; "${REPOPATH-}") REPOPATH=$1 ;; *) arg_error "Unknown argument: $1" ;; esac ;; esac shift done if is_subdirectory "$BRANCH"; then rm -r "$BRANCH" git worktree prune else arg_error "The first argument must be a worktree name" if ! [[ -v URL ]]; then arg_error "URL is required" fi if ! [[ -v REPOPATH ]]; then REPOPATH=$(basename "${URL%.git}") fi if ! [[ -v BRANCH_SPEC ]]; then BRANCH_SPEC=$(get_remote_head "$URL") fi local BRANCH_PATH=${BRANCH_SPEC#*:} BRANCH_NAME=${BRANCH_SPEC%:*} new_repo "${REPOPATH}" ( cd ${REPOPATH} git remote add origin "${URL}" git fetch origin new_worktree ${BRANCH_PATH} ${BRANCH_NAME} cd ${BRANCH_PATH} git checkout -B ${BRANCH_NAME} origin/${BRANCH_NAME} ) set_head "${REPOPATH}" "${BRANCH_NAME}" } action_convert() { local REPOPATH=${1:-${GIT_DIR:-.}} local REPOPATH=. MASTER while [[ $# -gt 0 ]]; do case $1 in -m|--master) MASTER="$2"; shift ;; *) case '' in "${REPOPATH-}") REPOPATH=$1 ;; *) arg_error "Unknown argument: $1" ;; esac ;; esac shift done local BRANCH_REF=$(git rev-parse --symbolic-full-name HEAD) local BRANCH=${BRANCH_REF#refs/heads/} Loading Loading @@ -266,15 +276,90 @@ action_convert() -name .backup.tar \ -o -exec mv '{}' "$STAGE"/ \; action_clone "$STAGE/.git" . git worktree add $BRANCH $BRANCH_REF local git_file=$(<"$BRANCH"/.git) rm -rf "$STAGE"/.git "$BRANCH"/* # Replicate all original heads & remotes in new repo rm -rf "$STAGE/.git/worktrees" cp -r .git/worktrees "$STAGE/.git/" rm -rf .git mv "$STAGE/.git" ./ mv "$BRANCH"/.git "$STAGE"/.git rm -rf "$BRANCH"/* mv "$STAGE"/* "$BRANCH"/ rmdir "$STAGE" git config core.bare true if [[ -v MASTER ]]; then git symbolic-ref HEAD refs/heads/$MASTER fi rm .backup.tar } action_add() { local BRANCH_SPEC FROM while [[ $# -gt 0 ]]; do case $1 in --from) FROM=$2; shift ;; *) case '' in ${BRANCH_SPEC-}) BRANCH_SPEC=$1 ;; *) arg_error "Unknown argument: $1" ;; esac ;; esac shift done if ! [[ -v BRANCH_SPEC ]]; then arg_error "A branch name is required" fi local BRANCH_NAME=${BRANCH_SPEC%:*} local BRANCH_PATH=${BRANCH_SPEC#*:} if branch_exists $BRANCH_NAME; then git worktree add $BRANCH_PATH $BRANCH_NAME elif branch_exists ${FROM:-HEAD}; then git worktree add -b $BRANCH_NAME $BRANCH_PATH ${FROM:-HEAD} else new_worktree $BRANCH_PATH $BRANCH_NAME fi } action_rm() { local BRANCH DELETE while [[ $# -gt 0 ]]; do case $1 in --delete) DELETE=yes ;; *) case '' in ${BRANCH-}) BRANCH=$1 ;; *) arg_error "Unknown argument: $1" ;; esac ;; esac shift done if ! [[ -v BRANCH ]]; then arg_error "A worktree name is required" fi if is_subdirectory "$BRANCH"; then local BRANCH_NAME=$(GIT_DIR="$BRANCH/.git" git rev-parse --abbrev-ref HEAD) rm -r "$BRANCH" git worktree prune else arg_error "The first argument must be a worktree name" fi if [[ -v DELETE ]]; then git branch -D $BRANCH_NAME fi } # Options & command while [ $# -gt 0 ]; do Loading Loading
git-supertree +195 −110 Original line number Diff line number Diff line #!/bin/bash set -eu set -eux shopt -s nullglob shopt -s dotglob Loading @@ -19,40 +19,41 @@ USAGE="Usage: $SCRIPT [-h] {init|clone|convert|add} [OPTION [OPTION ...]] \`git worktree add\` command. $SCRIPT init [REPOPATH] [[MAIN-TREE] BRANCH-NAME] Create a new super working tree at NAME (default: current directory) $SCRIPT init [WORKTREE-BASE] [-b BRANCH[:PATH]] Create a new super working tree at WORKTREE-BASE (default: current directory) $SCRIPT clone URL [REPOPATH] [[MAIN-TREE] BRANCH-NAME] Clone a remote repository to NAME (default: current directory) as a super working tree -b --branch Create an initial branch named BRANCH (default: 'master') checked out at PATH (default: BRANCH) relative to WORKTREE-BASE. $SCRIPT convert [REPOPATH] [MAIN-TREE] [NOT IMPLEMENTED YET] Convert a regular git working tree + repository into a super working tree. $SCRIPT clone URL [WORKTREE-BASE] [-b BRANCH[:PATH]] Clone a remote repository at URL in a super working tree at WORKTREE-BASE (default: base name of URL) $SCRIPT add [--from FROM] NAME Add a new worktree named NAME. If a matching branch does not exist it is created from FROM. -b --branch Create or check-out an initial branch named BRANCH (default: origin/HEAD) at PATH (default: BRANCH) relative to WORKTREE-BASE. $SCRIPT rm NAME Remove a worktree name NAME. $SCRIPT convert [REPOPATH] [MAIN-TREE] [-m MASTER] Convert a regular git working tree + repository at WORKTREE (default: PWD) into a super working tree. -m --master Point the base HEAD symbolic ref to MASTER (default: current HEAD) Options: $SCRIPT add [--from SOURCE] BRANCH[:PATH] Add a new worktree at PATH (default: BRANCH) with BRANCH checked-out. If a matching branch does not exist it is created from SOURCE (default: base 'HEAD'). -h --help Show this message and exit. $SCRIPT rm [--branch] NAME Remove a worktree named NAME. --branch Delete the local branch as well. Arguments: Global Options: REPOPATH The path to the top-level directory where a super-tree is to be created. URL A URL to a repository using one of the schemes known to git. BRANCH-NAME The initial branch to check out in the main working tree. MAIN-TREE The name of the main working tree, defaults to the initial branch name. NAME The name of a worktree. FROM A commit/branch to create a branch at. -h --help Show this message and exit. " Loading Loading @@ -81,7 +82,7 @@ arg_error() termcodes=( "\e[31;1m" "\e[m" ) fi echo "$USAGE" echo "Error:" echo "Error${COMMAND+ in $SCRIPT $COMMAND}:" printf " ${termcodes[0]}%s${termcodes[1]}\n" "$*" exit 2 } >&2 Loading @@ -98,7 +99,6 @@ is_empty() [[ ${#files[*]} -eq 0 ]] } is_subdirectory() { local CDIR=$PWD Loading Loading @@ -132,113 +132,123 @@ mkcommit() } prepare_init() new_repo() { case $# in 1) REPOPATH=$1 ;; 2) REPOPATH=$2 ;; 3) REPOPATH=$2 BRANCH_NAME=$3 ;; 4) REPOPATH=$2 MAIN_TREE=$3 BRANCH_NAME=$4 ;; *) arg_error "Too many arguments: $*" ;; esac REPOPATH=${REPOPATH:-.} BRANCH_NAME=${BRANCH_NAME:-master} MAIN_TREE=${MAIN_TREE:-$BRANCH_NAME} if [[ -d $REPOPATH ]]; then is_empty "$REPOPATH" || die "Cannot initialise a repository as the" \ "directory is not empty: $REPOPATH" else mkdir -p -- "$REPOPATH" if [[ -d "$1" ]] && ! is_empty "$1"; then die "Cannot initialise a repository as the directory is not empty: $1" fi mkdir -p -- "$1/.git" git init --bare "$1"/.git } action_init() set_head() ( cd "$1" local HEAD="${2-master}" REPO="${GIT_DIR-.git}" git symbolic-ref HEAD refs/heads/$HEAD ) get_remote_head() { local REPOPATH MAIN_TREE BRANCH_NAME prepare_init . "$@" # sets REPOPATH, MAIN_TREE, BRANCH_NAME mkdir -p "$REPOPATH/.git" git init --bare "$REPOPATH"/.git # -- "$REPOPATH"/"$MAIN_TREE" cd "$REPOPATH" mkcommit "Supertree commit" | tee .git/refs/heads/supertree__ > .git/refs/heads/$BRANCH_NAME echo "ref: refs/heads/supertree__" > .git/HEAD git symbolic-ref SUPERTREE_DEFAULT refs/heads/$BRANCH_NAME action_add $MAIN_TREE mv .git/refs/heads/supertree__ .git/HEAD rm .git/refs/heads/$BRANCH_NAME git ls-remote --symref ${1-origin} HEAD | sed -n 's!\tHEAD$!!;s!^ref: refs/heads/!!p' } action_clone() new_worktree() { local URL=$1; shift local BASE_NAME=$(basename "$URL") local REPOPATH MAIN_TREE BRANCH_NAME prepare_init ${BASE_NAME%.git} "$@" # sets REPOPATH, MAIN_TREE, BRANCH_NAME mkdir "$REPOPATH/.git" git clone --bare -- "$URL" "$REPOPATH/.git" cd "$REPOPATH" mkcommit "Supertree commit" > .git/HEAD git symbolic-ref SUPERTREE_DEFAULT $BRANCH_NAME action_add $MAIN_TREE --from $BRANCH_NAME local WORKTREE=$1 NAME=$2 # minimal manual worktree creation WT_GIT="$WORKTREE/.git" WT_REPO=".git/worktrees/$WORKTREE" mkdir -p -- "$WORKTREE" "$WT_REPO" PREFIX=gitdir gitlink $WT_GIT $WT_REPO gitlink "$WT_REPO/commondir" .git gitlink "$WT_REPO/gitdir" "$WT_GIT" echo "ref: refs/heads/$NAME" > "$WT_REPO/HEAD" } action_add() gitlink() { local BRANCH= FROM= printf "${PREFIX+$PREFIX: }" realpath --relative-to=$(dirname $1) $2 } > $1 while [ $# -gt 0 ]; do action_init() { local REPOPATH BRANCH_SPEC=master:master while [[ $# -gt 0 ]]; do case $1 in --from) FROM=$2; shift ;; -b|--branch) BRANCH_SPEC=$2; shift ;; *) case '' in $BRANCH) BRANCH=$1 ;; *) arg_error "Unknown argument $1 to add action" ;; esac "${REPOPATH-}") REPOPATH=$1 ;; *) arg_error "Unknown argument: $1" ;; esac ;; esac shift done test -n "$BRANCH" || arg_error "A branch name is required" if branch_exists $BRANCH; then git worktree add $BRANCH $BRANCH elif branch_exists ${FROM:-SUPERTREE_DEFAULT}; then git worktree add -b $BRANCH $BRANCH ${FROM:-SUPERTREE_DEFAULT} elif [ -z "$FROM" ]; then warn <<-MSG You cannot create new worktrees without a valid branch. Either use '--from BRANCH', commit something to the default branch (`git symbolic-ref --short SUPERTREE_DEFAULT`) or change the default branch with 'git symbolic-ref SUPERTREE_DEFAULT <BRANCH-NAME>' MSG else warn <<-MSG The branch $FROM does not exist. MSG fi new_repo "${REPOPATH:=.}" set_head "${REPOPATH}" "${BRANCH_SPEC%:*}" (cd "${REPOPATH}"; action_add "${BRANCH_SPEC}") } action_rm() action_clone() { local BRANCH="$1" test -n "$BRANCH" || arg_error "A worktree name is required" local URL REPOPATH BRANCH_SPEC while [[ $# -gt 0 ]]; do case $1 in -b|--branch) BRANCH_SPEC="$2"; shift ;; *) case '' in "${URL-}") URL=$1 ;; "${REPOPATH-}") REPOPATH=$1 ;; *) arg_error "Unknown argument: $1" ;; esac ;; esac shift done if is_subdirectory "$BRANCH"; then rm -r "$BRANCH" git worktree prune else arg_error "The first argument must be a worktree name" if ! [[ -v URL ]]; then arg_error "URL is required" fi if ! [[ -v REPOPATH ]]; then REPOPATH=$(basename "${URL%.git}") fi if ! [[ -v BRANCH_SPEC ]]; then BRANCH_SPEC=$(get_remote_head "$URL") fi local BRANCH_PATH=${BRANCH_SPEC#*:} BRANCH_NAME=${BRANCH_SPEC%:*} new_repo "${REPOPATH}" ( cd ${REPOPATH} git remote add origin "${URL}" git fetch origin new_worktree ${BRANCH_PATH} ${BRANCH_NAME} cd ${BRANCH_PATH} git checkout -B ${BRANCH_NAME} origin/${BRANCH_NAME} ) set_head "${REPOPATH}" "${BRANCH_NAME}" } action_convert() { local REPOPATH=${1:-${GIT_DIR:-.}} local REPOPATH=. MASTER while [[ $# -gt 0 ]]; do case $1 in -m|--master) MASTER="$2"; shift ;; *) case '' in "${REPOPATH-}") REPOPATH=$1 ;; *) arg_error "Unknown argument: $1" ;; esac ;; esac shift done local BRANCH_REF=$(git rev-parse --symbolic-full-name HEAD) local BRANCH=${BRANCH_REF#refs/heads/} Loading Loading @@ -266,15 +276,90 @@ action_convert() -name .backup.tar \ -o -exec mv '{}' "$STAGE"/ \; action_clone "$STAGE/.git" . git worktree add $BRANCH $BRANCH_REF local git_file=$(<"$BRANCH"/.git) rm -rf "$STAGE"/.git "$BRANCH"/* # Replicate all original heads & remotes in new repo rm -rf "$STAGE/.git/worktrees" cp -r .git/worktrees "$STAGE/.git/" rm -rf .git mv "$STAGE/.git" ./ mv "$BRANCH"/.git "$STAGE"/.git rm -rf "$BRANCH"/* mv "$STAGE"/* "$BRANCH"/ rmdir "$STAGE" git config core.bare true if [[ -v MASTER ]]; then git symbolic-ref HEAD refs/heads/$MASTER fi rm .backup.tar } action_add() { local BRANCH_SPEC FROM while [[ $# -gt 0 ]]; do case $1 in --from) FROM=$2; shift ;; *) case '' in ${BRANCH_SPEC-}) BRANCH_SPEC=$1 ;; *) arg_error "Unknown argument: $1" ;; esac ;; esac shift done if ! [[ -v BRANCH_SPEC ]]; then arg_error "A branch name is required" fi local BRANCH_NAME=${BRANCH_SPEC%:*} local BRANCH_PATH=${BRANCH_SPEC#*:} if branch_exists $BRANCH_NAME; then git worktree add $BRANCH_PATH $BRANCH_NAME elif branch_exists ${FROM:-HEAD}; then git worktree add -b $BRANCH_NAME $BRANCH_PATH ${FROM:-HEAD} else new_worktree $BRANCH_PATH $BRANCH_NAME fi } action_rm() { local BRANCH DELETE while [[ $# -gt 0 ]]; do case $1 in --delete) DELETE=yes ;; *) case '' in ${BRANCH-}) BRANCH=$1 ;; *) arg_error "Unknown argument: $1" ;; esac ;; esac shift done if ! [[ -v BRANCH ]]; then arg_error "A worktree name is required" fi if is_subdirectory "$BRANCH"; then local BRANCH_NAME=$(GIT_DIR="$BRANCH/.git" git rev-parse --abbrev-ref HEAD) rm -r "$BRANCH" git worktree prune else arg_error "The first argument must be a worktree name" fi if [[ -v DELETE ]]; then git branch -D $BRANCH_NAME fi } # Options & command while [ $# -gt 0 ]; do Loading