Skip to content

Instantly share code, notes, and snippets.

@stephanos
Created December 16, 2023 00:39
Show Gist options
  • Save stephanos/42612a144e0c7f07b2d4c5a0309c3246 to your computer and use it in GitHub Desktop.
Save stephanos/42612a144e0c7f07b2d4c5a0309c3246 to your computer and use it in GitHub Desktop.
switch-api-branch script
#!/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