#!/usr/pkg/bin/bash
#
# Switch the working tree to a different (or new) local branch
# Copyright (c) Yann Dirson, Petr Baudis  2005
#
# `cg-switch` can switch your current local branch (and working copy)
# to an existing branch, or create a new branch based on a given commit.
#
# Terminology note: This command concerns local branches (also called
# "heads"), not remote branches (those managed by `cg-branch-add`).
#
# Note that `cg-switch` is meant for permanent switching of your current
# local branch (permanent in the sense that you are going to work on it;
# you can obviously `cg-switch` again later). If you want to just casually
# explore the current state of a particular branch of commit, use `cg-seek`.
#
# OPTIONS
# -------
# -c:: Create new branch based on HEAD
#	Create a new branch of given name, based on your current commit
#	(HEAD). This option is equivalent to specifying '-r HEAD'. If
#	'-f' is passed and the branch already exists, it is forcibly
#	repointed to point to the current commit.
#
# -f:: Enable overwriting of existing branches
#	Force the branch's head pointer to be updated to whatever you
#	passed as the '-r' argument even if the branch already exists.
#	WARNING: The pointer to the original contents of the branch will
#	be lost! The contents itself will not be deleted right away,
#	`git-fsck-objects --unreachable` might help you to find it.
#	Besides, this can get very troublesome if you are pushing the
#	branch out - please refer to the documentation of a close
#	relative, `cg-admin-uncommit`.
#
# -l:: Preserve local changes in the branch
#	If your working tree has uncommitted local changes, the default
#	behaviour is that the changes will be reapplied to the new
#	branch after switching. With this option, however, the local
#	changes will be "kept" with your previous branch, you will
#	get a pristine tree of the new branch and when you switch back
#	to the original branch you will also get back the local changes.
#	(You do not need to pass any special switches when switching
#	back, '-l' has effect only on the branch you are switching _away_
#	from.)
#
# -n:: No switch; only create and update the branch
#	Do not switch your current branch to the given branch. This
#	will make cg-switch to only create or update the branch, but
#	leave your working copy alone.
#
# -p:: Do not touch the working copy
#	Do not touch the working copy when switching. This _will_ switch
#	your current branch, but the checked out working copy will have
#	the original contents kept (so further `cg-diff` will list a lot
#	of changes, relative to the new branch).
#
# -r COMMIT_ID:: Branch off the given COMMIT_ID, create branch if non-existing
#	Point the branch at the given commit. Required when creating
#	a new branch. When switching to an existing branch, the branch
#	pointer is modified if '-r' is passed and confirmed by '-f'.
#
# EXAMPLE USAGE
# -------------
# To create a "v1.x" branch based on the commit "v1.0" and switch the
# working copy to it, making it your current branch, do:
#
# 	$ cg-switch -r v1.0 v1.x
#
# If you want to create the branch (let's say based on the current
# commit) but don't switch your working copy to it (so that your
# current branch stays the same as before), do:
#
# 	$ cg-switch -n -c v1.x
#
# If you want to go back to the 'master' branch, just do:
#
# 	$ cg-switch master
#
# To change the "v1.x" branch to refer to the latest commit on the
# "testing" branch, do (WARNING: you will lose the pointer to the
# original contents of the "v1.x" branch, be careful!):
#
# 	$ cg-switch -f -r testing v1.x

# Testsuite: TODO

USAGE="cg-switch [-f] [-l | -n | -p] [-c | -r COMMIT_ID] BRANCH"
_git_requires_root=1
_git_wc_unneeded=1

. "${COGITO_LIB:-/usr/pkg/lib/cogito/}"cg-Xlib || exit 1

set -e


repoint_head() {
	# $oldcommit is optional
	local dsthead="$1" dstcommit="$2" oldcommit="$3"
	git-update-ref "refs/heads/$dsthead" "$dstcommit" $oldcommit
}


force=
savelocal=
seek=1
dstcommit=
roll=1
while optparse; do
	if optparse -f; then
		force=1
	elif optparse -l; then
		savelocal=1
	elif optparse -n; then
		seek=
	elif optparse -p; then
		roll=
	elif optparse -r=; then
		dstcommit="$(cg-object-id -c "$OPTARG")" || exit 1
	elif optparse -c; then
		dstcommit="$(cg-object-id -c)" || exit 1
	else
		optfail
	fi
done

[ "${#ARGS[@]}" -eq "1" ] || usage
[ "$seek$roll" = "" ] && usage
[ "$savelocal" = "1" -a "$seek$roll" != "11" ] && usage

[ "$_git_no_wc" ] && [ "$seek" ] &&
	die "only cg-switch -n allowed outside a working copy"

dsthead="${ARGS[0]}"
[ -s "$_git/branches/$dsthead" ] &&
	die "refusing to switch to a remote branch - see README for lengthy explanation; use cg-seek to just quickly inspect it"

[ "$(git-symbolic-ref HEAD)" != "refs/heads/$dsthead" ] || [ -n "$dstcommit" ] ||
	die "already on branch $dsthead"

if [ "$seek" ]; then
	[ -s "$_git/blocked" ] && die "switch blocked: $(cat "$_git/blocked")"
fi


curcommit="$(cg-object-id -c)"

if [ -s "$_git/refs/heads/$dsthead" ]; then
	# Existing branch
	[ -r "$_git/refs/heads/$dsthead" ] ||
		die "reference '$_git/refs/heads/$dsthead' is unreadable"
	if [ -n "$dstcommit" ]; then
		[ "$force" ] ||
			die "branch $dsthead already exists - use -f to force the switch"
		srccommit="$(cg-object-id -c "$dsthead")"
		echo "Repointing branch $dsthead: $srccommit -> $dstcommit"
		repoint_head "$dsthead" "$dstcommit" "$srccommit"
	fi

else
	# New branch
	[ "$dstcommit" ] || die "branch $dsthead does not exist - you must pass -r if you want to create a new branch"
	echo "Creating new branch $dsthead: $dstcommit"
	repoint_head "$dsthead" "$dstcommit"
fi


if [ "$seek" ]; then
	[ -n "$dstcommit" ] || dstcommit="$(cg-object-id -c "$dsthead")"
	if [ "$roll" ] && [ "x$curcommit" != "x$dstcommit" ]; then
		# Shelve local changes
		if [ "$savelocal" ]; then
			# TODO: Later, move this to cg-shelve or something and make it available for general use.
			echo "Saving local changes..."
			if [ -s "$_git/refs/heads/.cg-shelve-$_git_head" ]; then
				echo "Warning: Your current branch already has some local changes saved. Refusing to overwrite them." >&2
				echo "This could happen if you switched away using 'cg-switch -l' but did not switch back using cg-switch." >&2
				dirty="$_git_head-dirty"
				if [ -s "$_git/refs/heads/$dirty" ]; then
					i=1; while [ -s "$_git/refs/heads/$dirty$i" ]; do i=$((i+1)); done
					dirty="$dirty$i"
				fi
				mv "$_git/refs/heads/.cg-shelve-$_git_head" "$_git/refs/heads/$dirty"
				echo "I have created branch $dirty and made the old local changes available as its last commit." >&2
			fi
			cp "$_git/refs/heads/$_git_head" "$_git/refs/heads/.cg-shelve-$_git_head"
			git-symbolic-ref HEAD "refs/heads/.cg-shelve-$_git_head"
			cg-commit --no-hooks -m"[@internal@] cg-switch local changes shelve" -m"This commit for internal Cogito use stores uncommitted local changes at the time of cg-switch -l away from $_git_head." >/dev/null
			# Restoring HEAD now is useful in case anything bad happens later
			git-symbolic-ref HEAD "refs/heads/$_git_head"
			curcommit="$(cg-object-id -c "refs/heads/.cg-shelve-$_git_head")"
		fi

		echo "Switching to branch $dsthead..."
		# tree_timewarp returns false on local modifications
		tree_timewarp --no-head-update "along" "please rollback" "$curcommit" "$dstcommit" || :

		# Unshelve local changes
		if [ -s "$_git/refs/heads/.cg-shelve-$dsthead" ]; then
			# TODO: Later, move this to cg-unshelve or something and make it available for general use.
			# XXX: We will not properly restore merges, but that
			# doesn't matter now since we won't let you cg-switch
			# away from them in the first place. There are two
			# tricky issues:
			# * preserving the parents - not too tricky if you
			#   error out when the base branch changed in the
			#   meantime
			# * preserving the set of files with local changes
			#   ignored by the merge. We will need to do some
			#   extra bookkeeping here, possibly in the cmomit
			#   message
			# * when we get proper conflicts handling, we need
			#   to remember to override it when shelving and
			#   restore the list of commits after unshelving.
			echo "Restoring local changes..."
			shelvecommit="$(cg-object-id -c "refs/heads/.cg-shelve-$dsthead")"
			tree_timewarp --no-head-update "along" "please roll" "$dstcommit" "$shelvecommit" || :
			rm "$_git/refs/heads/.cg-shelve-$dsthead"
		fi
	else
		echo "Switching to branch $dsthead..."
	fi
	git-symbolic-ref HEAD "refs/heads/$dsthead"
fi
