Last active
August 15, 2023 22:21
-
-
Save dsoprea/84f2fcc8b02b1bd590dc4437c0985323 to your computer and use it in GitHub Desktop.
Generate a list of commit arguments from a submodules superproject for a Sentry release
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# MIT LICENSE | |
# | |
# Copyright 2023 Dustin Oprea | |
# | |
# Permission is hereby granted, free of charge, to any person obtaining a copy | |
# of this software and associated documentation files (the “Software”), to deal | |
# in the Software without restriction, including without limitation the rights | |
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
# copies of the Software, and to permit persons to whom the Software is | |
# furnished to do so, subject to the following conditions: | |
# | |
# The above copyright notice and this permission notice shall be included in all | |
# copies or substantial portions of the Software. | |
# | |
# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
# SOFTWARE. | |
import os | |
import re | |
import sys | |
import argparse | |
import subprocess | |
_DESCRIPTION = \ | |
"Given files with the output of `git ls-tree <revision>` for an earlier " \ | |
"and later commit, print a list of arguments that can be passed to " \ | |
"`sentry-cli releases set-commits`." | |
# Example: git@github.com:outandbackoutdoor/workflow_application.git | |
_GIT_SSH_REPO_URI_RE = re.compile(r'^([^:]+):([^:]+)/(.+)(\.git)?$') | |
# Example: https://github.com/outandbackoutdoor/product_extractor.git | |
_GIT_HTTP_REPO_URI_RE = re.compile(r'^https://([^/]+)/([^/]+)/([^.]+)(\.git)?$') | |
def _get_args(): | |
parser = \ | |
argparse.ArgumentParser( | |
description=_DESCRIPTION) | |
parser.add_argument( | |
'previous_commits_filepath', | |
help="Previous release commits filepath") | |
parser.add_argument( | |
'current_commits_filepath', | |
help="Current release commits filepath") | |
args = parser.parse_args() | |
return args | |
def _parse_commits_file(f): | |
"""Parse output of `git ls-tree <revision>`. | |
Example: | |
160000 commit 606912a11094c3aa0952dd3fa97413d9d7a9bdb0 asset_management | |
160000 commit 344e43a6f1b9921209e21d758cb707317e1cb412 ebay_client | |
160000 commit 67c6032ea927744c8cab2ec9e074d6755356ed2e entity_cache_layer | |
160000 commit 36e261e9f30e3c0f93ccbd65d10cc6b49d213458 extraction_api_client | |
160000 commit c8e3fa056e348ad6df7a05331d1db4dda7d67ed8 extraction_api_website | |
160000 commit c77afd2217dbeb1bbe39b1638c20b46c79468cf3 free_text_matcher | |
160000 commit a3db58057dfbaa26970a2199fa7931655ac81ea6 infrastructure_storage_layer | |
160000 commit 8e9f231a916d59217343624e8344d23dc87a270f obo_aws_adapters | |
160000 commit f7dc93fe3146d2b9d344b8621a8e9f05acadda4b product_extractor | |
160000 commit 6f55d29e333e8c62ae6487d1e4a67b60f7429318 search_utilities | |
160000 commit f306c76c11d3c9f2858af0c31565ac268930b12b service_support | |
160000 commit 7e8ec9cb24a4a6819f3818804d5681a1bc339ab1 shopify_layer | |
160000 commit ab9f8774675760bc245d5f60b66a10dda8856255 shortcut_scripts | |
160000 commit 41e3a49a8093481e6adf66e10e043cd3e3c84134 workflow_api_client | |
""" | |
revisions = {} | |
for line in f: | |
mode, type_, revision, project = line.split() | |
if type_ != 'commit': | |
continue | |
revisions[project] = revision | |
return revisions | |
def _revision_ranges_gen(previous_revisions, current_revisions): | |
# Identify the projects in both sets | |
previous_projects_s = set(previous_revisions.keys()) | |
current_projects_s = set(current_revisions.keys()) | |
projects = list(previous_projects_s | current_projects_s) | |
for project in projects: | |
previous_revision = previous_revisions.get(project) | |
current_revision = current_revisions.get(project) | |
if current_revision == previous_revision: | |
continue | |
yield project, previous_revision, current_revision | |
def _get_repo_origin(project_path): | |
cmd = ['git', 'remote', 'get-url', 'origin'] | |
origin_uri = \ | |
subprocess.check_output( | |
cmd, | |
cwd=project_path, | |
universal_newlines=True) | |
origin_uri = origin_uri.strip() | |
if origin_uri.startswith('git@') is True: | |
# Example: | |
# git@github.com:outandbackoutdoor/workflow_application.git | |
m = _GIT_SSH_REPO_URI_RE.match(origin_uri) | |
assert \ | |
m is not None, \ | |
"Could not parse Git SSH URI: [{}]".format( | |
origin_uri) | |
user_and_host_part = m.group(1) | |
account_part = m.group(2) | |
project_part = m.group(3) | |
elif origin_uri.startswith('https://') is True: | |
# Example: | |
# git@github.com:outandbackoutdoor/workflow_application.git | |
m = _GIT_HTTP_REPO_URI_RE.match(origin_uri) | |
assert \ | |
m is not None, \ | |
"Could not parse Git HTTP URI: [{}]".format( | |
origin_uri) | |
user_and_host_part = m.group(1) | |
account_part = m.group(2) | |
project_part = m.group(3) | |
else: | |
raise \ | |
Exception( | |
"Could not identify format of origin: [{}]".format( | |
origin_uri)) | |
print("REMOTE: [{}] -> [{}] [{}] [{}]".format( | |
origin_uri, user_and_host_part, account_part, project_part), | |
file=sys.stderr) | |
return user_and_host_part, account_part, project_part | |
def _main(): | |
args = _get_args() | |
with open(args.previous_commits_filepath) as f: | |
previous_revisions = _parse_commits_file(f) | |
with open(args.current_commits_filepath) as f: | |
current_revisions = _parse_commits_file(f) | |
revision_ranges = \ | |
_revision_ranges_gen( | |
previous_revisions, | |
current_revisions) | |
current_path = os.getcwd() | |
for rel_project_path, previous_revision, current_revision in revision_ranges: | |
if previous_revision is None: | |
continue | |
elif current_revision is None: | |
continue | |
# Skip anything that hasn't changed | |
if current_revision == previous_revisions: | |
continue | |
project_path = os.path.join(current_path, rel_project_path) | |
_, account, project_name = _get_repo_origin(project_path) | |
suffix = '.git' | |
if project_name.endswith(suffix) is True: | |
len_ = len(suffix) | |
project_name = project_name[:-len_] | |
print("PROJECT: [{}] [{}] -> [{}] -> [{}] [{}]".format( | |
current_path, rel_project_path, project_path, account, | |
project_name), file=sys.stderr) | |
sys.stdout.write( | |
"--commit {account}/{project}@{from_revision}..{to_revision} ".format( | |
account=account, project=project_name, from_revision=previous_revision, | |
to_revision=current_revision)) | |
print('') | |
_main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment