#!/usr/bin/env 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.
#
# -o REMBRANCH:: Set the default remote branch for the branch
#	Along switching the branches, this will also set the default
#	remote branch to be associated with the target branch (used as
#	default by e.g. `cg-update` and `cg-push`, falls back to 'origin').
#	You may want to combine this with '-n' to change this for a branch
#	other than the current one without actual switching. As a special
#	present just for you, in case of '-o' the BRANCH `cg-switch` argument
#	defaults to the current branch.
#
#	This setting is saved in Git configuration file under the key
#	'branch.<name>.merge' and is understood by core Git as well.
#
# -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] [-o REMBRANCH] [-c | -r COMMIT_ID] BRANCH"
_git_requires_root=1
_git_wc_unneeded=1

. "${COGITO_LIB:-/usr/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
origin=
while optparse; do
	if optparse -f; then
		force=1
	elif optparse -l; then
		savelocal=1
	elif optparse -n; then
		seek=
	elif optparse -o=; then
		origin="$OPTARG"
	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

[ "$origin" -o "${#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"

if [ "${#ARGS[@]}" -gt 0 ]; then
	dsthead="${ARGS[0]}"
else
	dsthead="$_git_head"
fi
[ -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"

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


if [ "$origin" ]; then
	[ -s "$_git/branches/$origin" ] ||
		die "$origin is not a valid remote branch"
	echo "Setting $dsthead's origin: $origin"
	git-repo-config "branch.$dsthead.merge" "refs/heads/$origin" || exit 1
fi


if ! [ "$(git-symbolic-ref HEAD)" != "refs/heads/$dsthead" -o -n "$dstcommit" ]; then
	if [ "$origin" ]; then
		exit 0
	else
		die "already on branch $dsthead"
	fi
fi


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

if exists_ref "refs/heads/$dsthead"; then
	# Existing branch
	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
			echo "Saving local changes..."
			shelve_changes # Sets $curcommit
		fi

		echo "Switching to branch $dsthead..."
		if ! tree_timewarp --no-head-update "along" "please rollback" "$curcommit" "$dstcommit"; then
			abort_shelve "$curcommit"
			exit 1
		fi

		export _git_head="$dsthead"
		unshelve_changes # Eats $dstcommit
	else
		# Shelve local changes
		if [ "$savelocal" ]; then
			echo "Saving local changes..."
			shelve_changes # Sets $curcommit
		fi

		echo "Switching to branch $dsthead..."

		export _git_head="$dsthead"
		unshelve_changes # Eats $dstcommit
	fi
	git-symbolic-ref HEAD "refs/heads/$dsthead"
fi
