aboutsummaryrefslogtreecommitdiff
path: root/bpkg-rep/manage.in
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2020-10-14 19:38:39 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2020-10-14 20:36:06 +0300
commit0d1d47c5e183adc61dc60f735a1fe2422ca6c864 (patch)
tree986310117fb3d257f6b45c2ff724f7b554e8b7a1 /bpkg-rep/manage.in
parentfa624b451a7d21165fd064d03b70c96f51e6b27c (diff)
Rename project/package from bpkg-rep to bpkg-util
Diffstat (limited to 'bpkg-rep/manage.in')
-rw-r--r--bpkg-rep/manage.in957
1 files changed, 0 insertions, 957 deletions
diff --git a/bpkg-rep/manage.in b/bpkg-rep/manage.in
deleted file mode 100644
index ceede3c..0000000
--- a/bpkg-rep/manage.in
+++ /dev/null
@@ -1,957 +0,0 @@
-#!/usr/bin/env bash
-
-# file : bpkg-rep/manage.in
-# license : MIT; see accompanying LICENSE file
-
-# Interactively migrate newly-submitted packages from a source git repository
-# to a destination git repository.
-#
-# Present the user with the list of commits that added the files currently in
-# the source repository's working directory and ask the user to select from a
-# menu the action to perform on a selection of these commits.
-#
-# As the files added by these commits are pending a move to the destination
-# repository, these commits will be referred to as "pending" commits.
-#
-# Actions that can be performed on a selection of pending commits include
-# moving them from the source repository to a single commit in the destination
-# repository and dropping them from the source repository.
-#
-# The flow of this script, in broad strokes, is as follows: for each file in
-# the source repository directory, find the hash of the commit that added it;
-# these are the pending commits. Arrange the pending commits in chronological
-# order. Display to the user the pending commits along with the files they
-# added. Let the user select one or more pending commits and an action to be
-# performed on them. Each successful action results in a commit to the source
-# and/or destination repositories and leaves both repositories in a clean
-# state. Once the action has been performed, redisplay the updated pending
-# commit list and prompt for the next action. Pushing to the remote
-# repositories, a supported operation, can be done at any time during the
-# session.
-#
-# <dir> The directory into which the source and destination repositories have
-# been checked out. If not specified, current directory is assumed.
-#
-usage="usage: $0 [<dir>]"
-
-# Source/destination repository inside <dir>. Note: also used in commit
-# messages.
-#
-src_repo_name=queue
-dst_repo_name=public
-
-owd="$(pwd)"
-trap "{ cd '$owd'; exit 1; }" ERR
-set -o errtrace # Trap in functions.
-
-@import bpkg-rep/utility@
-
-# Use the bpkg program from the script directory, if present. Otherwise, use
-# just 'bpkg'.
-#
-bpkg_rep_bpkg="$(dirname "$(realpath "${BASH_SOURCE[0]}")")/bpkg"
-
-if [ ! -x "$bpkg_rep_bpkg" ]; then
- bpkg_rep_bpkg=bpkg
-fi
-
-@import bpkg-rep/package-archive@
-
-# Set the working directory.
-#
-if [ $# -eq 0 ]; then
- dir="$owd"
-elif [ $# -eq 1 ]; then
- dir="${1%/}" # <dir> with trailing slash removed.
-else
- error "$usage"
-fi
-
-# The source and destination package repository directories.
-#
-# Note that, throughout this script, any path not explicitly prefixed with
-# "$src_dir/" or "$dst_dir/" is relative to the root of the source or
-# destination package repositories.
-#
-src_dir="$dir/$src_repo_name"
-dst_dir="$dir/$dst_repo_name"
-
-if [ ! -d "$src_dir" ]; then
- error "'$src_dir' does not exist or is not a directory"
-fi
-
-if [ ! -d "$dst_dir" ]; then
- error "'$dst_dir' does not exist or is not a directory"
-fi
-
-# Check that both git repositories are clean.
-#
-if [ -n "$(git -C $src_dir status --porcelain)" ]; then
- error "git repository in '$src_dir' is not clean"
-fi
-
-if [ -n "$(git -C $dst_dir status --porcelain)" ]; then
- error "git repository in '$dst_dir' is not clean"
-fi
-
-# Use run() to show the user that git is the source of the diagnostics.
-# "Already up to date", for example, is too vague.
-#
-run git -C "$src_dir" pull >&2
-run git -C "$dst_dir" pull >&2
-
-# Load the source and destination repositories' submit configurations (section
-# name/directory mappings and owners directory path).
-#
-# Each repository's settings are sourced into the temporary variables 'owners'
-# and 'sections' and copied from there to source- and destination-specific
-# variables.
-#
-declare owners
-declare -A sections
-source "$src_dir/submit.config.bash"
-
-src_owners="$owners"
-declare -A src_sections
-for s in "${!sections[@]}"; do
- src_sections["$s"]="${sections[$s]}"
-done
-
-owners=
-sections=()
-source "$dst_dir/submit.config.bash"
-
-dst_owners="$owners"
-declare -A dst_sections
-for s in "${!sections[@]}"; do
- dst_sections["$s"]="${sections[$s]}"
-done
-
-# Find all archive and owner manifest files in the source repository.
-#
-# Every file in a repository section directory except *.manifest is a package
-# archive and every file in the owners directory is a project or package owner
-# manifest. Therefore run find separately on each section directory and the
-# owners directory to build a list containing only package-related files.
-#
-# Store the relative to the repository directory file paths in an array used
-# to build the set of pending commits.
-#
-src_files=()
-for s in "${src_sections[@]}"; do
- while read f; do
- src_files+=("${f#$src_dir/}")
- done < <(find "$src_dir/$s" -type f -not -name "*.manifest")
-done
-
-if [[ -n "$src_owners" && -d "$src_dir/$src_owners" ]]; then
- while read f; do
- src_files+=("${f#$src_dir/}")
- done < <(find "$src_dir/$src_owners" -type f)
-fi
-
-# Build the set of pending commit hashes ("pending set").
-#
-# For each file in the source repository, find the most recent commit that
-# added it and store its abbreviated hash (as key) inside the 'pending_set'
-# associative array (note: unordered) and (as value) inside the 'file_commits'
-# associative array.
-#
-# 'file_commits' maps from the file path to the last commit to add it to the
-# repository. A file may have been added and removed by earlier commits and
-# could thus be migrated with the wrong commit unless care is taken (see
-# migrate() for an example).
-#
-declare -A pending_set
-declare -A file_commits
-for f in "${src_files[@]}"; do
-
- # -n 1: limit output to one commit (that is, the most recent)
- # --diff-filter=A: only show commits that added files
- # --pretty=format:%h: output only the abbreviated commit hash
- #
- h="$(git -C "$src_dir" log -n 1 --diff-filter=A --pretty=format:%h -- "$f")"
-
- # Note that the hash cannot be empty because, after our clean checks at the
- # top, every file on disk must have been added by some commit (that is,
- # there can be no untracked files).
- #
- pending_set["$h"]=true
- file_commits["$f"]="$h"
-done
-
-# Arrange the pending commits in the chronological order.
-#
-# Go through the most recent commits in the git log which added one or more
-# files, skipping those not present in the pending set and keeping count to
-# bail out as soon as we ordered all of them.
-#
-pending_seq=()
-for (( i=0; i != ${#pending_set[@]}; )); do
- read h # The abbreviated commit hash.
-
- # If this is a pending commit, prepend its hash to the ordered array.
- #
- if [ "${pending_set[$h]}" ]; then
- pending_seq=("$h" "${pending_seq[@]}")
- ((++i))
- fi
-done < <(git -C "$src_dir" log --diff-filter=A --pretty=format:%h)
-
-if [ "${#pending_seq[@]}" -eq 0 ]; then
- info "Good news, nothing to manage!"
- exit 0
-fi
-
-# Return the list of files a commit added to the source repository.
-#
-function commit_files () # <commit-hash>
-{
- local h="$1"
-
- # git-diff-tree arguments:
- #
- # --diff-filter=A: select only files that were added.
- # -z: don't munge file paths and separate output fields with
- # NULs.
- # -r: recurse into subtrees (directories).
- #
- git -C "$src_dir" diff-tree \
- --no-commit-id --name-only --diff-filter=A -z -r \
- "$h"
-}
-
-# Extract the package name, version, and project from a package archive's
-# manifest and print it to stdout in the '<name> <version> <project>' form. If
-# the manifest does not specify the project name, the package name is returned
-# as the project name.
-#
-function extract_pkg_info () # <archive>
-{
- local arc="$1"
-
- local r
- r=($(bpkg_rep_pkg_verify_archive "$arc")) # <name> <version> <project>
- if [ ! -v r[2] ]; then
- r[2]="${r[0]}"
- fi
-
- # Verify that the archive parent directory name matches the project.
- #
- local p="${r[2]}"
- if [ "$p" != "$(basename "$(dirname "$arc")")" ]; then
- error "'$arc' archive directory name does not match package project '$p'"
- fi
-
- echo -n "${r[@]}"
-}
-
-# Migrate a package archive or ownership manifest file from the source
-# repository to the destination repository.
-#
-# <src> is the path of the source file, relative to the source respository
-# directory. For example, '1/stable/foo/foo-1.2.3.tar.gz',
-# 'owners/foo/project-owner.manifest', or
-# 'owners/foo/foo/package-owner.manifest'.
-#
-# <dst> is the path of the destination directory, relative to the destination
-# repository directory. For example, '1/testing/foo', 'ownership/foo', or
-# 'ownership/foo/foo'.
-#
-# Note that the source and destination sections and owners directories may
-# differ (as they do in these examples) which is why those components must
-# be specified in both the source and destination paths.
-#
-# Move the file from the source repository directory to the destination
-# repository directory, creating directories if required; stage the addition
-# of the file to the destination repository; stage the removal of the file
-# from the source repository.
-#
-function migrate_file () # <src> <dst>
-{
- local src="$1"
- local dst="$2"
-
- mkdir -p "$dst_dir/$dst"
- mv "$src_dir/$src" "$dst_dir/$dst/"
- run git -C "$src_dir/" rm --quiet "$src"
- run git -C "$dst_dir/" add "$dst/$(basename "$src")"
-}
-
-# Migrate:
-#
-# 0. Assumptions:
-#
-# - All the packages in a bundle are migrating from/to the same sections
-# (enforce source part).
-#
-# - All the packages are from the same project (enforce).
-#
-# 1. Move files:
-#
-# - Owners to owners directory.
-#
-# - Packages into corresponding sections:
-#
-# alpha -> alpha
-# beta -> beta
-# stable -> testing|stable
-#
-# Bonus: replace revisions.
-# Bonus: offer to drop existing packages if moving to alpha or beta.
-#
-# 2. Come up with commit message for src and commit.
-#
-# "Migrate <project> to $dst_repo_name/<section>"
-#
-# "remove <package>/<version>"
-# "remove owners/<project>/*"
-#
-# 3. Come up with commit message for dst and commit.
-#
-# "Migrate <project> from $src_repo_name/<section>"
-#
-# "add <package>/<version>"
-# "replace <package>/<version> with <version>" (if replacing)
-# "add owners/<project>/*"
-#
-# 4. Commit.
-#
-# Note that when migrating we will need to confirm with git that each of a
-# commit's added files were actually most recently added by that commit. For
-# example (oldest commits first):
-#
-# commit 1: add foo.tar.gz, bar.tar.gz
-# commit 2: del foo.tar.gz
-# commit 3: add foo.tar.gz
-#
-# If the user chooses to migrate commit 1, only bar.tar.gz must be migrated,
-# despite foo.tar.gz existing on disk.
-#
-# The commit bundle associative array is the set of selected pending
-# commits. Its keys are the corresponding indexes of the 'pending_seq' array
-# (but offset by +1 and formatted to match the displayed commit numbers).
-# Note: the reason the commit bundle is an associative array is to prevent
-# duplicates.
-#
-declare -A bundle
-
-# Migrate the selected commit bundle from the source repository to the
-# destination repository. Set the global migrate_result variable to true if
-# the migration has been successful, or issue appropriate diagnostics and set
-# it to the empty string if any of the following is true:
-#
-# - The commit bundle is empty.
-#
-# - Files added by commits in the bundle are not from the same project (the
-# "bundle project") or, in the case of archives, the same repository section
-# (the "bundle section").
-#
-# - The required section does not exist in the destination repository.
-#
-# - An identical package archive (same name and version) already exists in the
-# destination repository.
-#
-# - Any file has an invalid path (for example, missing a valid project or
-# section component).
-#
-# The migration process proceeds as follows:
-#
-# - Move files: all of the files in the selected commit bundle are moved from
-# the source repository into the destination repository.
-#
-# Package archives may be removed and ownership manifests overwritten at the
-# destination. Candidate files for replacement are selected as follows:
-#
-# - In the alpha and beta sections, any package archive files in the
-# destination section directory belonging to the same package are
-# considered for replacement, regardless of their versions.
-#
-# - In other sections, any package archives in the destination section
-# directory with the same name and version but a different revision
-# (currently whether lower or higher) are automatically replaced.
-#
-# - Project or package ownership manifests will be replaced (that is, simply
-# overwritten) at the destination with any ownership manifests added by
-# the commit bundle because their presence implies that ownership
-# information has changed.
-#
-# Stage (but don't commit) the removal of the files from the source
-# repository and their addition to the destination repository.
-#
-# - Make commits to the source and destination respositories with appropriate
-# commit messages.
-#
-# If any part of the migration fails then all changes to the source and
-# destination repositories are undone, leaving two clean repositories.
-#
-function migrate ()
-{
- migrate_result=
-
- if [ "${#bundle[@]}" -eq 0 ]; then
- info "no commits selected"
- return
- fi
-
- # Check that every commit's added files are in the bundle section and/or
- # bundle project before migrating any of them. Build the bundle's list of
- # files as we go along, classifying them as package archives or ownership
- # manifests based on their paths.
- #
- # The bundle section is derived from the first package archive encountered
- # and the bundle project from the first package archive or owner manifest
- # encountered.
- #
- # Note that the bundle traversal is unordered.
- #
- local src_sect= # Source section name.
- local src_sect_dir= # Source section directory.
- local proj= # The bundle (source) project.
- local pkgs=() # The bundle's archive files.
- local owns=() # The bundle's ownership manifests.
-
- local i
- for i in "${!bundle[@]}"; do
- local h="${pending_seq[i-1]}" # The current commit's abbreviated hash.
-
- # Check the files added by the current commit.
- #
- local f
- while read -d '' f; do
- if [ "${file_commits[$f]}" != "$h" ]; then
- continue # This file was deleted by a subsequent commit.
- fi
-
- # Derive the project and/or section names from the file path.
- #
- # The project name is taken directly from the file path. In the case of
- # package archives, the section name is the key in the 'src_sections'
- # associative array which maps to the section directory extracted from
- # the file path.
- #
- local fproj= # Current file's project.
-
- if [[ -n "$src_owners" && ("$f" =~ ^"$src_owners"/([^/]+)/.+$) ]]; then
- fproj="${BASH_REMATCH[1]}"
- owns+=("$f")
- elif [[ "$f" =~ ^(.+)/([^/]+)/[^/]+$ ]]; then # Package archive?
- local fsect_dir="${BASH_REMATCH[1]}"
-
- fproj="${BASH_REMATCH[2]}"
- pkgs+=("$f")
-
- # Find the archive section name associated with the extracted section
- # directory in 'src_sections' (a value-to-key lookup).
- #
- local fsect=
-
- # The "*" key is a catch-all for unknown submitted section names and,
- # if present, will share a value (section directory) with one of the
- # known section names and therefore must be skipped.
- #
- # If there is no mapping in 'src_sections' to the extracted section
- # directory then the file path is invalid.
- #
- local k
- for k in "${!src_sections[@]}"; do
- if [[ ("${src_sections[$k]%/}" == "$fsect_dir") &&
- ("$k" != "*") ]]; then
- fsect="$k" # Current file's section name.
- break
- fi
- done
-
- if [ -z "$fsect" ]; then
- info "unable to find section name for file '$f'"
- return
- fi
-
- # Set the source section name and directory if unset; otherwise fail
- # if the current file is not from the source section.
- #
- if [ -z "$src_sect" ]; then
- src_sect="$fsect"
- src_sect_dir="$fsect_dir"
- elif [ "$fsect" != "$src_sect" ]; then
- info "cannot include commit $i: '$f' is not in section $src_sect"
- return
- fi
- else
- info "unrecognized type of file '$f'"
- return
- fi
-
- # Set the bundle project if unset; otherwise fail if the current file is
- # not from the bundle project.
- #
- # Note: $fproj cannot be empty here (see above).
- #
- if [ -z "$proj" ]; then
- proj="$fproj"
- elif [ "$fproj" != "$proj" ]; then
- info "cannot include commit $i: '$f' is not in project $proj"
- return
- fi
- done < <(commit_files "$h")
- done
-
- # Finalize migration variables the values of which depend on whether the
- # bundle contains at least one package archive or ownership manifests only.
- #
- # The source and destination commit messages are composed incrementally as
- # the migration process proceeds.
- #
- local dst_sect # Destination section name.
- local dst_sect_dir # Destination section directory.
- local src_cmsg # Source commit message.
- local dst_cmsg # Destination commit message.
-
- if [ ${#pkgs[@]} -ne 0 ]; then # Bundle contains package archive(s).
- dst_sect="$src_sect"
-
- # If it exists, 'testing' overrides 'stable' at the destination.
- #
- if [[ ("$dst_sect" == "stable") && -v dst_sections["testing"] ]]; then
- dst_sect="testing"
- fi
-
- # Fail if the target section does not exist in the destination repository.
- #
- if [ ! -v dst_sections["$dst_sect"] ]; then
- info "section '$dst_sect' does not exist in the destination repository"
- return
- fi
-
- dst_sect_dir="${dst_sections[$dst_sect]}"
-
- src_cmsg="Migrate $proj to $dst_repo_name/$dst_sect"$'\n\n'
- dst_cmsg="Migrate $proj from $src_repo_name/$src_sect"$'\n\n'
- else # Bundle consists only of ownership manifests.
-
- # The setup where the ownership authentication is disabled on the
- # destination but enabled on source is probably obscure, but let's
- # consider it possible since the submit-git handler allows such a setup.
- #
- if [ -n "$dst_owners" ]; then
- src_cmsg="Migrate $proj ownership info to $dst_repo_name"$'\n\n'
- dst_cmsg="Migrate $proj ownership info from $src_repo_name"$'\n\n'
- else
- src_cmsg="Remove $proj ownership info"$'\n\n'
- dst_cmsg= # Nothing to commit.
- fi
- fi
-
- # Ensure that the source and destination repositories are clean if the
- # migration of any file fails.
- #
- # Note that the source repository cannot have untracked files so we
- # git-clean only the destination repository.
- #
- function cleanup ()
- {
- info "migration failed; resetting and cleaning repositories"
-
- if ! run git -C "$src_dir" reset --hard ||
- ! run git -C "$dst_dir" reset --hard ||
- ! run git -C "$dst_dir" clean --force; then
- info "failed to reset/clean repositories -- manual intervention required"
- fi
- }
- trap cleanup EXIT
-
- # Migrate the bundle's package archive files.
- #
- for f in "${pkgs[@]}"; do
- # Get the current package's name and version from its embedded manifest
- # (we already have the source project in $proj).
- #
- local p
- p=($(extract_pkg_info "$src_dir/$f"))
-
- local name="${p[0]}"
- local src_version="${p[1]}"
-
- # Check for duplicate package in all sections. Use <name>-<version>.*
- # without .tar.gz in case we want to support more archive types later.
- #
- # Note that, for example, foo-bar version 1.0 and foo version bar-1.0 have
- # the same archive name foo-bar-1.0.tar.gz.
- #
- local s
- for s in "${!dst_sections[@]}"; do
- local p
- IFS=$'\n' eval \
- 'p=($(bpkg_rep_pkg_find_archive "$name-$src_version.*" \
- "$dst_dir/${dst_sections[$s]}"))'
-
- if [ "${#p[@]}" -ne 0 ]; then
- local n="${p[0]}"
- local v="${p[1]}"
- local a="${p[3]}"
-
- if [ "$n" == "$name" ]; then
- error "duplicate of $name/$src_version at '$a'"
- else
- error "conflict of $name/$src_version with $n/$v at '$a'"
- fi
- fi
- done
-
- # In the destination repository, find and remove package archive files
- # which are other alpha/beta versions or revisions of the current source
- # package.
- #
- local vpat # Version pattern.
- case "$dst_sect" in
- alpha|beta) vpat="*" ;; # All package versions.
- *) vpat="$src_version*" ;; # All package version revisions.
- esac
-
- # Packages in the destination repository to be considered for replacement.
- #
- local dst_files
-
- IFS=$'\n' eval \
- 'dst_files=($(bpkg_rep_pkg_find_archives "$name" \
- "$vpat" \
- "$dst_dir/$dst_sect_dir"))'
-
- # If true, the source package replaces one or more packages in the
- # destination repository.
- #
- local repl=
-
- local dst_f
- for dst_f in "${dst_files[@]}"; do
- local p
- p=($(extract_pkg_info "$dst_f"))
-
- local dst_version="${p[1]}"
- local dst_project="${p[2]}"
-
- # Ask whether or not to drop the current destination package.
- #
- # Include the project names in the prompt if the destination package's
- # project differs from that of the source package.
- #
- local src="$src_version"
- local dst="$name/$dst_version"
- if [ "$dst_project" != "$proj" ]; then
- src+=" ($proj)"
- dst+=" ($dst_project)"
- fi
-
- while true; do
- read -p "replace $dst with $src? [y/n]: " opt
-
- case "$opt" in
- "y")
- repl=true
- dst_cmsg+=" replace $name/$dst_version with $src_version"$'\n'
- run git -C "$dst_dir" rm --quiet "${dst_f#$dst_dir/}"
- break
- ;;
- "n")
- break
- ;;
- esac
- done
- done
-
- # Migrate the current package.
- #
- src_cmsg+=" remove $name/$src_version"$'\n'
- if [ ! "$repl" ]; then
- dst_cmsg+=" add $name/$src_version"$'\n'
- fi
- migrate_file "$f" "$dst_sect_dir/$proj"
- done
-
- # Migrate the bundle's ownership manifests.
- #
- # If ownership authentication is disabled on the destination repository,
- # only remove ownership manifests from the source repository (that is, don't
- # migrate).
- #
- for f in "${owns[@]}"; do
- src_cmsg+=" remove $(dirname $f)/*"$'\n'
-
- if [ -n "$dst_owners" ]; then
- local dp=$(dirname "${f/$src_owners/$dst_owners}") # Destination path.
-
- # Let the commit message reflect whether this is a new ownership
- # manifest or is replacing an existent one.
- #
- if [ ! -e "$dst_dir/$dp/$(basename "$f")" ]; then
- dst_cmsg+=" add $dp/*"$'\n'
- else
- dst_cmsg+=" update $dp/*"$'\n'
- fi
-
- migrate_file "$f" "$dp"
- else
- run git -C "$src_dir/" rm --quiet "$f"
- fi
- done
-
- # Commit the changes made to the source and destination repositories.
- #
- info
- run git -C "$src_dir" commit -m "$src_cmsg"
-
- if [ -n "$dst_cmsg" ]; then
- info
- run git -C "$dst_dir" commit -m "$dst_cmsg"
- fi
-
- info
-
- # Remove the migrated commits from the pending sequence and clear the
- # bundle.
- #
- for i in "${!bundle[@]}"; do
- unset pending_seq[i-1]
- done
- pending_seq=("${pending_seq[@]}") # Remove the gaps created by unset.
- bundle=()
-
- migrate_result=true
-
- # All files have been migrated successfully so clear the EXIT trap.
- #
- trap EXIT
-
- # Pause to give the operator a chance to look at the commits before the list
- # of remaining pending commits is displayed.
- #
- read -p "Press Enter to continue: "
-}
-
-# Push local changes to the remote source and/or destination git repositories.
-#
-# Push to the destination repository first because thus the migrated files
-# will be in both remote repositories until the completion of the subsequent
-# push to the source repository (which may fail or take long). Although this
-# is an inconsistent state, it is safe because other programs such as a
-# submission handler will be able to detect the duplicates and therefore
-# refuse to do anything. If, on the other hand, we pushed to the source first,
-# the migrated files would not exist in either remote repository until the
-# push to the destination repository completed. In this state the submission
-# handler would, for example, accept a resubmission of the migrated packages or
-# erroneously establish ownership for already owned project/package names.
-#
-function push ()
-{
- # Let's print additional diagnostics on git-push failure, to emphasize for
- # the user which of the two repositories we have failed to push.
- #
- if ! run git -C "$dst_dir" push; then
- error "push to $dst_repo_name failed"
- fi
-
- if ! run git -C "$src_dir" push; then
- error "push to $src_repo_name failed"
- fi
-}
-
-# Present the list of pending commits to the user, oldest first, marking files
-# that were deleted by subsequent commits with `*`:
-#
-# 001 (deadbeef) Add libfoo/1.2.3
-#
-# 1/testing/foo/libfoo-1.2.3.tar.gz
-# owners/foo/project-owner.manifest
-# owners/foo/libfoo/package-owner.manifest
-#
-# 002 (c00l0fff) Add bar/1.2.3
-#
-# * 1/testing/bar/libbar-1.2.3.tar.gz
-# 1/testing/bar/libbaz-1.2.3.tar.gz
-#
-# 003 (deadbabe) Add libbar/1.2.3+1
-#
-# 1/testing/bar/libbar-1.2.3+1.tar.gz
-#
-# Note that files deleted by subsequent commits may still be in the
-# repository. See migrate() for an example.
-#
-# Then prompt the user for the action (showing the current bundle):
-#
-# [001 002][<N>,m,c,p,q,l,?]:
-#
-# <N> - add commit to the commit bundle
-# m - migrate the selected commit bundle
-# c - clear the selected commit bundle
-# p - push source and destination repositories
-# l - print pending commits
-# q - quit (prompting to push if any actions have been taken)
-# ? - print this help
-#
-# The user interaction loop.
-#
-# In each iteration, present the list of pending commits, display the menu of
-# actions, read the user's input, and perform the chosen action.
-#
-# True if any changes have been made to the source and/or destination git
-# repositories (in which case the user will be asked whether or not to push
-# before quitting).
-#
-need_push=
-
-while true; do
- # Show the pending commits.
- #
- if [ "${#pending_seq[@]}" -eq 0 ]; then
- info "no more pending commits"
- fi
-
- for ((i=0; i != "${#pending_seq[@]}"; i++)); do
- h="${pending_seq[$i]}"
-
- # Print commit number, hash, and subject.
- #
- # The commit number is left-padded with 0s to 3 digits. Prefix with a
- # newline to separate the first commit from the git-pull output and the
- # rest from the previous commit info block.
- #
- subj="$(git -C "$src_dir" log -n 1 --pretty=format:%s "$h")"
- printf "\n%.3d (%s) %s\n\n" "$((i+1))" "$h" "$subj" >&2
-
- # Print this commit's files.
- #
- # Fetch from the git repository the list of files added by the current
- # commit. Print each file's path and, if it was deleted by a subsequent
- # commit, mark with an asterisk.
- #
- # Note that 'file_commits' is populated above from the list of files
- # currently in the source repository. Therefore, if git says a file was
- # added by a commit but it is associated with a different commit hash in
- # 'file_commits' it means the file was deleted and added back by later
- # commits; and if there is no mapping for the file it means it was deleted
- # but not added back (that is, it's no longer in the repository). So we
- # mark the re-added file with an exclamation.
- #
- while read -d '' f; do
- if [ "${file_commits[$f]}" == "$h" ]; then
- info " $f" # File was last added by the current commit.
- elif [ -v file_commits["$f"] ]; then
- info " ! $f" # File was deleted and added back by subsequent commits.
- else
- info " * $f" # File was deleted but not added back.
- fi
- done < <(commit_files "$h")
- done
-
- # Prompt the user for the action (showing the current bundle), get user
- # input, and perform the selected action.
- #
- # Note that we could adapt the menu according to the current state (don't
- # offer to migrate if the bundle array is empty, etc) but let's not
- # complicate the logic.
- #
- # Breaking out of this loop prints the pending commit list again.
- #
- while true; do
- # Sort commit bundle in ascending order.
- #
- # Expand the 'bundle' associative array's keys into a single word in which
- # they are separated by spaces (the first member of IFS) using the
- # ${!a[*]} syntax; replace each space with a newline before piping to
- # 'sort', which is newline-based; finally collect sort's output into an
- # array using the a=() syntax, which splits on newline (the last member of
- # IFS) because neither space nor tab characters (the other members of IFS)
- # can occur in the keys.
- #
- bundle_sorted=($(sed 's/ /\n/g' <<<"${!bundle[*]}" | sort -))
-
- printf "\n"
- read -p "[${bundle_sorted[*]}][<N>,m,c,p,l,q,?]: " opt
-
- case "$opt" in
- # Add commit to bundle.
- #
- [0-9]*)
- if [[ ("$opt" -gt 0) && ("$opt " -le "${#pending_seq[@]}") ]]; then
- printf -v opt "%.3d" "$opt" # Format as in pending commit list.
- if [ ! -v bundle["$opt"] ]; then
- bundle["$opt"]=true
- info "commit $opt (${pending_seq[$opt-1]}) added to selected bundle"
- else
- info "commit $opt is already in the bundle"
- fi
- else
- info "non-existent commit number $opt"
- fi
- ;;
- # Migrate the commit bundle.
- #
- m)
- migrate
- if [ "$migrate_result" ]; then
- need_push=true
- break
- fi
- ;;
- # Clear the commit bundle.
- #
- c)
- bundle=()
- break
- ;;
- # Push changes.
- #
- p)
- push
- need_push=
- break
- ;;
- # Redraw the pending commit list.
- #
- l)
- break
- ;;
- # Quit.
- #
- q)
- if [ ! "$need_push" ]; then
- exit 0
- fi
-
- while true; do
- read -p "push changes? [y/n/(c)ancel]: " opt
-
- case "$opt" in
- "c")
- break # Print options menu again.
- ;;
- "y")
- push
- exit 0
- ;;
- "n")
- exit 0
- ;;
- *)
- continue
- ;;
- esac
- done
- ;;
- # ? or invalid option: print menu.
- #
- *)
- cat <<-EOF
-
- <N> - add commit to the commit bundle
- m - migrate the selected commit bundle
- c - clear the selected commit bundle
- p - push source and destination repositories
- l - print pending commits
- q - quit (prompting to push if any actions have been taken)
- ? - print this help
-EOF
- ;;
- esac
- done
-done