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