├── .gitignore ├── MANIFEST.in ├── pyproject.toml ├── LICENSE ├── README.md ├── setup.py └── rename_github_default_branch.py /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE *.toml 2 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=30.3.0", "wheel", "setuptools_scm"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | line-length = 79 7 | exclude = ''' 8 | /( 9 | \.eggs 10 | | \.git 11 | | \.hg 12 | | \.mypy_cache 13 | | \.tox 14 | | \.venv 15 | | _build 16 | | buck-out 17 | | build 18 | | dist 19 | | docs/tutorials 20 | )/ 21 | ''' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dan Foreman-Mackey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The default git/GitHub branch name [is oppressive terminology](https://tools.ietf.org/id/draft-knodel-terminology-00.html#rfc.section.1.1) ([more info](https://mail.gnome.org/archives/desktop-devel-list/2019-May/msg00066.html)). 2 | It is easy to change the branch name [for a single repository](https://www.hanselman.com/blog/EasilyRenameYourGitDefaultBranchFromMasterToMain.aspx) or [for new repositories](https://leigh.net.au/writing/git-init-main/). 3 | This script makes it easy to rename your default branch on GitHub repositories in bulk. 4 | 5 | ## Usage 6 | 7 | ### Installation 8 | 9 | To install, run 10 | 11 | ```bash 12 | python -m pip install rename-github-default-branch 13 | ``` 14 | 15 | Then, create a [GitHub.com personal access token](https://github.com/settings/tokens) with the `repo` permission scope and set the environment variable: 16 | 17 | ```bash 18 | export RENAME_GITHUB_TOKEN=YOUR_PERSONAL_ACCESS_TOKEN 19 | ``` 20 | 21 | ### Renaming branches on GitHub 22 | 23 | Then to rename the default branch to `main` for a specific repository (you must have write access): 24 | 25 | ```bash 26 | rename-github-default-branch -r dfm/rename-github-default-branch -t main 27 | ``` 28 | 29 | Or for all the repos that you own (excluding forks): 30 | 31 | ```bash 32 | rename-github-default-branch -t main 33 | ``` 34 | 35 | You can also provide regular expressions to match against the repository name. For example: 36 | 37 | ```bash 38 | rename-github-default-branch -t main -p "dfm/*" -p "exoplanet-dev/*" 39 | ``` 40 | 41 | ### Updating local branches 42 | 43 | To update your local repository, you can run the following: 44 | 45 | ```bash 46 | git fetch origin main 47 | git checkout -b main origin/main 48 | git branch -D master 49 | ``` 50 | 51 | where `main` is the name of the new default branch and `origin` is the name of the git remote. 52 | 53 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import sys 3 | 4 | assert sys.version_info >= (3, 6, 0), "black_nbconvert requires Python 3.6+" 5 | from pathlib import Path # noqa E402 6 | 7 | CURRENT_DIR = Path(__file__).parent 8 | 9 | 10 | def get_long_description() -> str: 11 | readme_md = CURRENT_DIR / "README.md" 12 | with open(readme_md, encoding="utf8") as ld_file: 13 | return ld_file.read() 14 | 15 | 16 | setup( 17 | name="rename_github_default_branch", 18 | use_scm_version=True, 19 | description="A Python script for bulk renaming the default branch of your " 20 | "GitHub repositories using the API", 21 | long_description=get_long_description(), 22 | long_description_content_type="text/markdown", 23 | author="Dan Foreman-Mackey", 24 | author_email="foreman.mackey@gmail.com", 25 | url="https://github.com/dfm/rename_github_default_branch", 26 | license="MIT", 27 | py_modules=["rename_github_default_branch"], 28 | python_requires=">=3.6", 29 | zip_safe=False, 30 | install_requires=["requests", "click", "tqdm"], 31 | classifiers=[ 32 | "Development Status :: 4 - Beta", 33 | "Environment :: Console", 34 | "Intended Audience :: Developers", 35 | "License :: OSI Approved :: MIT License", 36 | "Operating System :: OS Independent", 37 | "Programming Language :: Python", 38 | "Programming Language :: Python :: 3.6", 39 | "Programming Language :: Python :: 3.7", 40 | "Programming Language :: Python :: 3 :: Only", 41 | "Topic :: Software Development :: Libraries :: Python Modules", 42 | "Topic :: Software Development :: Quality Assurance", 43 | ], 44 | entry_points={ 45 | "console_scripts": [ 46 | "rename-github-default-branch=rename_github_default_branch:main" 47 | ] 48 | }, 49 | ) 50 | -------------------------------------------------------------------------------- /rename_github_default_branch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import re 5 | import sys 6 | import logging 7 | from typing import Optional, List 8 | 9 | import tqdm 10 | import click 11 | import requests 12 | 13 | from pkg_resources import get_distribution, DistributionNotFound 14 | 15 | try: 16 | __version__ = get_distribution(__name__).version 17 | except DistributionNotFound: 18 | __version__ = None 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | GITHUB_API_URL = "https://api.github.com" 24 | 25 | 26 | def match_repo_name(patterns: List[str], name: str) -> bool: 27 | return len(patterns) == 0 or any(p.search(name) for p in patterns) 28 | 29 | 30 | def list_repos( 31 | session: requests.Session, current: str, patterns: List[str], 32 | ) -> List[str]: 33 | patterns = [re.compile(p, re.I) for p in patterns] 34 | params = {} if patterns else {"affiliation": "owner"} 35 | 36 | r = session.get(GITHUB_API_URL + "/user/repos", params=params) 37 | r.raise_for_status() 38 | 39 | # Deal with pagination 40 | repos = [] 41 | while True: 42 | repos += [ 43 | repo["full_name"] 44 | for repo in r.json() 45 | if not repo["fork"] 46 | and match_repo_name(patterns, repo["full_name"]) 47 | ] 48 | 49 | if "next" not in r.links: 50 | break 51 | url = r.links["next"]["url"] 52 | r = session.get(url) 53 | r.raise_for_status() 54 | 55 | return repos 56 | 57 | 58 | def rename_default_branch( 59 | session: requests.Session, 60 | repo_name: str, 61 | current: str, 62 | target: str, 63 | ) -> None: 64 | r = session.post( 65 | GITHUB_API_URL + f"/repos/{repo_name}/branches/{current}/rename", 66 | json={"new_name": target}, 67 | ) 68 | if r.status_code == 403: 69 | # This happens if the repo is read-only 70 | logger.info(f"Forbidden") 71 | return 72 | if r.status_code == 404: 73 | logger.info(f"no branch named {current} on {repo_name}") 74 | return 75 | else: 76 | r.raise_for_status() 77 | 78 | 79 | @click.command() 80 | @click.option( 81 | "--token", help="A personal access token for this user", type=str 82 | ) 83 | @click.option( 84 | "--current", 85 | "-c", 86 | help="The current default branch name to change", 87 | type=str, 88 | default="master", 89 | ) 90 | @click.option( 91 | "--target", 92 | "-t", 93 | help="The new default branch name to use", 94 | type=str, 95 | default="main", 96 | ) 97 | @click.option( 98 | "--repo", 99 | "-r", 100 | help="The name of a specific repository", 101 | multiple=True, 102 | type=str, 103 | ) 104 | @click.option( 105 | "--pattern", 106 | "-p", 107 | help="A regular expression to match against the repository name", 108 | multiple=True, 109 | type=str, 110 | ) 111 | @click.option("--version", help="Print the version number", is_flag=True) 112 | def _main( 113 | token: Optional[str], 114 | current: str, 115 | target: str, 116 | repo: List[str], 117 | pattern: List[str], 118 | version: bool, 119 | ) -> None: 120 | 121 | if version: 122 | print(f"rename-github-default-branch v{__version__}") 123 | return 0 124 | 125 | if not token: 126 | print( 127 | "A GitHub.com personal access token must be provided either via " 128 | "the environment variable 'RENAME_GITHUB_TOKEN' or the command " 129 | "line flag '--token'" 130 | ) 131 | return 1 132 | 133 | with requests.Session() as session: 134 | session.headers.update( 135 | { 136 | "Authorization": f"token {token}", 137 | "Content-Type": "application/json", 138 | "Accept": "application/vnd.github.v3+json", 139 | } 140 | ) 141 | 142 | if not repo: 143 | repo = list_repos(session, current, pattern) 144 | 145 | with tqdm.tqdm(total=len(repo)) as bar: 146 | for r in repo: 147 | bar.set_description_str(r) 148 | rename_default_branch( 149 | session, r, current, target 150 | ) 151 | bar.update() 152 | 153 | return 0 154 | 155 | 156 | def main(): 157 | return _main(auto_envvar_prefix="RENAME_GITHUB") 158 | 159 | 160 | if __name__ == "__main__": 161 | sys.exit(_main(auto_envvar_prefix="RENAME_GITHUB")) 162 | --------------------------------------------------------------------------------