├── .gitignore ├── CHANGES.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── pyproject.toml └── src └── mainer.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 1.0a1 (TBD) 2 | =========== 3 | 4 | - Initial pre-release. Updates issue comments to change master links to main 5 | links. 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [INSERT CONTACT METHOD]. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 121 | 122 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 123 | enforcement ladder](https://github.com/mozilla/diversity). 124 | 125 | [homepage]: https://www.contributor-covenant.org 126 | 127 | For answers to common questions about this code of conduct, see the FAQ at 128 | https://www.contributor-covenant.org/faq. Translations are available at 129 | https://www.contributor-covenant.org/translations. 130 | 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sean Gillies 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 | mainer 2 | ====== 3 | 4 | Mainer helps GitHub repo users switch "master" branches to "main". 5 | 6 | Branch renaming is the easy part. But you may be left with hundreds or 7 | thousands of links to "master" in GitHub issue and PR comments. This is where 8 | mainer comes in. 9 | 10 | Mainer uses the GitHub API to find issues, PRs, and comments which have text 11 | linking to the master branch of a repo (the same or another) and rewrites that 12 | text to link to the main branch instead. 13 | 14 | Note well: this is pre-release software. Do use mainer's `--dry-run` mode first 15 | to see what would happen in a real, text-altering use of the program. 16 | 17 | # Installation 18 | 19 | `python -m pip install --pre mainer` 20 | 21 | # Usage 22 | 23 | ``` 24 | $ python -m mainer --help 25 | Usage: mainer.py [OPTIONS] ORG/REPO 26 | 27 | Rewrite links in a repo's issue and PR comments so that they point to the 28 | linked repo's main branch instead of its master branch. 29 | 30 | Example: 31 | 32 | python -m mainer org1/repo1 --rewrite-links-to org2/repo2 --dry-run 33 | 34 | Options: 35 | --dry-run Print comment diffs to stderr, but make no edits. 36 | --rewrite-links-to TEXT The repo named in the links to be fixed. For 37 | example: 'org2/repo2'. Defaults to ORG/REPO. 38 | 39 | --access-token TEXT A GitHub access token. May also be set by 40 | GITHUB_ACCESS_TOKEN or GithubAccessToken in the 41 | environment. 42 | 43 | --help Show this message and exit. 44 | ``` 45 | 46 | ## Dry run mode 47 | 48 | With the `--dry-run` option, mainer prints diffs representing the changes it would make to comments of the target repo. 49 | 50 | ## Editing mode 51 | 52 | **Attention**: without the `--dry-run` option, mainer will edit comments without confirmation prompts or backups (like, for example `sed -i`). 53 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [tool.flit.metadata] 6 | module = "mainer" 7 | author = "Sean Gillies" 8 | author-email = "sean.gillies@gmail.com" 9 | home-page = "http://github.com/sgillies/mainer" 10 | classifiers = ["License :: OSI Approved :: MIT License"] 11 | requires = ["click", "pygithub", "structlog"] 12 | -------------------------------------------------------------------------------- /src/mainer.py: -------------------------------------------------------------------------------- 1 | """Help repo users move from master to main""" 2 | 3 | from difflib import unified_diff 4 | import itertools 5 | import logging 6 | import os 7 | import pathlib 8 | import re 9 | 10 | import click 11 | import github 12 | import structlog 13 | from structlog.stdlib import LoggerFactory 14 | 15 | __version__ = "1.0dev1" 16 | 17 | log = structlog.get_logger(__name__) 18 | 19 | 20 | @click.command() 21 | @click.argument("repo", metavar="ORG/REPO") 22 | @click.option( 23 | "--dry-run", 24 | is_flag=True, 25 | default=False, 26 | help="Print comment diffs to stderr, but make no edits.", 27 | ) 28 | @click.option( 29 | "--from-branch", default="master", help="Name of the branch to be renamed" 30 | ) 31 | @click.option( 32 | "--rewrite-links-to", 33 | "linked_repo", 34 | default=None, 35 | help="The repo named in the links to be fixed. For example: 'org2/repo2'. Defaults to ORG/REPO.", 36 | ) 37 | @click.option( 38 | "--access-token", 39 | default=None, 40 | help="A GitHub access token. May also be set by GITHUB_ACCESS_TOKEN or GithubAccessToken in the environment.", 41 | ) 42 | def main(repo, dry_run, from_branch, linked_repo, access_token): 43 | """Rewrite links in a repo's issue and PR comments so that they point to the linked repo's main branch instead of its master branch. 44 | 45 | Example: 46 | 47 | python -m mainer org1/repo1 --rewrite-links-to org2/repo2 --dry-run 48 | 49 | """ 50 | if access_token is None: 51 | access_token = os.getenv("GITHUB_ACCESS_TOKEN") or os.getenv( 52 | "GithubAccessToken" 53 | ) 54 | if linked_repo is None: 55 | linked_repo = repo 56 | 57 | g = github.Github(access_token) 58 | repo = g.get_repo(repo) 59 | 60 | pattern = re.compile(rf"{linked_repo}/(\w+)/{from_branch}") 61 | 62 | for item in itertools.chain( 63 | repo.get_issues(), 64 | repo.get_issues_comments(), 65 | repo.get_pulls(), 66 | repo.get_pulls_comments(), 67 | ): 68 | match = re.search(pattern, item.body) 69 | if match: 70 | 71 | def repl(match): 72 | return f"{linked_repo}/{match.group(1)}/main" 73 | 74 | modified_body = re.sub(pattern, repl, item.body) 75 | diff = unified_diff( 76 | item.body.splitlines(True), modified_body.splitlines(True) 77 | ) 78 | url = "/".join(pathlib.Path(item.url).parts[-4:]) 79 | 80 | if dry_run: 81 | click.echo(f"\nChanges for {url}", err=True) 82 | for line in diff: 83 | click.echo(line, nl=False, err=True) 84 | else: 85 | log.info( 86 | "Editing item", 87 | item=item, 88 | url=url, 89 | diff=list(line for line in diff), 90 | ) 91 | if isinstance( 92 | item, (github.Issue.Issue, github.PullRequest.PullRequest) 93 | ): 94 | item.edit(body=modified_body) 95 | else: 96 | item.edit(modified_body) 97 | 98 | 99 | if __name__ == "__main__": 100 | logging.basicConfig(level=logging.INFO) 101 | structlog.configure(logger_factory=LoggerFactory()) 102 | main() 103 | --------------------------------------------------------------------------------