add git-review script (v0.1)
This commit is contained in:
parent
01bf4de331
commit
3b28bc4ff5
1 changed files with 801 additions and 0 deletions
801
.config/shell/bin/git-review
Executable file
801
.config/shell/bin/git-review
Executable file
|
@ -0,0 +1,801 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# git-code-review - easier code reviews
|
||||||
|
# Usage - see `git-code-review --help`
|
||||||
|
|
||||||
|
# I know variables don't expand in single quotes, thank you shellcheck
|
||||||
|
# shellcheck disable=SC2016,SC1007
|
||||||
|
|
||||||
|
# Coding practice:
|
||||||
|
# - Keep main() looking simple, each line should read mostly as english,
|
||||||
|
# because this could get fairly cryptic otherwise. Make things into short
|
||||||
|
# functions to do this.
|
||||||
|
# - Try to make these ^ abstractions only one-level deep, so there isn't more
|
||||||
|
# abstraction than needed
|
||||||
|
|
||||||
|
# File layout:
|
||||||
|
# - error/logging functions (eg. error_basic)
|
||||||
|
# - git value getters (eg. get_git_remote_url)
|
||||||
|
# - value checkers (eg. exists_remote)
|
||||||
|
# - git mutating operations
|
||||||
|
# - adding operations (eg. create_remote)
|
||||||
|
# - removing operations (eg. remove_remote)
|
||||||
|
# - repo setup functions (eg. action_on_both_repos)
|
||||||
|
# - generic helper functions (eg. search_replace)
|
||||||
|
# - usage (help page)
|
||||||
|
# - argument parsing (eg. parse_ref)
|
||||||
|
# - main
|
||||||
|
|
||||||
|
# set -x
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# global for the name of the script
|
||||||
|
script_name="${0##*/}"
|
||||||
|
|
||||||
|
# Call printf, write to stderr
|
||||||
|
printf_err() {
|
||||||
|
printf "$@" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call echo, write to stderr
|
||||||
|
echo_err() {
|
||||||
|
echo "$@" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Errors for basic usage things
|
||||||
|
# $1 - message type (eg. "Error")
|
||||||
|
# $2 - message (eg. "Monkeys failed to climb trees")
|
||||||
|
# $@?- Additional message (will be printed with a space before each line)
|
||||||
|
error_basic() {
|
||||||
|
type="${1:-ERROR}"; shift
|
||||||
|
msg_main="${1:-UNDEFINED ERROR}"; shift
|
||||||
|
printf_err '%s ' "$script_name:" "$type:" "$msg_main"
|
||||||
|
printf_err '\n'
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
printf_err ' %s\n' "$@"
|
||||||
|
printf_err '\n'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Errors for internal issues
|
||||||
|
# $1 - caller name (your $0)
|
||||||
|
# $2 - error line (your $LINENO)
|
||||||
|
# $@ - options for error_basic
|
||||||
|
error() {
|
||||||
|
caller="$1"; shift
|
||||||
|
line="$1"; shift
|
||||||
|
printf_err '%s ' "$script_name: In function $caller, line $line:"
|
||||||
|
error_basic 'Error' "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Errors that should exit for internal issues
|
||||||
|
# $1 - message
|
||||||
|
# $2 - Additional message (will be printed with a space before each line)
|
||||||
|
fatal_error() {
|
||||||
|
error "$@"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Errors that should exit for user-facing issues
|
||||||
|
# See error_basic for usage
|
||||||
|
fatal_error_basic() {
|
||||||
|
error_basic "$@"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate the value of an argument and exit if it's bad
|
||||||
|
# $1 - prepend this to the err_msg variable
|
||||||
|
# $@ - command to run to validate argument
|
||||||
|
# $err_msg - a variable containing the error to print
|
||||||
|
arg_assert() {
|
||||||
|
# prepend to the error
|
||||||
|
err_msg_internal="$1${err_msg:-}"
|
||||||
|
shift
|
||||||
|
if ! "$@"; then
|
||||||
|
echo_err 'Error: Invalid argument:' "$err_msg_internal"
|
||||||
|
arg_error=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call this after all arg_assert calls to exit if there was an error
|
||||||
|
arg_assert_end() {
|
||||||
|
if [ "${arg_error:-0}" != 0 ]; then
|
||||||
|
exit "$arg_error"
|
||||||
|
else
|
||||||
|
unset arg_error
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prints variables passed into it
|
||||||
|
# $@ - the name of the variables (eg. 'myvar', not "$myvar")
|
||||||
|
log_vars() {
|
||||||
|
echo "Variable dump:"
|
||||||
|
for var; do
|
||||||
|
if [ "$var" = '' ]; then
|
||||||
|
echo
|
||||||
|
else
|
||||||
|
eval value="\"\$$var\""
|
||||||
|
# shellcheck disable=SC2154
|
||||||
|
printf ' %s\t%s\n' "$var" "$value"
|
||||||
|
fi
|
||||||
|
done | column -tl 2 | sed 's/^/ /'
|
||||||
|
}
|
||||||
|
|
||||||
|
# exit if cmd is false
|
||||||
|
# $1 - name of calling function (your $0)
|
||||||
|
# $2 - line called on
|
||||||
|
# $@ - the command to assert
|
||||||
|
assert() {
|
||||||
|
caller="$1"; shift
|
||||||
|
line="$1"; shift
|
||||||
|
if ! ("$@"); then
|
||||||
|
fatal_error "$caller" "$line" "Assertion failure: $*"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Git getters
|
||||||
|
#
|
||||||
|
|
||||||
|
# find the folder for a branch's worktree, if it has one
|
||||||
|
# $1 - branch to check for
|
||||||
|
get_worktree_folder() {
|
||||||
|
branch="$1"
|
||||||
|
assert "$0" "$LINENO" exists_branch "$branch"
|
||||||
|
git worktree list | grep -F "[$branch]" | rev | cut -d ' ' -f 3- | sed -E 's/^ +//g' | rev
|
||||||
|
}
|
||||||
|
|
||||||
|
get_current_remote() {
|
||||||
|
if ! git rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2> /dev/null; then
|
||||||
|
printf ''
|
||||||
|
:
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
get_git_remote_url_nofail() {
|
||||||
|
# Test a few potential remotes
|
||||||
|
# WARN: this is a bit hard-coded based on standard practices, as well as my own practices
|
||||||
|
remotes=(upstream origin "$(get_current_remote)")
|
||||||
|
for remote_url in "${remotes[@]}"; do
|
||||||
|
if remote_url="$(git remote get-url "$remote_url")"; then
|
||||||
|
printf '%s' "$remote_url"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
printf 'NONE'
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the url for a git repository's remote, choosing from a few options.
|
||||||
|
# Accepts no arguments.
|
||||||
|
get_git_remote_url() {
|
||||||
|
if ! get_git_remote_url_nofail; then
|
||||||
|
# If no remotes were found, we get here
|
||||||
|
fatal_error "$0" "$LINENO" \
|
||||||
|
"Couldn't find a valid remote. Please specify a remote url with the --remote-url option." \
|
||||||
|
"See $script_name --help for more information."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
get_git_branch_current() {
|
||||||
|
git rev-parse --abbrev-ref --symbolic-full-name HEAD
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Checker functions
|
||||||
|
#
|
||||||
|
|
||||||
|
# Check if a stream contains a stream
|
||||||
|
# $1 - regex to filter the stream for the search term (passed to grep -P)
|
||||||
|
# $2 - the search term (must match the whole line)
|
||||||
|
check_exists() {
|
||||||
|
regex="$1"; shift
|
||||||
|
term="$1"; shift
|
||||||
|
grep -Po "$regex" | grep -Fxq "$term"
|
||||||
|
}
|
||||||
|
|
||||||
|
exists_remote() {
|
||||||
|
remote="$1"
|
||||||
|
git remote | check_exists '^.+$' "$remote"
|
||||||
|
}
|
||||||
|
|
||||||
|
# check if a branch exists
|
||||||
|
# $1 - branch
|
||||||
|
exists_branch() {
|
||||||
|
branch="$1"
|
||||||
|
git branch | check_exists '..\K.+$' "$branch"
|
||||||
|
}
|
||||||
|
|
||||||
|
# check if a worktree exists with a specific branch attached to it
|
||||||
|
# $1 - branch
|
||||||
|
exists_branch_as_worktree() {
|
||||||
|
branch="$1"
|
||||||
|
git worktree list | check_exists '\[[^\]]+\]$' "[$branch]"
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Git mutating operation functions
|
||||||
|
#
|
||||||
|
|
||||||
|
# Fetch a remote
|
||||||
|
# $1 - the remote to fetch
|
||||||
|
fetch_remote() {
|
||||||
|
remote="$1"
|
||||||
|
shell_cmd git fetch "$remote"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add a remote if it doesn't exist
|
||||||
|
# $1 - Name of the remote
|
||||||
|
# $2 - URL to the remote
|
||||||
|
create_remote() {
|
||||||
|
name="$1"; shift
|
||||||
|
url="$1"; shift
|
||||||
|
# Check if we have the remote already
|
||||||
|
if ! exists_remote "$name"; then
|
||||||
|
shell_cmd git remote add "$name" "$url"
|
||||||
|
else
|
||||||
|
shell_cmd git remote set-url "$name" "$url"
|
||||||
|
fi
|
||||||
|
fetch_remote "$remote_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
# remove a remote
|
||||||
|
# $1 - remote to remove
|
||||||
|
remove_remote() {
|
||||||
|
remote="$1"
|
||||||
|
shell_cmd git remote remove "$remote"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create a git branch and set its upstream
|
||||||
|
# $1 - branch name
|
||||||
|
# $2 - remote name
|
||||||
|
# $3 - remote branch name
|
||||||
|
create_branch() {
|
||||||
|
branch="$1"; shift
|
||||||
|
remote="$1"; shift
|
||||||
|
remote_branch="$1"; shift
|
||||||
|
if ! exists_branch "$branch"; then
|
||||||
|
shell_cmd git branch "$branch" "$remote/$remote_branch"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# remove a branch
|
||||||
|
# $1 - branch to remove
|
||||||
|
remove_branch() {
|
||||||
|
branch="$1"; shift
|
||||||
|
shell_cmd git branch -d "$branch"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create a worktree if it doesn't exist
|
||||||
|
# $1 - where to put the worktree
|
||||||
|
# $2 - branch the worktree should point at (MUST ALREADY EXIST)
|
||||||
|
create_worktree() {
|
||||||
|
dir="$1"; shift
|
||||||
|
branch="$1"; shift
|
||||||
|
|
||||||
|
# Ensure the branch exists. It should be created before this function is called.
|
||||||
|
assert "$0" "$LINENO" exists_branch "$branch"
|
||||||
|
|
||||||
|
# See if a worktree already exists with the branch attached
|
||||||
|
if ! exists_branch_as_worktree "$branch"; then
|
||||||
|
shell_cmd git worktree add --checkout "$dir" "$branch"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove a worktree if it exists
|
||||||
|
# $1 - worktree dir to remove
|
||||||
|
remove_worktree() {
|
||||||
|
dir="$1"; shift
|
||||||
|
shell_cmd git worktree remove "$dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Repo setup functions
|
||||||
|
#
|
||||||
|
|
||||||
|
# Symlinks files from a git repository to another place
|
||||||
|
# $@ - command to evaluate, with '$f' passed where a file should be
|
||||||
|
# pipe - list of relative paths to files that should be linked from src to dst
|
||||||
|
action_on_both_repos() {
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
while IFS=$'\n' read -r f; do
|
||||||
|
# shellcheck disable=SC2294
|
||||||
|
eval "$@"
|
||||||
|
# error_basic 'Log' "Eval action:" "$@"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
get_ignored_files() {
|
||||||
|
git clean -ndX | grep -Po 'Would remove \K.*$' | sed -E 's-/$--'
|
||||||
|
}
|
||||||
|
|
||||||
|
get_submodules() {
|
||||||
|
git submodule | grep -Po '.[^ ]+ \K.*$' | rev | grep -Po '[^( ]+\( \K.*$' | rev
|
||||||
|
}
|
||||||
|
|
||||||
|
# See usage for `symlink_targets`. Takes no pipe input
|
||||||
|
symlink_ignored() {
|
||||||
|
get_ignored_files | action_on_both_repos cp -lr "$1"/'$f' "$2"/'$f'
|
||||||
|
}
|
||||||
|
|
||||||
|
symlink_subs() {
|
||||||
|
# Hardlink files
|
||||||
|
get_submodules | action_on_both_repos rmdir "$2"/'$f' '&&' cp -lr "$1"/'$f' "$2"/'$f'
|
||||||
|
# Update submodule ".git" files
|
||||||
|
get_submodules | action_on_both_repos rm "$2"/'$f'/.git '&&' echo 'gitdir: '"$1"'/.git/modules/$f' '>' "$2"/'$f'/.git
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_symlinked_ignores() {
|
||||||
|
get_ignored_files | action_on_both_repos rm -r "$2"/'$f'
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_symlinked_subs() {
|
||||||
|
# error_basic Info Removing submodules
|
||||||
|
get_submodules | action_on_both_repos rm -r "$2"/'$f'
|
||||||
|
get_submodules | action_on_both_repos git -C "$2" restore "$2"/'$f'
|
||||||
|
}
|
||||||
|
|
||||||
|
create_links() {
|
||||||
|
symlink_ignored "$@" || true
|
||||||
|
symlink_subs "$@" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_links() {
|
||||||
|
remove_symlinked_ignores "$@" || true
|
||||||
|
remove_symlinked_subs "$@" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Generic helper functions
|
||||||
|
#
|
||||||
|
|
||||||
|
# run a shell command and print the command that was run
|
||||||
|
shell_cmd() {
|
||||||
|
Green="[32m"
|
||||||
|
Nc="(B[m" # No Color
|
||||||
|
printf_err '%s' "$Green"
|
||||||
|
printf_err '%s' " \$ $*$Nc"
|
||||||
|
printf_err '\n'
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run a command in a directory without changing the current directory
|
||||||
|
# $1 - What directory to run it in
|
||||||
|
# $@ - Command to run
|
||||||
|
shell_cmd_dir() {
|
||||||
|
dir="$1"; shift
|
||||||
|
(
|
||||||
|
# Cd
|
||||||
|
shell_cmd cd "$dir" || fatal_error "$0" "$LINENO" "Couldn't \`cd\` into directory:" " $dir"
|
||||||
|
# Run
|
||||||
|
shell_cmd "$@"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract a field N fields from the end.
|
||||||
|
# $1? - how many fields from the end to select (default: 1)
|
||||||
|
# $2? - field separator (default: /)
|
||||||
|
get_from_end() {
|
||||||
|
dist_from_end="${1:-1}"; shift
|
||||||
|
sep="${1:-/}"; shift
|
||||||
|
rev | cut -d "${sep:0:1}" -f "$dist_from_end" | rev
|
||||||
|
}
|
||||||
|
|
||||||
|
# Search and replace a string *literal* (WITHOUT evaluating a regular expression or code of any kind)
|
||||||
|
# $1 - search term
|
||||||
|
# $2 - replace with
|
||||||
|
# pipe - input
|
||||||
|
search_replace() {
|
||||||
|
search="$1"; shift
|
||||||
|
replace="$1"; shift
|
||||||
|
awk -v search="$search" -v replace="$replace" '{sub(search, replace); print}'
|
||||||
|
}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Usage information (--help)
|
||||||
|
#
|
||||||
|
|
||||||
|
# FUTURE INTERFACE FOR `opt` (NOT YET IMPLEMENTED)
|
||||||
|
# # makes an option for getopts
|
||||||
|
# # $1 - short option, single character. (eg. 'f')
|
||||||
|
# # $2 - long option (eg. 'file')
|
||||||
|
# # $3 - option category, controls what array the options' descriptions are put in (eg. 'info' puts it in opts_info)
|
||||||
|
# # $4?- argument for the option, for `getopt` (eg. ':' or '::')
|
||||||
|
# # $4?- true/false to indicate whether the option is a boolean option. Requires
|
||||||
|
# # a long opt to be passed. Generates a corresponding version of the long
|
||||||
|
# # option with "no-" prepended to it. (eg. 'true', which generates
|
||||||
|
# # `--cool-opt` and `--no-cool-opt`)
|
||||||
|
|
||||||
|
# $1 - short option, single character. (eg. 'f')
|
||||||
|
# $2 - long option (eg. 'file')
|
||||||
|
# $3?- argument for the option, for `getopt` (eg. ':' or '::')
|
||||||
|
# $3?- true/false to indicate whether the option is a boolean option. Requires
|
||||||
|
# a long opt to be passed. Generates a corresponding version of the long
|
||||||
|
# option with "no-" prepended to it. (eg. 'true', which generates
|
||||||
|
# `--cool-opt` and `--no-cool-opt`)
|
||||||
|
# TODO: make this create data to go in the usage information
|
||||||
|
opt() {
|
||||||
|
# help_category="${1:-info}"; shift
|
||||||
|
short="${1:0:1}"; shift
|
||||||
|
long="$1"; shift
|
||||||
|
arg="${1:-false}"; shift
|
||||||
|
# arg_type="${1:-}"; shift
|
||||||
|
|
||||||
|
# # take data from pipe
|
||||||
|
# if [ -p /dev/stdin ];then
|
||||||
|
# desc="$(cat)"
|
||||||
|
# else
|
||||||
|
# fatal_error "$0" "$LINENO" "Received no description for command $long (category $help_category)"
|
||||||
|
# fi
|
||||||
|
|
||||||
|
case "$arg" in
|
||||||
|
true) arg= getopts_long+="${getopts_long:+,}no-$long" ;; #longbool_help="[no-]"
|
||||||
|
false) arg=;;
|
||||||
|
# :) arg_type="";;
|
||||||
|
:|::);;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$short" in
|
||||||
|
'') ;;#shorthelp=" "
|
||||||
|
*) getopts_short+="$short$arg";; #shorthelp="-$short,"
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$long" in
|
||||||
|
'') ;; #longhelp=
|
||||||
|
*) getopts_long+="${getopts_long:+,}$long$arg";; #longhelp="--$longbool_help$long"
|
||||||
|
esac
|
||||||
|
|
||||||
|
# # help stuff
|
||||||
|
# eval opts_"$help_category"+='("$shorthelp$longhelp" "$desc")'
|
||||||
|
}
|
||||||
|
|
||||||
|
opt_is_supported() {
|
||||||
|
[[ "$1" =~ $2 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
opt_accepts_arg() {
|
||||||
|
[[ "$1:" =~ $2 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
opt_requires_arg() {
|
||||||
|
! [[ "$1::" =~ $2 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
# ideas for new options
|
||||||
|
# $script_name review [(--no-cleanup | )] <REF> [CMD...] Open a shell to review code from REF, then clean up
|
||||||
|
# $script_name clean [REF] Clean the results of REF
|
||||||
|
# --no-cleanup Don't remove the remote and cloned code by default
|
||||||
|
|
||||||
|
cat << EOF
|
||||||
|
$script_name - review changes on a remote branch
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
$script_name [opts] <branch_ref> [cmd...]
|
||||||
|
$script_name review [opts] <branch_ref> [cmd...]
|
||||||
|
$script_name clean [opts] [branch_ref]
|
||||||
|
|
||||||
|
Subcommands: (NOT IMPLEMENTED)
|
||||||
|
(none) If no subcommand is specified, the review subcommand will be assumed.
|
||||||
|
review Review branch_ref and execute the cmd
|
||||||
|
clean Remove remotes, branches, and worktrees associated with a
|
||||||
|
specific branch_ref. If no branch_ref is provided, all remotes,
|
||||||
|
branches, and worktrees containing the "review" prefix will be
|
||||||
|
pruned.
|
||||||
|
|
||||||
|
Options for all subcommands: (NOT IMPLEMENTED)
|
||||||
|
$(printf "%s\n" "${opts_info[@]}" | paste - - | column -tc 80 -s $'\t' -N opt,desc -W desc -d)
|
||||||
|
|
||||||
|
Options for 'review' subcommand: (NOT IMPLEMENTED)
|
||||||
|
--[no-]clean-on-success Clean the worktree only if the command exits with
|
||||||
|
error code 0.
|
||||||
|
|
||||||
|
Options for 'clean' subcommand: (NOT IMPLEMENTED)
|
||||||
|
-f, --force Force clean worktree (any changes will be lost).
|
||||||
|
|
||||||
|
Definitions:
|
||||||
|
branch_ref - Where the remote code and branch is. One of the following:
|
||||||
|
BRANCH branch located on the default remote
|
||||||
|
USERNAME:BRANCH username and branch to use as the source for a remote.
|
||||||
|
Repository name is be assumed to be the same as the
|
||||||
|
default remote. (currently $(get_git_remote_url_nofail))
|
||||||
|
USERNAME/REPO username and repo to use as the source for the
|
||||||
|
remote (current branch is assumed to be the name of
|
||||||
|
the remote branch)
|
||||||
|
USERNAME/REPO:BRANCH repository, username, and branch name to use as the
|
||||||
|
|
||||||
|
cmd - A command and arguments to run on the remote. Default: \`bash\`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
$ pwd
|
||||||
|
~/git/gimp
|
||||||
|
$ git remote get-url origin
|
||||||
|
https://gitlab.gnome.org/GNOME/gimp
|
||||||
|
$ $script_name joe/cool-feature
|
||||||
|
# git remote add, git fetch, git worktree add
|
||||||
|
$ pwd
|
||||||
|
~/.cache/$script_name/gimp/joe-cool-feature
|
||||||
|
$ exit
|
||||||
|
# git worktree remove, rm -r, git remote remove
|
||||||
|
$ pwd
|
||||||
|
~/git/gimp
|
||||||
|
$ ls ~/.cache/$script_name/gimp/joe-cool-feature
|
||||||
|
No such file or directory.
|
||||||
|
|
||||||
|
The URL for the remote git repository is needed to be able to make new remotes.
|
||||||
|
Since many code forges follow the same format for their URL's, we are often
|
||||||
|
able to extrapolate the information we need to make a new remote based on just
|
||||||
|
the contents of the URL on the defult remote.
|
||||||
|
The assumed format for URL's on remotes is as follows:
|
||||||
|
https://git.example.net/joe-mama/code
|
||||||
|
git@forge.site.io:joe-mama/code
|
||||||
|
^~~~~~~^ ^~~^
|
||||||
|
USERNAME REPO
|
||||||
|
The USERNAME and REPO parts of the URL will be replaced with their
|
||||||
|
corresponding parts from the given branch_ref to create a new URL. If the remote
|
||||||
|
on your URL doesn't follow this format, specify the URL with the --remote-url
|
||||||
|
option. In this case, the specified branch_ref will still be used to get
|
||||||
|
information used for creating folders.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse a branch_ref into the ref_type, username, repo, and branch variables
|
||||||
|
# $1 - a branch_ref
|
||||||
|
# stdout - the normalized ref
|
||||||
|
normalize_ref() {
|
||||||
|
ref="$1"
|
||||||
|
|
||||||
|
# assume U/R:B form, and take stuff from it
|
||||||
|
# take the end of ref
|
||||||
|
branch_name_fromref="${ref##*:}"
|
||||||
|
# take the beginning of ref
|
||||||
|
username_fromref="${ref%%/*}"
|
||||||
|
# isolate the middle part of the ref
|
||||||
|
repo_fromref="${ref#*/}"
|
||||||
|
repo_fromref="${repo_fromref%:*}"
|
||||||
|
|
||||||
|
# TODO: process --remote-url here
|
||||||
|
remote_url_fromlocalrepo="$(get_git_remote_url)"
|
||||||
|
# remove all trailing slashes from the url
|
||||||
|
remote_url_fromlocalrepo="${remote_url_fromlocalrepo%%/}"
|
||||||
|
|
||||||
|
# Default value for the repo
|
||||||
|
## assign to the repo name part of the url - "foo[:/]username/(repo).git"
|
||||||
|
repo_fromremote="${remote_url_fromlocalrepo##*/}" # url with everything but the last slash
|
||||||
|
repo_fromremote="${repo_fromremote%.git}" # remove ".git" (if it's there)
|
||||||
|
## assign to the username part of the url - "foo[:/](username)/repo.git"
|
||||||
|
username_fromremote="$(echo "$remote_url_fromlocalrepo" | search_replace "/$repo_fromremote" "" | rev | grep -Po '^[^/:]+' | rev)"
|
||||||
|
|
||||||
|
# Decide how to proceed based on the format of ref
|
||||||
|
case "$ref" in
|
||||||
|
*/*:*)
|
||||||
|
ref_type="USERNAME/REPO:BRANCH"
|
||||||
|
username="$username_fromref"
|
||||||
|
repo="$repo_fromref"
|
||||||
|
branch="$branch_name_fromref"
|
||||||
|
;;
|
||||||
|
*:*)
|
||||||
|
ref_type="USERNAME:BRANCH"
|
||||||
|
username="${ref%:*}"
|
||||||
|
repo="$repo_fromremote"
|
||||||
|
branch="$branch_name_fromref"
|
||||||
|
;;
|
||||||
|
*/*)
|
||||||
|
ref_type="USERNAME/REPO"
|
||||||
|
username="$username_fromref"
|
||||||
|
repo="$repo_fromref"
|
||||||
|
branch="$(get_git_branch_current)"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
ref_type="BRANCH"
|
||||||
|
username="$username_fromremote"
|
||||||
|
repo="$repo_fromremote"
|
||||||
|
branch="$ref"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
ref="$username/$repo:$branch"
|
||||||
|
}
|
||||||
|
|
||||||
|
process_opt() {
|
||||||
|
case "$1" in
|
||||||
|
-h|--help) usage; exit;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set the username, repo, and branch_name_remote variables from a ref
|
||||||
|
# $1 - a branch ref to pass to normalize_ref
|
||||||
|
setup_from_ref() {
|
||||||
|
ref="$1"
|
||||||
|
|
||||||
|
normalize_ref "$1"
|
||||||
|
|
||||||
|
# take the beginning of ref
|
||||||
|
username="${ref%%/*}"
|
||||||
|
# isolate the middle part of the ref
|
||||||
|
repo="${ref#*/}"
|
||||||
|
repo="${repo%:*}"
|
||||||
|
# take the end of ref
|
||||||
|
branch_name_remote="${ref##*:}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse arguments into variables that the rest of the script needs
|
||||||
|
process_opts() {
|
||||||
|
opt h help
|
||||||
|
opt v verbose
|
||||||
|
opt r remote-url :
|
||||||
|
opt w worktree-dir :
|
||||||
|
opt f force
|
||||||
|
opt c clean-on-success true
|
||||||
|
|
||||||
|
# opts_long=('help,verbose,remote-url:,worktree-dir:,force,clean-on-success,no-clean-on-success')
|
||||||
|
# args=$(getopt \
|
||||||
|
# -o "$getopts_short" \
|
||||||
|
# --long "$getopts_long" \
|
||||||
|
# -n "$script_name" \
|
||||||
|
# -- "$@")
|
||||||
|
#
|
||||||
|
# getopt_exitcode=$?
|
||||||
|
# if [ $getopt_exitcode != 0 ]; then
|
||||||
|
# exit $getopt_exitcode
|
||||||
|
# fi
|
||||||
|
|
||||||
|
# track errors
|
||||||
|
error_encountered=false
|
||||||
|
# throw errors when passed --opt=arg when opt takes no argument
|
||||||
|
error_on_longopts_withEqualSigns_thatTakeNoArgs=true
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
case "$1" in
|
||||||
|
(--) # Process command
|
||||||
|
shift;
|
||||||
|
# now, ${@:-bash} is the command to run
|
||||||
|
cmd=("${@:-${SHELL}}")
|
||||||
|
break;
|
||||||
|
;;
|
||||||
|
(--*) # longopts
|
||||||
|
|
||||||
|
# --opt=arg -> --opt
|
||||||
|
opt="${1%%=*}"
|
||||||
|
|
||||||
|
# --opt=arg -> =arg
|
||||||
|
# --opt -> --opt
|
||||||
|
argeq="${1/#*=/=}"
|
||||||
|
|
||||||
|
shift
|
||||||
|
|
||||||
|
if opt_is_supported "$opt" "$getopts_long"; then
|
||||||
|
# Handle arguments set with equal signs
|
||||||
|
case "$argeq" in
|
||||||
|
=*) # --opt=arg
|
||||||
|
if opt_accepts_arg "$opt" "$getopts_long"; then
|
||||||
|
arg="${argeq#=}"
|
||||||
|
elif [ "$error_on_longopts_withEqualSigns_thatTakeNoArgs" = true ]; then
|
||||||
|
error="Option $opt recieved an argument ('$arg'), but does not accept arguments"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*) # --opt arg OR --opt --otheropt
|
||||||
|
if opt_requires_arg "$opt" "$getopts_long"; then
|
||||||
|
if [ $# = 0 ]; then
|
||||||
|
error="Option $opt requires an argument, but none was given"
|
||||||
|
else
|
||||||
|
arg="$1"
|
||||||
|
fi
|
||||||
|
elif opt_accepts_arg "$opt" "$getopts_long"; then
|
||||||
|
# If we get here, the option doesn't require an argument, but it may optionally accept one
|
||||||
|
if [ $# != 0 ]; then
|
||||||
|
case "$1" in
|
||||||
|
-*) ;; # option will get no argument since next argument is an option (whether the option is a valid option or not)
|
||||||
|
*) arg="$1"; shift;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Now our option is normalized and $@ has been shifted to not
|
||||||
|
# include it or any of the arguments it takes
|
||||||
|
|
||||||
|
# if arg is set (if we have an argument), pass it along
|
||||||
|
if [ "${arg:+true}" = true ]; then
|
||||||
|
process_opt "$opt" "$arg" "$@"
|
||||||
|
else
|
||||||
|
process_opt "$opt" "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
error="No such option: $opt"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
(-*);;
|
||||||
|
(*);;
|
||||||
|
esac
|
||||||
|
if [ "$error" != false ]; then
|
||||||
|
error=false
|
||||||
|
# error_encountered=true
|
||||||
|
error_basic 'Error' "$error"
|
||||||
|
shift
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if [ $# = 0 ]; then
|
||||||
|
error_basic 'Error' "Please pass at least one argument."
|
||||||
|
printf_err '\n'
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ensure running inside a git repository
|
||||||
|
if ! git_repo="$(git rev-parse --show-toplevel)"; then
|
||||||
|
fatal_error "$0" "$LINENO" "Please run from a git repository"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ref="$1"
|
||||||
|
setup_from_ref "$ref"
|
||||||
|
shift
|
||||||
|
|
||||||
|
cmd=("${@:-${SHELL}}")
|
||||||
|
|
||||||
|
# TODO: process --remote-url here
|
||||||
|
remote_url="$(echo "$remote_url_fromlocalrepo" | search_replace "$username_fromremote" "$username" | search_replace "$repo_fromremote" "$repo")"
|
||||||
|
|
||||||
|
err_msg=" in $ref_type-style branch_ref cannot be empty"
|
||||||
|
arg_assert url [ "$remote_url" != '' ]
|
||||||
|
arg_assert repo [ "$repo" != '' ]
|
||||||
|
arg_assert branch [ "$branch_name_remote" != '' ]
|
||||||
|
arg_assert username [ "$username" != '' ]
|
||||||
|
arg_assert_end
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
k_git_ref_prefix=review
|
||||||
|
|
||||||
|
# derive:
|
||||||
|
# - REMOTE (review-USER)
|
||||||
|
# - BRANCH_LOCAL (review-BRANCH)
|
||||||
|
# - WORKTREE_FOLDER (~/.cache/REPO/REMOTE_BRANCH)
|
||||||
|
|
||||||
|
# Derived variables
|
||||||
|
remote_name="$k_git_ref_prefix-$username"
|
||||||
|
branch_name_local="$k_git_ref_prefix-$username-$branch_name_remote"
|
||||||
|
worktree_folder="${XDG_CACHE_HOME:-"$HOME"/.cache}/$script_name/$repo/$username-$branch_name_remote"
|
||||||
|
|
||||||
|
clean_worktree=true
|
||||||
|
|
||||||
|
#
|
||||||
|
# Main actions
|
||||||
|
#
|
||||||
|
|
||||||
|
# make remote
|
||||||
|
create_remote "$remote_name" "$remote_url"
|
||||||
|
# make branch
|
||||||
|
create_branch "$branch_name_local" "$remote_name" "$branch_name_remote"
|
||||||
|
# make worktree if it doesn't exist
|
||||||
|
if ! exists_branch_as_worktree "$branch_name_local"; then
|
||||||
|
# make worktree
|
||||||
|
create_worktree "$worktree_folder" "$branch_name_local"
|
||||||
|
create_links "$git_repo" "$worktree_folder"
|
||||||
|
else
|
||||||
|
# worktree already exists, use it
|
||||||
|
worktree_folder="$(get_worktree_folder "$branch_name_local")"
|
||||||
|
# Since we didn't create the worktree, don't remove it
|
||||||
|
# clean_worktree=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# do the command in the worktree folder
|
||||||
|
shell_cmd_dir "$worktree_folder" "${cmd[@]}"
|
||||||
|
|
||||||
|
if [ "$clean_worktree" = true ]; then
|
||||||
|
# clean symlinked files
|
||||||
|
remove_links "$git_repo" "$worktree_folder" &&
|
||||||
|
# clean up worktree
|
||||||
|
remove_worktree "$worktree_folder" &&
|
||||||
|
# clean up branch
|
||||||
|
remove_branch "$branch_name_local" &&
|
||||||
|
# clean up remote
|
||||||
|
remove_remote "$remote_name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
Loading…
Add table
Add a link
Reference in a new issue