├── requirements.txt ├── .gitignore ├── README.md └── github-sync-upstream /requirements.txt: -------------------------------------------------------------------------------- 1 | argh==0.17.2 2 | -e git+https://github.com/n1k0/github3.py.git@64cb1a5c3d357649619f23b323d0d1794fc3b3cf#egg=github3.py-dev 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /env 2 | /repos 3 | 4 | *.py[cod] 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Packages 10 | *.egg 11 | *.egg-info 12 | dist 13 | build 14 | eggs 15 | parts 16 | bin 17 | var 18 | sdist 19 | develop-eggs 20 | .installed.cfg 21 | lib 22 | lib64 23 | 24 | # Installer logs 25 | pip-log.txt 26 | 27 | # Unit test / coverage reports 28 | .coverage 29 | .tox 30 | nosetests.xml 31 | 32 | # Translations 33 | *.mo 34 | 35 | # Mr Developer 36 | .mr.developer.cfg 37 | .project 38 | .pydevproject 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-sync-upstream 2 | 3 | This Python script automatically synchronizes a github user or an organization 4 | forked repositories with their upstream parents. 5 | 6 | This is how I keep all the repositories forked by 7 | [the casperjs organization](https://github.com/casperjs) in sync with the original 8 | repositories. 9 | 10 | **Warning:** this script is intended for synchronizing read-only repositories, 11 | and will basically perform an automatic merge from upstream before 12 | actually pushing the modifications. Use with caution. 13 | 14 | ## Setup 15 | 16 | Create a virtualenv, install pip requirements from `requirements.txt`. 17 | 18 | Set the following environment variables: 19 | 20 | - `GITHUB_USERNAME`: your github username 21 | - `GITHUB_PASSWORD`: your github password 22 | - `GITHUB_ORGANIZATION`: a target github organization (optional) 23 | 24 | ## Usage 25 | 26 | $ ./github-sync-upstream 27 | 28 | To have it running with no prompting for confirmation (convenient for cronjobs): 29 | 30 | $ ./github-sync-upstream --no_interactive 31 | 32 | To directly pass env variables to the script (hazardous but convenient for cronjobs): 33 | 34 | $ GITHUB_USERNAME=foo GITHUB_PASSWORD=bar /path/to/github-sync-upstream --no_interactive 35 | 36 | ## Compatibility 37 | 38 | This script requires Python 2.7+. 39 | 40 | ## License 41 | 42 | Copyright (c) 2012 Nicolas Perriault 43 | 44 | Permission is hereby granted, free of charge, to any person obtaining a copy 45 | of this software and associated documentation files (the "Software"), to deal 46 | in the Software without restriction, including without limitation the rights 47 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 48 | copies of the Software, and to permit persons to whom the Software is furnished 49 | to do so, subject to the following conditions: 50 | 51 | The above copyright notice and this permission notice shall be included in all 52 | copies or substantial portions of the Software. 53 | 54 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 55 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 56 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 57 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 58 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 59 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 60 | THE SOFTWARE. 61 | -------------------------------------------------------------------------------- /github-sync-upstream: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2012 Nicolas Perriault 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 furnished 10 | # 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 21 | # THE SOFTWARE. 22 | 23 | import os 24 | import subprocess 25 | import sys 26 | 27 | from argh import * 28 | from github3.api import login 29 | from github3.models import GitHubError 30 | from github3.repos import Repository 31 | 32 | GITHUB_USERNAME = os.environ.get('GITHUB_USERNAME') 33 | GITHUB_PASSWORD = os.environ.get('GITHUB_PASSWORD') 34 | GITHUB_ORGANIZATION = os.environ.get('GITHUB_ORGANIZATION') 35 | ROOT_DIR = os.path.abspath(os.path.dirname(__file__)) 36 | REPOS_ROOT_DIR = os.path.join(ROOT_DIR, 'repos') 37 | 38 | 39 | def confirm(question): 40 | if not raw_input(question + ' (yN): ') in ('y', 'Y', 'yes'): 41 | error(u'Aborted.') 42 | 43 | 44 | def connect(username, password): 45 | assert username and password 46 | try: 47 | return login(username, password=password) 48 | except GitHubError as err: 49 | error(u'Error encountered: %s' % err) 50 | 51 | 52 | def execute(cmd): 53 | assert isinstance(cmd, (list, tuple)) 54 | try: 55 | return subprocess.check_output(cmd, stderr=subprocess.STDOUT) 56 | except subprocess.CalledProcessError as err: 57 | error([u"Error invoking `%s`:" % ' '.join(err.cmd), 58 | err.output]) 59 | 60 | 61 | def check_repo_dir(repo): 62 | repo_dir = os.path.join(REPOS_ROOT_DIR, repo.name) 63 | if not os.path.exists(repo_dir): 64 | log(u"Checking out repo %s from %s..." % (repo.name, repo.ssh_url)) 65 | log(execute(['git', 'clone', repo.ssh_url, repo_dir])) 66 | os.chdir(repo_dir) 67 | log(execute(['git', 'remote', 'add', 'upstream', repo.parent.git_url])) 68 | os.chdir(ROOT_DIR) 69 | return repo_dir 70 | 71 | 72 | def error(messages, exit=True, status=1): 73 | if isinstance(messages, basestring): 74 | messages = [messages] 75 | for message in messages: 76 | sys.stderr.write(message + os.linesep) 77 | if exit: 78 | sys.exit(status) 79 | 80 | 81 | def get_repos(github, organization=None): 82 | if organization: 83 | repos = github.organization(organization).iter_repos() 84 | else: 85 | repos = github.iter_repos() 86 | try: 87 | return [r.refresh() for r in repos if r.is_fork()] 88 | except GitHubError as err: 89 | error(u"Github error: %s" % err) 90 | 91 | 92 | def log(messages): 93 | if isinstance(messages, basestring): 94 | messages = [messages] 95 | for message in messages: 96 | sys.stdout.write(message + os.linesep) 97 | 98 | 99 | def update_repo(repo): 100 | assert isinstance(repo, Repository) 101 | repo_dir = check_repo_dir(repo) 102 | os.chdir(repo_dir) 103 | log([execute(['git', 'fetch', 'upstream']), 104 | execute(['git', 'merge', 'upstream/%s' % repo.master_branch]), 105 | execute(['git', 'push'])]) 106 | os.chdir(ROOT_DIR) 107 | log(u'Updated %s' % repo.name) 108 | 109 | 110 | @command 111 | def run(no_interactive=False): 112 | """Run this process.""" 113 | if not all([GITHUB_USERNAME, GITHUB_PASSWORD]): 114 | error(u'Missing github username and/or password.') 115 | log(u'Authenticated to github API...') 116 | gh = connect(GITHUB_USERNAME, GITHUB_PASSWORD) 117 | log(u'Authenticated.') 118 | log(u'Fetching forked repositories...') 119 | repos = get_repos(gh, organization=GITHUB_ORGANIZATION) 120 | num_repos = len(repos) 121 | if not num_repos: 122 | error(u'No forked repository found.') 123 | log(u'Found %d forked repositories to synchronize.' % num_repos) 124 | if no_interactive is not True: 125 | if GITHUB_ORGANIZATION: 126 | confirm(u'Sync %d forked repositories for the "%s" organization?' 127 | % (num_repos, GITHUB_ORGANIZATION)) 128 | else: 129 | confirm(u"Synchronize %d %s's forked repositories? (yN): " 130 | % (num_repos, gh.user().name),) 131 | for repo in repos: 132 | log('Updating %s...' % repo) 133 | update_repo(repo) 134 | log('Done.') 135 | log(u'All done.') 136 | 137 | 138 | if __name__ == '__main__': 139 | try: 140 | dispatch_command(run) 141 | except KeyboardInterrupt: 142 | error(u'Aborted.') 143 | --------------------------------------------------------------------------------