Commit 833ee275 authored by Dom Sekotill's avatar Dom Sekotill
Browse files

Add git-supertree

parents
Loading
Loading
Loading
Loading

git-supertree

0 → 100755
+290 −0
Original line number Diff line number Diff line
#!/bin/bash
set -eu
shopt -s nullglob
shopt -s dotglob

SCRIPT=$(basename $0)
USAGE="Usage: $SCRIPT [-h] {init|clone|convert|add} [OPTION [OPTION ...]]

  Create 'super working trees', which are a trees of working trees. This is
  useful for projects with a great deal of work-churn (constant changing of
  development focus). Each piece of work can have it's own working tree with
  in-development changes without having to stash and unstash constantly.

  Working trees are a core part of git, but normally the repository is stored
  under the main (first) working tree, and new working trees must be created
  from there.  In 'super working trees' the repository is stored in the
  top-level directory and each working tree (including the main one) is a
  subdirectory. New working trees can be created from the top-level with the
  \`git worktree add\` command.


$SCRIPT init [REPOPATH] [[MAIN-TREE] BRANCH-NAME]
  Create a new super working tree at NAME (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

$SCRIPT convert [REPOPATH] [MAIN-TREE]
  [NOT IMPLEMENTED YET] Convert a regular git working tree + repository into a
  super working tree.

$SCRIPT add [--from FROM] NAME
  Add a new worktree named NAME. If a matching branch does not exist it is
  created from FROM.

$SCRIPT rm NAME
  Remove a worktree name NAME.


Options:

  -h --help  Show this message and exit.


Arguments:

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


declare    COMMAND
declare -a ARGUMENTS


help()
{
	echo "$USAGE"
	exit
}

die()
{
	local exit_code=${EXIT_CODE:-$?}
	[ $exit_code -gt 0 ] || exit_code=1
	echo "$SCRIPT [CRITICAL]: $*"
	exit $exit_code
} >&2

arg_error()
{
	declare -a termcodes=( "" "" )
	if [ -t 1 ]; then
		termcodes=( "\e[31;1m" "\e[m" )
	fi
	echo "$USAGE"
	echo "Error:"
	printf "  ${termcodes[0]}%s${termcodes[1]}\n" "$*"
	exit 2
} >&2

info() { fmt -w $(tput cols); }
warn() { { printf "WARNING: "; cat; } | info >&2; }


is_empty()
{
	declare -a files=( $1/* )
	[[ ${#files[*]} -eq 1 ]] &&
	[[ ${files[0]} -ef $1/.backup.tar ]] ||
	[[ ${#files[*]} -eq 0 ]]
}


is_subdirectory()
{
	local CDIR=$PWD
	pushd "$1" >/dev/null
	while [[ ! $PWD -ef / ]]; do
		cd ..
		if [[ $PWD -ef $CDIR ]]; then
			popd >/dev/null
			return 0
		fi
	done
	popd >/dev/null
	return 1
}


branch_exists()
{
	git rev-parse --verify $1 >/dev/null 2>&1
}


mkcommit()
{
	local tree=$(git mktree </dev/null)
	if [ $# -gt 0 ]; then
		printf "%s\n" "$@" | git commit-tree $tree
	else
		git commit-tree $tree
	fi
}


prepare_init()
{
	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"
	fi
}

action_init()
{
	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
}

action_clone()
{
	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
}

action_add()
{
	local BRANCH= FROM=

	while [ $# -gt 0 ]; do
		case $1 in
			--from) FROM=$2; shift ;;
			*) case '' in
				$BRANCH) BRANCH=$1 ;;
				*) arg_error "Unknown argument $1 to add action" ;;
			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
}

action_rm()
{
	local BRANCH="$1"

	test -n "$BRANCH" || arg_error "A worktree name is required"

	if is_subdirectory "$BRANCH"; then
		rm -r "$BRANCH"
		git worktree prune
	else
		arg_error "The first argument must be a worktree name"
	fi
}

action_convert()
{
	local REPOPATH=${1:-${GIT_DIR:-.}}
	local BRANCH_REF=$(git rev-parse --symbolic-full-name HEAD)
	local BRANCH=${BRANCH_REF#refs/heads/}

	cd $REPOPATH

	local is_super=yes
	for f in ./*; do
		if [ "$f" -ef .git ] || [ -e "$f/.git" ]; then
			continue
		else
			is_super=no
		fi
	done
	test $is_super = no || die "$REPOPATH/ looks like it is already a supertree"
	
	# make a backup of the repo & worktree
	if [ -e .backup.tar ]; then
		die "There is already a $REPOPATH/.backup.tar present. A previous" \
		    "attempt at conversion may have been interrupted."
	fi
	tar -cf .backup.tar ./*

	STAGE=$(mktemp -d)
	find -mindepth 1 -maxdepth 1 \
		-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"/*
	mv "$STAGE"/* "$BRANCH"/

	rm .backup.tar
}


# Options & command
while [ $# -gt 0 ]; do
	case $1 in
		-h|--help) help ;;
		init|clone|convert|add|rm) COMMAND=$1 ;;
		*) break ;;
	esac
	shift
done

[[ -v COMMAND ]] || arg_error "'init', 'clone' or 'convert' is required"
action_$COMMAND "$@"