Compare commits

...

99 Commits

Author SHA1 Message Date
Jose Diaz-Gonzalez
f8be34562b Release version 0.21.1 2018-12-25 06:28:28 -05:00
Jose Diaz-Gonzalez
ec05204aa9 Merge pull request #101 from ecki/patch-2
Mark options which are not included in --all
2018-12-25 06:27:58 -05:00
Bernd
628f2cbf73 Mark options which are not included in --all
As discussed in Issue #100
2018-12-24 04:19:29 +01:00
Jose Diaz-Gonzalez
38bf438d2f Release version 0.21.0 2018-11-28 01:59:03 -05:00
Jose Diaz-Gonzalez
899cf42b57 Merge pull request #97 from whwright/94-fix-user-repos
Correctly download repos when user arg != authenticated user
2018-11-28 01:58:37 -05:00
W. Harrison Wright
b5972aaaf0 Correctly download repos when user arg != authenticated user 2018-11-11 19:40:46 -06:00
Jose Diaz-Gonzalez
d860f369e9 Release version 0.20.1 2018-09-29 00:16:48 -04:00
Jose Diaz-Gonzalez
77ab1bda15 Merge pull request #92 from whwright/87-fix-starred-bug
Clone the specified user's starred repos/gists, not the authenticated user
2018-09-29 00:16:32 -04:00
W. Harrison Wright
4a4a317331 Clone the specified user's gists, not the authenticated user 2018-09-28 21:59:50 -05:00
W. Harrison Wright
5a8e1ac275 Clone the specified user's starred repos, not the authenticated user 2018-09-28 21:46:28 -05:00
Jose Diaz-Gonzalez
0de341eab4 Release version 0.20.0 2018-03-24 15:00:26 -04:00
Jose Diaz-Gonzalez
b0130fdf94 chore: drop Python 2.6 2018-03-24 15:00:05 -04:00
Jose Diaz-Gonzalez
b49f399037 feat: simplify release script 2018-03-24 14:59:56 -04:00
Jose Diaz-Gonzalez
321414d352 Release version 0.19.2 2018-03-24 01:16:34 -04:00
Jose Diaz-Gonzalez
413d4381cc fix: cleanup pep8 violations 2018-03-24 01:16:28 -04:00
Jose Diaz-Gonzalez
0110ea40ed Release version 0.19.1 2018-03-24 01:04:35 -04:00
Jose Diaz-Gonzalez
8d2ef2f528 Release version 0.19.0 2018-03-24 00:54:34 -04:00
Jose Diaz-Gonzalez
1a79f755a5 Merge pull request #77 from mayflower/pull-details
Pull Details
2018-03-23 23:40:22 -04:00
Jose Diaz-Gonzalez
abf45d5b54 Merge pull request #84 from johbo/fix-python36-skip-existing
Mark string as binary in comparison for skip_existing
2018-02-26 10:44:12 -05:00
Johannes Bornhold
fd33037b1c Mark string as binary in comparison for skip_existing
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.
2018-02-26 11:21:25 +01:00
Jose Diaz-Gonzalez
87dab293ed Release version 0.18.0 2018-02-22 12:13:06 -05:00
Jose Diaz-Gonzalez
0244af4e05 Merge pull request #82 from sgreene570/add-followers
Add option to fetch followers/following JSON data
2018-02-22 12:11:48 -05:00
Stephen Greene
eca9f0f7df Add option to fetch followers/following JSON data 2018-02-21 19:29:59 -08:00
Jose Diaz-Gonzalez
afa2a6d587 Release version 0.17.0 2018-02-20 13:06:48 -05:00
Jose Diaz-Gonzalez
b77ea48d74 Merge pull request #81 from whwright/gists
Add ability to back up gists
2018-02-19 15:29:20 -05:00
W. Harrison Wright
f378254188 Short circuit gists backup process 2018-02-07 21:46:59 -06:00
W. Harrison Wright
83128e986a Formatting 2018-02-07 21:30:55 -06:00
W. Harrison Wright
17e4f9a125 Add ability to backup gists 2018-02-07 21:29:49 -06:00
Jose Diaz-Gonzalez
e59d1e3a68 Release version 0.16.0 2018-01-22 12:49:31 -05:00
Jose Diaz-Gonzalez
de860ee5a9 Merge pull request #78 from whwright/clone-starred-repos
Clone starred repos
2018-01-22 12:36:42 -05:00
Jose Diaz-Gonzalez
cb054c2631 Update README.rst 2018-01-22 12:36:32 -05:00
W. Harrison Wright
c142707a90 Update documentation 2018-01-22 11:34:27 -06:00
W. Harrison Wright
7cccd42ec9 Change option to --all-starred 2018-01-14 10:22:10 -06:00
W. Harrison Wright
9a539b1d6b JK don't update documentation 2018-01-14 10:18:51 -06:00
W. Harrison Wright
cd2372183e Update documentation 2018-01-13 17:44:09 -06:00
W. Harrison Wright
bd346de898 Put starred clone repoistories under a new option 2018-01-13 17:43:00 -06:00
W. Harrison Wright
6e3cbe841a Add comment 2018-01-13 14:12:26 -06:00
W. Harrison Wright
8b95f187ad Add ability to clone starred repos 2018-01-13 14:08:36 -06:00
Robin Gloster
ef88248c41 Add additional output for the current request
This is useful to have some progress indication for huge repositories.
2017-12-29 23:33:53 +01:00
Robin Gloster
0a4decfb3b Add option to backup additional PR details
Some payload is only included when requesting a single pull request
2017-12-29 21:39:59 +01:00
Jose Diaz-Gonzalez
2b9549ffde Release version 0.15.0 2017-12-11 11:46:16 -05:00
Jose Diaz-Gonzalez
fb2c3ca921 Merge pull request #75 from slibby/slibby-patch-windows
update check_io() to allow scripts to run on Windows
2017-12-11 11:45:58 -05:00
Sam Libby
4f4785085d update logging_subprocess function
1. added newline for return
2. added one-time warning (once per subprocess)
2017-12-11 09:25:49 -07:00
Sam Libby
76895dcf69 update check_io() to allow scripts to run on Windows 2017-12-10 21:44:26 -07:00
Jose Diaz-Gonzalez
1d50a4038b Release version 0.14.1 2017-10-11 16:18:21 -04:00
Jose Diaz-Gonzalez
9d31ccfba9 Merge pull request #70 from epfremmer/patch-1
Fix arg not defined error
2017-10-11 16:17:58 -04:00
Edward Pfremmer
27a1ba2d04 Fix arg not defined error
Ref: https://github.com/josegonzalez/python-github-backup/issues/69
2017-10-11 15:12:34 -05:00
Jose Diaz-Gonzalez
f157ea107f Release version 0.14.0 2017-10-11 11:52:16 -04:00
Jose Diaz-Gonzalez
a129cc759a Merge pull request #68 from pieterclaerhout/master
Added support for LFS clones
2017-10-11 11:51:57 -04:00
pieterclaerhout
bb551a83f4 Updated the readme 2017-10-11 15:14:13 +02:00
pieterclaerhout
9b1b4a9ebc Added a check to see if git-lfs is installed when doing an LFS clone 2017-10-11 15:11:14 +02:00
pieterclaerhout
e6b6eb8bef Added support for LFS clones 2017-10-10 19:52:07 +02:00
Jose Diaz-Gonzalez
0b3f120e2b Merge pull request #66 from albertyw/python3
Explicitly support python 3
2017-09-30 21:01:27 -04:00
Albert Wang
990249b80b Add pypi info to readme 2017-09-30 17:16:38 -07:00
Albert Wang
cefb226545 Explicitly support python 3 in package description 2017-09-30 17:13:47 -07:00
Jose Diaz-Gonzalez
ea22ffdf26 Merge pull request #65 from mumblez/master
add couple examples to help new users
2017-05-30 11:58:42 -06:00
Yusuf Tran
0f21d7b8a4 add couple examples to help new users 2017-05-30 18:52:11 +01:00
Jose Diaz-Gonzalez
cb33b9bab7 Release version 0.13.2 2017-05-06 14:14:08 -06:00
Jose Diaz-Gonzalez
68c48cb0b3 Merge pull request #64 from karlicoss/fix-remotes
Fix remotes while updating repository
2017-05-06 14:13:46 -06:00
Dima Gerasimov
922a3c5a6e Fix remotes while updating repository 2017-05-06 12:58:42 +01:00
Jose Diaz-Gonzalez
d4055eb99c Release version 0.13.1 2017-04-11 09:40:13 -06:00
Jose Diaz-Gonzalez
d8a330559c Merge pull request #61 from McNetic/fix_empty_updated_at
Fix error when repository has no updated_at value
2017-04-11 09:37:15 -06:00
Nicolai Ehemann
de93824498 Fix error when repository has no updated_at value 2017-04-11 11:10:03 +02:00
Jose Diaz-Gonzalez
2efeaa7580 Release version 0.13.0 2017-04-05 11:49:49 -04:00
Jose Diaz-Gonzalez
647810a2f0 Merge pull request #59 from martintoreilly/master
Add support for storing PAT in OSX keychain
2017-04-05 11:49:24 -04:00
Martin O'Reilly
0dfe5c342a Add OS check for OSX specific keychain args
Keychain arguments are only supported on Mac OSX.
Added check for operating system so we give a
"Keychain arguments are only supported on Mac OSX"
error message rather than a "No password item matching the
provided name and account could be found in the osx keychain"
error message
2017-04-05 16:36:52 +01:00
Martin O'Reilly
1d6e1abab1 Add support for storing PAT in OSX keychain
Added additional optional arguments and README guidance for storing
and accessing a Github personal access token (PAT) in the OSX
keychain
2017-04-05 15:17:52 +01:00
Jose Diaz-Gonzalez
dd2b96b172 Release version 0.12.1 2017-03-27 14:55:11 -06:00
Jose Diaz-Gonzalez
7a589f1e63 Merge pull request #57 from acdha/reuse-existing-remotes
Avoid remote branch name churn
2017-03-27 14:54:02 -06:00
Chris Adams
92c619cd01 Avoid remote branch name churn
This avoids the backup output having lots of "[new branch]" messages
because removing the old remote name removed all of the existing branch
references.
2017-03-27 16:26:19 -04:00
Jose Diaz-Gonzalez
9a91dd7733 Merge pull request #55 from amaczuga/master
Fix detection of bare git directories
2016-11-22 13:36:52 -07:00
Andrzej Maczuga
6592bd8196 Fix detection of bare git directories 2016-11-22 20:11:26 +00:00
Jose Diaz-Gonzalez
e9e3b18512 Release version 0.12.0 2016-11-22 10:56:56 -07:00
Jose Diaz-Gonzalez
88148b4c95 pep8: E501 line too long (83 > 79 characters) 2016-11-22 10:55:37 -07:00
Jose Diaz-Gonzalez
8448add464 pep8: E128 continuation line under-indented for visual indent 2016-11-22 10:51:04 -07:00
Jose Diaz-Gonzalez
5b30b7ebdd fix: properly import version from github_backup package 2016-11-22 10:49:18 -07:00
Jose Diaz-Gonzalez
c3a17710d3 fix: support alternate git status output 2016-11-22 10:48:07 -07:00
Jose Diaz-Gonzalez
4462412ec7 Merge pull request #54 from amaczuga/master
Support archivization using bare git clones
2016-11-22 09:44:54 -07:00
Andrzej Maczuga
8d61538e5e Support archivization using bare git clones 2016-11-22 13:07:52 +00:00
Jose Diaz-Gonzalez
4d37ad206f Merge pull request #53 from trel/master
fix typo, 3x
2016-11-18 15:35:53 -05:00
Terrell Russell
1f983863fc fix typo, 3x 2016-11-18 15:17:42 -05:00
Jose Diaz-Gonzalez
f0b28567b9 Release version 0.11.0 2016-10-26 14:14:00 -06:00
Jose Diaz-Gonzalez
77ede50b19 Merge pull request #52 from bjodah/fix-gh-51
Support --token file:///home/user/token.txt (fixes gh-51)
2016-10-26 14:13:35 -06:00
Björn Dahlgren
97e4fbbacb Support --token file:///home/user/token.txt (fixes gh-51) 2016-10-26 01:57:33 +02:00
Jose Diaz-Gonzalez
03604cc654 Merge pull request #48 from albertyw/python3
Support Python 3
2016-10-25 17:38:05 -06:00
Albert Wang
73a62fdee1 Fix some linting 2016-09-11 01:14:36 -07:00
Albert Wang
94e1d62ad5 Fix byte/string conversion for python 3 2016-09-11 01:14:31 -07:00
Albert Wang
54cef11ce7 Support python 3 2016-09-11 01:14:19 -07:00
Jose Diaz-Gonzalez
56397eba1c Merge pull request #46 from remram44/encode-password
Encode special characters in password
2016-09-06 14:31:35 -04:00
Remi Rampin
9f861efccf Encode special characters in password 2016-09-06 14:27:47 -04:00
Jose Diaz-Gonzalez
c1c9ce6dca Merge pull request #45 from remram44/cli-programname
Fix program name
2016-09-06 12:42:45 -04:00
Jose Diaz-Gonzalez
ab18d8aee0 Merge pull request #44 from remram44/readme-git-https
Don't install over insecure connection
2016-09-06 12:42:30 -04:00
Remi Rampin
9d7d98b19e Update README.rst 2016-09-06 12:28:44 -04:00
Remi Rampin
0233bff696 Don't pretend program name is "Github Backup" 2016-09-06 12:24:51 -04:00
Remi Rampin
6154ceda15 Don't install over insecure connection
The git:// protocol is unauthenticated and unencrypted, and no longer advertised by GitHub. Using HTTPS shouldn't impact performance.
2016-09-06 12:11:29 -04:00
Jose Diaz-Gonzalez
9023052e9c Release version 0.10.3 2016-08-20 20:50:29 -04:00
Jose Diaz-Gonzalez
874c235ba5 Merge pull request #30 from jonasrmichel/master
Fixes #29
2016-08-20 20:50:25 -04:00
Jonas Michel
1e5a90486c Fixes #29
Reporting an error when the user's rate limit is exceeded causes
the script to terminate after resuming execution from a rate limit
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.
2016-01-20 14:48:02 -06:00
Jonas Michel
9b74aff20b Fixes #29
The errors list was not being cleared out after resuming a backup
from a rate limit sleep. When the backup was resumed, the non-empty
errors list caused the backup to quit after the next `retrieve_data`
request.
2016-01-17 11:10:28 -06:00
6 changed files with 634 additions and 111 deletions

View File

@@ -1,7 +1,219 @@
Changelog Changelog
========= =========
0.10.2 (2016-08-20) 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]
Ref: https://github.com/josegonzalez/python-github-backup/issues/69
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.
Added check for operating system so we give a
"Keychain arguments are only supported on Mac OSX"
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
and accessing a Github personal access token (PAT) in the OSX
keychain
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
the script to terminate after resuming execution from a rate limit
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
from a rate limit sleep. When the backup was resumed, the non-empty
errors list caused the backup to quit after the next `retrieve_data`
request.
0.10.2 (2016-08-21)
------------------- -------------------
- Add a note regarding git version requirement. [Jose Diaz-Gonzalez] - Add a note regarding git version requirement. [Jose Diaz-Gonzalez]

View File

@@ -2,6 +2,8 @@
github-backup github-backup
============= =============
|PyPI| |Python Versions|
backup a github user or organization backup a github user or organization
Requirements Requirements
@@ -18,21 +20,25 @@ Using PIP via PyPI::
Using PIP via Github:: Using PIP via Github::
pip install git+git://github.com/josegonzalez/python-github-backup.git#egg=github-backup pip install git+https://github.com/josegonzalez/python-github-backup.git#egg=github-backup
Usage Usage
===== =====
CLI Usage is as follows:: CLI Usage is as follows::
Github Backup [-h] [-u USERNAME] [-p PASSWORD] [-t TOKEN] github-backup [-h] [-u USERNAME] [-p PASSWORD] [-t TOKEN]
[-o OUTPUT_DIRECTORY] [--starred] [--watched] [--all] [-o OUTPUT_DIRECTORY] [-i] [--starred] [--all-starred]
[--watched] [--followers] [--following] [--all]
[--issues] [--issue-comments] [--issue-events] [--pulls] [--issues] [--issue-comments] [--issue-events] [--pulls]
[--pull-comments] [--pull-commits] [--labels] [--hooks] [--pull-comments] [--pull-commits] [--labels] [--hooks]
[--milestones] [--repositories] [--wikis] [--milestones] [--repositories] [--bare] [--lfs]
[--skip-existing] [-L [LANGUAGES [LANGUAGES ...]]] [--wikis] [--gists] [--starred-gists] [--skip-existing]
[-N NAME_REGEX] [-H GITHUB_HOST] [-O] [-R REPOSITORY] [-L [LANGUAGES [LANGUAGES ...]]] [-N NAME_REGEX]
[-P] [-F] [--prefer-ssh] [-v] [-H GITHUB_HOST] [-O] [-R REPOSITORY] [-P] [-F]
[--prefer-ssh] [-v]
[--keychain-name OSX_KEYCHAIN_ITEM_NAME]
[--keychain-account OSX_KEYCHAIN_ITEM_ACCOUNT]
USER USER
Backup a github account Backup a github account
@@ -48,11 +54,16 @@ CLI Usage is as follows::
password for basic auth. If a username is given but password for basic auth. If a username is given but
not a password, the password will be prompted for. not a password, the password will be prompted for.
-t TOKEN, --token TOKEN -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 -o OUTPUT_DIRECTORY, --output-directory OUTPUT_DIRECTORY
directory at which to backup the repositories directory at which to backup the repositories
--starred include starred repositories in backup -i, --incremental incremental backup
--starred include JSON output of starred repositories in backup
--all-starred include starred repositories in backup
--watched include watched 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 --all include everything in backup
--issues include issues in backup --issues include issues in backup
--issue-comments include issue comments in backup --issue-comments include issue comments in backup
@@ -65,7 +76,12 @@ CLI Usage is as follows::
authenticated) authenticated)
--milestones include milestones in backup --milestones include milestones in backup
--repositories include repository clone in backup --repositories include repository clone in backup
--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 --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 --skip-existing skip project if a backup directory exists
-L [LANGUAGES [LANGUAGES ...]], --languages [LANGUAGES [LANGUAGES ...]] -L [LANGUAGES [LANGUAGES ...]], --languages [LANGUAGES [LANGUAGES ...]]
only allow these languages only allow these languages
@@ -80,6 +96,12 @@ CLI Usage is as follows::
-F, --fork include forked repositories -F, --fork include forked repositories
--prefer-ssh Clone repositories using SSH instead of HTTPS --prefer-ssh Clone repositories using SSH instead of HTTPS
-v, --version show program's version number and exit -v, --version show program's version number and exit
--keychain-name OSX_KEYCHAIN_ITEM_NAME
OSX ONLY: name field of password item in OSX keychain
that holds the personal access or OAuth token
--keychain-account OSX_KEYCHAIN_ITEM_ACCOUNT
OSX ONLY: account field of password item in OSX
keychain that holds the personal access or OAuth token
The package can be used to backup an *entire* organization or repository, including issues and wikis in the most appropriate format (clones for wikis, json files for issues). The package can be used to backup an *entire* organization or repository, including issues and wikis in the most appropriate format (clones for wikis, json files for issues).
@@ -88,3 +110,46 @@ Authentication
============== ==============
Note: Password-based authentication will fail if you have two-factor authentication enabled. Note: Password-based authentication will fail if you have two-factor authentication enabled.
Using the Keychain on Mac OSX
=============================
Note: On Mac OSX the token can be stored securely in the user's keychain. To do this:
1. Open Keychain from "Applications -> Utilities -> Keychain Access"
2. Add a new password item using "File -> New Password Item"
3. Enter a name in the "Keychain Item Name" box. You must provide this name to github-backup using the --keychain-name argument.
4. Enter an account name in the "Account Name" box, enter your Github username as set above. You must provide this name to github-backup using the --keychain-account argument.
5. Enter your Github personal access token in the "Password" box
Note: When you run github-backup, you will be asked whether you want to allow "security" to use your confidential information stored in your keychain. You have two options:
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

View File

@@ -16,9 +16,23 @@ import select
import subprocess import subprocess
import sys import sys
import time import time
import urlparse import platform
import urllib try:
import urllib2 # python 3
from urllib.parse import urlparse
from urllib.parse import quote as urlquote
from urllib.parse import urlencode
from urllib.error import HTTPError, URLError
from urllib.request import urlopen
from urllib.request import Request
except ImportError:
# python 2
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 github_backup import __version__ from github_backup import __version__
@@ -55,11 +69,15 @@ def logging_subprocess(popenargs,
""" """
child = subprocess.Popen(popenargs, stdout=subprocess.PIPE, child = subprocess.Popen(popenargs, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, **kwargs) 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, log_level = {child.stdout: stdout_log_level,
child.stderr: stderr_log_level} child.stderr: stderr_log_level}
def check_io(): def check_io():
if sys.platform == 'win32':
return
ready_to_read = select.select([child.stdout, child.stderr], ready_to_read = select.select([child.stdout, child.stderr],
[], [],
[], [],
@@ -80,8 +98,8 @@ def logging_subprocess(popenargs,
rc = child.wait() rc = child.wait()
if rc != 0: if rc != 0:
print(u'{} returned {}:'.format(popenargs[0], rc), file=sys.stderr) print('{} returned {}:'.format(popenargs[0], rc), file=sys.stderr)
print('\t', u' '.join(popenargs), file=sys.stderr) print('\t', ' '.join(popenargs), file=sys.stderr)
return rc return rc
@@ -96,8 +114,9 @@ def mkdir_p(*args):
else: else:
raise raise
def mask_password(url, secret='*****'): def mask_password(url, secret='*****'):
parsed = urlparse.urlparse(url) parsed = urlparse(url)
if not parsed.password: if not parsed.password:
return url return url
@@ -106,9 +125,9 @@ def mask_password(url, secret='*****'):
return url.replace(parsed.password, secret) return url.replace(parsed.password, secret)
def parse_args(): def parse_args():
parser = argparse.ArgumentParser(description='Backup a github account', parser = argparse.ArgumentParser(description='Backup a github account')
prog='Github Backup')
parser.add_argument('user', parser.add_argument('user',
metavar='USER', metavar='USER',
type=str, type=str,
@@ -126,7 +145,7 @@ def parse_args():
parser.add_argument('-t', parser.add_argument('-t',
'--token', '--token',
dest='token', dest='token',
help='personal access or OAuth token') help='personal access or OAuth token, or path to token (file://...)') # noqa
parser.add_argument('-o', parser.add_argument('-o',
'--output-directory', '--output-directory',
default='.', default='.',
@@ -140,15 +159,27 @@ def parse_args():
parser.add_argument('--starred', parser.add_argument('--starred',
action='store_true', action='store_true',
dest='include_starred', 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', parser.add_argument('--watched',
action='store_true', action='store_true',
dest='include_watched', 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', parser.add_argument('--all',
action='store_true', action='store_true',
dest='include_everything', dest='include_everything',
help='include everything in backup') help='include everything in backup (not including [*])')
parser.add_argument('--issues', parser.add_argument('--issues',
action='store_true', action='store_true',
dest='include_issues', dest='include_issues',
@@ -173,6 +204,10 @@ def parse_args():
action='store_true', action='store_true',
dest='include_pull_commits', dest='include_pull_commits',
help='include pull request commits in backup') 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', parser.add_argument('--labels',
action='store_true', action='store_true',
dest='include_labels', dest='include_labels',
@@ -180,7 +215,7 @@ def parse_args():
parser.add_argument('--hooks', parser.add_argument('--hooks',
action='store_true', action='store_true',
dest='include_hooks', dest='include_hooks',
help='include hooks in backup (works only when authenticated)') help='include hooks in backup (works only when authenticated)') # noqa
parser.add_argument('--milestones', parser.add_argument('--milestones',
action='store_true', action='store_true',
dest='include_milestones', dest='include_milestones',
@@ -189,10 +224,26 @@ def parse_args():
action='store_true', action='store_true',
dest='include_repository', dest='include_repository',
help='include repository clone in backup') help='include repository clone in backup')
parser.add_argument('--bare',
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', parser.add_argument('--wikis',
action='store_true', action='store_true',
dest='include_wiki', dest='include_wiki',
help='include wiki clone in backup') 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', parser.add_argument('--skip-existing',
action='store_true', action='store_true',
dest='skip_existing', dest='skip_existing',
@@ -222,39 +273,71 @@ def parse_args():
parser.add_argument('-P', '--private', parser.add_argument('-P', '--private',
action='store_true', action='store_true',
dest='private', dest='private',
help='include private repositories') help='include private repositories [*]')
parser.add_argument('-F', '--fork', parser.add_argument('-F', '--fork',
action='store_true', action='store_true',
dest='fork', dest='fork',
help='include forked repositories') help='include forked repositories [*]')
parser.add_argument('--prefer-ssh', parser.add_argument('--prefer-ssh',
action='store_true', action='store_true',
help='Clone repositories using SSH instead of HTTPS') help='Clone repositories using SSH instead of HTTPS')
parser.add_argument('-v', '--version', parser.add_argument('-v', '--version',
action='version', action='version',
version='%(prog)s ' + __version__) version='%(prog)s ' + __version__)
parser.add_argument('--keychain-name',
dest='osx_keychain_item_name',
help='OSX ONLY: name field of password item in OSX keychain that holds the personal access or OAuth token')
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')
return parser.parse_args() return parser.parse_args()
def get_auth(args, encode=True): def get_auth(args, encode=True):
auth = None auth = None
if args.token: if args.osx_keychain_item_name:
if not args.osx_keychain_item_account:
log_error('You must specify both name and account fields for osx keychain password items')
else:
if platform.system() != 'Darwin':
log_error("Keychain arguments are only supported on Mac OSX")
try:
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())
auth = token + ':' + 'x-oauth-basic'
except:
log_error('No password item matching the provided name and account could be found in the osx keychain.')
elif args.osx_keychain_item_account:
log_error('You must specify both name and account fields for osx keychain password items')
elif args.token:
_path_specifier = 'file://'
if args.token.startswith(_path_specifier):
args.token = open(args.token[len(_path_specifier):],
'rt').readline().strip()
auth = args.token + ':' + 'x-oauth-basic' auth = args.token + ':' + 'x-oauth-basic'
elif args.username: elif args.username:
if not args.password: if not args.password:
args.password = getpass.getpass() args.password = getpass.getpass()
auth = args.username + ':' + args.password if encode:
password = args.password
else:
password = urlquote(args.password)
auth = args.username + ':' + password
elif args.password: elif args.password:
log_error('You must specify a username for basic auth') log_error('You must specify a username for basic auth')
if not auth: if not auth:
return None return None
if encode == False: if not encode:
return auth return auth
return base64.b64encode(auth) return base64.b64encode(auth.encode('ascii'))
def get_github_api_host(args): def get_github_api_host(args):
@@ -274,22 +357,27 @@ def get_github_host(args):
return host return host
def get_github_repo_url(args, repository): def get_github_repo_url(args, repository):
if args.prefer_ssh: if args.prefer_ssh:
return repository['ssh_url'] return repository['ssh_url']
if repository.get('is_gist'):
return repository['git_pull_url']
auth = get_auth(args, False) auth = get_auth(args, False)
if auth: if auth:
repo_url = 'https://{0}@{1}/{2}/{3}.git'.format( repo_url = 'https://{0}@{1}/{2}/{3}.git'.format(
auth, auth,
get_github_host(args), get_github_host(args),
args.user, repository['owner']['login'],
repository['name']) repository['name'])
else: else:
repo_url = repository['clone_url'] repo_url = repository['clone_url']
return repo_url return repo_url
def retrieve_data(args, template, query_args=None, single_request=False): def retrieve_data(args, template, query_args=None, single_request=False):
auth = get_auth(args) auth = get_auth(args)
query_args = get_query_args(query_args) query_args = get_query_args(query_args)
@@ -309,7 +397,7 @@ def retrieve_data(args, template, query_args=None, single_request=False):
errors.append(template.format(status_code, r.reason)) errors.append(template.format(status_code, r.reason))
log_error(errors) log_error(errors)
response = json.loads(r.read()) response = json.loads(r.read().decode('utf-8'))
if len(errors) == 0: if len(errors) == 0:
if type(response) == list: if type(response) == list:
data.extend(response) data.extend(response)
@@ -341,11 +429,11 @@ def _get_response(request, auth, template):
while True: while True:
should_continue = False should_continue = False
try: try:
r = urllib2.urlopen(request) r = urlopen(request)
except urllib2.HTTPError as exc: except HTTPError as exc:
errors, should_continue = _request_http_error(exc, auth, errors) # noqa errors, should_continue = _request_http_error(exc, auth, errors) # noqa
r = exc r = exc
except urllib2.URLError: except URLError:
should_continue = _request_url_error(template, retry_timeout) should_continue = _request_url_error(template, retry_timeout)
if not should_continue: if not should_continue:
raise raise
@@ -358,14 +446,15 @@ def _get_response(request, auth, template):
def _construct_request(per_page, page, query_args, template, auth): def _construct_request(per_page, page, query_args, template, auth):
querystring = urllib.urlencode(dict({ querystring = urlencode(dict(list({
'per_page': per_page, 'per_page': per_page,
'page': page 'page': page
}.items() + query_args.items())) }.items()) + list(query_args.items())))
request = urllib2.Request(template + '?' + querystring) request = Request(template + '?' + querystring)
if auth is not None: if auth is not None:
request.add_header('Authorization', 'Basic ' + auth) request.add_header('Authorization', 'Basic '.encode('ascii') + auth)
log_info('Requesting {}?{}'.format(template, querystring))
return request return request
@@ -392,10 +481,9 @@ def _request_http_error(exc, auth, errors):
print('Exceeded rate limit of {} requests; waiting {} seconds to reset'.format(limit, delta), # noqa print('Exceeded rate limit of {} requests; waiting {} seconds to reset'.format(limit, delta), # noqa
file=sys.stderr) file=sys.stderr)
ratelimit_error = 'No more requests remaining'
if auth is None: if auth is None:
ratelimit_error += '; authenticate to raise your GitHub rate limit' # noqa print('Hint: Authenticate to raise your GitHub rate limit',
errors.append(ratelimit_error) file=sys.stderr)
time.sleep(delta) time.sleep(delta)
should_continue = True should_continue = True
@@ -415,11 +503,33 @@ def _request_url_error(template, retry_timeout):
return False return False
def retrieve_repositories(args): 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.')
sys.exit(1)
def retrieve_repositories(args, authenticated_user):
log_info('Retrieving repositories') log_info('Retrieving repositories')
single_request = False 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( template = 'https://{0}/user/repos'.format(
get_github_api_host(args)) get_github_api_host(args))
else:
if args.private:
log_error('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: if args.organization:
template = 'https://{0}/orgs/{1}/repos'.format( template = 'https://{0}/orgs/{1}/repos'.format(
get_github_api_host(args), get_github_api_host(args),
@@ -432,13 +542,44 @@ def retrieve_repositories(args):
args.user, args.user,
args.repository) 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, repositories): def filter_repositories(args, unfiltered_repositories):
log_info('Filtering repositories') log_info('Filtering repositories')
repositories = [r for r in repositories if r['owner']['login'] == args.user] repositories = []
for r in unfiltered_repositories:
# 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 name_regex = None
if args.name_regex: if args.name_regex:
@@ -449,11 +590,11 @@ def filter_repositories(args, repositories):
languages = [x.lower() for x in args.languages] languages = [x.lower() for x in args.languages]
if not args.fork: 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: 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: 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: if name_regex:
repositories = [r for r in repositories if name_regex.match(r['name'])] repositories = [r for r in repositories if name_regex.match(r['name'])]
@@ -465,7 +606,7 @@ def backup_repositories(args, output_directory, repositories):
repos_template = 'https://{0}/repos'.format(get_github_api_host(args)) repos_template = 'https://{0}/repos'.format(get_github_api_host(args))
if args.incremental: if args.incremental:
last_update = max(repository['updated_at'] for repository in repositories) last_update = max(list(repository['updated_at'] for repository in repositories) or [time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())]) # noqa
last_update_path = os.path.join(output_directory, 'last_update') last_update_path = os.path.join(output_directory, 'last_update')
if os.path.exists(last_update_path): if os.path.exists(last_update_path):
args.since = open(last_update_path).read().strip() args.since = open(last_update_path).read().strip()
@@ -475,23 +616,45 @@ def backup_repositories(args, output_directory, repositories):
args.since = None args.since = None
for repository in repositories: for repository in repositories:
backup_cwd = os.path.join(output_directory, 'repositories') if repository.get('is_gist'):
repo_cwd = os.path.join(backup_cwd, repository['name']) 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_dir = os.path.join(repo_cwd, 'repository')
repo_url = get_github_repo_url(args, repository) repo_url = get_github_repo_url(args, repository)
if args.include_repository or args.include_everything: include_gists = (args.include_gists or args.include_starred_gists)
fetch_repository(repository['name'], 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_url,
repo_dir, repo_dir,
skip_existing=args.skip_existing) skip_existing=args.skip_existing,
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) download_wiki = (args.include_wiki or args.include_everything)
if repository['has_wiki'] and download_wiki: if repository['has_wiki'] and download_wiki:
fetch_repository(repository['name'], fetch_repository(repository['name'],
repo_url.replace('.git', '.wiki.git'), repo_url.replace('.git', '.wiki.git'),
os.path.join(repo_cwd, 'wiki'), os.path.join(repo_cwd, 'wiki'),
skip_existing=args.skip_existing) skip_existing=args.skip_existing,
bare_clone=args.bare_clone,
lfs_clone=args.lfs_clone)
if args.include_issues or args.include_everything: if args.include_issues or args.include_everything:
backup_issues(args, repo_cwd, repository, repos_template) backup_issues(args, repo_cwd, repository, repos_template)
@@ -511,6 +674,7 @@ def backup_repositories(args, output_directory, repositories):
if args.incremental: if args.incremental:
open(last_update_path, 'w').write(last_update) open(last_update_path, 'w').write(last_update)
def backup_issues(args, repo_cwd, repository, repos_template): def backup_issues(args, repo_cwd, repository, repos_template):
has_issues_dir = os.path.isdir('{0}/issues/.git'.format(repo_cwd)) has_issues_dir = os.path.isdir('{0}/issues/.git'.format(repo_cwd))
if args.skip_existing and has_issues_dir: if args.skip_existing and has_issues_dir:
@@ -526,6 +690,7 @@ def backup_issues(args, repo_cwd, repository, repos_template):
_issue_template = '{0}/{1}/issues'.format(repos_template, _issue_template = '{0}/{1}/issues'.format(repos_template,
repository['full_name']) repository['full_name'])
should_include_pulls = args.include_pulls or args.include_everything
issue_states = ['open', 'closed'] issue_states = ['open', 'closed']
for issue_state in issue_states: for issue_state in issue_states:
query_args = { query_args = {
@@ -541,18 +706,21 @@ def backup_issues(args, repo_cwd, repository, repos_template):
for issue in _issues: for issue in _issues:
# skip pull requests which are also returned as issues # skip pull requests which are also returned as issues
# if retrieving pull requests is requested as well # if retrieving pull requests is requested as well
if 'pull_request' in issue and (args.include_pulls or args.include_everything): if 'pull_request' in issue and should_include_pulls:
issues_skipped += 1 issues_skipped += 1
continue continue
issues[issue['number']] = issue issues[issue['number']] = issue
if issues_skipped: if issues_skipped:
issues_skipped_message = ' (skipped {0} pull requests)'.format(issues_skipped) issues_skipped_message = ' (skipped {0} pull requests)'.format(
log_info('Saving {0} issues to disk{1}'.format(len(issues.keys()), issues_skipped_message)) issues_skipped)
log_info('Saving {0} issues to disk{1}'.format(
len(list(issues.keys())), issues_skipped_message))
comments_template = _issue_template + '/{0}/comments' comments_template = _issue_template + '/{0}/comments'
events_template = _issue_template + '/{0}/events' events_template = _issue_template + '/{0}/events'
for number, issue in issues.iteritems(): for number, issue in list(issues.items()):
if args.include_issue_comments or args.include_everything: if args.include_issue_comments or args.include_everything:
template = comments_template.format(number) template = comments_template.format(number)
issues[number]['comment_data'] = retrieve_data(args, template) issues[number]['comment_data'] = retrieve_data(args, template)
@@ -577,16 +745,17 @@ def backup_pulls(args, repo_cwd, repository, repos_template):
pulls = {} pulls = {}
_pulls_template = '{0}/{1}/pulls'.format(repos_template, _pulls_template = '{0}/{1}/pulls'.format(repos_template,
repository['full_name']) repository['full_name'])
pull_states = ['open', 'closed']
for pull_state in pull_states:
query_args = { query_args = {
'filter': 'all', 'filter': 'all',
'state': pull_state, 'state': 'all',
'sort': 'updated', 'sort': 'updated',
'direction': 'desc', 'direction': 'desc',
} }
if not args.include_pull_details:
pull_states = ['open', 'closed']
for pull_state in pull_states:
query_args['state'] = pull_state
# It'd be nice to be able to apply the args.since filter here... # It'd be nice to be able to apply the args.since filter here...
_pulls = retrieve_data(args, _pulls = retrieve_data(args,
_pulls_template, _pulls_template,
@@ -594,11 +763,23 @@ def backup_pulls(args, repo_cwd, repository, repos_template):
for pull in _pulls: for pull in _pulls:
if not args.since or pull['updated_at'] >= args.since: if not args.since or pull['updated_at'] >= args.since:
pulls[pull['number']] = pull pulls[pull['number']] = pull
else:
_pulls = retrieve_data(args,
_pulls_template,
query_args=query_args)
for pull in _pulls:
if not args.since or pull['updated_at'] >= args.since:
pulls[pull['number']] = retrieve_data(
args,
_pulls_template + '/{}'.format(pull['number']),
single_request=True
)
log_info('Saving {0} pull requests to disk'.format(len(pulls.keys()))) log_info('Saving {0} pull requests to disk'.format(
len(list(pulls.keys()))))
comments_template = _pulls_template + '/{0}/comments' comments_template = _pulls_template + '/{0}/comments'
commits_template = _pulls_template + '/{0}/commits' commits_template = _pulls_template + '/{0}/commits'
for number, pull in pulls.iteritems(): for number, pull in list(pulls.items()):
if args.include_pull_comments or args.include_everything: if args.include_pull_comments or args.include_everything:
template = comments_template.format(number) template = comments_template.format(number)
pulls[number]['comment_data'] = retrieve_data(args, template) pulls[number]['comment_data'] = retrieve_data(args, template)
@@ -632,8 +813,9 @@ def backup_milestones(args, repo_cwd, repository, repos_template):
for milestone in _milestones: for milestone in _milestones:
milestones[milestone['number']] = milestone milestones[milestone['number']] = milestone
log_info('Saving {0} milestones to disk'.format(len(milestones.keys()))) log_info('Saving {0} milestones to disk'.format(
for number, milestone in milestones.iteritems(): len(list(milestones.keys()))))
for number, milestone in list(milestones.items()):
milestone_file = '{0}/{1}.json'.format(milestone_cwd, number) milestone_file = '{0}/{1}.json'.format(milestone_cwd, number)
with codecs.open(milestone_file, 'w', encoding='utf-8') as f: with codecs.open(milestone_file, 'w', encoding='utf-8') as f:
json_dump(milestone, f) json_dump(milestone, f)
@@ -670,7 +852,21 @@ def backup_hooks(args, repo_cwd, repository, repos_template):
log_info("Unable to read hooks, skipping") log_info("Unable to read hooks, skipping")
def fetch_repository(name, remote_url, local_dir, skip_existing=False): def fetch_repository(name,
remote_url,
local_dir,
skip_existing=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) == b"true\n"
else:
clone_exists = False
else:
clone_exists = os.path.exists(os.path.join(local_dir, '.git')) clone_exists = os.path.exists(os.path.join(local_dir, '.git'))
if clone_exists and skip_existing: if clone_exists and skip_existing:
@@ -678,26 +874,50 @@ def fetch_repository(name, remote_url, local_dir, skip_existing=False):
masked_remote_url = mask_password(remote_url) masked_remote_url = mask_password(remote_url)
initalized = subprocess.call('git ls-remote ' + remote_url, initialized = subprocess.call('git ls-remote ' + remote_url,
stdout=FNULL, stdout=FNULL,
stderr=FNULL, stderr=FNULL,
shell=True) shell=True)
if initalized == 128: if initialized == 128:
log_info("Skipping {0} ({1}) since it's not initalized".format(name, masked_remote_url)) log_info("Skipping {0} ({1}) since it's not initialized".format(
name, masked_remote_url))
return return
if clone_exists: if clone_exists:
log_info('Updating {0} in {1}'.format(name, local_dir)) log_info('Updating {0} in {1}'.format(name, local_dir))
remotes = subprocess.check_output(['git', 'remote', 'show'],
cwd=local_dir)
remotes = [i.strip() for i in remotes.decode('utf-8').splitlines()]
if 'origin' not in remotes:
git_command = ['git', 'remote', 'rm', 'origin'] git_command = ['git', 'remote', 'rm', 'origin']
logging_subprocess(git_command, None, cwd=local_dir) logging_subprocess(git_command, None, cwd=local_dir)
git_command = ['git', 'remote', 'add', 'origin', remote_url] git_command = ['git', 'remote', 'add', 'origin', remote_url]
logging_subprocess(git_command, None, cwd=local_dir) logging_subprocess(git_command, None, cwd=local_dir)
git_command = ['git', 'fetch', '--all', '--tags', '--prune'] else:
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) logging_subprocess(git_command, None, cwd=local_dir)
else: else:
log_info('Cloning {0} repository from {1} to {2}'.format(name, log_info('Cloning {0} repository from {1} to {2}'.format(
name,
masked_remote_url, masked_remote_url,
local_dir)) 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] git_command = ['git', 'clone', remote_url, local_dir]
logging_subprocess(git_command, None) logging_subprocess(git_command, None)
@@ -706,21 +926,37 @@ def backup_account(args, output_directory):
account_cwd = os.path.join(output_directory, 'account') account_cwd = os.path.join(output_directory, 'account')
if args.include_starred or args.include_everything: if args.include_starred or args.include_everything:
output_file = '{0}/starred.json'.format(account_cwd) output_file = "{0}/starred.json".format(account_cwd)
template = "https://{0}/users/{1}/starred" template = "https://{0}/users/{1}/starred".format(get_github_api_host(args), args.user)
template = template.format(get_github_api_host(args), args.user)
_backup_data(args, _backup_data(args,
'starred repositories', "starred repositories",
template, template,
output_file, output_file,
account_cwd) account_cwd)
if args.include_watched or args.include_everything: if args.include_watched or args.include_everything:
output_file = '{0}/watched.json'.format(account_cwd) output_file = "{0}/watched.json".format(account_cwd)
template = "https://{0}/users/{1}/subscriptions" template = "https://{0}/users/{1}/subscriptions".format(get_github_api_host(args), args.user)
template = template.format(get_github_api_host(args), args.user)
_backup_data(args, _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, template,
output_file, output_file,
account_cwd) account_cwd)
@@ -755,9 +991,13 @@ def main():
log_info('Create output directory {0}'.format(output_directory)) log_info('Create output directory {0}'.format(output_directory))
mkdir_p(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)) 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) repositories = filter_repositories(args, repositories)
backup_repositories(args, output_directory, repositories) backup_repositories(args, output_directory, repositories)
backup_account(args, output_directory) backup_account(args, output_directory)

View File

@@ -1 +1 @@
__version__ = '0.10.2' __version__ = '0.21.1'

11
release
View File

@@ -1,8 +1,13 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -eo pipefail; [[ $RELEASE_TRACE ]] && set -x set -eo pipefail; [[ $RELEASE_TRACE ]] && set -x
PACKAGE_NAME='github-backup' if [[ ! -f setup.py ]]; then
INIT_PACKAGE_NAME='github_backup' 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" PUBLIC="true"
# Colors # Colors
@@ -34,7 +39,7 @@ fi
echo -e "\n${GREEN}STARTING RELEASE PROCESS${COLOR_OFF}\n" echo -e "\n${GREEN}STARTING RELEASE PROCESS${COLOR_OFF}\n"
set +e; set +e;
git status | grep "working directory clean" &> /dev/null git status | grep -Eo "working (directory|tree) clean" &> /dev/null
if [ ! $? -eq 0 ]; then # working directory is NOT clean if [ ! $? -eq 0 ]; then # working directory is NOT clean
echo -e "${RED}WARNING: You have uncomitted changes, you may have forgotten something${COLOR_OFF}\n" echo -e "${RED}WARNING: You have uncomitted changes, you may have forgotten something${COLOR_OFF}\n"
exit 1 exit 1

View File

@@ -37,8 +37,9 @@ setup(
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'Topic :: System :: Archiving :: Backup', 'Topic :: System :: Archiving :: Backup',
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
], ],
description='backup a github user or organization', description='backup a github user or organization',
long_description=open_file('README.rst').read(), long_description=open_file('README.rst').read(),