blob: 8e497c782ede2b3195d8084d9846efc91fc392ab [file] [log] [blame]
# shellcheck shell=bash
# Copyright 2018-2023 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.
# Bail out if the current project is git-repo or platform/manifest
#
# When running on a mirror workspace, git-repo and the upstream
# platform/manifest project both show up in the workspace. Usually we don't want
# to touch them; repo forall commands should just skip them. To do so, call this
# function early in the 'repo forall' command.
#
# Globals:
# REPO_PROJECT
skip_repo_mirror_internal_project()
{
case "${REPO_PROJECT}" in
# AOSP tooling and manifest
git-repo|platform/manifest)
exit 0
;;
# Qualcomm/CodeLinaro manifests
la/system/manifest|la/vendor/manifest)
exit 0
;;
# Fairphone Gerrit manifest
manifest)
exit 0
;;
esac
}
# 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}"
}
# Check if the current project is a git mirror.
#
# Globals:
# None
# Arguments:
# None
# Returns:
# 0 if it's a mirror, 1 otherwise.
_project_is_mirror()
{
local is_mirror=""
# We can safely assume that if `git config` reports failure, it's because we are not on a
# mirror, and .mirror is just not set.
is_mirror=$(git config --local --get core.bare || true)
if [ "${is_mirror}" = true ]; then
return 0
else
return 1
fi
}
# Resolve name of local copy of remote branch
#
# Globals:
# REPO_REMOTE
# Arguments:
# branch Unqualified branch name.
# Returns:
# Fully qualified reference to remote branch name gets posted to stdout.
_resolve_remote_branch()
{
local branch_name="$1"
local ref
if _project_is_mirror; then
# mirrors have no "remote" section, but instead a direct copy of the
# upstream repository.
ref="refs/heads/${branch_name}"
else
# Non-mirrors need to point to the upstream branch.
ref="refs/remotes/${REPO_REMOTE}/${branch_name}"
fi
if ! _local_ref_exists "${ref}"; then
log_e "Unable to find branch '${branch_name}' in local repository (expected to find reference ${ref})."
fi
echo "${ref}"
}
# 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 src_ref="${1:-}"
local target_ref="${2:-}"
if [ -z "${src_ref}" ]; then
log_e "Source reference is empty, which would lead to delete on remote. Aborting."
elif [ -z "${target_ref}" ]; then
log_e "Target reference is empty."
fi
local ref_spec="${src_ref}:${target_ref}"
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. In case of tags, a local
# copy of the remote tag is create. Branches just keep referring to the remote
# branch. Verbose error checks are done in case of failures.
#
# Globals:
# REPO_PROJECT
# REPO_REMOTE
# Arguments:
# from_ref The explicit ref path of the ref to fetch.
# Returns:
# None
fetch_remote_ref()
{
local from_ref=$1
local ref_spec
if [[ "${from_ref}" == refs/tags/* ]]; then
# Create local copy of remote tag
ref_spec="${from_ref}:${from_ref}"
elif [[ "${from_ref}" == refs/heads/* ]]; then
# Fetch via branch name. This create a local reference under the remote.
ref_spec="${from_ref#refs/heads/}"
else
log_e "Invalid source reference: \"${from_ref}\""
fi
# 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 "${ref_spec}"
)
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_PROJECT
# REPO_REMOTE
# Arguments:
# branch
# Returns:
# None
fetch_remote_branch()
{
local branch="$1"
[ -n "${branch}" ] || log_e "Missing branch to fetch"
fetch_remote_ref "refs/heads/${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}"
}
# 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.
#
# Push either a remote (upstream) source branch or the current HEAD to the
# target gerrit.
#
# Globals:
# DRY_RUN
# PUSH_SKIP_VALIDATION
# REPO_REMOTE
# TARGET_GERRIT_NAME
# Arguments:
# branch_src Name of the remote branch to push, or "HEAD".
# 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}"
if [ "${branch_src}" = "HEAD" ]; then
# Push the current HEAD. On mirrored repositories, `repo sync` updates
# HEAD as well (even there is no working directory), thus we can use
# HEAD for both regular tree and mirrors.
from_ref=HEAD
else
from_ref=$(_resolve_remote_branch "${branch_src}")
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 a known reference from source to target
#
# This unifies pushing any known reference, possibly coming from per-project manifest attributes.
# References are qualified if enabled (TRY_QUALIFYING_REFS), unshallowed if necessary, and branches
# get prefixed with PUSH_BRANCH_PREFIX if enabled.
#
# Globals:
# DRY_RUN
# PUSH_BRANCH_PREFIX
# PUSH_SKIP_VALIDATION
# REPO_PROJECT
# REPO_REMOTE
# TARGET_GERRIT_NAME
# TRY_QUALIFYING_REFS
# Arguments:
# None
# Returns:
# None
_push_reference()
{
local ref_parm="$1"
local result=0
reference=$(_qualify_remote_ref "${ref_parm}") || result=$?
if [ "${result}" -ne 0 ]; then
log_e \
"Cannot push on project ${REPO_PROJECT}: Cannot resolve target reference ${ref_parm}."
fi
# Fetch and unshallow if necessary.
fetch_remote_ref "${reference}"
local src_ref
local target_ref
if [[ "${reference}" == refs/tags/* ]]; then
src_ref="${reference}"
target_ref="${reference}"
elif [[ "${reference}" == refs/heads/* ]]; then
local branch_name="${reference/refs\/heads\//}"
src_ref=$(_resolve_remote_branch "${branch_name}")
target_ref="refs/heads/${PUSH_BRANCH_PREFIX}${branch_name}"
else
log_e "Can't resolve source ref from ${ref_parm}, resolved to ${reference}."
fi
_git_push "${src_ref}" "${target_ref}"
}
# Push the remote revision that the manifest is currently pointing to.
#
# Globals:
# DRY_RUN
# PUSH_BRANCH_PREFIX
# PUSH_SKIP_VALIDATION
# REPO_PROJECT
# REPO_REMOTE
# REPO_RREV
# TARGET_GERRIT_NAME
# TRY_QUALIFYING_REFS
# Arguments:
# None
# Returns:
# None
push_current_remote_revision()
{
if [ -z "${REPO_RREV:-}" ]; then
log_e \
"REPO_RREV is not set in project ${REPO_PROJECT}. This should never be the case."
fi
_push_reference "${REPO_RREV}"
}
# Push the remote revision that the manifest is currently pointing to.
#
# Globals:
# DRY_RUN
# PUSH_BRANCH_PREFIX
# PUSH_SKIP_VALIDATION
# 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
_push_reference "${REPO_UPSTREAM}"
}
if [ -n "${DEBUG:-}" ]; then
set -x
fi