You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/scripts/build/tools/sync_repo.py

151 lines
5.8 KiB
Python

#
# Copyright (c) Contributors to the Open 3D Engine Project.
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
#
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
import argparse
import boto3
import logging
import os
import subprocess
import sys
from botocore.exceptions import ClientError
from urllib.parse import urlparse, urlunparse
log = logging.getLogger(__name__)
log.setLevel(logging.INFO)
DEFAULT_BRANCH = "main"
DEFAULT_WORKSPACE_ROOT = "."
class MergeError(Exception):
pass
class SyncRepo:
"""A git repo with configured remotes to sync with GitHub.
Used by the sync pipeline to push branches to GitHub and pull down latest from main. Changes flow from
the upstream remote down to origin. Remotes can be swapped to pull changes in the other direction.
Attributes:
origin: URL for the origin repo. This is the target for the sync.
upstream: URL for the upstream repo. This is the source with the latest changes.
workspace_root: Path to the parent directory for the local workspace.
parameter: Name of the parameter used to store GitHub credentials.
"""
def __init__(self, origin, upstream, workspace_root, region=None, parameter=None):
self.workspace_root = workspace_root
self.parameter = parameter
self.region = region
if self.parameter and self.region:
log.info(f"Adding credentials from {self.parameter} in {self.region}")
self.origin = self._add_credentials(origin)
self.upstream = self._add_credentials(upstream)
else:
self.origin = origin
self.upstream = upstream
self.origin_name = self.origin.split("/")[-1]
self.upstream_name = self.upstream.split("/")[-1]
self.workspace = os.path.join(workspace_root, self.origin_name)
def _add_credentials(self, url):
"""Add credentials to a github repo URL from parameter store."""
parsed_url = urlparse(url)
if parsed_url.netloc == "github.com":
try:
ssm = boto3.client("ssm", self.region)
credentials = ssm.get_parameter(
Name=self.parameter,
WithDecryption=True
)["Parameter"]["Value"]
url = urlunparse(parsed_url._replace(netloc=f"{credentials}@github.com"))
except ClientError as e:
log.error(f"Error retrieving credentials from parameter store: {e}")
return url
def clone(self):
"""Clones repo to the instance workspace. Refreshes remote configs for existing repos."""
if not os.path.exists(self.workspace):
os.mkdir(self.workspace)
if subprocess.run(["git", "rev-parse", "--is-inside-work-tree"], cwd=self.workspace).returncode != 0:
log.info(f"Cloning repo {self.origin} to {self.workspace}.")
subprocess.run(["git", "clone", self.origin, self.origin_name], cwd=self.workspace_root, check=True)
subprocess.run(["git", "remote", "add", "upstream", self.upstream], cwd=self.workspace)
else:
log.info("Update remote config for existing repos.")
subprocess.run(["git", "remote", "set-url", "origin", self.origin], cwd=self.workspace)
subprocess.run(["git", "remote", "set-url", "upstream", self.upstream], cwd=self.workspace)
def sync(self, branch):
"""Fetches latest from upstream and syncs changes to origin.
Syncs are one-way and conflicts are not expected. Fast-forward merges are performed if possible. If a
fast-forward merge is not possible, a merge will not be attempted and will raise an exception.
The checkout command will create a new branch from upstream/<branch> if it does not exist in origin. The
remote will be remapped to origin during the push.
Args:
branch: Name of the upstream branch to sync with origin.
Raises:
MergeError: An error occured when attempting to merge to the target branch.
"""
subprocess.run(["git", "fetch", "origin"], cwd=self.workspace, check=True)
subprocess.run(["git", "fetch", "upstream"], cwd=self.workspace, check=True)
subprocess.run(["git", "checkout", branch], cwd=self.workspace, check=True)
# If the branch exists in origin, merge from upstream. New branches do not require a merge.
if subprocess.run(["git", "ls-remote", "--exit-code", "-h", "origin", branch], cwd=self.workspace).returncode == 0:
subprocess.run(["git", "reset", "--hard", "HEAD"], cwd=self.workspace, check=True)
subprocess.run(["git", "pull"], cwd=self.workspace, check=True)
if subprocess.run(["git", "merge", "--ff-only", f"upstream/{branch}"], cwd=self.workspace).returncode != 0:
raise MergeError(f"Unable to perform ff merge to target branch: {self.origin}/{branch} Intervention required.")
subprocess.run(["git", "push", "-u", "origin", branch], cwd=self.workspace, check=True)
def process_args():
"""Process arguements.
Example:
sync_repo.py <upstream> <origin> [Options]
"""
parser = argparse.ArgumentParser()
parser.add_argument("upstream")
parser.add_argument("origin")
parser.add_argument("-b", "--branch", default=DEFAULT_BRANCH)
parser.add_argument("-w", "--workspace-root", default=DEFAULT_WORKSPACE_ROOT)
parser.add_argument("-r", "--region", default=None)
parser.add_argument("-p", "--parameter", default=None)
return parser.parse_args()
def main():
args = process_args()
repo = SyncRepo(args.origin, args.upstream, args.workspace_root, args.region, args.parameter)
repo.clone()
try:
repo.sync(args.branch)
except MergeError as e:
log.error(e)
if __name__ == "__main__":
sys.exit(main())