Prefering main to master

I recently noticed that git and Github and others have been transitioning away from the use of master as the default branch name in source code repos, preferring instead main. Since new repos were being created with this new default, I decided it would be worthwhile to unify my existing repos to match the future expectation. Since I maintain hundreds of repos, I’d like to automate the steps necessary to make the transition.

Xonsh is my shell of choice, so I set out to write the routine. I quickly came up with the following:

def retire_master():
    git checkout master
    git pull
    git checkout -b main
    gpo
    hub api -X PATCH /repos/@(get_repo_url()) -F default_branch=main
    git push origin :master
    git branch -d master

aliases['retire-master'] = retire_master

This routine depended on two existing routines I already had:

def git_push_new_branch():
    head = $(git rev-parse --abbrev-ref HEAD).rstrip()
    git push --set-upstream origin @(head)

aliases['gpo'] = git_push_new_branch

def get_repo_url():
    raw = $(git remote get-url --push origin).rstrip()
    return raw.replace('https://github.com/', '')

With this in the .xonshrc, I could readily invoke retire-master in a repo and it would take care of making sure the latest code was present, create the new branch locally, push that branch to the origin, then invoke the Github API to set the default branch to this new branch, then finally to delete the old branch remotely and locally.

Unfortunately, I discovered that deleting the default branch has consequences for repos with outstanding pull requests - all of the requests to that branch get closed summarily.

So I added a bit more sophistication to update any outstanding pull requests:

def move_prs(repo):
    import json
    prs = json.loads($(hub api /repos/@(repo)/pulls?base=master))
    nums = [pr['number'] for pr in prs]
    for pr_num in nums:
        hub api -X PATCH /repos/@(repo)/pulls/@(pr_num) -F base=main

This routine, given a repo path, would query for any pulls targeting the master branch and then update each PR.

Some of the projects use Read The Docs, and those docs build stopped working with the new branch name. To update the branch name in RTD, I devised another routine:

def move_rtd(repo):
    org, _, project = repo.partition('/')
    rtd_name = project.replace('.', '').replace('_', '-')
    rp = pathlib.Path('README.rst')
    readme = rp.read_text() if rp.is_file() else ''
    if f'{rtd_name}.readthedocs' not in readme:
        return
    auth = 'Token ' + keyring.get_password('https://api.readthedocs.org/', 'token')
    http patch https://readthedocs.org/api/v3/projects/@(rtd_name)/ Authorization:@(auth) default_branch=main

Then, one small change to the retire function puts it all together:

def retire_master():
    git checkout master
    git pull
    git checkout -b main
    gpo
    repo = get_repo_url()
    move_prs(repo)
    move_rtd(repo)
    hub api -X PATCH /repos/@(repo) -F default_branch=main
    git push origin :master
    git branch -d master

Now, running the retire-master command will readily make the switch in any repo. I’ve switched several today and will get through the rest as I encounter them.

Written on December 12, 2020