| # 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 |