initial commit master
authorAndrew Sichevoi <kondor@thekondor.net>
Sat, 1 Sep 2018 05:44:41 +0000 (08:44 +0300)
committerAndrew Sichevoi <kondor@thekondor.net>
Sat, 1 Sep 2018 05:44:41 +0000 (08:44 +0300)
README.md [new file with mode: 0644]
git-wtree.sh [new file with mode: 0755]
test-git-wtree.sh [new file with mode: 0755]

diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..ca8505c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,76 @@
+# git-wtree
+
+Several naive `(ba)sh`-shortcuts for `git worktree` subcommand.
+
+## Overview
+
+Creation of `git` worktrees and switching between them is quite verbose thing to do for my regular use-cases. `git-wtree` is intended to simplify this routine.
+This one is a simple shell script supposed to be sourced inside of a shell. It was intentionally developed in a shell-agnostic way (no bashishm's are used) but tested with `bash` only and `git` 2.18.0.
+
+## Installation
+
+Inside of `~/.bashrc`:
+
+    # The comment for the line below could be removed to establish short aliases for provided commands
+    # GIT_WTREE_ALIAS_ENABLED=true
+
+    . git-wtree/git-wtree.sh
+
+## Usage
+
+Inside of `git` repository where worktrees are supposed to be intensively used:
+
+    git config --local worktree.root <ROOT_DIR>
+
+where `<ROOT_DIR>` is a directory where all worktrees are created and removed from.
+
+### Create worktree
+
+    git_wtree_cmd_new --name <BRANCH_NAME> --dir <DIR>
+    
+    # alias:
+    git.wtree:ls --name <BRANCH_NAME> --dir <DIR>
+
+where:
+
+- `<BRANCH_NAME>` is name of the branch to be created;
+- `<DIR>` is name of directory inside of `<ROOT_DIR>`
+
+vs native `git worktree add -b <BRANCH_NAME> <ROOT_DIR>/<DIR>`.
+
+### Delete worktree
+
+    ```bash
+    git_wtree_cmd_drop --name <BRANCH_NAME>
+    # alias:
+    git.wtree:drop --name <BRANCH_NAME>
+    ```
+
+where `<BRANCH_NAME>` is a name of the branch to be deleted.
+
+vs native `git worktreee remove <FULL_PATH_TO_DIR_OF_BRANCH_NAME>`
+
+### List branches inside worktrees
+
+    git_wtree_cmd_ls
+    # alias:
+    git.wtree:ls
+
+vs native `git worktree list` and futher grepping for a branch name.
+
+### Switch to a worktree dir
+
+    git_wtree_cmd_tool_cd <BRANCH_NAME>
+    # alias:
+    git.wtree:cd <BRANCH_NAME>
+
+    git_wtree_cmd_tool_pushd <BRANCH_NAME>
+    # alias:
+    git.wtree:pushd <BRANCH_NAME>
+
+vs native `git worktree list`, grepping and furhter cd/pushd execution.
+
+# License
+
+MIT
+
diff --git a/git-wtree.sh b/git-wtree.sh
new file mode 100755 (executable)
index 0000000..fe04aea
--- /dev/null
@@ -0,0 +1,254 @@
+#!/bin/sh
+
+### git_wtree_new <name:branch name> [dir:<dir name>]
+### git_wtree_drop <name:branch name>
+### git_wtree_{cd, pushd} <name:branch name>
+### git_wtree_root
+### git_wtree_ls
+
+# TODO: use 'git check-ref-format --branch' to check branch's name validness
+
+### GIT_WTREE_ALIAS_ENABLED=true
+
+### For debugging purposes:
+### GIT_WTREE_DRY_RUN=true
+### GIT_WTREE_DEBUG=true
+
+_git_wtree_last_error_dir=$(mktemp --suffix=git.wtree. -d)
+_git_wtree_last_error_msg=${_git_wtree_last_error_dir}/msg
+_git_wtree_last_error_out=${_git_wtree_last_error_dir}/out
+trap "test -d ${_git_wtree_last_error_dir} && rm -rf '${_git_wtree_last_error_dir}'" EXIT QUIT
+
+### Execute `git' subcommand with passed arguments
+### $1     : routine description
+### $2     : git subcommand, e.g.: worktree
+### ${n,}  : git subcomand's arguments
+_git_wtree_exec() {
+    _git_wtree_set_last_error "$1 FAILED"
+    shift
+
+    cmd="git $*"
+    if [ -n "${GIT_WTREE_DRY_RUN}" ]; then
+        echo "[git-wtree:DRY-RUN]: ${cmd}"
+        return 0
+    fi
+
+    [ -n "${GIT_WTREE_DEBUG}" ] && echo -e "git:{\n"
+    ${cmd}
+    rc=$?
+    [ -n "${GIT_WTREE_DEBUG}" ] && echo -e "\n}:git"
+
+    return $rc
+}
+
+### Return key's value from arguments
+### $1    : arg's name
+### ${2,n}: argv to search in
+### NOTE: for the sake of simplicity no long values are supported
+_git_wtree_arg() {
+    arg_name=$1
+    shift
+
+    while [ ! $# -eq 0 ]; do
+        case "$1" in
+            --${arg_name}) echo -n $2 && break ;;
+            *) shift ;;
+        esac
+    done
+}
+
+_git_wtree_show_last_error() {
+    echo "[git-wtree:ERROR] $(cat ${_git_wtree_last_error_msg})" >&2
+    [ -s "${_git_wtree_last_error_out}" ] && echo "[git-wtree:ERROR] $(cat ${_git_wtree_last_error_out})" >&2
+    true
+}
+
+_git_wtree_set_last_error() {
+    echo "$*" > ${_git_wtree_last_error_msg}
+}
+
+_git_wtree_worktree_root() {
+    worktree_root=$(git config worktree.root 2>${_git_wtree_last_error_out} || echo -n)
+    [ -n "${worktree_root}" -a -d "${worktree_root}" ] && echo -n "${worktree_root}" && return 0
+    _git_wtree_set_last_error "Non-defined or not accessible worktree root '${worktree_root}'. 'worktree.root' in .git/config should point to an existing dir."
+    return 1
+}
+
+_git_wtree_arg_dir_name() {
+    dir_name=$(_git_wtree_arg dir $*)
+    _git_wtree_set_last_error "Missing directory name. Shall be specified with --dir argument"
+    [ -n "${dir_name}" ] && echo -n "${dir_name}" && return 0
+    return 1
+}
+
+_git_wtree_arg_branch_name() {
+    branch_name=$(_git_wtree_arg branch $*)
+
+    _git_wtree_set_last_error "Missing branch name. Shall be specified with either --branch or --name argument"
+    [ -z "${branch_name}" ] && branch_name=$(_git_wtree_arg name $*)
+    [ -n "${branch_name}" ] && echo -n "${branch_name}" && return 0
+    return 1
+}
+
+_git_wtree_dir_by_branch_name() {
+    branch_name="$1"
+    candidates_amount=$(git worktree list 2>${_git_wtree_last_error_out} | grep "${branch_name}" | wc -l)
+
+    ### TODO: won't work for names with a common prefix
+    _git_wtree_set_last_error "No exact branch '${branch_name}' is available but ${candidates_amount} candidates"
+    [ 1 -eq "${candidates_amount}" ] || return 1
+
+    _git_wtree_set_last_error "No worktree dir is found for branch '${branch_name}'"
+    dir_name=$(git worktree list 2>/dev/null | grep "${branch_name}" | cut -d ' ' -f 1)
+    [ -n "${dir_name}" ] || return 1
+
+    _git_wtree_set_last_error "No worktree dir '${dir_name}' is available for branch '${branch_name}'"
+    [ -d "${dir_name}" ] || return 1
+
+    echo -n "${dir_name}"
+    return 0
+}
+
+###
+### Locate a corresponding directory of specified branch.
+### Arguments:
+### --<name|branch> <branch_name>
+###
+git_wtree_cmd_locate() {
+    current_top_level=$(git rev-parse --show-toplevel 2>${_git_wtree_last_error_out} || echo -n)
+    _git_wtree_set_last_error "'$(pwd)' is not a git directory"
+    [ -z "${current_top_level}" ] && _git_wtree_show_last_error && return 1
+
+    branch_name=$(_git_wtree_arg_branch_name $*)
+    [ -z "${branch_name}" ] && _git_wtree_show_last_error && return 1
+
+    branch_dir_name=$(_git_wtree_dir_by_branch_name ${branch_name})
+    [ -z "${branch_dir_name}" ] && _git_wtree_show_last_error && return 1
+
+    echo -n "${branch_dir_name}"
+    return 0
+}
+
+####
+#### Same as `git_wtree_cmd_locate`. Suppresses an error output. A corresponding exit code is preserved.
+####
+git_wtree_cmd_locate_noerror() {
+    branch_dir_name=$(git_wtree_cmd_locate $* 2>/dev/null)
+    [ 0 -eq $? ] || return 1
+
+    echo -n "${branch_dir_name}"
+    return 0;
+}
+
+###
+### Drop worktree's directory
+### Arguments:
+### --<name|branch> <branch_name>
+###   Should be specified in a relative way
+###
+git_wtree_cmd_drop() {
+    branch_name=$(_git_wtree_arg_branch_name $*)
+    [ -z "${branch_name}" ] && _git_wtree_show_last_error && return 1
+
+    branch_dir_name=$(_git_wtree_dir_by_branch_name ${branch_name})
+    [ -z "${branch_dir_name}" ] && _git_wtree_show_last_error && return 1
+
+    # echo "- '${branch_name}' to be dropped from '${branch_dir_name}'"
+
+    cmd="git worktree remove ${branch_dir_name}"
+    _git_wtree_exec                                 \
+        "Drop directory of branch '${branch_name}'" \
+        worktree remove ${branch_dir_name}
+    [ 0 -ne $? ] && _git_wtree_show_last_error && return 1
+    return 0
+}
+
+###
+### Create a new worktree branch
+### Arguments:
+### --<name|branch> <branch_name>
+### --dir <worktree_directory name>
+###   Should be specified in a relative way
+###
+git_wtree_cmd_new() {
+    branch_name=$(_git_wtree_arg_branch_name $*)
+    [ -z "${branch_name}" ] && _git_wtree_show_last_error && return 1
+    branch_dir_name=$(_git_wtree_arg_dir_name $*)
+    [ -z "${branch_dir_name}" ] && _git_wtree_show_last_error && return 1
+    parent_branch_dir_name=$(_git_wtree_worktree_root)
+    [ -z "${parent_branch_dir_name}" ] && _git_wtree_show_last_error && return 1
+    
+    branch_dir_name="${parent_branch_dir_name}/${branch_dir_name}"
+    _git_wtree_set_last_error "'${branch_dir_name}' already exists"
+    [ -e "${branch_dir_name}" ] && _git_wtree_show_last_error && return 1
+
+    # echo "- '${branch_name}' to be created in '${branch_dir_name}'"
+   
+    _git_wtree_exec                                              \
+        "Create branch '${branch_name}' in '${branch_dir_name}'" \
+         worktree add -b ${branch_name} ${branch_dir_name} $(_git_wtree_arg commit $*)
+    [ 0 -ne $? ] && _git_wtree_show_last_error && return 1
+    return 0
+}
+
+###
+### List all available worktrees
+### Arguments: none
+###
+git_wtree_cmd_ls() {
+    _git_wtree_exec                \
+        "List available worktrees" \
+        worktree list | awk -F'[][]' '{print $2}'
+    [ 0 -ne $? ] && _git_wtree_show_last_error && return
+}
+
+###
+### Helper to change a current directory to a worktree's one using a specified change dir command. Nothing is executed in a case of any error.
+### Arguments:
+### $1 - change dir command. E.g.: cd, pushd etc.
+### $2 - branch name
+###
+git_wtree_cmd_tool_chdir() {
+    chdir_cmd=$1
+    shift
+    branch_dir_name=$(git_wtree_cmd_locate_noerror --name $1)
+    [ 0 -eq $? ] && eval "${chdir_cmd} ${branch_dir_name}"
+}
+
+###
+### Helper to change a current directory to a worktree's one using 'cd'
+### Arguments:
+### $1 - branch name
+###
+git_wtree_cmd_tool_cd() {
+    git_wtree_cmd_tool_chdir cd $1
+}
+
+###
+### Helper to change a current directory to a worktree's one using 'pushd'
+### Arguments:
+### $1 - branch name
+###
+git_wtree_cmd_tool_pushd() {
+    git_wtree_cmd_tool_chdir pushd $1
+}
+
+git_wtree() {
+    [ 0 -eq $# ] && _git_wtree_set_last_error "Command expected: new, drop, locate" && _git_wtree_show_last_error && return
+
+    cmd=git_wtree_cmd_$1
+    shift
+
+    ${cmd} $*
+    return $?
+}
+
+if [ -n "${GIT_WTREE_ALIAS_ENABLED}" ]; then
+    alias git.wtree=git_wtree
+    alias git.wtree:ls=git_wtree_cmd_ls
+    alias git.wtree:new=git_wtree_cmd_new
+    alias git.wtree:drop=git_wtree_cmd_drop
+    alias git.wtree:cd=git_wtree_cmd_tool_cd
+    alias git.wtree:pushd=git_wtree_cmd_tool_pushd
+fi
+
diff --git a/test-git-wtree.sh b/test-git-wtree.sh
new file mode 100755 (executable)
index 0000000..996f456
--- /dev/null
@@ -0,0 +1,103 @@
+#!/bin/sh
+
+. $(dirname $0)/git-wtree.sh
+
+#set -e
+
+SELF_PID=$$
+SELF_ROOT=$(readlink -e $(dirname $0))
+FIXED_FAKE_ROOT=/tmp/git-wtree-test.root
+FAKE_ROOT=$([ -z "${GIT_WTREE_TEST_FIXED_ROOT}" ] && mktemp -d || (mkdir ${FIXED_FAKE_ROOT} && ${FIXED_FAKE_ROOT}))
+trap cleanup  KILL QUIT EXIT
+
+fail() {
+    echo -n "*** FAILED: "
+    [ $# -gt 0 ] && echo "$*" || echo "unknown reason"
+    kill -9 ${SELF_PID}
+}
+
+cleanup() {
+    cd "${SELF_ROOT}"
+    [ -d "${FAKE_ROOT}" ] && (echo "- Clean the fake root ${FAKE_ROOT} up"; rm -rf "${FAKE_ROOT}")
+}
+
+echo "- Fake root: ${FAKE_ROOT}"
+[ -n "${FAKE_ROOT}" -a -d "${FAKE_ROOT}" ] || fail "fake root ${FAKE_ROOT} location is set"
+
+echo "- Prepare fake git repository"
+mkdir ${FAKE_ROOT}/main.git || fail "fake root ${FAKE_ROOT} is created"
+
+cd ${FAKE_ROOT}/main.git
+git init . || fail "fake git repository is initialized"
+
+touch dummy.file || fail "dummy file in the git repository is created"
+git add dummy.file || fail "dummy file is added to the git repository"
+git commit -m "initial auto commit" || fail "initial commit is made to the git repository"
+
+echo "= TEST: Master is a single available worktree"
+worktrees=$(git_wtree_cmd_ls | wc -l)
+[ 1 -eq ${worktrees} ] || fail "master is an only one workspace"
+
+echo "= TEST(new)"
+echo "== requires --name argument"
+git_wtree_cmd_new 2>&1 | grep -qi 'ERROR.*missing branch name'
+[ 0 -eq $? ] || "error message about missing '--name' argument"
+
+echo "== requires --dir argument"
+git_wtree_cmd_new --name branch-name 2>&1 | grep -qi 'ERROR.*missing directory name'
+[ 0 -eq $? ] || fail "error message about missing '--dir' argument"
+
+echo "== requires 'worktree.root' in config"
+git_wtree_cmd_new --name worktree --dir worktree.d 2>&1 | grep -qi "ERROR.*worktree.root.*should point"
+[ 0 -eq $? ] || fail "error message about missing 'worktree.root' variable"
+
+echo "- Set worktree.root"
+git config --local worktree.root ${FAKE_ROOT} || fail "'worktree.root' is set to a config"
+git config --local worktree.root | grep -qi "${FAKE_ROOT}"
+[ 0 -eq $? ] || fail "'worktree.root' is available through config"
+
+echo "== creates worktree"
+git_wtree_cmd_new --name test-branch-name --dir test-branch.d || fail "new worktree is created"
+git worktree list | grep -q 'test-branch.d.*test-branch-name'
+[ 0 -eq $? ] || fail "newly created worktree is listed"
+
+echo "== fails on a duplicated name"
+git_wtree_cmd_new --name test-branch-name --dir alternative-branch.d 2>&1 | grep -q FAILED
+[ 0 -eq $? ] || fail "error message about failed creation of already existing branch"
+
+echo "= TEST(ls)"
+echo "== Lists worktrees"
+worktrees=$(git_wtree_cmd_ls | grep -E '^master|^test-branch-name' | wc -l)
+[ 2 -eq ${worktrees} ] || fail "Master and the newly created worktree are listed: ${worktrees}"
+
+echo "= TEST(cd)"
+echo "== PWD is changed to worktree"
+pwd | grep -qi ${FAKE_ROOT}/main.git
+[ 0 -eq $? ] || fail "initial directory is main fake root"
+git_wtree_cmd_tool_cd test-branch-name 2>&1
+[ x"${FAKE_ROOT}/test-branch.d" = x"$(pwd)" ] || fail "current directory is test-branche's one"
+
+echo "== PWD is changed to master"
+pwd | grep -qi ${FAKE_ROOT}/test-branch.d
+[ 0 -eq $? ] || fail "initial directory is worktree's one"
+git_wtree_cmd_tool_cd master 2>&1
+[ x"${FAKE_ROOT}/main.git" = x"$(pwd)" ] || fail "current directory is master's one"
+
+echo "= TEST(drop)"
+cd ${FAKE_ROOT}/main.git
+
+echo "== requires --name argument"
+git_wtree_cmd_drop 2>&1 | grep -qi "ERROR.*missing branch name"
+[ 0 -eq $? ] || fail "error message about no candidates to drop"
+
+echo "== drops worktree directory"
+git_wtree_cmd_drop --name test-branch-name || fail "drop command is succeeded"
+worktrees=$(git_wtree_cmd_ls | grep master | wc -l)
+[ 1 -eq ${worktrees} ] || fail "only master branch is left"
+
+echo "== fails on already dropped worktree directory"
+for branch_name in test-branch-name never-existed-branch-name; do
+    git_wtree_cmd_drop --name ${branch_name} 2>&1 | grep -qi "ERROR.*0 candidates"
+    [ 0 -eq $? ] || fail "error message about no candidates to drop"
+done
+