mirror of
https://github.com/josegonzalez/python-github-backup.git
synced 2025-12-05 16:18:02 +01:00
Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b73079daf2 | ||
|
|
eca8a70666 | ||
|
|
e74765ba7f | ||
|
|
6db5bd731b | ||
|
|
7305871c20 | ||
|
|
baf7b1a9b4 | ||
|
|
121fa68294 | ||
|
|
44dfc79edc | ||
|
|
89f59cc7a2 | ||
|
|
ad8c5b8768 | ||
|
|
921aab3729 | ||
|
|
ea4c3d0f6f | ||
|
|
9b6400932d | ||
|
|
de0c3f46c6 | ||
|
|
73b069f872 | ||
|
|
3d3f512074 | ||
|
|
1c3078992d | ||
|
|
4b40ae94d7 | ||
|
|
a18fda9faf | ||
|
|
41130fc8b0 | ||
|
|
2340a02fc6 | ||
|
|
cafff4ae80 | ||
|
|
3193d120e5 | ||
|
|
da4b29a2d6 | ||
|
|
d05c96ecef | ||
|
|
c86163bfe6 | ||
|
|
eff6e36974 | ||
|
|
63e458bafb | ||
|
|
57ab5ce1a2 | ||
|
|
d148f9b900 | ||
|
|
89ee22c2be | ||
|
|
9e472b74e6 | ||
|
|
4b459f9af8 | ||
|
|
b70ea87db7 | ||
|
|
f8be34562b | ||
|
|
ec05204aa9 | ||
|
|
628f2cbf73 | ||
|
|
38bf438d2f | ||
|
|
899cf42b57 | ||
|
|
b5972aaaf0 | ||
|
|
d860f369e9 | ||
|
|
77ab1bda15 | ||
|
|
4a4a317331 | ||
|
|
5a8e1ac275 | ||
|
|
0de341eab4 | ||
|
|
b0130fdf94 | ||
|
|
b49f399037 | ||
|
|
321414d352 | ||
|
|
413d4381cc | ||
|
|
0110ea40ed | ||
|
|
8d2ef2f528 | ||
|
|
1a79f755a5 | ||
|
|
abf45d5b54 | ||
|
|
fd33037b1c | ||
|
|
87dab293ed | ||
|
|
0244af4e05 | ||
|
|
eca9f0f7df | ||
|
|
afa2a6d587 | ||
|
|
b77ea48d74 | ||
|
|
f378254188 | ||
|
|
83128e986a | ||
|
|
17e4f9a125 | ||
|
|
e59d1e3a68 | ||
|
|
de860ee5a9 | ||
|
|
cb054c2631 | ||
|
|
c142707a90 | ||
|
|
7cccd42ec9 | ||
|
|
9a539b1d6b | ||
|
|
cd2372183e | ||
|
|
bd346de898 | ||
|
|
6e3cbe841a | ||
|
|
8b95f187ad | ||
|
|
ef88248c41 | ||
|
|
0a4decfb3b | ||
|
|
2b9549ffde | ||
|
|
fb2c3ca921 | ||
|
|
4f4785085d | ||
|
|
76895dcf69 | ||
|
|
1d50a4038b | ||
|
|
9d31ccfba9 | ||
|
|
27a1ba2d04 | ||
|
|
f157ea107f | ||
|
|
a129cc759a | ||
|
|
bb551a83f4 | ||
|
|
9b1b4a9ebc | ||
|
|
e6b6eb8bef | ||
|
|
0b3f120e2b | ||
|
|
990249b80b | ||
|
|
cefb226545 | ||
|
|
ea22ffdf26 | ||
|
|
0f21d7b8a4 |
227
CHANGES.rst
227
CHANGES.rst
@@ -1,19 +1,162 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
0.13.2 (2017-05-06)
|
||||
0.25.0 (2019-07-03)
|
||||
-------------------
|
||||
------------------------
|
||||
- Issue 119: Change retrieve_data to be a generator. [2a]
|
||||
|
||||
See issue #119.
|
||||
|
||||
|
||||
0.24.0 (2019-06-27)
|
||||
-------------------
|
||||
- QKT-45: include assets - update readme. [Ethan Timm]
|
||||
|
||||
update readme with flag information for including assets alongside their respective releases
|
||||
- Make assets it's own flag. [Harrison Wright]
|
||||
- Fix super call for python2. [Harrison Wright]
|
||||
- Fix redirect to s3. [Harrison Wright]
|
||||
- WIP: download assets. [Harrison Wright]
|
||||
- QKT-42: releases - add readme info. [ethan]
|
||||
- QKT-42 update: shorter command flag. [ethan]
|
||||
- QKT-42: support saving release information. [ethan]
|
||||
- Fix pull details. [Harrison Wright]
|
||||
|
||||
|
||||
0.23.0 (2019-06-04)
|
||||
-------------------
|
||||
- Avoid to crash in case of HTTP 502 error. [Gael de Chalendar]
|
||||
|
||||
Survive also on socket.error connections like on HTTPError or URLError.
|
||||
|
||||
This should solve issue #110.
|
||||
|
||||
|
||||
0.22.2 (2019-02-21)
|
||||
-------------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- Warn instead of error. [Jose Diaz-Gonzalez]
|
||||
|
||||
Refs #106
|
||||
|
||||
|
||||
0.22.1 (2019-02-21)
|
||||
-------------------
|
||||
- Log URL error https://github.com/josegonzalez/python-github-
|
||||
backup/issues/105. [JOHN STETIC]
|
||||
|
||||
|
||||
0.22.0 (2019-02-01)
|
||||
-------------------
|
||||
- Remove unnecessary sys.exit call. [W. Harrison Wright]
|
||||
- Add org check to avoid incorrect log output. [W. Harrison Wright]
|
||||
- Fix accidental system exit with better logging strategy. [W. Harrison
|
||||
Wright]
|
||||
|
||||
|
||||
0.21.1 (2018-12-25)
|
||||
-------------------
|
||||
- Mark options which are not included in --all. [Bernd]
|
||||
|
||||
As discussed in Issue #100
|
||||
|
||||
|
||||
0.21.0 (2018-11-28)
|
||||
-------------------
|
||||
- Correctly download repos when user arg != authenticated user. [W.
|
||||
Harrison Wright]
|
||||
|
||||
|
||||
0.20.1 (2018-09-29)
|
||||
-------------------
|
||||
- Clone the specified user's gists, not the authenticated user. [W.
|
||||
Harrison Wright]
|
||||
- Clone the specified user's starred repos, not the authenticated user.
|
||||
[W. Harrison Wright]
|
||||
|
||||
|
||||
0.20.0 (2018-03-24)
|
||||
-------------------
|
||||
- Chore: drop Python 2.6. [Jose Diaz-Gonzalez]
|
||||
- Feat: simplify release script. [Jose Diaz-Gonzalez]
|
||||
|
||||
|
||||
0.19.2 (2018-03-24)
|
||||
-------------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
- Cleanup pep8 violations. [Jose Diaz-Gonzalez]
|
||||
|
||||
|
||||
0.19.0 (2018-03-24)
|
||||
-------------------
|
||||
- Add additional output for the current request. [Robin Gloster]
|
||||
|
||||
This is useful to have some progress indication for huge repositories.
|
||||
- Add option to backup additional PR details. [Robin Gloster]
|
||||
|
||||
Some payload is only included when requesting a single pull request
|
||||
- Mark string as binary in comparison for skip_existing. [Johannes
|
||||
Bornhold]
|
||||
|
||||
Found out that the flag "--skip-existing" did not work out as expected on Python
|
||||
3.6. Tracked it down to the comparison which has to be against a string of bytes
|
||||
in Python3.
|
||||
|
||||
|
||||
0.18.0 (2018-02-22)
|
||||
-------------------
|
||||
- Add option to fetch followers/following JSON data. [Stephen Greene]
|
||||
|
||||
|
||||
0.17.0 (2018-02-20)
|
||||
-------------------
|
||||
- Short circuit gists backup process. [W. Harrison Wright]
|
||||
- Formatting. [W. Harrison Wright]
|
||||
- Add ability to backup gists. [W. Harrison Wright]
|
||||
|
||||
|
||||
0.16.0 (2018-01-22)
|
||||
-------------------
|
||||
- Change option to --all-starred. [W. Harrison Wright]
|
||||
- JK don't update documentation. [W. Harrison Wright]
|
||||
- Put starred clone repoistories under a new option. [W. Harrison
|
||||
Wright]
|
||||
- Add comment. [W. Harrison Wright]
|
||||
- Add ability to clone starred repos. [W. Harrison Wright]
|
||||
|
||||
|
||||
0.14.1 (2017-10-11)
|
||||
-------------------
|
||||
- Fix arg not defined error. [Edward Pfremmer]
|
||||
|
||||
|
||||
0.14.0 (2017-10-11)
|
||||
-------------------
|
||||
- Added a check to see if git-lfs is installed when doing an LFS clone.
|
||||
[pieterclaerhout]
|
||||
- Added support for LFS clones. [pieterclaerhout]
|
||||
- Add pypi info to readme. [Albert Wang]
|
||||
- Explicitly support python 3 in package description. [Albert Wang]
|
||||
- Add couple examples to help new users. [Yusuf Tran]
|
||||
|
||||
|
||||
0.13.2 (2017-05-06)
|
||||
-------------------
|
||||
- Fix remotes while updating repository. [Dima Gerasimov]
|
||||
|
||||
|
||||
0.13.1 (2017-04-11)
|
||||
-------------------
|
||||
|
||||
- Fix error when repository has no updated_at value. [Nicolai Ehemann]
|
||||
|
||||
|
||||
0.13.0 (2017-04-05)
|
||||
-------------------
|
||||
|
||||
- Add OS check for OSX specific keychain args. [Martin O'Reilly]
|
||||
|
||||
Keychain arguments are only supported on Mac OSX.
|
||||
@@ -22,8 +165,6 @@ Changelog
|
||||
error message rather than a "No password item matching the
|
||||
provided name and account could be found in the osx keychain"
|
||||
error message
|
||||
|
||||
|
||||
- Add support for storing PAT in OSX keychain. [Martin O'Reilly]
|
||||
|
||||
Added additional optional arguments and README guidance for storing
|
||||
@@ -33,62 +174,48 @@ Changelog
|
||||
|
||||
0.12.1 (2017-03-27)
|
||||
-------------------
|
||||
|
||||
- Avoid remote branch name churn. [Chris Adams]
|
||||
|
||||
This avoids the backup output having lots of "[new branch]" messages
|
||||
because removing the old remote name removed all of the existing branch
|
||||
references.
|
||||
|
||||
|
||||
- Fix detection of bare git directories. [Andrzej Maczuga]
|
||||
|
||||
|
||||
0.12.0 (2016-11-22)
|
||||
-------------------
|
||||
|
||||
Fix
|
||||
~~~
|
||||
|
||||
- Properly import version from github_backup package. [Jose Diaz-
|
||||
Gonzalez]
|
||||
|
||||
- Support alternate git status output. [Jose Diaz-Gonzalez]
|
||||
|
||||
Other
|
||||
~~~~~
|
||||
|
||||
- Pep8: E501 line too long (83 > 79 characters) [Jose Diaz-Gonzalez]
|
||||
|
||||
- Pep8: E128 continuation line under-indented for visual indent. [Jose
|
||||
Diaz-Gonzalez]
|
||||
|
||||
- Support archivization using bare git clones. [Andrzej Maczuga]
|
||||
|
||||
- Fix typo, 3x. [Terrell Russell]
|
||||
|
||||
|
||||
0.11.0 (2016-10-26)
|
||||
-------------------
|
||||
|
||||
- Support --token file:///home/user/token.txt (fixes gh-51) [Björn
|
||||
Dahlgren]
|
||||
|
||||
- Fix some linting. [Albert Wang]
|
||||
|
||||
- Fix byte/string conversion for python 3. [Albert Wang]
|
||||
|
||||
- Support python 3. [Albert Wang]
|
||||
|
||||
- Encode special characters in password. [Remi Rampin]
|
||||
|
||||
- Don't pretend program name is "Github Backup" [Remi Rampin]
|
||||
|
||||
- Don't install over insecure connection. [Remi Rampin]
|
||||
|
||||
The git:// protocol is unauthenticated and unencrypted, and no longer advertised by GitHub. Using HTTPS shouldn't impact performance.
|
||||
|
||||
|
||||
0.10.3 (2016-08-21)
|
||||
-------------------
|
||||
|
||||
- Fixes #29. [Jonas Michel]
|
||||
|
||||
Reporting an error when the user's rate limit is exceeded causes
|
||||
@@ -96,8 +223,6 @@ Other
|
||||
sleep. Instead of generating an explicit error we just want to
|
||||
inform the user that the script is going to sleep until their rate
|
||||
limit count resets.
|
||||
|
||||
|
||||
- Fixes #29. [Jonas Michel]
|
||||
|
||||
The errors list was not being cleared out after resuming a backup
|
||||
@@ -108,14 +233,13 @@ Other
|
||||
|
||||
0.10.2 (2016-08-21)
|
||||
-------------------
|
||||
|
||||
- Add a note regarding git version requirement. [Jose Diaz-Gonzalez]
|
||||
|
||||
Closes #37
|
||||
|
||||
|
||||
0.10.0 (2016-08-18)
|
||||
-------------------
|
||||
|
||||
- Implement incremental updates. [Robert Bradshaw]
|
||||
|
||||
Guarded with an --incremental flag.
|
||||
@@ -128,12 +252,11 @@ Other
|
||||
|
||||
0.9.0 (2016-03-29)
|
||||
------------------
|
||||
|
||||
- Fix cloning private repos with basic auth or token. [Kazuki Suda]
|
||||
|
||||
|
||||
0.8.0 (2016-02-14)
|
||||
------------------
|
||||
|
||||
- Don't store issues which are actually pull requests. [Enrico Tröger]
|
||||
|
||||
This prevents storing pull requests twice since the Github API returns
|
||||
@@ -144,43 +267,31 @@ Other
|
||||
|
||||
0.7.0 (2016-02-02)
|
||||
------------------
|
||||
|
||||
- Softly fail if not able to read hooks. [Albert Wang]
|
||||
|
||||
- Add note about 2-factor auth. [Albert Wang]
|
||||
|
||||
- Make user repository search go through endpoint capable of reading
|
||||
private repositories. [Albert Wang]
|
||||
|
||||
- Prompt for password if only username given. [Alex Hall]
|
||||
|
||||
|
||||
0.6.0 (2015-11-10)
|
||||
------------------
|
||||
|
||||
- Force proper remote url. [Jose Diaz-Gonzalez]
|
||||
|
||||
- Improve error handling in case of HTTP errors. [Enrico Tröger]
|
||||
|
||||
In case of a HTTP status code 404, the returned 'r' was never assigned.
|
||||
In case of URL errors which are not timeouts, we probably should bail
|
||||
out.
|
||||
|
||||
|
||||
- Add --hooks to also include web hooks into the backup. [Enrico Tröger]
|
||||
|
||||
- Create the user specified output directory if it does not exist.
|
||||
[Enrico Tröger]
|
||||
|
||||
Fixes #17.
|
||||
|
||||
|
||||
- Add missing auth argument to _get_response() [Enrico Tröger]
|
||||
|
||||
When running unauthenticated and Github starts rate-limiting the client,
|
||||
github-backup crashes because the used auth variable in _get_response()
|
||||
was not available. This change should fix it.
|
||||
|
||||
|
||||
- Add repository URL to error message for non-existing repositories.
|
||||
[Enrico Tröger]
|
||||
|
||||
@@ -191,40 +302,28 @@ Other
|
||||
|
||||
0.5.0 (2015-10-10)
|
||||
------------------
|
||||
|
||||
- Add release script. [Jose Diaz-Gonzalez]
|
||||
|
||||
- Refactor to both simplify codepath as well as follow PEP8 standards.
|
||||
[Jose Diaz-Gonzalez]
|
||||
|
||||
- Retry 3 times when the connection times out. [Mathijs Jonker]
|
||||
|
||||
- Made unicode output defalut. [Kirill Grushetsky]
|
||||
|
||||
- Import alphabetised. [Kirill Grushetsky]
|
||||
|
||||
- Preserve Unicode characters in the output file. [Kirill Grushetsky]
|
||||
|
||||
Added option to preserve Unicode characters in the output file
|
||||
|
||||
- Josegonzales/python-github-backup#12 Added backup of labels and
|
||||
milestones. [aensley]
|
||||
|
||||
- Fixed indent. [Mathijs Jonker]
|
||||
|
||||
- Skip unitialized repo's. [mjonker-embed]
|
||||
|
||||
These gave me errors which caused mails from crontab.
|
||||
|
||||
- Added prefer-ssh. [mjonker-embed]
|
||||
|
||||
Was needed for my back-up setup, code includes this but readme wasn't updated
|
||||
|
||||
- Retry API requests which failed due to rate-limiting. [Chris Adams]
|
||||
|
||||
This allows operation to continue, albeit at a slower pace,
|
||||
if you have enough data to trigger the API rate limits
|
||||
|
||||
- Logging_subprocess: always log when a command fails. [Chris Adams]
|
||||
|
||||
Previously git clones could fail without any indication
|
||||
@@ -234,21 +333,15 @@ Other
|
||||
Now a non-zero return code will always output a message to
|
||||
stderr and will display the executed command so it can be
|
||||
rerun for troubleshooting.
|
||||
|
||||
|
||||
- Switch to using ssh_url. [Chris Adams]
|
||||
|
||||
The previous commit used the wrong URL for a private repo. This was
|
||||
masked by the lack of error loging in logging_subprocess (which will be
|
||||
in a separate branch)
|
||||
|
||||
|
||||
- Add an option to prefer checkouts over SSH. [Chris Adams]
|
||||
|
||||
This is really useful with private repos to avoid being nagged
|
||||
for credentials for every repository
|
||||
|
||||
|
||||
- Add pull request support. [Kevin Laude]
|
||||
|
||||
Back up reporitory pull requests by passing the --include-pulls
|
||||
@@ -260,8 +353,6 @@ Other
|
||||
|
||||
Pull requests are automatically backed up when the --all argument is
|
||||
uesd.
|
||||
|
||||
|
||||
- Add GitHub Enterprise support. [Kevin Laude]
|
||||
|
||||
Pass the -H or --github-host argument with a GitHub Enterprise hostname
|
||||
@@ -271,35 +362,21 @@ Other
|
||||
|
||||
0.2.0 (2014-09-22)
|
||||
------------------
|
||||
|
||||
- Add support for retrieving repositories. Closes #1. [Jose Diaz-
|
||||
Gonzalez]
|
||||
|
||||
- Fix PEP8 violations. [Jose Diaz-Gonzalez]
|
||||
|
||||
- Add authorization to header only if specified by user. [Ioannis
|
||||
Filippidis]
|
||||
|
||||
- Fill out readme more. [Jose Diaz-Gonzalez]
|
||||
|
||||
- Fix import. [Jose Diaz-Gonzalez]
|
||||
|
||||
- Properly name readme. [Jose Diaz-Gonzalez]
|
||||
|
||||
- Create MANIFEST.in. [Jose Diaz-Gonzalez]
|
||||
|
||||
- Create .gitignore. [Jose Diaz-Gonzalez]
|
||||
|
||||
- Create setup.py. [Jose Diaz-Gonzalez]
|
||||
|
||||
- Create requirements.txt. [Jose Diaz-Gonzalez]
|
||||
|
||||
- Create __init__.py. [Jose Diaz-Gonzalez]
|
||||
|
||||
- Create LICENSE.txt. [Jose Diaz-Gonzalez]
|
||||
|
||||
- Create README.md. [Jose Diaz-Gonzalez]
|
||||
|
||||
- Create github-backup. [Jose Diaz-Gonzalez]
|
||||
|
||||
|
||||
|
||||
56
README.rst
56
README.rst
@@ -2,6 +2,8 @@
|
||||
github-backup
|
||||
=============
|
||||
|
||||
|PyPI| |Python Versions|
|
||||
|
||||
backup a github user or organization
|
||||
|
||||
Requirements
|
||||
@@ -26,11 +28,13 @@ Usage
|
||||
CLI Usage is as follows::
|
||||
|
||||
github-backup [-h] [-u USERNAME] [-p PASSWORD] [-t TOKEN]
|
||||
[-o OUTPUT_DIRECTORY] [-i] [--starred] [--watched]
|
||||
[--all] [--issues] [--issue-comments] [--issue-events]
|
||||
[--pulls] [--pull-comments] [--pull-commits] [--labels]
|
||||
[--hooks] [--milestones] [--repositories] [--bare]
|
||||
[--wikis] [--skip-existing]
|
||||
[-o OUTPUT_DIRECTORY] [-i] [--starred] [--all-starred]
|
||||
[--watched] [--followers] [--following] [--all]
|
||||
[--issues] [--issue-comments] [--issue-events] [--pulls]
|
||||
[--pull-comments] [--pull-commits] [--labels] [--hooks]
|
||||
[--milestones] [--repositories] [--releases] [--assets]
|
||||
[--bare] [--lfs] [--wikis] [--gists] [--starred-gists]
|
||||
[--skip-existing]
|
||||
[-L [LANGUAGES [LANGUAGES ...]]] [-N NAME_REGEX]
|
||||
[-H GITHUB_HOST] [-O] [-R REPOSITORY] [-P] [-F]
|
||||
[--prefer-ssh] [-v]
|
||||
@@ -51,12 +55,16 @@ CLI Usage is as follows::
|
||||
password for basic auth. If a username is given but
|
||||
not a password, the password will be prompted for.
|
||||
-t TOKEN, --token TOKEN
|
||||
personal access or OAuth token
|
||||
personal access or OAuth token, or path to token
|
||||
(file://...)
|
||||
-o OUTPUT_DIRECTORY, --output-directory OUTPUT_DIRECTORY
|
||||
directory at which to backup the repositories
|
||||
-i, --incremental incremental backup
|
||||
--starred include starred repositories in backup
|
||||
--starred include JSON output of starred repositories in backup
|
||||
--all-starred include starred repositories in backup
|
||||
--watched include watched repositories in backup
|
||||
--followers include JSON output of followers in backup
|
||||
--following include JSON output of following users in backup
|
||||
--all include everything in backup
|
||||
--issues include issues in backup
|
||||
--issue-comments include issue comments in backup
|
||||
@@ -69,8 +77,14 @@ CLI Usage is as follows::
|
||||
authenticated)
|
||||
--milestones include milestones in backup
|
||||
--repositories include repository clone in backup
|
||||
--releases include repository releases' information without assets or binaries
|
||||
--assets include assets alongside release information; only applies if including releases
|
||||
--bare clone bare repositories
|
||||
--lfs clone LFS repositories (requires Git LFS to be
|
||||
installed, https://git-lfs.github.com)
|
||||
--wikis include wiki clone in backup
|
||||
--gists include gists in backup
|
||||
--starred-gists include starred gists in backup
|
||||
--skip-existing skip project if a backup directory exists
|
||||
-L [LANGUAGES [LANGUAGES ...]], --languages [LANGUAGES [LANGUAGES ...]]
|
||||
only allow these languages
|
||||
@@ -114,3 +128,31 @@ Note: When you run github-backup, you will be asked whether you want to allow "
|
||||
|
||||
1. **Allow:** In this case you will need to click "Allow" each time you run `github-backup`
|
||||
2. **Always Allow:** In this case, you will not be asked for permission when you run `github-backup` in future. This is less secure, but is required if you want to schedule `github-backup` to run automatically
|
||||
|
||||
About Git LFS
|
||||
=============
|
||||
|
||||
When you use the "--lfs" option, you will need to make sure you have Git LFS installed.
|
||||
|
||||
Instructions on how to do this can be found on https://git-lfs.github.com.
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
Backup all repositories::
|
||||
|
||||
export ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
||||
github-backup WhiteHouse --token $ACCESS_TOKEN --organization --output-directory /tmp/white-house --repositories
|
||||
|
||||
Backup a single organization repository with everything else (wiki, pull requests, comments, issues etc)::
|
||||
|
||||
export ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
||||
ORGANIZATION=docker
|
||||
REPO=cli
|
||||
# e.g. git@github.com:docker/cli.git
|
||||
github-backup $ORGANIZATION -P -t $ACCESS_TOKEN -o . --all -O -R $REPO
|
||||
|
||||
.. |PyPI| image:: https://img.shields.io/pypi/v/github-backup.svg
|
||||
:target: https://pypi.python.org/pypi/github-backup/
|
||||
.. |Python Versions| image:: https://img.shields.io/pypi/pyversions/github-backup.svg
|
||||
:target: https://github.com/albertyw/github-backup
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import print_function
|
||||
import socket
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
@@ -17,6 +18,7 @@ import subprocess
|
||||
import sys
|
||||
import time
|
||||
import platform
|
||||
PY2 = False
|
||||
try:
|
||||
# python 3
|
||||
from urllib.parse import urlparse
|
||||
@@ -25,14 +27,19 @@ try:
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.request import urlopen
|
||||
from urllib.request import Request
|
||||
from urllib.request import HTTPRedirectHandler
|
||||
from urllib.request import build_opener
|
||||
except ImportError:
|
||||
# python 2
|
||||
PY2 = True
|
||||
from urlparse import urlparse
|
||||
from urllib import quote as urlquote
|
||||
from urllib import urlencode
|
||||
from urllib2 import HTTPError, URLError
|
||||
from urllib2 import urlopen
|
||||
from urllib2 import Request
|
||||
from urllib2 import HTTPRedirectHandler
|
||||
from urllib2 import build_opener
|
||||
|
||||
from github_backup import __version__
|
||||
|
||||
@@ -40,16 +47,17 @@ FNULL = open(os.devnull, 'w')
|
||||
|
||||
|
||||
def log_error(message):
|
||||
if type(message) == str:
|
||||
message = [message]
|
||||
|
||||
for msg in message:
|
||||
sys.stderr.write("{0}\n".format(msg))
|
||||
|
||||
"""
|
||||
Log message (str) or messages (List[str]) to stderr and exit with status 1
|
||||
"""
|
||||
log_warning(message)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def log_info(message):
|
||||
"""
|
||||
Log message (str) or messages (List[str]) to stdout
|
||||
"""
|
||||
if type(message) == str:
|
||||
message = [message]
|
||||
|
||||
@@ -57,6 +65,17 @@ def log_info(message):
|
||||
sys.stdout.write("{0}\n".format(msg))
|
||||
|
||||
|
||||
def log_warning(message):
|
||||
"""
|
||||
Log message (str) or messages (List[str]) to stderr
|
||||
"""
|
||||
if type(message) == str:
|
||||
message = [message]
|
||||
|
||||
for msg in message:
|
||||
sys.stderr.write("{0}\n".format(msg))
|
||||
|
||||
|
||||
def logging_subprocess(popenargs,
|
||||
logger,
|
||||
stdout_log_level=logging.DEBUG,
|
||||
@@ -69,11 +88,15 @@ def logging_subprocess(popenargs,
|
||||
"""
|
||||
child = subprocess.Popen(popenargs, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, **kwargs)
|
||||
if sys.platform == 'win32':
|
||||
log_info("Windows operating system detected - no subprocess logging will be returned")
|
||||
|
||||
log_level = {child.stdout: stdout_log_level,
|
||||
child.stderr: stderr_log_level}
|
||||
|
||||
def check_io():
|
||||
if sys.platform == 'win32':
|
||||
return
|
||||
ready_to_read = select.select([child.stdout, child.stderr],
|
||||
[],
|
||||
[],
|
||||
@@ -155,15 +178,27 @@ def parse_args():
|
||||
parser.add_argument('--starred',
|
||||
action='store_true',
|
||||
dest='include_starred',
|
||||
help='include starred repositories in backup')
|
||||
help='include JSON output of starred repositories in backup')
|
||||
parser.add_argument('--all-starred',
|
||||
action='store_true',
|
||||
dest='all_starred',
|
||||
help='include starred repositories in backup [*]')
|
||||
parser.add_argument('--watched',
|
||||
action='store_true',
|
||||
dest='include_watched',
|
||||
help='include watched repositories in backup')
|
||||
help='include JSON output of watched repositories in backup')
|
||||
parser.add_argument('--followers',
|
||||
action='store_true',
|
||||
dest='include_followers',
|
||||
help='include JSON output of followers in backup')
|
||||
parser.add_argument('--following',
|
||||
action='store_true',
|
||||
dest='include_following',
|
||||
help='include JSON output of following users in backup')
|
||||
parser.add_argument('--all',
|
||||
action='store_true',
|
||||
dest='include_everything',
|
||||
help='include everything in backup')
|
||||
help='include everything in backup (not including [*])')
|
||||
parser.add_argument('--issues',
|
||||
action='store_true',
|
||||
dest='include_issues',
|
||||
@@ -188,6 +223,10 @@ def parse_args():
|
||||
action='store_true',
|
||||
dest='include_pull_commits',
|
||||
help='include pull request commits in backup')
|
||||
parser.add_argument('--pull-details',
|
||||
action='store_true',
|
||||
dest='include_pull_details',
|
||||
help='include more pull request details in backup [*]')
|
||||
parser.add_argument('--labels',
|
||||
action='store_true',
|
||||
dest='include_labels',
|
||||
@@ -208,10 +247,22 @@ def parse_args():
|
||||
action='store_true',
|
||||
dest='bare_clone',
|
||||
help='clone bare repositories')
|
||||
parser.add_argument('--lfs',
|
||||
action='store_true',
|
||||
dest='lfs_clone',
|
||||
help='clone LFS repositories (requires Git LFS to be installed, https://git-lfs.github.com) [*]')
|
||||
parser.add_argument('--wikis',
|
||||
action='store_true',
|
||||
dest='include_wiki',
|
||||
help='include wiki clone in backup')
|
||||
parser.add_argument('--gists',
|
||||
action='store_true',
|
||||
dest='include_gists',
|
||||
help='include gists in backup [*]')
|
||||
parser.add_argument('--starred-gists',
|
||||
action='store_true',
|
||||
dest='include_starred_gists',
|
||||
help='include starred gists in backup [*]')
|
||||
parser.add_argument('--skip-existing',
|
||||
action='store_true',
|
||||
dest='skip_existing',
|
||||
@@ -241,11 +292,11 @@ def parse_args():
|
||||
parser.add_argument('-P', '--private',
|
||||
action='store_true',
|
||||
dest='private',
|
||||
help='include private repositories')
|
||||
help='include private repositories [*]')
|
||||
parser.add_argument('-F', '--fork',
|
||||
action='store_true',
|
||||
dest='fork',
|
||||
help='include forked repositories')
|
||||
help='include forked repositories [*]')
|
||||
parser.add_argument('--prefer-ssh',
|
||||
action='store_true',
|
||||
help='Clone repositories using SSH instead of HTTPS')
|
||||
@@ -258,6 +309,15 @@ def parse_args():
|
||||
parser.add_argument('--keychain-account',
|
||||
dest='osx_keychain_item_account',
|
||||
help='OSX ONLY: account field of password item in OSX keychain that holds the personal access or OAuth token')
|
||||
parser.add_argument('--releases',
|
||||
action='store_true',
|
||||
dest='include_releases',
|
||||
help='include release information, not including assets or binaries'
|
||||
)
|
||||
parser.add_argument('--assets',
|
||||
action='store_true',
|
||||
dest='include_assets',
|
||||
help='include assets alongside release information; only applies if including releases')
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -271,12 +331,12 @@ def get_auth(args, encode=True):
|
||||
if platform.system() != 'Darwin':
|
||||
log_error("Keychain arguments are only supported on Mac OSX")
|
||||
try:
|
||||
with open(os.devnull,'w') as devnull:
|
||||
with open(os.devnull, 'w') as devnull:
|
||||
token = (subprocess.check_output([
|
||||
'security','find-generic-password',
|
||||
'-s',args.osx_keychain_item_name,
|
||||
'-a',args.osx_keychain_item_account,
|
||||
'-w' ], stderr=devnull).strip())
|
||||
'security', 'find-generic-password',
|
||||
'-s', args.osx_keychain_item_name,
|
||||
'-a', args.osx_keychain_item_account,
|
||||
'-w'], stderr=devnull).strip())
|
||||
auth = token + ':' + 'x-oauth-basic'
|
||||
except:
|
||||
log_error('No password item matching the provided name and account could be found in the osx keychain.')
|
||||
@@ -330,12 +390,15 @@ def get_github_repo_url(args, repository):
|
||||
if args.prefer_ssh:
|
||||
return repository['ssh_url']
|
||||
|
||||
if repository.get('is_gist'):
|
||||
return repository['git_pull_url']
|
||||
|
||||
auth = get_auth(args, False)
|
||||
if auth:
|
||||
repo_url = 'https://{0}@{1}/{2}/{3}.git'.format(
|
||||
auth,
|
||||
get_github_host(args),
|
||||
args.user,
|
||||
repository['owner']['login'],
|
||||
repository['name'])
|
||||
else:
|
||||
repo_url = repository['clone_url']
|
||||
@@ -343,12 +406,11 @@ def get_github_repo_url(args, repository):
|
||||
return repo_url
|
||||
|
||||
|
||||
def retrieve_data(args, template, query_args=None, single_request=False):
|
||||
def retrieve_data_gen(args, template, query_args=None, single_request=False):
|
||||
auth = get_auth(args)
|
||||
query_args = get_query_args(query_args)
|
||||
per_page = 100
|
||||
page = 0
|
||||
data = []
|
||||
|
||||
while True:
|
||||
page = page + 1
|
||||
@@ -357,6 +419,16 @@ def retrieve_data(args, template, query_args=None, single_request=False):
|
||||
|
||||
status_code = int(r.getcode())
|
||||
|
||||
retries = 0
|
||||
while retries < 3 and status_code == 502:
|
||||
print('API request returned HTTP 502: Bad Gateway. Retrying in 5 seconds')
|
||||
retries += 1
|
||||
time.sleep(5)
|
||||
request = _construct_request(per_page, page, query_args, template, auth) # noqa
|
||||
r, errors = _get_response(request, auth, template)
|
||||
|
||||
status_code = int(r.getcode())
|
||||
|
||||
if status_code != 200:
|
||||
template = 'API request returned HTTP {0}: {1}'
|
||||
errors.append(template.format(status_code, r.reason))
|
||||
@@ -365,11 +437,12 @@ def retrieve_data(args, template, query_args=None, single_request=False):
|
||||
response = json.loads(r.read().decode('utf-8'))
|
||||
if len(errors) == 0:
|
||||
if type(response) == list:
|
||||
data.extend(response)
|
||||
for resp in response:
|
||||
yield resp
|
||||
if len(response) < per_page:
|
||||
break
|
||||
elif type(response) == dict and single_request:
|
||||
data.append(response)
|
||||
yield response
|
||||
|
||||
if len(errors) > 0:
|
||||
log_error(errors)
|
||||
@@ -377,8 +450,8 @@ def retrieve_data(args, template, query_args=None, single_request=False):
|
||||
if single_request:
|
||||
break
|
||||
|
||||
return data
|
||||
|
||||
def retrieve_data(args, template, query_args=None, single_request=False):
|
||||
return list(retrieve_data_gen(args, template, query_args, single_request))
|
||||
|
||||
def get_query_args(query_args=None):
|
||||
if not query_args:
|
||||
@@ -398,7 +471,13 @@ def _get_response(request, auth, template):
|
||||
except HTTPError as exc:
|
||||
errors, should_continue = _request_http_error(exc, auth, errors) # noqa
|
||||
r = exc
|
||||
except URLError:
|
||||
except URLError as e:
|
||||
log_warning(e.reason)
|
||||
should_continue = _request_url_error(template, retry_timeout)
|
||||
if not should_continue:
|
||||
raise
|
||||
except socket.error as e:
|
||||
log_warning(e.strerror)
|
||||
should_continue = _request_url_error(template, retry_timeout)
|
||||
if not should_continue:
|
||||
raise
|
||||
@@ -419,6 +498,7 @@ def _construct_request(per_page, page, query_args, template, auth):
|
||||
request = Request(template + '?' + querystring)
|
||||
if auth is not None:
|
||||
request.add_header('Authorization', 'Basic '.encode('ascii') + auth)
|
||||
log_info('Requesting {}?{}'.format(template, querystring))
|
||||
return request
|
||||
|
||||
|
||||
@@ -467,11 +547,65 @@ def _request_url_error(template, retry_timeout):
|
||||
return False
|
||||
|
||||
|
||||
def retrieve_repositories(args):
|
||||
class S3HTTPRedirectHandler(HTTPRedirectHandler):
|
||||
"""
|
||||
A subclassed redirect handler for downloading Github assets from S3.
|
||||
|
||||
urllib will add the Authorization header to the redirected request to S3, which will result in a 400,
|
||||
so we should remove said header on redirect.
|
||||
"""
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||
if PY2:
|
||||
# HTTPRedirectHandler is an old style class
|
||||
request = HTTPRedirectHandler.redirect_request(self, req, fp, code, msg, headers, newurl)
|
||||
else:
|
||||
request = super(S3HTTPRedirectHandler, self).redirect_request(req, fp, code, msg, headers, newurl)
|
||||
del request.headers['Authorization']
|
||||
return request
|
||||
|
||||
|
||||
def download_file(url, path, auth):
|
||||
request = Request(url)
|
||||
request.add_header('Accept', 'application/octet-stream')
|
||||
request.add_header('Authorization', 'Basic '.encode('ascii') + auth)
|
||||
opener = build_opener(S3HTTPRedirectHandler)
|
||||
response = opener.open(request)
|
||||
|
||||
chunk_size = 16 * 1024
|
||||
with open(path, 'wb') as f:
|
||||
while True:
|
||||
chunk = response.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
f.write(chunk)
|
||||
|
||||
|
||||
def get_authenticated_user(args):
|
||||
template = 'https://{0}/user'.format(get_github_api_host(args))
|
||||
data = retrieve_data(args, template, single_request=True)
|
||||
return data[0]
|
||||
|
||||
|
||||
def check_git_lfs_install():
|
||||
exit_code = subprocess.call(['git', 'lfs', 'version'])
|
||||
if exit_code != 0:
|
||||
log_error('The argument --lfs requires you to have Git LFS installed.\nYou can get it from https://git-lfs.github.com.')
|
||||
|
||||
|
||||
def retrieve_repositories(args, authenticated_user):
|
||||
log_info('Retrieving repositories')
|
||||
single_request = False
|
||||
if args.user == authenticated_user['login']:
|
||||
# we must use the /user/repos API to be able to access private repos
|
||||
template = 'https://{0}/user/repos'.format(
|
||||
get_github_api_host(args))
|
||||
else:
|
||||
if args.private and not args.organization:
|
||||
log_warning('Authenticated user is different from user being backed up, thus private repositories cannot be accessed')
|
||||
template = 'https://{0}/users/{1}/repos'.format(
|
||||
get_github_api_host(args),
|
||||
args.user)
|
||||
|
||||
if args.organization:
|
||||
template = 'https://{0}/orgs/{1}/repos'.format(
|
||||
get_github_api_host(args),
|
||||
@@ -484,7 +618,34 @@ def retrieve_repositories(args):
|
||||
args.user,
|
||||
args.repository)
|
||||
|
||||
return retrieve_data(args, template, single_request=single_request)
|
||||
repos = retrieve_data(args, template, single_request=single_request)
|
||||
|
||||
if args.all_starred:
|
||||
starred_template = 'https://{0}/users/{1}/starred'.format(get_github_api_host(args), args.user)
|
||||
starred_repos = retrieve_data(args, starred_template, single_request=False)
|
||||
# flag each repo as starred for downstream processing
|
||||
for item in starred_repos:
|
||||
item.update({'is_starred': True})
|
||||
repos.extend(starred_repos)
|
||||
|
||||
if args.include_gists:
|
||||
gists_template = 'https://{0}/users/{1}/gists'.format(get_github_api_host(args), args.user)
|
||||
gists = retrieve_data(args, gists_template, single_request=False)
|
||||
# flag each repo as a gist for downstream processing
|
||||
for item in gists:
|
||||
item.update({'is_gist': True})
|
||||
repos.extend(gists)
|
||||
|
||||
if args.include_starred_gists:
|
||||
starred_gists_template = 'https://{0}/gists/starred'.format(get_github_api_host(args))
|
||||
starred_gists = retrieve_data(args, starred_gists_template, single_request=False)
|
||||
# flag each repo as a starred gist for downstream processing
|
||||
for item in starred_gists:
|
||||
item.update({'is_gist': True,
|
||||
'is_starred': True})
|
||||
repos.extend(starred_gists)
|
||||
|
||||
return repos
|
||||
|
||||
|
||||
def filter_repositories(args, unfiltered_repositories):
|
||||
@@ -492,7 +653,8 @@ def filter_repositories(args, unfiltered_repositories):
|
||||
|
||||
repositories = []
|
||||
for r in unfiltered_repositories:
|
||||
if r['owner']['login'] == args.user:
|
||||
# gists can be anonymous, so need to safely check owner
|
||||
if r.get('owner', {}).get('login') == args.user or r.get('is_starred'):
|
||||
repositories.append(r)
|
||||
|
||||
name_regex = None
|
||||
@@ -504,11 +666,11 @@ def filter_repositories(args, unfiltered_repositories):
|
||||
languages = [x.lower() for x in args.languages]
|
||||
|
||||
if not args.fork:
|
||||
repositories = [r for r in repositories if not r['fork']]
|
||||
repositories = [r for r in repositories if not r.get('fork')]
|
||||
if not args.private:
|
||||
repositories = [r for r in repositories if not r['private']]
|
||||
repositories = [r for r in repositories if not r.get('private') or r.get('public')]
|
||||
if languages:
|
||||
repositories = [r for r in repositories if r['language'] and r['language'].lower() in languages] # noqa
|
||||
repositories = [r for r in repositories if r.get('language') and r.get('language').lower() in languages] # noqa
|
||||
if name_regex:
|
||||
repositories = [r for r in repositories if name_regex.match(r['name'])]
|
||||
|
||||
@@ -530,17 +692,36 @@ def backup_repositories(args, output_directory, repositories):
|
||||
args.since = None
|
||||
|
||||
for repository in repositories:
|
||||
backup_cwd = os.path.join(output_directory, 'repositories')
|
||||
repo_cwd = os.path.join(backup_cwd, repository['name'])
|
||||
if repository.get('is_gist'):
|
||||
repo_cwd = os.path.join(output_directory, 'gists', repository['id'])
|
||||
elif repository.get('is_starred'):
|
||||
# put starred repos in -o/starred/${owner}/${repo} to prevent collision of
|
||||
# any repositories with the same name
|
||||
repo_cwd = os.path.join(output_directory, 'starred', repository['owner']['login'], repository['name'])
|
||||
else:
|
||||
repo_cwd = os.path.join(output_directory, 'repositories', repository['name'])
|
||||
|
||||
repo_dir = os.path.join(repo_cwd, 'repository')
|
||||
repo_url = get_github_repo_url(args, repository)
|
||||
|
||||
if args.include_repository or args.include_everything:
|
||||
fetch_repository(repository['name'],
|
||||
include_gists = (args.include_gists or args.include_starred_gists)
|
||||
if (args.include_repository or args.include_everything) \
|
||||
or (include_gists and repository.get('is_gist')):
|
||||
repo_name = repository.get('name') if not repository.get('is_gist') else repository.get('id')
|
||||
fetch_repository(repo_name,
|
||||
repo_url,
|
||||
repo_dir,
|
||||
skip_existing=args.skip_existing,
|
||||
bare_clone=args.bare_clone)
|
||||
bare_clone=args.bare_clone,
|
||||
lfs_clone=args.lfs_clone)
|
||||
|
||||
if repository.get('is_gist'):
|
||||
# dump gist information to a file as well
|
||||
output_file = '{0}/gist.json'.format(repo_cwd)
|
||||
with codecs.open(output_file, 'w', encoding='utf-8') as f:
|
||||
json_dump(repository, f)
|
||||
|
||||
continue # don't try to back anything else for a gist; it doesn't exist
|
||||
|
||||
download_wiki = (args.include_wiki or args.include_everything)
|
||||
if repository['has_wiki'] and download_wiki:
|
||||
@@ -548,7 +729,8 @@ def backup_repositories(args, output_directory, repositories):
|
||||
repo_url.replace('.git', '.wiki.git'),
|
||||
os.path.join(repo_cwd, 'wiki'),
|
||||
skip_existing=args.skip_existing,
|
||||
bare_clone=args.bare_clone)
|
||||
bare_clone=args.bare_clone,
|
||||
lfs_clone=args.lfs_clone)
|
||||
|
||||
if args.include_issues or args.include_everything:
|
||||
backup_issues(args, repo_cwd, repository, repos_template)
|
||||
@@ -565,6 +747,10 @@ def backup_repositories(args, output_directory, repositories):
|
||||
if args.include_hooks or args.include_everything:
|
||||
backup_hooks(args, repo_cwd, repository, repos_template)
|
||||
|
||||
if args.include_releases or args.include_everything:
|
||||
backup_releases(args, repo_cwd, repository, repos_template,
|
||||
include_assets=args.include_assets or args.include_everything)
|
||||
|
||||
if args.incremental:
|
||||
open(last_update_path, 'w').write(last_update)
|
||||
|
||||
@@ -639,23 +825,38 @@ def backup_pulls(args, repo_cwd, repository, repos_template):
|
||||
pulls = {}
|
||||
_pulls_template = '{0}/{1}/pulls'.format(repos_template,
|
||||
repository['full_name'])
|
||||
|
||||
pull_states = ['open', 'closed']
|
||||
for pull_state in pull_states:
|
||||
query_args = {
|
||||
'filter': 'all',
|
||||
'state': pull_state,
|
||||
'state': 'all',
|
||||
'sort': 'updated',
|
||||
'direction': 'desc',
|
||||
}
|
||||
|
||||
# It'd be nice to be able to apply the args.since filter here...
|
||||
_pulls = retrieve_data(args,
|
||||
if not args.include_pull_details:
|
||||
pull_states = ['open', 'closed']
|
||||
for pull_state in pull_states:
|
||||
query_args['state'] = pull_state
|
||||
_pulls = retrieve_data_gen(args,
|
||||
_pulls_template,
|
||||
query_args=query_args)
|
||||
for pull in _pulls:
|
||||
if args.since and pull['updated_at'] < args.since:
|
||||
break
|
||||
if not args.since or pull['updated_at'] >= args.since:
|
||||
pulls[pull['number']] = pull
|
||||
else:
|
||||
_pulls = retrieve_data_gen(args,
|
||||
_pulls_template,
|
||||
query_args=query_args)
|
||||
for pull in _pulls:
|
||||
if args.since and pull['updated_at'] < args.since:
|
||||
break
|
||||
if not args.since or pull['updated_at'] >= args.since:
|
||||
pulls[pull['number']] = retrieve_data(
|
||||
args,
|
||||
_pulls_template + '/{}'.format(pull['number']),
|
||||
single_request=True
|
||||
)[0]
|
||||
|
||||
log_info('Saving {0} pull requests to disk'.format(
|
||||
len(list(pulls.keys()))))
|
||||
@@ -734,17 +935,45 @@ def backup_hooks(args, repo_cwd, repository, repos_template):
|
||||
log_info("Unable to read hooks, skipping")
|
||||
|
||||
|
||||
def backup_releases(args, repo_cwd, repository, repos_template, include_assets=False):
|
||||
repository_fullname = repository['full_name']
|
||||
|
||||
# give release files somewhere to live & log intent
|
||||
release_cwd = os.path.join(repo_cwd, 'releases')
|
||||
log_info('Retrieving {0} releases'.format(repository_fullname))
|
||||
mkdir_p(repo_cwd, release_cwd)
|
||||
|
||||
query_args = {}
|
||||
|
||||
release_template = '{0}/{1}/releases'.format(repos_template, repository_fullname)
|
||||
releases = retrieve_data(args, release_template, query_args=query_args)
|
||||
|
||||
# for each release, store it
|
||||
log_info('Saving {0} releases to disk'.format(len(releases)))
|
||||
for release in releases:
|
||||
release_name = release['tag_name']
|
||||
output_filepath = os.path.join(release_cwd, '{0}.json'.format(release_name))
|
||||
with codecs.open(output_filepath, 'w+', encoding='utf-8') as f:
|
||||
json_dump(release, f)
|
||||
|
||||
if include_assets:
|
||||
assets = retrieve_data(args, release['assets_url'])
|
||||
for asset in assets:
|
||||
download_file(asset['url'], os.path.join(release_cwd, asset['name']), get_auth(args))
|
||||
|
||||
|
||||
def fetch_repository(name,
|
||||
remote_url,
|
||||
local_dir,
|
||||
skip_existing=False,
|
||||
bare_clone=False):
|
||||
bare_clone=False,
|
||||
lfs_clone=False):
|
||||
if bare_clone:
|
||||
if os.path.exists(local_dir):
|
||||
clone_exists = subprocess.check_output(['git',
|
||||
'rev-parse',
|
||||
'--is-bare-repository'],
|
||||
cwd=local_dir) == "true\n"
|
||||
cwd=local_dir) == b"true\n"
|
||||
else:
|
||||
clone_exists = False
|
||||
else:
|
||||
@@ -780,6 +1009,9 @@ def fetch_repository(name,
|
||||
git_command = ['git', 'remote', 'set-url', 'origin', remote_url]
|
||||
logging_subprocess(git_command, None, cwd=local_dir)
|
||||
|
||||
if lfs_clone:
|
||||
git_command = ['git', 'lfs', 'fetch', '--all', '--force', '--tags', '--prune']
|
||||
else:
|
||||
git_command = ['git', 'fetch', '--all', '--force', '--tags', '--prune']
|
||||
logging_subprocess(git_command, None, cwd=local_dir)
|
||||
else:
|
||||
@@ -788,7 +1020,13 @@ def fetch_repository(name,
|
||||
masked_remote_url,
|
||||
local_dir))
|
||||
if bare_clone:
|
||||
if lfs_clone:
|
||||
git_command = ['git', 'lfs', 'clone', '--mirror', remote_url, local_dir]
|
||||
else:
|
||||
git_command = ['git', 'clone', '--mirror', remote_url, local_dir]
|
||||
else:
|
||||
if lfs_clone:
|
||||
git_command = ['git', 'lfs', 'clone', remote_url, local_dir]
|
||||
else:
|
||||
git_command = ['git', 'clone', remote_url, local_dir]
|
||||
logging_subprocess(git_command, None)
|
||||
@@ -798,21 +1036,37 @@ def backup_account(args, output_directory):
|
||||
account_cwd = os.path.join(output_directory, 'account')
|
||||
|
||||
if args.include_starred or args.include_everything:
|
||||
output_file = '{0}/starred.json'.format(account_cwd)
|
||||
template = "https://{0}/users/{1}/starred"
|
||||
template = template.format(get_github_api_host(args), args.user)
|
||||
output_file = "{0}/starred.json".format(account_cwd)
|
||||
template = "https://{0}/users/{1}/starred".format(get_github_api_host(args), args.user)
|
||||
_backup_data(args,
|
||||
'starred repositories',
|
||||
"starred repositories",
|
||||
template,
|
||||
output_file,
|
||||
account_cwd)
|
||||
|
||||
if args.include_watched or args.include_everything:
|
||||
output_file = '{0}/watched.json'.format(account_cwd)
|
||||
template = "https://{0}/users/{1}/subscriptions"
|
||||
template = template.format(get_github_api_host(args), args.user)
|
||||
output_file = "{0}/watched.json".format(account_cwd)
|
||||
template = "https://{0}/users/{1}/subscriptions".format(get_github_api_host(args), args.user)
|
||||
_backup_data(args,
|
||||
'watched repositories',
|
||||
"watched repositories",
|
||||
template,
|
||||
output_file,
|
||||
account_cwd)
|
||||
|
||||
if args.include_followers or args.include_everything:
|
||||
output_file = "{0}/followers.json".format(account_cwd)
|
||||
template = "https://{0}/users/{1}/followers".format(get_github_api_host(args), args.user)
|
||||
_backup_data(args,
|
||||
"followers",
|
||||
template,
|
||||
output_file,
|
||||
account_cwd)
|
||||
|
||||
if args.include_following or args.include_everything:
|
||||
output_file = "{0}/following.json".format(account_cwd)
|
||||
template = "https://{0}/users/{1}/following".format(get_github_api_host(args), args.user)
|
||||
_backup_data(args,
|
||||
"following",
|
||||
template,
|
||||
output_file,
|
||||
account_cwd)
|
||||
@@ -847,9 +1101,13 @@ def main():
|
||||
log_info('Create output directory {0}'.format(output_directory))
|
||||
mkdir_p(output_directory)
|
||||
|
||||
if args.lfs_clone:
|
||||
check_git_lfs_install()
|
||||
|
||||
log_info('Backing up user {0} to {1}'.format(args.user, output_directory))
|
||||
|
||||
repositories = retrieve_repositories(args)
|
||||
authenticated_user = get_authenticated_user(args)
|
||||
repositories = retrieve_repositories(args, authenticated_user)
|
||||
repositories = filter_repositories(args, repositories)
|
||||
backup_repositories(args, output_directory, repositories)
|
||||
backup_account(args, output_directory)
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = '0.13.2'
|
||||
__version__ = '0.25.0'
|
||||
|
||||
9
release
9
release
@@ -1,8 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail; [[ $RELEASE_TRACE ]] && set -x
|
||||
|
||||
PACKAGE_NAME='github-backup'
|
||||
INIT_PACKAGE_NAME='github_backup'
|
||||
if [[ ! -f setup.py ]]; then
|
||||
echo -e "${RED}WARNING: Missing setup.py${COLOR_OFF}\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PACKAGE_NAME="$(cat setup.py | grep "name='" | head | cut -d "'" -f2)"
|
||||
INIT_PACKAGE_NAME="$(echo "${PACKAGE_NAME//-/_}")"
|
||||
PUBLIC="true"
|
||||
|
||||
# Colors
|
||||
|
||||
3
setup.py
3
setup.py
@@ -37,8 +37,9 @@ setup(
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Topic :: System :: Archiving :: Backup',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
],
|
||||
description='backup a github user or organization',
|
||||
long_description=open_file('README.rst').read(),
|
||||
|
||||
Reference in New Issue
Block a user