blob: 952506ff1a143d6f7cbd239f609e2f378fb11190 [file] [log] [blame]
# shellcheck shell=bash
# Copyright 2018-2021 Fairphone B.V.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# repo forall -c environment:
# pwd is the project's working directory. If the current client is a
# mirror client, then pwd is the Git repository.
#
# REPO_PROJECT is set to the unique name of the project.
#
# REPO_PATH is the path relative the root of the client.
#
# REPO_REMOTE is the name of the remote system from the manifest.
#
# REPO_LREV is the name of the revision from the manifest, translated to a
# local tracking branch. If you need to pass the manifest revision to a
# locally executed git command, use REPO_LREV.
#
# REPO_RREV is the name of the revision from the manifest, exactly as
# written in the manifest.
#
# REPO_COUNT is the total number of projects being iterated.
#
# REPO_I is the current (1-based) iteration count. Can be used in
# conjunction with REPO_COUNT to add a simple progress indicator to your
# command.
#
# REPO__* are any extra environment variables, specified by the
# "annotation" element under any project element. This can be useful for
# differentiating trees based on user-specific criteria, or simply
# annotating tree details.#
#
#
# Globals:
# SHELL_LIBS
set -e
# Includes:
. "${SHELL_LIBS}/utils.sh"
# Environment variables:
VERBOSE="${VERBOSE:-}"
DEBUG="${DEBUG:-}"
DRY_RUN="${DRY_RUN:-}" #'--dry-run'. If set, run ssh/git commands without modify remote.
PUSH_SKIP_VALIDATION="${PUSH_SKIP_VALIDATION:-false}"
TARGET_GERRIT_NAME="${TARGET_GERRIT_NAME:-fairphone}"
TARGET_GERRIT_URL="${TARGET_GERRIT_URL:-review.fairphone.software}"
TARGET_GERRIT_PORT="${TARGET_GERRIT_PORT:-29418}"
# Check if a reference exists on project remote.
#
# Globals:
# REPO_REMOTE
# Arguments:
# ref The reference to check.
# Returns:
# 0 if found, otherwise the exit code for 'git ls-remote'
_remote_ref_exists()
{
local ref="$1"
local result=0
git ls-remote --exit-code "${REPO_REMOTE}" "${ref}" > /dev/null 2>&1 \
|| result=$?
case $result in
0)
;;
2)
log_e "${ref} does not exist on remote '${REPO_REMOTE}'"
;;
128)
log_e "${REPO_REMOTE} is not a valid remote"
;;
*)
log_e "[$result]: Unable to 'git ls-remote ${REPO_REMOTE} ${ref}'"
;;
esac
return "${result}"
}
# Check and optionally qualify a reference
#
# Check if a reference is a fully qualified branch or tag. If
# TRY_QUALIFYING_REFS is enabled, try resolving unqualified via querying on the
# remote. This will fail if the reference exists both as branch and tag (or if
# it does not exist).
#
# Globals:
# REPO_REMOTE
# TRY_QUALIFYING_REFS
# Arguments:
# ref The reference to check and qualify.
# Returns:
# 0 if found, otherwise 1
# If found, the fully qualified name gets posted to stdout. Other messages are
# posted to stderr.
_qualify_remote_ref()
{
local ref="$1"
if [ "${TRY_QUALIFYING_REFS}" != true ]; then
# Disabled, skip the logic and leave ref as it is.
echo "${ref}"
return 0
fi
# Must start with refs/heads/ or refs/tags/. Ignore special refs
# (refs/changes/ etc from Gerrit), we shouldn't see them in manifests.
if echo "${ref}" | grep -qEe '^refs\/(heads|tags)\/'; then
# All good, already fully qualified.
echo "${ref}"
return 0
fi
if echo "${ref}" | grep -qEe '^refs\/'; then
# Don't deal with special refs; just fail early.
log_e "${ref} is a special ref, not a branch or tag."
fi
local result=0
local qualified_refs
# Don't use `--exit-code`; we'll check below if there are matching refs.
qualified_refs=$(git ls-remote "${REPO_REMOTE}" \
"refs/heads/${ref}" "refs/tags/${ref}" 2>/dev/null) || result=$?
if [ "${result}" -ne 0 ]; then
log_e "[${result}]: Unable to 'git ls-remote' on ${REPO_REMOTE}."
fi
# Skip the revision sha1 column
qualified_refs=$(echo "${qualified_refs}" | awk '{ print $2 }')
# If the ref does not exist or is ambiguous, return 1 and let the caller
# handle the situation.
local num_matching_refs
num_matching_refs=$(echo "${qualified_refs}" | wc -l)
case "${num_matching_refs}" in
0)
log_w "No matching remote refs unqualified ref \"${ref}\"." >&2
return 1
;;
1)
log_i "Resolving unqualified ref \"${ref}\" to" \
"\"${qualified_refs}\"." >&2
echo "${qualified_refs}"
return 0
;;
*)
log_w "Unqualified ref \"${ref}\" is ambiguous." \
"Found matching: ${qualified_refs}" >&2
return 1
;;
esac
}
# Check if a reference exists.
#
# Globals:
# None
# Arguments:
# ref The explicit reference path to check.
# Returns:
# Exit code of 'git show-ref'
_local_ref_exists()
{
local ref="$1"
local result=0
git show-ref --quiet --verify "${ref}" || result=$?
return "${result}"
}
# Fetch a reference from a remote.
#
# If we have a shallow copy of the project, remove this condition.
#
# Globals:
# REPO_PROJECT
# REPO_REMOTE
# VERBOSE
# Arguments:
# ref_spec Specifies which refs to fetch and which local refs to update.
# Returns:
# None
_git_fetch()
{
local ref_spec="$1"
local args=()
# Update shallow copies if needed
if [ -f .git/shallow ] ; then
log_i "Removing shallow clone for ${REPO_PROJECT}"
args+=(--unshallow)
fi
if [ -n "${VERBOSE}" ]; then
args+=("${VERBOSE}")
fi
local result=0
git fetch --no-tags "${args[@]}" "${REPO_REMOTE}" "${ref_spec}" || result=$?
case $result in
0)
;;
*)
log_e "[$result]: Error fetching ${ref_spec} from ${REPO_REMOTE}."
;;
esac
}
# Push a reference to the remote gerrit.
#
# Globals:
# DRY_RUN
# PUSH_SKIP_VALIDATION
# TARGET_GERRIT_NAME
# Arguments:
# ref_spec Specify what destination ref to update with what source object.
# Returns:
# None
_git_push()
{
local ref_spec="$1"
local args=()
if [ -n "${DRY_RUN}" ]; then
args+=("${DRY_RUN}")
fi
if [ "${PUSH_SKIP_VALIDATION:-}" = true ]; then
args+=(-o skip-validation)
fi
local result=0
git push "${args[@]}" "${TARGET_GERRIT_NAME}" "${ref_spec}" || result=$?
case $result in
0)
;;
128)
remote_url="$(git config --get "remote.${TARGET_GERRIT_NAME}.url")"
log_e "${remote_url} is not a valid remote."
;;
*)
log_e "[$result]: Error pushing ${ref_spec} to" \
"${TARGET_GERRIT_NAME}."
;;
esac
}
# Run a gerrit command on the remote gerrit server.
#
# Globals:
# TARGET_GERRIT_PORT
# TARGET_GERRIT_URL
# Arguments:
# cmd The gerrit command
# Returns:
# Result of the gerrit command
_ssh_gerrit()
{
local result=0
ssh -p "${TARGET_GERRIT_PORT}" "${TARGET_GERRIT_URL}" gerrit "$@" \
|| result=$?
case $result in
0)
;;
*)
log_e "[$result]: Error running 'ssh gerrit $*'"
;;
esac
return "${result}"
}
# Add remote gerrit to local git project if missing.
#
# Globals:
# TARGET_GERRIT_NAME
# TARGET_GERRIT_PORT
# TARGET_GERRIT_URL
# REPO_PROJECT
# Arguments:
# None
# Returns:
# None
add_git_remote()
{
if [ -z "$(git config --get "remote.${TARGET_GERRIT_NAME}.url")" ] ; then
log_i "Adding remote '${TARGET_GERRIT_NAME}' to ${REPO_PROJECT}"
local gerrit="ssh://${TARGET_GERRIT_URL}:${TARGET_GERRIT_PORT}"
git remote add "${TARGET_GERRIT_NAME}" "${gerrit}/${REPO_PROJECT}"
fi
}
# Add missing project to remote gerrit if missing.
#
# Globals:
# TARGET_GERRIT_URL
# REPO_PROJECT
# DRY_RUN
# Arguments:
# None
# Returns:
# None
add_project_to_gerrit()
{
if [ -z "${TARGET_GERRIT_PARENT_PROJECT:-}" ]; then
# Don't set a default for this rather dangerous parameter. It must be
# set in the calling code.
log_e "TARGET_GERRIT_PARENT_PROJECT not defined."
fi
# Does project exist on remote?
if [ -n "$(_ssh_gerrit ls-projects -r "^${REPO_PROJECT}$")" ]; then
return
fi
if [ -z "${DRY_RUN}" ] ; then
local result=0
_ssh_gerrit create-project "${REPO_PROJECT}" \
-p "${TARGET_GERRIT_PARENT_PROJECT}" || result=$?
if [ $result -eq 0 ] ; then
log_i " ${REPO_PROJECT} added to ${TARGET_GERRIT_URL}"
fi
else
# No dry-run options exists for 'gerrit create-project'
log_i "ssh gerrit create-project ${REPO_PROJECT} -p" \
"${TARGET_GERRIT_PARENT_PROJECT}"
fi
}
# Fetch and save a reference from a remote.
#
# If we have a shallow copy, remove this condition.
#
# Globals:
# REPO_PROJECT
# REPO_REMOTE
# VERBOSE
# Arguments:
# from_ref The explicit ref path of the ref to fetch.
# to_ref The explicit ref path of where to save the ref.
# Returns:
# None
fetch_remote_ref()
{
local from_ref=$1
local to_ref=$2
# Try fetching first. In case of failures, use `git ls-remote` to check for
# details. Don't always call it; otherwise upstream will block connections
# because of too many requests.
(
# _git_fetch exits in case of errors, so run it in a sub shell.
_git_fetch "${from_ref}:${to_ref}"
)
local result=$?
if [ "${result}" -ne 0 ]; then
# Try getting some details on what went wrong:
_remote_ref_exists "${from_ref}" || true
fi
}
# Fetch a branch from a remote.
#
# Globals:
# REPO_REMOTE
# Arguments:
# branch
# Returns:
# None
fetch_remote_branch()
{
local branch="$1"
[ -n "${branch}" ] || log_e "Missing branch to fetch"
local from_branch="refs/heads/${branch}"
local to_branch="refs/remotes/${REPO_REMOTE}/${branch}"
fetch_remote_ref "${from_branch}" "${to_branch}"
}
# Fetch a tag from a remote.
#
# Globals:
# REPO_PROJECT
# REPO_REMOTE
# VERBOSE
# Arguments:
# tag
# Returns:
# None
fetch_remote_tag()
{
local tag="$1"
[ -n "${tag}" ] || log_e "Missing tag to fetch"
fetch_remote_ref "refs/tags/${tag}" "refs/tags/${tag}"
}
# Globals:
# REPO_PROJECT
# REPO_REMOTE
# REPO_RREV
# VERBOSE
# Arguments:
# None
# Returns:
# None
remove_shallow_clone()
{
# Update shallow copies if needed
if [ -f .git/shallow ] ; then
_git_fetch "${REPO_RREV}:${REPO_RREV}"
fi
}
# Create a new, optionally annotated git tag locally
#
# Globals:
# NEW_TAG_ANNOTATION_MESSAGE
# Optional annotation for newly created tags.
# Arguments:
# tag Name of newly created tag.
# Returns:
# None
create_new_tag()
{
local tag="${1:-}"
if [ -z "${tag:-}" ]; then
log_e 'Missing tag parameter'
fi
local extra_params=()
if [ -n "${NEW_TAG_ANNOTATION_MESSAGE:-}" ]; then
log_i "Creating tag with annotation: ${NEW_TAG_ANNOTATION_MESSAGE}"
extra_params+=(
--annotate
-m "${NEW_TAG_ANNOTATION_MESSAGE}"
)
fi
git tag "${extra_params[@]}" "${tag}"
}
# Push a reference to the remote gerrit.
#
# If the branch exists both locally and on remote, we default to pushing the
# local branch.
#
# Globals:
# DRY_RUN
# PUSH_SKIP_VALIDATION
# REPO_REMOTE
# TARGET_GERRIT_NAME
# Arguments:
# branch_src Name of the local or remote branch to push.
# branch_target Name of the target branch to push to.
# Returns:
# None
push_branch_to_gerrit()
{
local branch_src="$1"
local branch_target="$2"
[ -n "${branch_src}" ] || log_e 'Missing branch parameter'
[ -n "${branch_target}" ] || log_e 'Missing target branch parameter'
local from_ref=
local to_ref="refs/heads/${branch_target}"
local result=0
_local_ref_exists "refs/heads/${branch_src}" || result=$?
if [ $result -eq 0 ] ; then
from_ref="refs/heads/${branch_src}"
else
result=0
_local_ref_exists "refs/remotes/${REPO_REMOTE}/${branch_src}" \
|| result=$?
if [ $result -eq 0 ] ; then
from_ref="refs/remotes/${REPO_REMOTE}/${branch_src}"
else
log_e "Unable to find local branch '${branch_src}' to push."
fi
fi
if [ -n "${from_ref}" ] ; then
_git_push "${from_ref}:${to_ref}"
fi
}
# Push a tag to the remote gerrit.
#
# Globals:
# DRY_RUN
# PUSH_SKIP_VALIDATION
# TARGET_GERRIT_NAME
# Arguments:
# Tag Name of the tag to push.
# Returns:
# None
push_tag_to_gerrit()
{
local tag="${1:-}"
if [ -z "${tag:-}" ]; then
log_e 'Missing tag parameter'
fi
local tag_ref="refs/tags/${tag}"
local result=0
_local_ref_exists "${tag_ref}" || result=$?
if [ $result -eq 0 ] ; then
_git_push "${tag_ref}:${tag_ref}"
fi
}
# Fetch tag, or create locally if not available on the source gerrit. Then push
# to the target gerrit.
#
# Globals:
# NEW_TAG_ANNOTATION_MESSAGE
# PUSH_SKIP_VALIDATION
# REPO_PROJECT
# REPO_REMOTE
# TARGET_GERRIT_NAME
# VERBOSE
# Arguments:
# Tag Name of the tag to push.
# Returns:
# None
fetch_and_push_tag_to_gerrit_fall_back_to_new_tag()
{
local tag="$1"
if [ -z "${tag}" ]; then
log_e 'Missing tag parameter'
fi
# Can can have the following cases:
# 1. Source gerrit has the tag, target gerrit may or may not have it.
# -> Fetch tag and push to target. That way we will see if the target
# gerrit might have a conflicting version of that tag.
# -> If tags on source and target gerrit conflict, we just fail. To be
# checked manually what to do.
# 2. Source gerrit does not, but target gerrit has the tag.
# -> Nothing more to do, just assume the tag is valid.
# 3. None of the gerrits has the tag.
# -> Apply the workaround: Create the tag locally.
local tag_ref="refs/tags/${tag}"
local source_fetch_result=0
# Run in sub-shell to catch usually expected abort on fetch failure.
(
fetch_remote_tag "${tag}"
) || source_fetch_result=$?
if [ "${source_fetch_result}" -eq 0 ]; then
# Case 1
log_i "Fetch succeeded. Skipping work around with locally created tag."
fi
# Case 2 or 3
if [ "${source_fetch_result}" -ne 0 ]; then
log_i "Fetch failed. Checking for tag on target gerrit."
local target_fetch_result=0
(
# Modifying global REPO_REMOTE within sub-shell is safe; it won't
# propagate back to us.
# shellcheck disable=SC2030
export REPO_REMOTE="${TARGET_GERRIT_NAME}"
fetch_remote_tag "${tag}"
) || target_fetch_result=$?
if [ "${target_fetch_result}" -eq 0 ]; then
# Case 2: Keep the tag on the target gerrit. Nothing more to do.
log_i "Tag already present on target gerrit. Done."
return 0
fi
# Case 3
log_w "[WORKAROUND] Neither source nor target gerrit have tag ${tag}." \
"Creating the tag locally."
create_new_tag "${tag}"
fi
# Case 1 or 3 wrap up: Sanity check on local ref and push to target.
if ! _local_ref_exists "${tag_ref}"; then
log_e "[assert] Tag ${tag} does not exist locally when it should now."
fi
# This might fail if the target gerrit has a conflicting version of the tag.
_git_push "${tag_ref}:${tag_ref}"
}
# Push the remote revision that the manifest is currently pointing to.
#
# Globals:
# DRY_RUN
# PUSH_SKIP_VALIDATION
# REPO_REMOTE
# TARGET_GERRIT_NAME
# TRY_QUALIFYING_REFS
# Arguments:
# None
# Returns:
# None
push_current_remote_revision()
{
local target_ref
local result=0
target_ref=$(_qualify_remote_ref "${REPO_RREV}") || result=$?
if [ "${result}" -ne 0 ]; then
log_e "Cannot push current revision on project ${REPO_PROJECT};" \
"cannot resolve target reference."
fi
_git_push "${REPO_LREV}:${target_ref}"
}
# Push the remote revision that the manifest is currently pointing to.
#
# Globals:
# REPO_PROJECT
# REPO_REMOTE
# REPO_UPSTREAM
# TARGET_GERRIT_NAME
# TRY_QUALIFYING_REFS
# VERBOSE
# Arguments:
# None
# Returns:
# None
push_current_upstream_revision()
{
if [ -z "${REPO_UPSTREAM:-}" ]; then
log_e "REPO_UPSTREAM not set in manifest project ${REPO_PROJECT}. Use" \
"a (release) manifest that has 'upstream' attributes defined."
fi
local target_ref
local result=0
target_ref=$(_qualify_remote_ref "${REPO_UPSTREAM}") || result=$?
if [ "${result}" -ne 0 ]; then
log_e "Cannot push upstream revision on project ${REPO_PROJECT};" \
"cannot resolve target reference ${REPO_UPSTREAM}."
fi
# remove_shallow_clone works on REPO_RREV, so we can't use it here.
if [ -f .git/shallow ] ; then
_git_fetch "${target_ref}:${target_ref}"
fi
local src_ref
if [[ "${target_ref}" == refs/tags/* ]]; then
src_ref="${target_ref}"
elif [[ "${target_ref}" == refs/heads/* ]]; then
# See case of SC2030 above.
# shellcheck disable=SC2031
src_ref="${REPO_REMOTE}/${target_ref/refs\/heads\//}"
else
log_e "Can't resolve source ref from ${REPO_UPSTREAM}, resolved to" \
"${target_ref}."
fi
_git_push "${src_ref}:${target_ref}"
}
if [ -n "${DEBUG:-}" ]; then
set -x
fi