#!/bin/bash set -eE -o pipefail # GLOBAL VARIABLES: # - UPSTREAM_BRANCH: The branch in the upstream repo used for updates (defaults to remote's head if set) # - REMOTE: Name of the remote used for updates # - REMOTE_URL: Name of URL used for synchronization # USAGE: # # Initialize a project: # git-update-agent --init https://git.example.com # # Update: # git-update-agent CMD_PROMPT="[#] " PROGRAM="${0##*/}" die () { if [[ $# -eq 0 ]]; then echo -n "$PROGRAM: " 1>&2 cat <&0 1>&2 elif [[ $# -eq 1 ]]; then echo "$PROGRAM: $1" 1>&2 fi exit 1 } depends() { if ! type "$1" >/dev/null; then die "\"$1\" must be installed and in your \$PATH" fi } cmd() { echo "$CMD_PROMPT $*" "$@" } stash() { if ! git \ -c "user.email=git-updater@localhost" \ -c "user.name=git-updater" \ stash save --include-untracked &>/dev/null then die <<_EOF Could not stash in $(pwd) Please stash/commit manually if you need to keep your changes. If you would like to destroy all changes, run: cd $(pwd) git reset --hard origin/main _EOF fi } current_version() { git rev-parse -q --verify HEAD } die_no_upstream_found() { local remote remote="$1" die <<_EOF $remote/HEAD is used as the reference for updates $remote/HEAD is not set and UPSTREAM_BRANCH override not provided You can set the upstream branch with: git remote set-head $remote _EOF } upstream_branch() { local upstream_branch local remote remote="$1" if [[ -z "$UPSTREAM_BRANCH" ]]; then upstream_branch="$(git symbolic-ref "refs/remotes/$remote/HEAD" 2>/dev/null)" upstream_branch="${upstream_branch#refs/remotes/$remote/}" else upstream_branch="$UPSTREAM_BRANCH" fi echo "$upstream_branch" } local_branch() { local branch branch="$(git symbolic-ref HEAD)" branch="${branch#refs/heads/}" echo "$branch" } merge() { local STASHED local remote local remote_branch local local_branch remote="$1" remote_branch="$2" local_branch="$3" local remote_ref remote_ref="$remote/$remote_branch" if [[ -n "$(git status --untracked-files=all --porcelain 2>/dev/null)" ]]; then # Abort rebase or merge if initiated git merge --abort &>/dev/null git rebase --abort &>/dev/null stash STASHED="1" fi # Attempt fetch to REMOTE/UPSTREAM_BRANCH if ! git fetch --tags --force "$remote" \ "refs/heads/$remote_branch:refs/remotes/$remote_ref" \ 2>/dev/null then die "Cannot fetch from $remote_ref from $local_branch" fi # Try fast-forward merge git merge --no-edit --ff "$remote_ref" \ --strategy=recursive \ --strategy-option=ours \ --strategy-option=ignore-all-space \ >/dev/null if [[ -n "$STASHED" && -z "$NO_STASH_POP" ]]; then git stash pop &>/dev/null fi } remote() { local remote if [[ -z "$REMOTE" ]]; then remote="origin" else remote="$REMOTE" fi echo "$remote" } cd_or_die() { cd "$1" || die "Cannot enter directory $1. Exiting." } die_no_git_dir() { die <<_EOF Cannot find a .git directory in $(pwd). Try initializing repo with: $PROGRAM --init-with https://git.example.com To automatically create a git repo, consider setting "REMOTE_URL" _EOF } init_if_empty() { if [[ ! -d ".git" ]]; then if [[ -z "$REMOTE_URL" ]]; then die_no_git_dir else init_repo "$1" "$REMOTE_URL" echo "INITIALIZED" fi fi } should_track() { echo $(git config updateagent.track) } set_should_track() { git config updateagent.track "$1" } die_not_tracking() { die <<_EOF $PROGRAM is not tracking $(pwd). To enable tracking: $PROGRAM --track _EOF } print_usage() { cat <<_EOF Usage: ${PROGRAM} [OPTIONS]... -h, --help output this help info -d, --dir update in directory --init-with initialize a project with the given URL --track track a git repository not initialized with $PROGRAM --disable-tracking disable tracking --version output version _EOF } # # SUBCOMMANDS # set_tracking() { local dir dir="$1" local track track="$2" cd_or_die "$dir" if [[ ! -d ".git" ]]; then die <<_EOF $(pwd) is not the root of a git project _EOF fi set_should_track "$2" } update() { local dir local previous_version local next_version dir="$1" cd_or_die "$dir" previous_version="$(current_version)" if [[ "$(should_track)" != "true" ]]; then die_not_tracking fi local remote local remote_branch local local_branch remote="$(remote)" remote_branch="$(upstream_branch "$remote")" if [[ -z "$remote_branch" ]]; then die_no_upstream_found "$remote" fi local_branch="$(local_branch)" merge "$remote" "$remote_branch" "$local_branch" next_version="$(current_version)" if [[ "$previous_version" == "" ]]; then echo "Initialized $PROGRAM with \"$previous_version\"" elif [[ "$previous_version" != "$next_version" ]]; then echo "Updated $PROGRAM from version \"$previous_version\" to \"$next_version\"" else echo "$PROGRAM is already up to date." fi } clean_partial_clone() { if [[ -n "${INIT_DIR}" ]]; then rm -rf "${INIT_DIR}" else rm -rf .git fi } die_init_failure() { die<<_EOF A failure occurred while configuring and cloning the repo _EOF } init_repo() { local dir local remote local remote_url local upstream_branch local remote_ref dir="$1" if [[ ! -d "$dir" ]]; then if ! mkdir "$dir"; then die "Cannot create $dir directory" fi INIT_DIR="$(pwd)/$dir" fi trap 'clean_partial_clone' EXIT cd_or_die "$dir" remote_url="$2" remote="$(remote)" DEFAULT_BRANCH="main" git init &>/dev/null git config "remote.${remote}.url" "$remote_url" git config "remote.${remote}.fetch" "+refs/heads/*:refs/remotes/$remote/*" set_should_track true if ! git fetch --force --tags "$remote" 2>/dev/null; then die "Could not download content from \"$remote_url\" on \"$DEFAULT_BRANCH\"." fi git remote set-head "$remote" --auto >/dev/null git checkout "$DEFAULT_BRANCH" trap - EXIT } print_version() { echo "0.0.5" } # # API # git_update_agent() { local dir local option local remote_url local track local disable_tracking while [[ $# -gt 0 ]]; do case "$1" in -h|-\?|--help|--usage) print_usage; exit 0 ;; --dir) shift if [[ $# -eq 0 ]]; then die "--dir requires an option" fi dir="$1" shift ;; --init-with) shift if [[ $# -eq 0 ]]; then die "--init-with requires an option" fi remote_url="$1" shift ;; --track) track=1; shift ;; --disable-tracking) disable_tracking=1; shift ;; --debug) debug=1; shift ;; --version) print_version; exit 0 ;; *) die "$(print_usage)" ;; esac done depends git if [[ -n "$debug" ]]; then set -x fi if [[ -z "$dir" ]]; then dir="." fi if [[ -n "$remote_url" ]]; then init_repo "$dir" "$remote_url" exit 0 fi if [[ -n "$track" ]]; then set_tracking "$dir" "true" exit 0 fi if [[ -n "$disable_tracking" ]]; then set_tracking "$dir" "false" exit 0 fi update "$dir" } if [[ -z "$NO_EXEC" ]]; then git_update_agent "$@" fi