Skip to content

Instantly share code, notes, and snippets.

@tsibley
Created January 25, 2024 23:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tsibley/65a0e95fb79530d932638a372e0deded to your computer and use it in GitHub Desktop.
Save tsibley/65a0e95fb79530d932638a372e0deded to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
#
#
# Copyright 2013-2020 - Ingy döt Net <ingy@ingy.net>
#
### INLINED License (0.4.6, 110b9eb13f259986fffcf11e8fb187b8cce50921)
# The MIT License (MIT)
#
# Copyright (c) 2013-2020 Ingy döt Net
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
### END INLINED License
### INLINED lib/git-subrepo.d/bash+.bash (0.4.6, 110b9eb13f259986fffcf11e8fb187b8cce50921)
# bash+ - Modern Bash Programming
#
# Copyright (c) 2013-2020 Ingy döt Net
set -e
[[ ${BASHPLUS_VERSION-} ]] && return 0
BASHPLUS_VERSION=0.1.0
bash+:version-check() {
local cmd want got out
IFS=' ' read -r -a cmd <<< "${1:?}"
IFS=. read -r -a want <<< "${2:?}"
: "${want[0]:=0}"
: "${want[1]:=0}"
: "${want[2]:=0}"
if [[ ${cmd[*]} == bash ]]; then
got=("${BASH_VERSINFO[@]}")
BASHPLUS_VERSION_CHECK=${BASH_VERSION-}
else
[[ ${#cmd[*]} -gt 1 ]] || cmd+=(--version)
out=$("${cmd[@]}") ||
{ echo "Failed to run '${cmd[*]}'" >&2; exit 1; }
[[ $out =~ ([0-9]+\.[0-9]+(\.[0-9]+)?) ]] ||
{ echo "Can't determine version number from '${cmd[*]}'" >&2; exit 1; }
BASHPLUS_VERSION_CHECK=${BASH_REMATCH[1]}
IFS=. read -r -a got <<< "$BASHPLUS_VERSION_CHECK"
fi
: "${got[2]:=0}"
(( got[0] > want[0] || ((
got[0] == want[0] && ((
got[1] > want[1] || ((
got[1] == want[1] && got[2] >= want[2]
)) )) )) ))
}
bash+:version-check bash 3.2 ||
{ echo "The 'bashplus' library requires 'Bash 3.2+'." >&2; exit 1; }
@() (echo "$@") # XXX do we want to keep this?
bash+:export:std() {
set -o pipefail
if bash+:version-check bash 4.4; then
set -o nounset
shopt -s inherit_errexit
fi
echo use die warn
}
# Source a bash library call import on it:
bash+:use() {
local library_name=${1:?bash+:use requires library name}; shift
local library_path=; library_path=$(bash+:findlib "$library_name") || true
[[ $library_path ]] ||
bash+:die "Can't find library '$library_name'." 1
source "$library_path"
if bash+:can "$library_name:import"; then
"$library_name:import" "$@"
else
bash+:import "$@"
fi
}
# Copy bash+: functions to unprefixed functions
bash+:import() {
local arg=
for arg; do
if [[ $arg =~ ^: ]]; then
# Word splitting required here
# shellcheck disable=2046
bash+:import $(bash+:export"$arg")
else
bash+:fcopy "bash+:$arg" "$arg"
fi
done
}
# Function copy
bash+:fcopy() {
bash+:can "${1:?bash+:fcopy requires an input function name}" ||
bash+:die "'$1' is not a function" 2
local func
func=$(type "$1" 3>/dev/null | tail -n+3)
[[ ${3-} ]] && "$3"
eval "${2:?bash+:fcopy requires an output function name}() $func"
}
# Find the path of a library
bash+:findlib() {
local library_name
library_name=$(tr '[:upper:]' '[:lower:]' <<< "${1//:://}").bash
local lib=${BASHPLUSLIB:-${BASHLIB:-$PATH}}
library_name=${library_name//+/\\+}
IFS=':' read -r -a libs <<< "$lib"
find "${libs[@]}" -name "${library_name##*/}" 2>/dev/null |
grep -E "$library_name\$" |
head -n1
}
bash+:die() {
local msg=${1:-Died}
msg=${msg//\\n/$'\n'}
printf "%s" "$msg" >&2
if [[ $msg == *$'\n' ]]; then
exit 1
else
printf "\n"
fi
local c
IFS=' ' read -r -a c <<< "$(caller "${DIE_STACK_LEVEL:-${2:-0}}")"
if (( ${#c[@]} == 2 )); then
msg=" at line %d of %s"
else
msg=" at line %d in %s of %s"
fi
# shellcheck disable=2059
printf "$msg\n" "${c[@]}" >&2
exit 1
}
bash+:warn() {
local msg=${1:-Warning}
printf "%s" "${msg//\\n/$'\n'}\n" >&2
}
bash+:can() {
[[ $(type -t "${1:?bash+:can requires a function name}") == function ]]
}
### END INLINED lib/git-subrepo.d/bash+.bash
### INLINED lib/git-subrepo.d/help-functions.bash (0.4.6, 110b9eb13f259986fffcf11e8fb187b8cce50921)
#!/usr/bin/env bash
# DO NOT EDIT. This file generated by pkg/bin/generate-help-functions.pl.
set -e
help:all() {
cat <<'...'
branch branch <subdir>|--all [-f] [-F]
clean clean <subdir>|--all|--ALL [-f]
clone clone <repository> [<subdir>] [-b <branch>] [-f] [-m <msg>] [--file=<msg file>] [-e] [--method <merge|rebase>]
commit commit <subdir> [<subrepo-ref>] [-m <msg>] [--file=<msg file>] [-e] [-f] [-F]
config config <subdir> <option> [<value>] [-f]
fetch fetch <subdir>|--all [-r <remote>] [-b <branch>]
help help [<command>|--all]
init init <subdir> [-r <remote>] [-b <branch>] [--method <merge|rebase>]
pull pull <subdir>|--all [-M|-R|-f] [-m <msg>] [--file=<msg file>] [-e] [-b <branch>] [-r <remote>] [-u]
push push <subdir>|--all [<branch>] [-m msg] [--file=<msg file>] [-r <remote>] [-b <branch>] [-M|-R] [-u] [-f] [-s] [-N]
status status [<subdir>|--all|--ALL] [-F] [-q|-v]
upgrade upgrade
version version [-q|-v]
...
}
help:branch() {
cat <<'...'
Usage: git subrepo branch <subdir>|--all [-f] [-F]
Create a branch with local subrepo commits.
Scan the history of the mainline for all the commits that affect the `subdir`
and create a new branch from them called `subrepo/<subdir>`.
This is useful for doing `pull` and `push` commands by hand.
Use the `--force` option to write over an existing `subrepo/<subdir>` branch.
The `branch` command accepts the `--all`, `--fetch` and `--force` options.
...
}
help:clean() {
cat <<'...'
Usage: git subrepo clean <subdir>|--all|--ALL [-f]
Remove artifacts created by `fetch` and `branch` commands.
The `fetch` and `branch` operations (and other commands that call them)
create temporary things like refs, branches and remotes. This command
removes all those things.
Use `--force` to remove refs. Refs are not removed by default because they
are sometimes needed between commands.
Use `--all` to clean up after all the current subrepos. Sometimes you might
change to a branch where a subrepo doesn't exist, and then `--all` won't find
it. Use `--ALL` to remove any artifacts that were ever created by subrepo.
To remove ALL subrepo artifacts:
git subrepo clean --ALL --force
The `clean` command accepts the `--all`, `--ALL`, and `--force` options.
...
}
help:clone() {
cat <<'...'
Usage: git subrepo clone <repository> [<subdir>] [-b <branch>] [-f] [-m <msg>] [--file=<msg file>] [-e] [--method <merge|rebase>]
Add a repository as a subrepo in a subdir of your repository.
This is similar in feel to `git clone`. You just specify the remote repo
url, and optionally a sub-directory and/or branch name. The repo will be
fetched and merged into the subdir.
The subrepo history is /squashed/ into a single commit that contains the
reference information. This information is also stored in a special file
called `<subdir>/.gitrepo`. The presence of this file indicates that the
directory is a subrepo.
All subsequent commands refer to the subrepo by the name of the /subdir/.
From the subdir, all the current information about the subrepo can be
obtained.
The `--force` option will "reclone" (completely replace) an existing subdir.
The `--method` option will decide how the join process between branches are
performed. The default option is merge.
The `clone` command accepts the `--branch=` `--edit`, `--file`, `--force`
and `--message=` options.
...
}
help:commit() {
cat <<'...'
Usage: git subrepo commit <subdir> [<subrepo-ref>] [-m <msg>] [--file=<msg file>] [-e] [-f] [-F]
Add subrepo branch to current history as a single commit.
This command is generally used after a hand-merge. You have done a `subrepo
branch` and merged (rebased) it with the upstream. This command takes the
HEAD of that branch, puts its content into the subrepo subdir and adds a new
commit for it to the top of your mainline history.
This command requires that the upstream HEAD be in the `subrepo/<subdir>`
branch history. That way the same branch can push upstream. Use the
`--force` option to commit anyway.
The `commit` command accepts the `--edit`, `--fetch`, `--file`, `--force`
and `--message=` options.
...
}
help:config() {
cat <<'...'
Usage: git subrepo config <subdir> <option> [<value>] [-f]
Read or update configuration values in the subdir/.gitrepo file.
Because most of the values stored in the .gitrepo file are generated you
will need to use `--force` if you want to change anything else then the
`method` option.
Example to update the `method` option for a subrepo:
git subrepo config foo method rebase
...
}
help:fetch() {
cat <<'...'
Usage: git subrepo fetch <subdir>|--all [-r <remote>] [-b <branch>]
Fetch the remote/upstream content for a subrepo.
It will create a Git reference called `subrepo/<subdir>/fetch` that points at
the same commit as `FETCH_HEAD`. It will also create a remote called
`subrepo/<subdir>`. These are temporary and you can easily remove them with
the subrepo `clean` command.
The `fetch` command accepts the `--all`, `--branch=` and `--remote=` options.
...
}
help:help() {
cat <<'...'
Usage: git subrepo help [<command>|--all]
Same as `git help subrepo`. Will launch the manpage. For the shorter usage,
use `git subrepo -h`.
Use `git subrepo help <command>` to get help for a specific command. Use
`--all` to get a summary of all commands.
The `help` command accepts the `--all` option.
...
}
help:init() {
cat <<'...'
Usage: git subrepo init <subdir> [-r <remote>] [-b <branch>] [--method <merge|rebase>]
Turn an existing subdirectory into a subrepo.
If you want to expose a subdirectory of your project as a published subrepo,
this command will do that. It will split out the content of a normal
subdirectory into a branch and start tracking it as a subrepo. Afterwards
your original repo will look exactly the same except that there will be a
`<subdir>/.gitrepo` file.
If you specify the `--remote` (and optionally the `--branch`) option, the
values will be added to the `<subdir>/.gitrepo` file. The `--remote` option
is the upstream URL, and the `--branch` option is the upstream branch to push
to. These values will be needed to do a `git subrepo push` command, but they
can be provided later on the `push` command (and saved to `<subdir>/.gitrepo`
if you also specify the `--update` option).
Note: You will need to create the empty upstream repo and push to it on your
own, using `git subrepo push <subdir>`.
The `--method` option will decide how the join process between branches
are performed. The default option is merge.
The `init` command accepts the `--branch=` and `--remote=` options.
...
}
help:pull() {
cat <<'...'
Usage: git subrepo pull <subdir>|--all [-M|-R|-f] [-m <msg>] [--file=<msg file>] [-e] [-b <branch>] [-r <remote>] [-u]
Update the subrepo subdir with the latest upstream changes.
The `pull` command fetches the latest content from the remote branch pointed
to by the subrepo's `.gitrepo` file, and then tries to merge the changes into
the corresponding subdir. It does this by making a branch of the local
commits to the subdir and then merging or rebasing (see below) it with the
fetched upstream content. After the merge, the content of the new branch
replaces your subdir, the `.gitrepo` file is updated and a single 'pull'
commit is added to your mainline history.
The `pull` command will attempt to do the following commands in one go:
git subrepo fetch <subdir>
git subrepo branch <subdir>
git merge/rebase subrepo/<subdir>/fetch subrepo/<subdir>
git subrepo commit <subdir>
# Only needed for a consequential push:
git update-ref refs/subrepo/<subdir>/pull subrepo/<subdir>
In other words, you could do all the above commands yourself, for the same
effect. If any of the commands fail, subrepo will stop and tell you to finish
this by hand. Generally a failure would be in the merge or rebase part, where
conflicts can happen. Since Git has lots of ways to resolve conflicts to your
personal tastes, the subrepo command defers to letting you do this by hand.
When pulling new data, the method selected in clone/init is used. This has
no effect on the final result of the pull, since it becomes a single commit.
But it does affect the resulting `subrepo/<subdir>` branch, which is often
used for a subrepo `push` command. See 'push' below for more information.
If you want to change the method you can use the `config` command for this.
When you pull you can assume a fast-forward strategy (default) or you can
specify a `--rebase`, `--merge` or `--force` strategy. The latter is the same
as a `clone --force` operation, using the current remote and branch.
Like the `clone` command, `pull` will squash all the changes (since the last
pull or clone) into one commit. This keeps your mainline history nice and
clean. You can easily see the subrepo's history with the `git log` command:
git log refs/subrepo/<subdir>/fetch
The set of commands used above are described in detail below.
The `pull` command accepts the `--all`, `--branch=`, `--edit`, `--file`,
`--force`, `--message=`, `--remote=` and `--update` options.
...
}
help:push() {
cat <<'...'
Usage: git subrepo push <subdir>|--all [<branch>] [-m msg] [--file=<msg file>] [-r <remote>] [-b <branch>] [-M|-R] [-u] [-f] [-s] [-N]
Push a properly merged subrepo branch back upstream.
This command takes the subrepo branch from a successful pull command and
pushes the history back to its designated remote and branch. You can also use
the `branch` command and merge things yourself before pushing if you want to
(although that is probably a rare use case).
The `push` command requires a branch that has been properly merged/rebased
with the upstream HEAD (unless the upstream HEAD is empty, which is common
when doing a first `push` after an `init`). That means the upstream HEAD is
one of the commits in the branch.
By default the branch ref `refs/subrepo/<subdir>/pull` will be pushed, but
you can specify a (properly merged) branch to push.
After that, the `push` command just checks that the branch contains the
upstream HEAD and then pushes it upstream.
The `--force` option will do a force push. Force pushes are typically
discouraged. Only use this option if you fully understand it. (The `--force`
option will NOT check for a proper merge. ANY branch will be force pushed!)
The `push` command accepts the `--all`, `--branch=`, `--dry-run`, `--file`,
`--force`, `--merge`, `--message`, `--rebase`, `--remote=`, `--squash` and
`--update` options.
...
}
help:status() {
cat <<'...'
Usage: git subrepo status [<subdir>|--all|--ALL] [-F] [-q|-v]
Get the status of a subrepo. Uses the `--all` option by default. If the
`--quiet` flag is used, just print the subrepo names, one per line.
The `--verbose` option will show all the recent local and upstream commits.
Use `--ALL` to show the subrepos of the subrepos (ie the "subsubrepos"), if
any.
The `status` command accepts the `--all`, `--ALL`, `--fetch`, `--quiet` and
`--verbose` options.
...
}
help:upgrade() {
cat <<'...'
Usage: git subrepo upgrade
Upgrade the `git-subrepo` software itself. This simply does a `git pull` on
the git repository that the code is running from. It only works if you are on
the `master` branch. It won't work if you installed `git-subrepo` using `make
install`; in that case you'll need to `make install` from the latest code.
...
}
help:version() {
cat <<'...'
Usage: git subrepo version [-q|-v]
This command will display version information about git-subrepo and its
environment. For just the version number, use `git subrepo --version`. Use
`--verbose` for more version info, and `--quiet` for less.
The `version` command accepts the `--quiet` and `--verbose` options.
...
}
# vim: set sw=2 lisp:
### END INLINED lib/git-subrepo.d/help-functions.bash
# shellcheck disable=1090,1091,2034
# Exit on any errors:
set -e
export FILTER_BRANCH_SQUELCH_WARNING=1
bash+:import :std can version-check
VERSION=0.4.6
REQUIRED_BASH_VERSION=4.0
REQUIRED_GIT_VERSION=2.7.0
GIT_TMP=$(git rev-parse --git-common-dir 2> /dev/null || echo .git)/tmp
# `git rev-parse` turns this into a getopt parser and a command usage message:
GETOPT_SPEC="\
git subrepo <command> <arguments> <options>
Commands:
clone Clone a remote repository into a local subdirectory
init Turn a current subdirectory into a subrepo
pull Pull upstream changes to the subrepo
push Push local subrepo changes upstream
fetch Fetch a subrepo's remote branch (and create a ref for it)
branch Create a branch containing the local subrepo commits
commit Commit a merged subrepo branch into the mainline
status Get status of a subrepo (or all of them)
clean Remove branches, remotes and refs for a subrepo
config Set subrepo configuration properties
help Documentation for git-subrepo (or specific command)
version Display git-subrepo version info
upgrade Upgrade the git-subrepo software itself
See 'git help subrepo' for complete documentation and usage of each command.
Options:
--
h Show the command summary
help Help overview
version Print the git-subrepo version number
a,all Perform command on all current subrepos
A,ALL Perform command on all subrepos and subsubrepos
b,branch= Specify the upstream branch to push/pull/fetch
e,edit Edit commit message
f,force Force certain operations
F,fetch Fetch the upstream content first
M,method= Join method: 'merge' (default) or 'rebase'
m,message= Specify a commit message
file= Specify a commit message file
r,remote= Specify the upstream remote to push/pull/fetch
s,squash Squash commits on push
u,update Add the --branch and/or --remote overrides to .gitrepo
q,quiet Show minimal output
v,verbose Show verbose output
d,debug Show the actual commands used
x,DEBUG Turn on -x Bash debugging
"
#------------------------------------------------------------------------------
# Top level function:
#------------------------------------------------------------------------------
main() {
# Define global variables:
local command= # Subrepo subcommand to run
local command_arguments=() # Command args after getopt parsing
local commit_msg_args=() # Arguments to show in the commit msg
local subrepos=() # List of multiple subrepos
local all_wanted=false # Apply command to all subrepos
local ALL_wanted=false # Apply command to all subrepos and subsubrepos
local force_wanted=false # Force certain operations
local fetch_wanted=false # Fetch requested before a command
local squash_wanted=false # Squash commits on push
local update_wanted=false # Update .gitrepo with --branch and/or --remote
local quiet_wanted=false # Output should be quiet
local verbose_wanted=false # Output should be verbose
local debug_wanted=false # Show debug messages
local subdir= # Subdirectory of the subrepo being used
local subref= # Valid git ref format of subdir
local gitrepo= # Path to .gitrepo file
local worktree= # Worktree created by 'git worktree'
local start_pwd
start_pwd=$(pwd) # Store the original directory
local original_head_commit= # HEAD commit id at start of command
local original_head_branch= # HEAD ref at start of command
local upstream_head_commit= # HEAD commit id from a subrepo fetch
local subrepo_remote= # Remote url for subrepo's upstream repo
local subrepo_branch= # Upstream branch to clone/push/pull
local subrepo_commit= # Upstream HEAD from previous clone/pull
local subrepo_parent= # Local commit from before previous clone/pull
local subrepo_former= # A retired gitrepo key that might still exist
local refs_subrepo_branch= # A subrepo ref -> commit of branch/pull command
local refs_subrepo_commit= # A subrepo ref -> commit last merged
local refs_subrepo_fetch= # A subrepo ref -> FETCH_HEAD after fetch
local refs_subrepo_push= # A subrepo ref -> branch after push
local override_remote= # Remote specified with -r
local override_branch= # Remote specified with -b
local edit_wanted=false # Edit commit message using -e
local wanted_commit_message= # Custom commit message using -m
local commit_msg_file= # Custom commit message using --file
local join_method= # Current join method (rebase/merge)
local FAIL=true # Flag for RUN: fail on error
local OUT=false # Flag for RUN: put output in $output
local TTY=false # Flag for RUN: print output directly
local SAY=true # Flag for RUN: print command for verbose
local EXEC=false # Flag for RUN: run subprocess
local OK=true # Flag that commands have succeeded
local CODE=0 # Failure reason code
local INDENT= # Verbose indentation
local git_version= # Git version in use
# Check environment and parse CLI options:
assert-environment-ok
# Parse and validate command options:
get-command-options "$@"
# Make sure repo is in the proper state:
assert-repo-is-ready
command-init
if $all_wanted && [[ ! $command =~ ^(help|status)$ ]]; then
if [[ -n $subrepo_branch ]]; then
error "options --branch and --all are not compatible"
fi
# Run the command on all subrepos
local args=( "${command_arguments[@]}" )
get-all-subrepos
for subdir in ${subrepos[*]}; do
command-prepare
subrepo_remote=
subrepo_branch=
command_arguments=( "$subdir" "${args[@]}" )
"command:$command"
done
else
# Run the command on a specific subrepo
command-prepare
"command:$command"
fi
}
#------------------------------------------------------------------------------
# API command functions.
#
# Most of these commands call a subrepo:$command function to do the actual
# work. The user facing output (via `say`) is done up here. The
# subrepo:* worker functions are meant to be called internally and don't print
# info to the user.
#------------------------------------------------------------------------------
# `git subrepo clone <url> [<subdir>]` command:
command:clone() {
command-setup +subrepo_remote subdir:guess-subdir
# Clone (or reclone) the subrepo into the subdir:
local reclone_up_to_date=false
subrepo:clone
if "$reclone_up_to_date"; then
say "Subrepo '$subdir' is up to date."
return
fi
# Successful command output:
local re=
$force_wanted && re=re
local remote=$subrepo_remote
say "Subrepo '$remote' ($subrepo_branch) ${re}cloned into '$subdir'."
}
# `git subrepo init <subdir>` command:
command:init() {
command-setup +subdir
local remote=${subrepo_remote:=none}
local branch=${subrepo_branch:=master}
# Init new subrepo from the subdir:
subrepo:init
if OK; then
if [[ $remote == none ]]; then
say "Subrepo created from '$subdir' (with no remote)."
else
say "Subrepo created from '$subdir' with remote '$remote' ($branch)."
fi
else
die "Unknown init error code: '$CODE'"
fi
return 0
}
# `git subrepo pull <subdir>` command:
command:pull() {
command-setup +subdir
subrepo:pull
if OK; then
say "Subrepo '$subdir' pulled from '$subrepo_remote' ($subrepo_branch)."
elif [[ $CODE -eq -1 ]]; then
say "Subrepo '$subdir' is up to date."
elif [[ $CODE -eq 1 ]]; then
error-join
return "$CODE"
else
die "Unknown pull error code: '$CODE'"
fi
return 0
}
# `git subrepo push <subdir>` command:
command:push() {
local branch=
command-setup +subdir branch
subrepo:push
if OK; then
say "Subrepo '$subdir' pushed to '$subrepo_remote' ($subrepo_branch)."
elif [[ $CODE -eq -2 ]]; then
say "Subrepo '$subdir' has no new commits to push."
elif [[ $CODE -eq 1 ]]; then
error-join
return "$CODE"
else
die "Unknown push error code: '$CODE'"
fi
return 0
}
# `git subrepo fetch <subdir>` command
command:fetch() {
command-setup +subdir
if [[ $subrepo_remote == none ]]; then
say "Ignored '$subdir', no remote."
else
subrepo:fetch
say "Fetched '$subdir' from '$subrepo_remote' ($subrepo_branch)."
fi
}
# `git subrepo branch <subdir>` command:
command:branch() {
command-setup +subdir
if $fetch_wanted; then
CALL subrepo:fetch
fi
local branch=subrepo/$subref
if $force_wanted; then
# We must make sure that the worktree is removed as well
worktree=$GIT_TMP/$branch
git:delete-branch "$branch"
fi
if git:branch-exists "$branch"; then
error "Branch '$branch' already exists. Use '--force' to override."
fi
# Create the subrepo branch:
subrepo:branch
say "Created branch '$branch' and worktree '$worktree'."
}
# `git subrepo commit <subdir>` command
command:commit() {
command-setup +subdir subrepo_commit_ref
if "$fetch_wanted"; then
CALL subrepo:fetch
fi
git:rev-exists "$refs_subrepo_fetch" ||
error "Can't find ref '$refs_subrepo_fetch'. Try using -F."
upstream_head_commit=$(git rev-parse "$refs_subrepo_fetch")
[[ ${subrepo_commit_ref-} ]] ||
subrepo_commit_ref=subrepo/$subref
subrepo:commit
say "Subrepo commit '$subrepo_commit_ref' committed as"
say "subdir '$subdir/' to branch '$original_head_branch'."
}
# `git subrepo status [<subdir>]` command:
command:status() {
subrepo:status | ${GIT_SUBREPO_PAGER}
}
status-refs() {
local output=
while read -r line; do
[[ $line =~ ^([0-9a-f]+)\ refs/subrepo/$subref/([a-z]+) ]] || continue
local sha1=; sha1=$(git rev-parse --short "${BASH_REMATCH[1]}")
local type=${BASH_REMATCH[2]}
local ref=refs/subrepo/$subref/$type
if [[ $type == branch ]]; then
output+=" Branch Ref: $sha1 ($ref)"$'\n'
elif [[ $type == commit ]]; then
output+=" Commit Ref: $sha1 ($ref)"$'\n'
elif [[ $type == fetch ]]; then
output+=" Fetch Ref: $sha1 ($ref)"$'\n'
elif [[ $type == pull ]]; then
output+=" Pull Ref: $sha1 ($ref)"$'\n'
elif [[ $type == push ]]; then
output+=" Push Ref: $sha1 ($ref)"$'\n'
fi
done < <(git show-ref)
if [[ $output ]]; then
printf " Refs:\n%s" "$output"
fi
}
# `git subrepo clean <subdir>` command
command:clean() {
command-setup +subdir
local clean_list=()
subrepo:clean
for item in "${clean_list[@]}"; do
say "Removed $item."
done
}
# Wrap git config $gitrepo
command:config() {
command-setup +subdir +config_option config_value
# shellcheck disable=2154
o "Update '$subdir' configuration with $config_option=${config_value-}"
if [[ ! $config_option =~ ^(branch|cmdver|commit|method|remote|version)$ ]]; then
error "Option $config_option not recognized"
fi
if [[ -z ${config_value-} ]]; then
OUT=true RUN git config --file="$gitrepo" "subrepo.$config_option"
say "Subrepo '$subdir' option '$config_option' has value '$output'."
return
fi
if ! $force_wanted; then
# Only allow changing method without force
if [[ $config_option != method ]]; then
error "This option is autogenerated, use '--force' to override."
fi
fi
if [[ $config_option == method ]]; then
if [[ ! $config_value =~ ^(merge|rebase)$ ]]; then
error "Not a valid method. Valid options are 'merge' or 'rebase'."
fi
fi
RUN git config --file="$gitrepo" "subrepo.$config_option" "$config_value"
say "Subrepo '$subdir' option '$config_option' set to '$config_value'."
}
# Launch the manpage viewer:
command:help() {
local cmd=${command_arguments[0]}
if [[ $cmd ]]; then
if can "help:$cmd"; then
"help:$cmd"
echo
else
err "No help found for '$cmd'"
fi
elif $all_wanted; then
help:all
else
exec git help subrepo
fi
msg_ok=0
}
# Print version info.
# TODO: Add short commit id after version.
# Will need to get it from repo or make install can put it somewhere.
command:version() {
cat <<...
git-subrepo Version: $VERSION
Copyright 2013-2020 Ingy döt Net
https://github.com/ingydotnet/git-subrepo
${BASH_SOURCE[0]}
Git Version: $git_version
...
:
}
command:upgrade() {
local path=$0
if [[ $path =~ ^/ && $path =~ ^(.*/git-subrepo)/lib/git-subrepo$ ]]; then
local subrepo_root=${BASH_REMATCH[1]}
(
o "Change directory to '$subrepo_root'."
cd "${BASH_REMATCH[1]}"
branch_name=$(git rev-parse --abbrev-ref HEAD)
if [[ $branch_name != master ]]; then
error "git-subrepo repo is not on the 'master' branch"
fi
o "'git pull' latest version."
RUN git pull --ff-only
say "git-subrepo is up to date."
)
else
die "\
Sorry. Your installation can't use the 'git subrepo upgrade' command. The
command only works if you installed git subrepo by adding
'/path/to/git-subrepo' to your PATH.
If you used 'make install' to install git-subrepo, then just do this:
cd /path/to/git-subrepo
git pull
make install
"
fi
}
#------------------------------------------------------------------------------
# Subrepo command worker functions.
#------------------------------------------------------------------------------
# Clone by fetching remote content into our subdir:
subrepo:clone() {
FAIL=false RUN git rev-parse HEAD
if ! OK; then
error "You can't clone into an empty repository"
fi
# Turn off force unless really a reclone:
if $force_wanted && [[ ! -f $gitrepo ]]; then
force_wanted=false
fi
if $force_wanted; then
o "--force indicates a reclone."
CALL subrepo:fetch
read-gitrepo-file
o "Check if we already are up to date."
if [[ $upstream_head_commit == "$subrepo_commit" ]]; then
reclone_up_to_date=true
return
fi
o "Remove the old subdir."
RUN git rm -r -- "$subdir"
else
assert-subdir-empty
if [[ -z $subrepo_branch ]]; then
o "Determine the upstream head branch."
get-upstream-head-branch
subrepo_branch=$output
fi
CALL subrepo:fetch
fi
o "Make the directory '$subdir/' for the clone."
RUN mkdir -p -- "$subdir"
o "Commit the new '$subdir/' content."
subrepo_commit_ref=$upstream_head_commit
CALL subrepo:commit
}
# Init a new subrepo from current repo:
subrepo:init() {
local branch_name=subrepo/${subref:??}
# Check if subdir is proper candidate for this init:
assert-subdir-ready-for-init
o "Put info into '$subdir/.gitrepo' file."
update-gitrepo-file
o "Add the new '$subdir/.gitrepo' file."
# -f from pull request #219. TODO needs test.
RUN git add -f -- "$gitrepo"
o "Commit new subrepo to the '$original_head_branch' branch."
subrepo_commit_ref=$original_head_commit
RUN git commit -m "$(get-commit-message)"
o "Create ref '$refs_subrepo_commit'."
git:make-ref "$refs_subrepo_commit" "$subrepo_commit_ref"
}
# Properly merge a local subrepo branch with upstream and commit to mainline:
subrepo:pull() {
CALL subrepo:fetch
# If forced pull, then clone instead
if $force_wanted; then
CALL subrepo:clone
return
fi
# Check if we already are up to date
# If the -u flag is present, always perform the operation
if [[ $upstream_head_commit == "$subrepo_commit" ]] && ! $update_wanted; then
OK=false; CODE=-1; return
fi
local branch_name=subrepo/$subref
git:delete-branch "$branch_name"
subrepo_commit_ref=$branch_name
o "Create subrepo branch '$branch_name'."
CALL subrepo:branch
cd "$worktree";
if [[ $join_method == rebase ]]; then
o "Rebase changes to $refs_subrepo_fetch"
FAIL=false OUT=true RUN git rebase "$refs_subrepo_fetch" "$branch_name"
if ! OK; then
say "The \"git rebase\" command failed:"
say
say " ${output//$'\n'/$'\n' }"
CODE=1
return
fi
else
o "Merge in changes from $refs_subrepo_fetch"
FAIL=false RUN git merge "$refs_subrepo_fetch"
if ! OK; then
say "The \"git merge\" command failed:"
say
say " ${output//$'\n'/$'\n' }"
CODE=1
return
fi
fi
o "Back to $start_pwd"
cd "$start_pwd";
o "Create ref '$refs_subrepo_branch' for branch '$branch_name'."
git:make-ref "$refs_subrepo_branch" "$branch_name"
o "Commit the new '$subrepo_commit_ref' content."
CALL subrepo:commit
}
# Push a properly merged subrepo branch upstream:
subrepo:push() {
local branch_name=$branch
local new_upstream=false
local branch_created=false
if [[ -z $branch_name ]]; then
FAIL=false OUT=false CALL subrepo:fetch
if ! OK; then
# Check if we are pushing to a new upstream repo (or branch) and just
# push the commit directly. This is common after a `git subrepo init`:
# Force to case in
local re="(^|"$'\n'")fatal: couldn't find remote ref "
if [[ ${output,,} =~ $re ]]; then
o "Pushing to new upstream: $subrepo_remote ($subrepo_branch)."
new_upstream=true
else
error "Fetch for push failed: $output"
fi
else
# Check that we are up to date:
o "Check upstream head against .gitrepo commit."
if ! $force_wanted; then
if [[ $upstream_head_commit != "$subrepo_commit" ]]; then
error "There are new changes upstream, you need to pull first."
fi
fi
fi
branch_name=subrepo/$subref
# We must make sure that a stale worktree is removed as well
worktree=$GIT_TMP/$branch_name
git:delete-branch "$branch_name"
if $squash_wanted; then
o "Squash commits"
subrepo_parent=HEAD^
fi
o "Create subrepo branch '$branch_name'."
CALL subrepo:branch "$branch_name"
cd "$worktree";
if [[ $join_method == rebase ]]; then
o "Rebase changes to $refs_subrepo_fetch"
FAIL=false OUT=true RUN git rebase "$refs_subrepo_fetch" "$branch_name"
if ! OK; then
say "The \"git rebase\" command failed:"
say
say " ${output//$'\n'/$'\n' }"
CODE=1
return
fi
fi
branch_created=true
cd "$start_pwd"
else
if $squash_wanted; then
error "Squash option (-s) can't be used with branch parameter"
fi
fi
o "Make sure that '$branch_name' exists."
git:branch-exists "$branch_name" ||
error "No subrepo branch '$branch_name' to push."
o "Check if we have something to push"
new_upstream_head_commit=$(git rev-parse "$branch_name")
if ! $new_upstream; then
if [[ $upstream_head_commit == "$new_upstream_head_commit" ]]; then
if $branch_created; then
o "Remove branch '$branch_name'."
git:delete-branch "$branch_name"
fi
OK=false
CODE=-2
return
fi
fi
if ! $force_wanted; then
o "Make sure '$branch_name' contains the '$refs_subrepo_fetch' HEAD."
if ! git:commit-in-rev-list "$upstream_head_commit" "$branch_name"; then
error "Can't commit: '$branch_name' doesn't contain upstream HEAD: " \
"$upstream_head_commit"
fi
fi
local force=''
"$force_wanted" && force=' --force'
o "Push$force branch '$branch_name' to '$subrepo_remote' ($subrepo_branch)."
# shellcheck disable=2086
RUN git push$force "$subrepo_remote" "$branch_name":"$subrepo_branch"
o "Create ref '$refs_subrepo_push' for branch '$branch_name'."
git:make-ref "$refs_subrepo_push" "$branch_name"
if $branch_created; then
o "Remove branch '$branch_name'."
git:delete-branch "$branch_name"
fi
o "Put updates into '$subdir/.gitrepo' file."
upstream_head_commit=$new_upstream_head_commit
subrepo_commit_ref=$upstream_head_commit
update-gitrepo-file
local commit_message
if [[ $wanted_commit_message ]]; then
commit_message=$wanted_commit_message
else
commit_message=$(get-commit-message)
fi
if [[ $commit_msg_file ]]; then
RUN git command --file "$commit_msg_file"
else
RUN git commit -m "$commit_message"
fi
}
# Fetch the subrepo's remote branch content:
subrepo:fetch() {
if [[ $subrepo_remote == none ]]; then
error "Can't fetch subrepo. Remote is 'none' in '$subdir/.gitrepo'."
fi
o "Fetch the upstream: $subrepo_remote ($subrepo_branch)."
RUN git fetch --no-tags --quiet "$subrepo_remote" "$subrepo_branch"
OK || return
o "Get the upstream subrepo HEAD commit."
OUT=true RUN git rev-parse FETCH_HEAD^0
upstream_head_commit=$output
o "Create ref '$refs_subrepo_fetch'."
git:make-ref "$refs_subrepo_fetch" FETCH_HEAD^0
}
# Create a subrepo branch containing all changes
# shellcheck disable=2120
subrepo:branch() {
local branch=${1:-"subrepo/$subref"}
o "Check if the '$branch' branch already exists."
git:branch-exists "$branch" && return
local last_gitrepo_commit=
local first_gitrepo_commit=
o "Subrepo parent: $subrepo_parent"
if [[ $subrepo_parent ]]; then
local prev_commit=
local ancestor=
o "Create new commits with parents into the subrepo fetch"
OUT=true RUN git rev-list --reverse --ancestry-path --topo-order "$subrepo_parent..HEAD"
local commit_list=$output
for commit in $commit_list; do
o "Working on $commit"
FAIL=false OUT=true RUN git config --blob \
"$commit:$subdir/.gitrepo" "subrepo.commit"
if [[ -z $output ]]; then
o "Ignore commit, no .gitrepo file"
continue
fi
local gitrepo_commit=$output
o ".gitrepo reference commit: $gitrepo_commit"
# Only include the commit if it's a child of the previous commit
# This way we create a single path between $subrepo_parent..HEAD
if [[ $ancestor ]]; then
local is_direct_child
is_direct_child=$(
git show -s --pretty=format:"%P" "$commit" |
grep "$ancestor"
) || true
o "is child: $is_direct_child"
if [[ -z $is_direct_child ]]; then
o "Ignore $commit, it's not in the selected path"
continue
fi
fi
# Remember the previous commit from the parent repo path
ancestor=$commit
o "Check for rebase"
if git:rev-exists "$refs_subrepo_fetch"; then
if ! git:commit-in-rev-list "$gitrepo_commit" "$refs_subrepo_fetch"; then
error "Local repository does not contain $gitrepo_commit. Try to 'git subrepo fetch $subref' or add the '-F' flag to always fetch the latest content."
fi
fi
o "Find parents"
local first_parent second_parent
first_parent=()
[[ $prev_commit ]] && first_parent=(-p "$prev_commit")
second_parent=()
if [[ -z $first_gitrepo_commit ]]; then
first_gitrepo_commit=$gitrepo_commit
second_parent=(-p "$gitrepo_commit")
fi
if [[ $join_method != rebase ]]; then
# In the rebase case we don't create merge commits
if [[ $gitrepo_commit != "$last_gitrepo_commit" ]]; then
second_parent=(-p "$gitrepo_commit")
last_gitrepo_commit=$gitrepo_commit
fi
fi
o "Create a new commit ${first_parent[*]} ${second_parent[*]}"
FAIL=false RUN git cat-file -e "$commit":"$subdir"
if OK; then
o "Create with content"
local PREVIOUS_IFS=$IFS
IFS=$'\n'
local author_info
mapfile -t author_info < <(git log -1 --date=default --format=%ad%n%ae%n%an "$commit")
IFS=$PREVIOUS_IFS
# When we create new commits we leave the author information unchanged
# the committer will though be updated to the current user
# This should be analog how cherrypicking is handled allowing git
# to store both the original author but also the responsible committer
# that created the local version of the commit and pushed it.
prev_commit=$(git log -n 1 --date=default --format=%B "$commit" |
GIT_AUTHOR_DATE=${author_info[0]} \
GIT_AUTHOR_EMAIL=${author_info[1]} \
GIT_AUTHOR_NAME=${author_info[2]} \
git commit-tree -F - "${first_parent[@]}" "${second_parent[@]}" "$commit":"$subdir")
else
o "Create empty placeholder"
prev_commit=$(git commit-tree -m "EMPTY" \
"${first_parent[*]}" "${second_parent[*]}" "4b825dc642cb6eb9a060e54bf8d69288fbee4904")
fi
done
o "Create branch '$branch' for this new commit set $prev_commit."
RUN git branch "$branch" "$prev_commit"
else
o "No parent setting, use the subdir content."
RUN git branch "$branch" HEAD
TTY=true FAIL=false RUN git filter-branch -f --subdirectory-filter \
"$subref" "$branch"
fi
o "Remove the .gitrepo file from $first_gitrepo_commit..$branch"
local filter=$branch
[[ $first_gitrepo_commit ]] && filter=$first_gitrepo_commit..$branch
FAIL=false RUN git filter-branch -f --prune-empty --tree-filter \
"rm -f .gitrepo" "$filter"
git:create-worktree "$branch"
o "Create ref '$refs_subrepo_branch'."
git:make-ref "$refs_subrepo_branch" "$branch"
}
# Commit a merged subrepo branch:
subrepo:commit() {
o "Check that '$subrepo_commit_ref' exists."
git:rev-exists "$subrepo_commit_ref" ||
error "Commit ref '$subrepo_commit_ref' does not exist."
if ! "$force_wanted"; then
local upstream=$upstream_head_commit
o "Make sure '$subrepo_commit_ref' contains the upstream HEAD."
if ! git:commit-in-rev-list "$upstream" "$subrepo_commit_ref"; then
error \
"Can't commit: '$subrepo_commit_ref' doesn't contain upstream HEAD."
fi
fi
if git ls-files -- "$subdir" | grep -q .; then
o "Remove old content of the subdir."
RUN git rm -r -- "$subdir"
fi
o "Put remote subrepo content into '$subdir/'."
RUN git read-tree --prefix="$subdir" -u "$subrepo_commit_ref"
o "Put info into '$subdir/.gitrepo' file."
update-gitrepo-file
RUN git add -f -- "$gitrepo"
local commit_message
if [[ $wanted_commit_message ]]; then
commit_message=$wanted_commit_message
else
commit_message=$(get-commit-message)
fi
local edit_flag=
$edit_wanted && edit_flag=--edit
[[ $commit_message ]] || commit_message=$(get-commit-message)
local edit_flag=
$edit_wanted && edit_flag=--edit
o "Commit to the '$original_head_branch' branch."
if [[ $original_head_commit != none ]]; then
if [[ $commit_msg_file ]]; then
RUN git commit $edit_flag --file "$commit_msg_file"
else
RUN git commit $edit_flag -m "$commit_message"
fi
else
# We had cloned into an empty repo, side effect of prior git reset --mixed
# command is that subrepo's history is now part of the index. Commit
# without that history.
OUT=true RUN git write-tree
if [[ $commit_msg_file ]]; then
OUT=true RUN git commit-tree $edit_flag --file "$commit_msg_file" "$output"
else
OUT=true RUN git commit-tree $edit_flag -m "$commit_message" "$output"
fi
RUN git reset --hard "$output"
fi
# Clean up worktree to indicate that we are ready
git:remove-worktree
o "Create ref '$refs_subrepo_commit'."
git:make-ref "$refs_subrepo_commit" "$subrepo_commit_ref"
}
subrepo:status() {
if [[ ${#command_arguments[@]} -eq 0 ]]; then
get-all-subrepos
local count=${#subrepos[@]}
if ! "$quiet_wanted"; then
if [[ $count -eq 0 ]]; then
echo "No subrepos."
return
else
local s=; [[ $count -eq 1 ]] || s=s
echo "$count subrepo$s:"
echo
fi
fi
else
subrepos=("${command_arguments[@]}")
fi
for subdir in "${subrepos[@]}"; do
check-and-normalize-subdir
encode-subdir
if [[ ! -f $subdir/.gitrepo ]]; then
echo "'$subdir' is not a subrepo"
echo
continue
fi
refs_subrepo_fetch=refs/subrepo/$subref/fetch
upstream_head_commit=$(
git rev-parse --short "$refs_subrepo_fetch" 2> /dev/null || true
)
subrepo_remote=
subrepo_branch=
read-gitrepo-file
if $fetch_wanted; then
subrepo:fetch
fi
if $quiet_wanted; then
echo "$subdir"
continue
fi
echo "Git subrepo '$subdir':"
git:branch-exists "subrepo/$subref" &&
echo " Subrepo Branch: subrepo/$subref"
local remote=subrepo/$subref
FAIL=false OUT=true RUN git config "remote.$remote.url"
[[ $output ]] &&
echo " Remote Name: subrepo/$subref"
echo " Remote URL: $subrepo_remote"
[[ $upstream_head_commit ]] &&
echo " Upstream Ref: $upstream_head_commit"
echo " Tracking Branch: $subrepo_branch"
[[ -z $subrepo_commit ]] ||
echo " Pulled Commit: $(git rev-parse --short "$subrepo_commit")"
if [[ $subrepo_parent ]]; then
echo " Pull Parent: $(git rev-parse --short "$subrepo_parent")"
# TODO Remove this eventually:
elif [[ $subrepo_former ]]; then
printf " Former Commit: %s" "$(git rev-parse --short "$subrepo_former")"
echo " *** DEPRECATED ***"
fi
# Grep for directory, branch can be in detached state due to conflicts
local _worktree
_worktree=$(
git worktree list |
grep "$GIT_TMP/subrepo/$subdir "
) || true
if [[ $_worktree ]]; then
echo " Worktree: $_worktree"
fi
if "$verbose_wanted"; then
status-refs
fi
echo
done
}
subrepo:clean() {
# Remove subrepo branches if exist:
local branch=subrepo/$subref
local ref=refs/heads/$branch
local worktree=$GIT_TMP/$branch
o "Clean $subdir"
git:remove-worktree
if git:branch-exists "$branch"; then
o "Remove branch '$branch'."
RUN git update-ref -d "$ref"
clean_list+=("branch '$branch'")
fi
if "$force_wanted"; then
o "Remove all subrepo refs."
local suffix=''
if ! $all_wanted; then
suffix=$subref/
fi
git show-ref | while read -r hash ref; do
if [[ $ref == "refs/subrepo/$suffix"* ]]; then
git update-ref -d "$ref"
fi
done
fi
}
#------------------------------------------------------------------------------
# Support functions:
#------------------------------------------------------------------------------
# TODO:
# Collect original options and arguments into an array for commit message
# They should be normalized and pruned
# Parse command line options:
get-command-options() {
[[ $# -eq 0 ]] && set -- --help
[[ ${GIT_SUBREPO_QUIET-} ]] && quiet_wanted=true
[[ ${GIT_SUBREPO_VERBOSE-} ]] && verbose_wanted=true
[[ ${GIT_SUBREPO_DEBUG-} ]] && debug_wanted=true
eval "$(
echo "$GETOPT_SPEC" |
git rev-parse --parseopt -- "$@" ||
echo exit $?
)"
while [[ $# -gt 0 ]]; do
local option=$1; shift
case "$option" in
--) break ;;
-a) all_wanted=true ;;
-A) ALL_wanted=true
all_wanted=true ;;
-b) subrepo_branch=$1
override_branch=$1
commit_msg_args+=("--branch=$1")
shift ;;
-e) edit_wanted=true ;;
-f) force_wanted=true
commit_msg_args+=("--force") ;;
-F) fetch_wanted=true ;;
-m)
if [[ $commit_msg_file ]]; then
error "fatal: options '-m' and '--file' cannot be used together"
fi
wanted_commit_message=$1
shift;;
-M) join_method=$1
shift;;
-r) subrepo_remote=$1
override_remote=$1
commit_msg_args+=("--remote=$1")
shift ;;
-s) squash_wanted=true ;;
-u) update_wanted=true
commit_msg_args+=("--update") ;;
-q) quiet_wanted=true ;;
-v) verbose_wanted=true ;;
-d) debug_wanted=true ;;
-x) set -x ;;
--file)
if [[ $wanted_commit_message ]]; then
error "fatal: options '-m' and '--file' cannot be used together"
fi
if [ -f "$1" ]; then
commit_msg_file="$1"
else
error "Commit msg file at $1 not found"
fi
shift ;;
--version)
echo "$VERSION"
exit ;;
*) usage-error "Unexpected option: '$option'." ;;
esac
done
# Set subrepo command:
command=$1; shift
# Make sure command exists:
can "command:$command" ||
usage-error "'$command' is not a command. See 'git subrepo help'."
command_arguments=("$@")
if [[ ${command_arguments[*]-} && ${#command_arguments[@]} -gt 0 ]]; then
local first=${command_arguments[0]}
first=${first%/}
command_arguments[0]=$first
fi
commit_msg_args+=("${command_arguments[@]}")
for option in all ALL edit fetch force squash; do
var=${option}_wanted
if ${!var}; then
check_option $option
fi
done
if [[ $override_branch ]]; then
check_option branch
fi
if [[ $override_remote ]]; then
check_option remote
fi
if [[ $wanted_commit_message || $commit_msg_file ]]; then
check_option message
fi
if $update_wanted; then
check_option update
if [[ -z $subrepo_branch && -z $subrepo_remote ]]; then
usage-error "Can't use '--update' without '--branch' or '--remote'."
fi
fi
}
options_help='all'
options_branch='all fetch force'
options_clean='ALL all force'
options_clone='branch edit force message method'
options_config='force'
options_commit='edit fetch force message'
options_fetch='all branch remote'
options_init='branch remote method'
options_pull='all branch edit force message remote update'
options_push='all branch force message remote squash update'
options_status='ALL all fetch'
check_option() {
local var=options_${command//-/_}
[[ ${!var} =~ $1 ]] ||
usage-error "Invalid option '--$1' for '$command'."
}
#------------------------------------------------------------------------------
# Command argument validation:
#------------------------------------------------------------------------------
command-init() {
# Export variable to let other processes (possibly git hooks) know that they
# are running under git-subrepo. Set to current process pid, so it can be
# further verified if need be:
export GIT_SUBREPO_RUNNING=$$
export GIT_SUBREPO_COMMAND=$command
: "${GIT_SUBREPO_PAGER:=${PAGER:-less}}"
if [[ $GIT_SUBREPO_PAGER == less ]]; then
GIT_SUBREPO_PAGER='less -FRX'
fi
}
command-prepare() {
local output=
if git:rev-exists HEAD; then
git:get-head-branch-commit
fi
original_head_commit=${output:-none}
}
# Do the setup steps needed by most of the subrepo subcommands:
command-setup() {
get-params "$@"
check-and-normalize-subdir
encode-subdir
gitrepo=$subdir/.gitrepo
if ! $force_wanted; then
o "Check for worktree with branch subrepo/$subdir"
local _worktree
_worktree=$(
git worktree list |
grep "\[subrepo/$subdir\]" |
cut -d ' ' -f1
) || true
if [[ $command =~ ^(commit)$ && -z $_worktree ]]; then
error "There is no worktree available, use the branch command first"
elif [[ ! $command =~ ^(branch|clean|commit|push)$ && $_worktree ]]; then
if [[ -e $gitrepo ]]; then
error "There is already a worktree with branch subrepo/$subdir.
Use the --force flag to override this check or perform a subrepo clean
to remove the worktree."
else
error "There is already a worktree with branch subrepo/$subdir.
Use the --force flag to override this check or remove the worktree with
1. rm -rf $_worktree
2. git worktree prune
"
fi
fi
fi
# Set refs_ variables:
refs_subrepo_branch=refs/subrepo/$subref/branch
refs_subrepo_commit=refs/subrepo/$subref/commit
refs_subrepo_fetch=refs/subrepo/$subref/fetch
refs_subrepo_push=refs/subrepo/$subref/push
# Read/parse the .gitrepo file (unless clone/init; doesn't exist yet)
if [[ ! $command =~ ^(clone|init)$ ]]; then
read-gitrepo-file
fi
true
}
# Parse command line args according to a simple dsl spec:
# shellcheck disable=2059
get-params() {
local i=0
local num=${#command_arguments[@]}
for arg in "$@"; do
local value=${command_arguments[i]-}
value=${value//%/%%}
value=${value//\\/\\\\}
# If arg starts with '+' then it is required
if [[ $arg == +* ]]; then
if [[ $i -ge $num ]]; then
usage-error "Command '$command' requires arg '${arg#+}'."
fi
printf -v ${arg#+} -- "$value"
# Look for function name after ':' to provide a default value
else
if [[ $i -lt $num ]]; then
printf -v ${arg%:*} -- "$value"
elif [[ $arg =~ : ]]; then
"${arg#*:}"
fi
fi
: $((i++))
done
# Check for extra arguments:
if [[ $num -gt $i ]]; then
set -- "${command_arguments[@]}"
for ((j = 1; j <= i; j++)); do shift; done
error "Unknown argument(s) '$*' for '$command' command."
fi
}
check-and-normalize-subdir() {
# Sanity check subdir:
[[ $subdir ]] ||
die "subdir not set"
[[ $subdir =~ ^/ || $subdir =~ ^[A-Z]: ]] &&
usage-error "The subdir '$subdir' should not be absolute path."
subdir=${subdir#./}
subdir=${subdir%/}
[[ $subdir != *//* ]] || subdir=$(tr -s / <<< "$subdir")
}
# Determine the correct subdir path to use:
guess-subdir() {
local dir=$subrepo_remote
dir=${dir%.git}
dir=${dir%/}
dir=${dir##*/}
[[ $dir =~ ^[-_a-zA-Z0-9]+$ ]] ||
error "Can't determine subdir from '$subrepo_remote'."
subdir=$dir
check-and-normalize-subdir
encode-subdir
}
# Encode the subdir as a valid git ref format
#
# Input: env $subdir
# Output: env $subref
#
# For detail rules about valid git refs, see the manual of git-check-ref-format:
# URL: https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
# Shell: git check-ref-format --help
#
encode-subdir() {
subref=$subdir
if [[ ! $subref ]] || git check-ref-format "subrepo/$subref"; then
return
fi
## 0. escape %, ensure the subref can be (almost) decoded back to subdir
subref=${subref//%/%25}
## 1. They can include slash / for hierarchical (directory) grouping,
## but no slash-separated component can begin with a dot . or
## end with the sequence .lock.
subref=/$subref/
subref=${subref//\/.//%2e}
subref=${subref//.lock\//%2elock/}
subref=${subref#/}
subref=${subref%/}
## 2. They must contain at least one /.
## Note: 'subrepo/' be will prefixed, so this is always true.
## 3. They cannot have two consecutive dots .. anywhere.
subref=${subref//../%2e%2e}
subref=${subref//%2e./%2e%2e}
subref=${subref//.%2e/%2e%2e}
## 4. They cannot have ASCII control characters
## (i.e. bytes whose values are lower than \040, or \177 DEL), space,
## tilde ~, caret ^, or colon : anywhere.
## 5. They cannot have question-mark ?, asterisk *,
## or open bracket [ anywhere.
local i
for (( i = 1; i < 32; ++i )); do
# skip substitute NUL char (i=0), as bash will skip NUL in env
local x
x=$(printf "%02x" "$i")
subref=${subref//$(printf "%b" "\x$x")/%$x}
done
subref=${subref//$'\177'/%7f}
subref=${subref// /%20}
subref=${subref//\~/%7e}
subref=${subref//^/%5e}
subref=${subref//:/%3a}
subref=${subref//\?/%3f}
subref=${subref//\*/%2a}
subref=${subref//\[/%5b}
subref=${subref//$'\n'/%0a}
## 6. They cannot begin or end with a slash / or contain multiple
## consecutive slashes.
## Note: This rule is not revertable.
[[ $subref != *//* ]] || subref=$(tr -s / <<< "$subref")
## 7. They cannot end with a dot ..
case "$subref" in
*.) subref=${subref%.}
subref+=%2e
;;
esac
## 8. They cannot contain a sequence @\{.
subref=${subref//@\{/%40\{}
## 9. They cannot be the single character @.
## Note: 'subrepo/' be will prefixed, so this is always true.
## 10. They cannot contain a \.
subref=${subref//\\/%5c}
subref=$(git check-ref-format --normalize --allow-onelevel "$subref") ||
error "Can't determine valid subref from '$subdir'."
}
#------------------------------------------------------------------------------
# State file (`.gitrepo`) functions:
#------------------------------------------------------------------------------
# Set subdir and gitrepo vars:
read-gitrepo-file() {
gitrepo=$subdir/.gitrepo
if [[ ! -f $gitrepo ]]; then
error "No '$gitrepo' file."
fi
# Read .gitrepo values:
if [[ -z $subrepo_remote ]]; then
SAY=false OUT=true RUN git config --file="$gitrepo" subrepo.remote
subrepo_remote=$output
fi
if [[ -z $subrepo_branch ]]; then
SAY=false OUT=true RUN git config --file="$gitrepo" subrepo.branch
subrepo_branch=$output
fi
SAY=false OUT=true RUN git config --file="$gitrepo" subrepo.commit
subrepo_commit=$output
FAIL=false \
SAY=false OUT=true RUN git config --file="$gitrepo" subrepo.parent
subrepo_parent=$output
FAIL=false \
SAY=false OUT=true RUN git config --file="$gitrepo" subrepo.method
if [[ $output == rebase ]]; then
join_method=rebase
else
# This is the default method
join_method=merge
fi
if [[ -z $subrepo_parent ]]; then
FAIL=false \
SAY=false OUT=true RUN git config --file="$gitrepo" subrepo.former
subrepo_former=$output
fi
}
# Update the subdir/.gitrepo state file:
update-gitrepo-file() {
local short_commit=
local newfile=false
if [[ ! -e $gitrepo ]]; then
FAIL=false RUN git cat-file -e "$original_head_commit":"$gitrepo"
if OK; then
o "Try to recreate gitrepo file from $original_head_commit"
git cat-file -p "$original_head_commit":"$gitrepo" > "$gitrepo"
else
newfile=true
cat <<... > "$gitrepo"
; DO NOT EDIT (unless you know what you are doing)
;
; This subdirectory is a git "subrepo", and this file is maintained by the
; git-subrepo command. See https://github.com/ingydotnet/git-subrepo#readme
;
...
fi
fi
# TODO: only update remote and branch if supplied and $update_wanted
if $newfile || [[ $update_wanted && $override_remote ]]; then
RUN git config --file="$gitrepo" subrepo.remote "$subrepo_remote"
fi
if $newfile || [[ $update_wanted && $override_branch ]]; then
RUN git config --file="$gitrepo" subrepo.branch "$subrepo_branch"
fi
RUN git config --file="$gitrepo" subrepo.commit "$upstream_head_commit"
# Only write new parent when we are at the head of upstream
if [[ $upstream_head_commit && $subrepo_commit_ref ]]; then
OUT=true RUN git rev-parse "$subrepo_commit_ref"
o "$upstream_head_commit == $output"
if [[ $upstream_head_commit == "$output" ]]; then
RUN git config --file="$gitrepo" subrepo.parent "$original_head_commit"
fi
fi
[[ -z $join_method ]] && join_method=merge
RUN git config --file="$gitrepo" subrepo.method "$join_method"
RUN git config --file="$gitrepo" subrepo.cmdver "$VERSION"
RUN git add -f -- "$gitrepo"
}
#------------------------------------------------------------------------------
# Enviroment checks:
#------------------------------------------------------------------------------
# Check that system is ok for this command:
assert-environment-ok() {
type git &> /dev/null ||
error "Can't find your 'git' command in '$PATH'."
git_version=$(git --version | cut -d ' ' -f3)
version-check bash "$REQUIRED_BASH_VERSION" || {
echo "The 'bashplus' library requires that 'Bash ${REQUIRED_BASH_VERSION}+' is installed." >&2
echo "It doesn't need to be your shell, but it must be in your PATH." >&2
if [[ ${OSTYPE-} == darwin* ]]; then
echo "You appear to be on macOS." >&2
echo "Try: 'brew install bash'." >&2
echo "This will not change your user shell, it just installs 'Bash 5.x'." >&2
fi
exit 1
}
version-check git "$REQUIRED_GIT_VERSION" ||
error "Requires git version $REQUIRED_GIT_VERSION or higher; "`
`"you have '$git_version'."
if [[ ${BASH_VERSINFO[0]} -lt 4 ]] ; then
echo "The git-subrepo command requires that 'Bash 4+' is installed."
echo "It doesn't need to be your shell, but it must be in your PATH."
if [[ $OSTYPE == darwin* ]]; then
echo "You appear to be on macOS."
echo "Try: 'brew install bash'."
echo "This will not change your user shell, it just installs 'Bash 5.x'."
fi
exit 1
fi
}
# Make sure git repo is ready:
assert-repo-is-ready() {
# Skip this for trivial info commands:
[[ $command =~ ^(help|version|upgrade)$ ]] && return
# We must be inside a git repo:
git rev-parse --git-dir &> /dev/null ||
error "Not inside a git repository."
# Get the original branch and commit:
git:get-head-branch-name
original_head_branch=$output
# If a subrepo branch is currently checked out, then note it:
if [[ $original_head_branch =~ ^subrepo/(.*) ]]; then
error "Can't '$command' while subrepo branch is checked out."
fi
# Make sure we are on a branch:
[[ $original_head_branch == HEAD || -z $original_head_branch ]] &&
error "Must be on a branch to run this command."
# In a work-tree:
SAY=false OUT=true RUN git rev-parse --is-inside-work-tree
[[ $output == true ]] ||
error "Can't 'subrepo $command' outside a working tree."
# HEAD exists:
[[ $command == clone ]] ||
RUN git rev-parse --verify HEAD
assert-working-copy-is-clean
# For now, only support actions from top of repo:
if [[ $(git rev-parse --show-prefix) ]]; then
error "Need to run subrepo command from top level directory of the repo."
fi
}
assert-working-copy-is-clean() {
# Repo is in a clean state:
if [[ $command =~ ^(clone|init|pull|push|branch|commit)$ ]]; then
# TODO: Should we check for untracked files?
local pwd
pwd=$(pwd)
o "Assert that working copy is clean: $pwd"
git update-index -q --ignore-submodules --refresh
git diff-files --quiet --ignore-submodules ||
error "Can't $command subrepo. Unstaged changes. ($pwd)"
if [[ $command != clone ]] || git:rev-exists HEAD; then
git diff-index --quiet --ignore-submodules HEAD ||
error "Can't $command subrepo. Working tree has changes. ($pwd)"
git diff-index --quiet --cached --ignore-submodules HEAD ||
error "Can't $command subrepo. Index has changes. ($pwd)"
else
# Repo has no commits and we're cloning a subrepo. Working tree won't
# possibly have changes as there was nothing initial to change.
[[ -z $(git ls-files) ]] ||
error "Can't $command subrepo. Index has changes. ($pwd)"
fi
fi
}
# If subdir exists, make sure it is empty:
assert-subdir-ready-for-init() {
if [[ ! -e $subdir ]]; then
error "The subdir '$subdir' does not exist."
fi
if [[ -e $subdir/.gitrepo ]]; then
error "The subdir '$subdir' is already a subrepo."
fi
# Check that subdir is part of the repo
if [[ -z $(git log -1 --date=default -- "$subdir") ]]; then
error "The subdir '$subdir' is not part of this repo."
fi
}
# If subdir exists, make sure it is empty:
assert-subdir-empty() {
if [[ -e $subdir ]] && [[ $(ls -A "$subdir") ]]; then
error "The subdir '$subdir' exists and is not empty."
fi
}
#------------------------------------------------------------------------------
# Getters of various information:
#------------------------------------------------------------------------------
# Find all the current subrepos by looking for all the subdirectories that
# contain a `.gitrepo` file.
get-all-subrepos() {
local paths
mapfile -t paths < <(git ls-files | sed -n 's!/\.gitrepo$!!p' | sort)
subrepos=()
local path
for path in "${paths[@]}"; do
add-subrepo "$path"
done
}
add-subrepo() {
if ! $ALL_wanted; then
for path in "${subrepos[@]}"; do
[[ $1 =~ ^$path/ ]] && return
done
fi
subrepos+=("$1")
}
# Determine the upstream's default head branch:
get-upstream-head-branch() {
local remotes branch
OUT=true RUN git ls-remote --symref "$subrepo_remote"
remotes=$output
[[ $remotes ]] ||
error "Failed to 'git ls-remote --symref $subrepo_remote'."
# 'ref: refs/heads/master HEAD'
branch=$(
echo "$remotes" |
grep "^ref:" | grep 'HEAD$' | cut -f2 -d':' | cut -f1 |
head -n1
)
branch=${branch/ }
[[ $branch =~ refs/heads/ ]] ||
error "Problem finding remote default head branch."
output=${branch#refs/heads/}
}
# Commit msg for an action commit:
# Don't use RUN here as it will pollute commit message
get-commit-message() {
local commit=none
if git:rev-exists "$upstream_head_commit"; then
commit=$(git rev-parse --short "$upstream_head_commit")
fi
local args=() debug_wanted=false
if $all_wanted; then
args+=("$subdir")
fi
args+=("${commit_msg_args[@]}")
# Find the specific git-subrepo code used:
local command_remote='???'
local command_commit='???'
get-command-info
local merged=none
if git:rev-exists "$subrepo_commit_ref"; then
merged=$(git rev-parse --short "$subrepo_commit_ref")
fi
local is_merge=''
if [[ $command != push ]]; then
if git:is_merge_commit "$subrepo_commit_ref"; then
is_merge=" (merge)"
fi
fi
# TODO: Consider output for push!
# Format subrepo commit message:
cat <<...
git subrepo $command$is_merge ${args[*]}
subrepo:
subdir: "$subdir"
merged: "$merged"
upstream:
origin: "$subrepo_remote"
branch: "$subrepo_branch"
commit: "$commit"
git-subrepo:
version: "$VERSION"
origin: "$command_remote"
commit: "$command_commit"
...
}
# Get location and version info about the git-subrepo command itself. This
# info goes into commit messages, so we can find out exactly how the commits
# were done.
get-command-info() {
local bin=$0
if [[ $bin =~ / ]]; then
local lib
lib=$(dirname "$bin")
# XXX Makefile needs to install these symlinks:
# If `git-subrepo` was system-installed (`make install`):
if [[ -e $lib/git-subrepo.d/upstream ]] &&
[[ -e $lib/git-subrepo.d/commit ]]; then
command_remote=$(readlink "$lib/git-subrepo.d/upstream")
command_commit=$(readlink "$lib/git-subrepo.d/commit")
elif [[ $lib =~ / ]]; then
lib=$(dirname "$lib")
if [[ -d $lib/.git ]]; then
local remote
remote=$(
GIT_DIR=$lib/.git git remote -v |
grep '^origin' |
head -n1 |
cut -f2 |
cut -d ' ' -f1
)
if [[ $remote ]]; then
command_remote=$remote
else
local remote
remote=$(
GIT_DIR=$lib/.git git remote -v |
head -n1 |
cut -f2 |
cut -d ' ' -f1
)
if [[ $remote ]]; then
command_remote=$remote
fi
fi
local commit
commit=$(GIT_DIR=$lib/.git git rev-parse --short HEAD)
if [[ $commit ]]; then
command_commit=$commit
fi
fi
fi
fi
}
#------------------------------------------------------------------------------
# Instructional errors:
#------------------------------------------------------------------------------
error-join() {
cat <<...
You will need to finish the $command by hand. A new working tree has been
created at $worktree so that you can resolve the conflicts
shown in the output above.
This is the common conflict resolution workflow:
1. cd $worktree
2. Resolve the conflicts (see "git status").
3. "git add" the resolved files.
...
if [[ $join_method == rebase ]]; then
cat <<...
4. git rebase --continue
...
else
cat <<...
4. git commit
...
fi
cat <<...
5. If there are more conflicts, restart at step 2.
6. cd $start_pwd
...
local branch_name=${branch:=subrepo/$subdir}
if [[ $command == push ]]; then
cat <<...
7. git subrepo push $subdir $branch_name
...
else
cat <<...
7. git subrepo commit $subdir
...
fi
if [[ $command == pull && $join_method == rebase ]]; then
cat <<...
After you have performed the steps above you can push your local changes
without repeating the rebase by:
1. git subrepo push $subdir $branch_name
...
fi
cat <<...
See "git help $join_method" for details.
Alternatively, you can abort the $command and reset back to where you started:
1. git subrepo clean $subdir
See "git help subrepo" for more help.
...
}
#------------------------------------------------------------------------------
# Git command wrappers:
#------------------------------------------------------------------------------
git:branch-exists() {
git:rev-exists "refs/heads/$1"
}
git:rev-exists() {
git rev-list "$1" -1 &> /dev/null
}
git:ref-exists() {
[[ $(git for-each-ref "$1") ]]
}
git:get-head-branch-name() {
output=
local name
name=$(git symbolic-ref --short --quiet HEAD) || true
[[ $name == HEAD ]] && return
output=$name
}
git:get-head-branch-commit() {
output=$(git rev-parse HEAD)
}
git:commit-in-rev-list() {
local commit=$1
local list_head=$2
git rev-list "$list_head" | grep -q "^$commit"
}
git:make-ref() {
local ref_name=$1
local commit
commit=$(git rev-parse "$2")
RUN git update-ref "$ref_name" "$commit"
}
git:is_merge_commit() {
local commit=$1
git show --summary "$commit" | grep -q ^Merge:
}
git:create-worktree() {
local branch=$1
worktree=$GIT_TMP/$branch
RUN git worktree add "$worktree" "$branch"
}
git:remove-worktree() {
o "Remove worktree: $worktree"
if [[ -d $worktree ]]; then
o "Check worktree for unsaved changes"
cd "$worktree"
assert-working-copy-is-clean
cd "$start_pwd"
o "Clean up worktree $worktree"
rm -rf "$worktree"
RUN git worktree prune
fi
}
git:delete-branch() {
local branch=$1
o "Deleting old '$branch' branch."
# Remove worktree first, otherwise you can't delete the branch
git:remove-worktree
FAIL=false RUN git branch -D "$branch"
}
#------------------------------------------------------------------------------
# Low level sugar commands:
#------------------------------------------------------------------------------
# Smart command runner:
RUN() {
$debug_wanted && $SAY && say ">>> $*"
if $EXEC; then
"$@"
return $?
fi
OK=true
set +e
local rc=
local out=
if $debug_wanted && $TTY && interactive; then
"$@"
else
if $OUT; then
out=$("$@" 2>/dev/null)
else
out=$("$@" 2>&1)
fi
fi
rc=$?
set -e
if [[ $rc -ne 0 ]]; then
OK=false
$FAIL && error "Command failed: '$*'.\n$out"
fi
output=$out
}
interactive() {
if [[ -t 0 && -t 1 ]]; then
return 0
else
return 1
fi
}
# Call a function with indent increased:
CALL() {
local INDENT=" $INDENT"
"$@" || true
}
# Print verbose steps for commands with steps:
o() {
if $verbose_wanted; then
echo "$INDENT* $*"
fi
}
# Print unless quiet mode:
say() {
$quiet_wanted || echo "$@"
}
# Print to stderr:
err() {
echo "$@" >&2
}
# Check if OK:
OK() {
$OK
}
# Nicely report common error messages:
usage-error() {
local msg="git-subrepo: $1" usage=
if [[ $GIT_SUBREPO_TEST_ERRORS != true ]]; then
if can "help:$command"; then
msg=$'\n'"$msg"$'\n'"$("help:$command")"$'\n'
fi
fi
echo "$msg" >&2
exit 1
}
# Nicely report common error messages:
error() {
echo -e "git-subrepo: $1" >&2
exit 1
}
# Start at the end:
[[ ${BASH_SOURCE[0]} != "$0" ]] || main "$@"
# Local Variables:
# tab-width: 2
# sh-indentation: 2
# sh-basic-offset: 2
# End:
# vim: set ft=sh sw=2 lisp:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment