Created
December 16, 2023 00:39
-
-
Save stephanos/42612a144e0c7f07b2d4c5a0309c3246 to your computer and use it in GitHub Desktop.
switch-api-branch script
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 | |
""" | |
First, you'll need the following repos checked out side by side: | |
src (name doesn't matter) | |
|-- api https://github.com/temporalio/api/ | |
|-- api-go https://github.com/temporalio/api-go/ | |
|-- sdk-go https://github.com/temporalio/sdk-go/ | |
\-- temporal https://github.com/temporalio/temporal/ | |
Next, pick a branch name. For example, we'll use "fig". | |
Go into src or any of the repos. | |
Run "switch-api-branch fig". This does the following: | |
- In api: | |
- switch to branch fig, creating it if necessary | |
- make proto to check it's good | |
- In api-go: | |
- switch to branch fig, creating it if necessary | |
- set the url and branch of the proto/api submodule to point to your local api repo | |
- update the proto/api submodule with the latest commit in api | |
- make proto | |
- In sdk-go: | |
- switch to branch fig, creating it if necessary | |
- add or update a replace directive in go.mod for api-go | |
- In temporal: | |
- switch to branch fig, creating it if necessary | |
- set the url and branch of the proto/api submodule to point to your local api repo | |
- update the proto/api submodule with the latest commit in api | |
- add or update replace directives in go.mod for api-go and sdk-go | |
- make proto | |
After all that happens, you should be good to develop on your branch across | |
repos. You can make changes in any of the four repos and run "switch-api-branch | |
fig" again, and everything will be updated and regenerated. (It's idempotent.) | |
To go back to normal, run "switch-api-branch main" (or "master"). | |
switch-api-branch will create "NOT FOR MERGE: ..." commits with its changes, but | |
it will never push anything. You have to manually push and make PRs to api, | |
sdk-go, and temporal, as necessary. (PRs to api-go are usually not necessary.) | |
Suggested procedure: Let the PR to api land first, then api-go will be updated | |
automatically. Rebase your changes to temporal and/or sdk-go on top of master, | |
using an interactive rebase, and during that step, remove the "NOT FOR MERGE" | |
changes. Make sure they still work, then send PRs. | |
--- | |
""" | |
# TODO: automate that rebasing procedure | |
# TODO: generate rpc client wrappers in server too | |
# TODO: run whatever codegen steps make sense in sdk-go | |
# TODO: do stuff for internal repos too? | |
import sys, os, argparse, subprocess | |
from contextlib import contextmanager | |
REPOS = ['api', 'api-go', 'sdk-go', 'temporal'] | |
ARGS = None | |
def run(*args, **kwargs): | |
print(f"+++ {' '.join(args)}") | |
return subprocess.run(args, check=True, **kwargs) | |
def git(*args, **kwargs): | |
return run("git", *args, **kwargs) | |
def git_out(*args, **kwargs): | |
return git(*args, capture_output=True, **kwargs).stdout.decode() | |
def repo_path(repo): | |
return os.path.join(ARGS.base, repo) | |
@contextmanager | |
def repo(repo): | |
print(f"+++ [[ chdir {repo} ]]") | |
os.chdir(repo_path(repo)) | |
dirty = uncommitted_changes() | |
if dirty: | |
if ARGS.stash: | |
git('stash') | |
else: | |
raise Exception(f"You have uncommitted changes in {repo}") | |
git('fetch', 'upstream') | |
switch_or_create(ARGS.branch) | |
yield | |
if dirty: | |
git('stash', 'pop') | |
def find_base(): | |
while os.getcwd() != '/': | |
if set(REPOS) <= set(os.listdir()): | |
return os.getcwd() | |
os.chdir('..') | |
raise Exception("can't find dir containing all repos") | |
def is_main(b): | |
return b in ('main', 'master') | |
def find_main(): | |
out = git_out('branch', '--format', '%(refname:short) %(upstream:remotename)') | |
for line in out.splitlines(): | |
try: | |
short, remote = line.split() | |
if is_main(short): | |
return short, remote | |
except ValueError: | |
pass | |
raise Exception(f"Can't find main/master branch in {os.getcwd()}") | |
def switch_or_create(branch, initial='FETCH_HEAD'): | |
main, remote = find_main() | |
if is_main(branch): # back to normal | |
branch = main | |
if (git_out('branch', '--list', branch) or | |
git_out('branch', '--list', '--remotes', f'{remote}/{branch}')): | |
git('switch', branch) | |
else: # branch does not exist, create it | |
git('fetch', remote, main) | |
git('switch', '-c', branch, initial) | |
# always update submodules when creating/switching branches | |
git('submodule', 'update', '--init', '--recursive') | |
def uncommitted_changes(): | |
return git_out('status', '--short', '--untracked-files=no') | |
def git_ci(msg, *paths): | |
msg = "NOT FOR MERGE: " + msg | |
try: | |
if not paths: paths = ['-a'] | |
git('commit', '-m', msg, *paths) | |
except subprocess.CalledProcessError: | |
# ignore "nothing to commit". TODO: check the details of the error | |
pass | |
def set_submodule(sub, path, branch): | |
# Note: path must be an absolute path here! Otherwise git will do something | |
# very unexpected. | |
git('submodule', 'set-url', sub, path) | |
git('submodule', 'set-branch', '--branch', branch, sub) | |
git_ci("setting submodule", '.gitmodules') | |
def update_submodules(*subs): | |
git('submodule', 'update', '--init', '--recursive', '--remote', *subs) | |
git_ci("updating submodule", *subs) | |
def go_mod_replace(pairs): | |
for pkg, path in pairs: | |
run('go', 'mod', 'edit', f'-replace={pkg}={path}') | |
run('go', 'mod', 'tidy') | |
git_ci("updating go.mod", 'go.mod', 'go.sum') | |
def make_proto(*commit_paths): | |
run('make', 'proto') | |
if commit_paths: | |
# use explicit paths to catch any potential new files, but try to avoid | |
# deliberate untracked files that might be lying around | |
git_ci("make proto", *commit_paths) | |
def switch(): | |
resetting = is_main(ARGS.branch) | |
with repo('api'): | |
if not resetting: | |
make_proto() | |
with repo('api-go'): | |
if not resetting: | |
set_submodule('proto/api', repo_path('api'), ARGS.branch) | |
update_submodules('proto/api') | |
make_proto('.') # generated files are at root of repo | |
with repo('sdk-go'): | |
if not resetting: | |
go_mod_replace([('go.temporal.io/api', repo_path('api-go'))]) | |
with repo('temporal'): | |
if not resetting: | |
set_submodule('proto/api', repo_path('api'), ARGS.branch) | |
update_submodules('proto/api') | |
go_mod_replace([('go.temporal.io/api', repo_path('api-go')), | |
('go.temporal.io/sdk', repo_path('sdk-go'))]) | |
make_proto('api') # generated files are all contained in api/ | |
print(f"ready to develop on branch {ARGS.branch}") | |
def main(): | |
p = argparse.ArgumentParser(usage=__doc__) | |
p.add_argument('branch', type=str, help="branch to switch to") | |
p.add_argument('--base', type=str, help="parent dir of all repos") | |
p.add_argument('--stash', action='store_true', help="whether to stash git state first") | |
global ARGS | |
ARGS = p.parse_args() | |
if ARGS.base is None: | |
ARGS.base = find_base() | |
switch() | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment