blob: 0ac6d79b2924801f471902ff21cac7c167800934 [file] [log] [blame]
# shellcheck shell=bash
# Copyright 2018-2022 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: Refer to the `repo` tool documentation for details
# about REPO_* variables that are used below.
#
#
# Globals:
# SHELL_LIBS
set -e
set -u
# Includes:
. "${SHELL_LIBS}/utils.sh"
# Environment variables:
DEBUG="${DEBUG:-}"
DRY_RUN="${DRY_RUN:-false}" # If set to true, run ssh/git commands without modify remote.
# 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
# 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
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 [ "${DRY_RUN}" = true ]; 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 [ "${DRY_RUN}" != true ] ; 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
# 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
# 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
# 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
}
# Push the remote revision that the manifest is currently pointing to.
#
# Globals:
# DRY_RUN
# PUSH_BRANCH_PREFIX
# 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
if [[ "${target_ref}" == refs/heads/* ]]; then
local branch_name="${target_ref/refs\/heads\//}"
target_ref="refs/heads/${PUSH_BRANCH_PREFIX}${branch_name}"
fi
_git_push "${REPO_LREV}:${target_ref}"
}
# Push the remote revision that the manifest is currently pointing to.
#
# Globals:
# DRY_RUN
# PUSH_BRANCH_PREFIX
# REPO_PROJECT
# REPO_REMOTE
# REPO_UPSTREAM
# TARGET_GERRIT_NAME
# TRY_QUALIFYING_REFS
# 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 upstream_ref
local result=0
upstream_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 "${upstream_ref}:${upstream_ref}"
fi
local src_ref
local target_ref
if [[ "${upstream_ref}" == refs/tags/* ]]; then
src_ref="${upstream_ref}"
target_ref="${upstream_ref}"
elif [[ "${upstream_ref}" == refs/heads/* ]]; then
local branch_name="${upstream_ref/refs\/heads\//}"
# See case of SC2030 above.
# shellcheck disable=SC2031
src_ref="${REPO_REMOTE}/${branch_name}"
target_ref="refs/heads/${PUSH_BRANCH_PREFIX}${branch_name}"
else
log_e "Can't resolve source ref from ${REPO_UPSTREAM}, resolved to" \
"${upstream_ref}."
fi
_git_push "${src_ref}:${target_ref}"
}
if [ -n "${DEBUG:-}" ]; then
set -x
fi