Commit 5f71c243 authored by Dom Sekotill's avatar Dom Sekotill
Browse files

Big refactor of git-supertree

parent 833ee275
Loading
Loading
Loading
Loading
+195 −110
Original line number Diff line number Diff line
#!/bin/bash
set -eu
set -eux
shopt -s nullglob
shopt -s dotglob

@@ -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.
"


@@ -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
@@ -98,7 +99,6 @@ is_empty()
	[[ ${#files[*]} -eq 0 ]]
}


is_subdirectory()
{
	local CDIR=$PWD
@@ -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/}

@@ -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