github-backup 0.62.0__tar.gz → 0.63.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {github_backup-0.62.0 → github_backup-0.63.0}/CHANGES.rst +103 -1
- {github_backup-0.62.0/github_backup.egg-info → github_backup-0.63.0}/PKG-INFO +16 -11
- {github_backup-0.62.0 → github_backup-0.63.0}/README +15 -10
- {github_backup-0.62.0 → github_backup-0.63.0}/README.rst +15 -10
- github_backup-0.63.0/github_backup/__init__.py +1 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/github_backup/github_backup.py +90 -70
- {github_backup-0.62.0 → github_backup-0.63.0/github_backup.egg-info}/PKG-INFO +16 -11
- {github_backup-0.62.0 → github_backup-0.63.0}/github_backup.egg-info/SOURCES.txt +1 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/release-requirements.txt +2 -2
- github_backup-0.63.0/tests/test_disabled_issues_pulls.py +105 -0
- github_backup-0.62.0/github_backup/__init__.py +0 -1
- {github_backup-0.62.0 → github_backup-0.63.0}/LICENSE.txt +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/MANIFEST.in +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/github_backup/__main__.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/github_backup/cli.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/github_backup/graphql_queries.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/github_backup.egg-info/dependency_links.txt +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/github_backup.egg-info/entry_points.txt +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/github_backup.egg-info/top_level.txt +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/github_backup.egg-info/zip-safe +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/requirements.txt +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/setup.cfg +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/setup.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_all_starred.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_attachments.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_auth.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_case_sensitivity.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_discussions.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_http_451.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_incremental_per_repository.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_json_dump_if_changed.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_pagination.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_pull_incremental_pagination.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_pull_reviews.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_releases.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_retrieve_data.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_skip_assets_on.py +0 -0
- {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_starred_skip_size_over.py +0 -0
|
@@ -1,9 +1,111 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
-
0.
|
|
4
|
+
0.63.0 (2026-06-17)
|
|
5
5
|
-------------------
|
|
6
6
|
------------------------
|
|
7
|
+
- Skip repositories with pull requests disabled (HTTP 404) instead of
|
|
8
|
+
crashing (#511) [Rodos]
|
|
9
|
+
|
|
10
|
+
Also harden the advisories/hooks 404 checks to match HTTPError.code instead of a substring.
|
|
11
|
+
- Chore(deps): bump pytest in the python-packages group.
|
|
12
|
+
[dependabot[bot]]
|
|
13
|
+
|
|
14
|
+
Bumps the python-packages group with 1 update: [pytest](https://github.com/pytest-dev/pytest).
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
Updates `pytest` from 9.0.3 to 9.1.0
|
|
18
|
+
- [Release notes](https://github.com/pytest-dev/pytest/releases)
|
|
19
|
+
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
|
|
20
|
+
- [Commits](https://github.com/pytest-dev/pytest/compare/9.0.3...9.1.0)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
updated-dependencies:
|
|
24
|
+
- dependency-name: pytest
|
|
25
|
+
dependency-version: 9.1.0
|
|
26
|
+
dependency-type: direct:production
|
|
27
|
+
update-type: version-update:semver-minor
|
|
28
|
+
dependency-group: python-packages
|
|
29
|
+
...
|
|
30
|
+
- Chore(deps): bump black in the python-packages group.
|
|
31
|
+
[dependabot[bot]]
|
|
32
|
+
|
|
33
|
+
Bumps the python-packages group with 1 update: [black](https://github.com/psf/black).
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
Updates `black` from 26.3.1 to 26.5.1
|
|
37
|
+
- [Release notes](https://github.com/psf/black/releases)
|
|
38
|
+
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
|
|
39
|
+
- [Commits](https://github.com/psf/black/compare/26.3.1...26.5.1)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
updated-dependencies:
|
|
43
|
+
- dependency-name: black
|
|
44
|
+
dependency-version: 26.5.1
|
|
45
|
+
dependency-type: direct:production
|
|
46
|
+
update-type: version-update:semver-minor
|
|
47
|
+
dependency-group: python-packages
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
0.62.1 (2026-04-30)
|
|
52
|
+
-------------------
|
|
53
|
+
- Document that nothing is saved by default. [Changaco]
|
|
54
|
+
- Eliminate trailing spaces. [Changaco]
|
|
55
|
+
- Remove pointless and unsafe `export`s in examples. [Changaco]
|
|
56
|
+
- Try to clarify what `--incremental` actually does. [Changaco]
|
|
57
|
+
- Fix a typo in the README. [Changaco]
|
|
58
|
+
- Document that `--all` doesn't imply `--attachments` [Changaco]
|
|
59
|
+
- Rename a function to match what it actually does. [Changaco]
|
|
60
|
+
- Don't leave files open. [Changaco]
|
|
61
|
+
- Remove legacy code in `mkdir_p` function. [Changaco]
|
|
62
|
+
- Don't pass stdin when doing so can't do any good. [Changaco]
|
|
63
|
+
|
|
64
|
+
When the child process doesn't inherit stderr, it can't ask the user for input, so it shouldn't inherit stdin either.
|
|
65
|
+
- Use `subprocess.DEVNULL` instead of emulating it. [Changaco]
|
|
66
|
+
- Remove bad invocation of the system shell. [Changaco]
|
|
67
|
+
- Add missing `context` argument to `urlopen` call. [Changaco]
|
|
68
|
+
- Suppress output of call to `git lfs version` [Changaco]
|
|
69
|
+
- Handle more network errors. [Changaco]
|
|
70
|
+
|
|
71
|
+
```python-traceback
|
|
72
|
+
Traceback (most recent call last):
|
|
73
|
+
File ".local/bin/github-backup", line 6, in <module>
|
|
74
|
+
sys.exit(main())
|
|
75
|
+
~~~~^^
|
|
76
|
+
File ".local/share/pipx/venvs/github-backup/lib/python3.14/site-packages/github_backup/cli.py", line 83, in main
|
|
77
|
+
backup_repositories(args, output_directory, repositories)
|
|
78
|
+
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
79
|
+
File ".local/share/pipx/venvs/github-backup/lib/python3.14/site-packages/github_backup/github_backup.py", line 1845, in backup_repositories
|
|
80
|
+
backup_pulls(args, repo_cwd, repository, repos_template)
|
|
81
|
+
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
82
|
+
File ".local/share/pipx/venvs/github-backup/lib/python3.14/site-packages/github_backup/github_backup.py", line 2019, in backup_pulls
|
|
83
|
+
pulls[number]["commit_data"] = retrieve_data(args, template)
|
|
84
|
+
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
|
|
85
|
+
File ".local/share/pipx/venvs/github-backup/lib/python3.14/site-packages/github_backup/github_backup.py", line 766, in retrieve_data
|
|
86
|
+
return list(fetch_all())
|
|
87
|
+
File ".local/share/pipx/venvs/github-backup/lib/python3.14/site-packages/github_backup/github_backup.py", line 717, in fetch_all
|
|
88
|
+
response = json.loads(http_response.read().decode("utf-8"))
|
|
89
|
+
~~~~~~~~~~~~~~~~~~^^
|
|
90
|
+
File "/usr/lib/python3.14/http/client.py", line 500, in read
|
|
91
|
+
s = self._safe_read(self.length)
|
|
92
|
+
File "/usr/lib/python3.14/http/client.py", line 648, in _safe_read
|
|
93
|
+
data = self.fp.read(cursize)
|
|
94
|
+
File "/usr/lib/python3.14/socket.py", line 725, in readinto
|
|
95
|
+
return self._sock.recv_into(b)
|
|
96
|
+
~~~~~~~~~~~~~~~~~~~~^^^
|
|
97
|
+
File "/usr/lib/python3.14/ssl.py", line 1304, in recv_into
|
|
98
|
+
return self.read(nbytes, buffer)
|
|
99
|
+
~~~~~~~~~^^^^^^^^^^^^^^^^
|
|
100
|
+
File "/usr/lib/python3.14/ssl.py", line 1138, in read
|
|
101
|
+
return self._sslobj.read(len, buffer)
|
|
102
|
+
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
|
|
103
|
+
ConnectionResetError: [Errno 104] Connection reset by peer
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
0.62.0 (2026-04-29)
|
|
108
|
+
-------------------
|
|
7
109
|
- Skip checkpoint-equal incremental items. [Duncan Ogilvie]
|
|
8
110
|
- Avoid redundant release asset list requests. [Duncan Ogilvie]
|
|
9
111
|
- Reduce unnecessary pull requests with incremental fetching. [Duncan
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-backup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.63.0
|
|
4
4
|
Summary: backup a github user or organization
|
|
5
5
|
Home-page: http://github.com/josegonzalez/python-github-backup
|
|
6
6
|
Author: Jose Diaz-Gonzalez
|
|
@@ -52,7 +52,7 @@ Using PIP via PyPI::
|
|
|
52
52
|
Using PIP via Github (more likely the latest version)::
|
|
53
53
|
|
|
54
54
|
pip install git+https://github.com/josegonzalez/python-github-backup.git#egg=github-backup
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
*Install note for python newcomers:*
|
|
57
57
|
|
|
58
58
|
Python scripts are unlikely to be included in your ``$PATH`` by default, this means it cannot be run directly in terminal with ``$ github-backup ...``, you can either add python's install path to your environments ``$PATH`` or call the script directly e.g. using ``$ ~/.local/bin/github-backup``.*
|
|
@@ -279,7 +279,7 @@ Note: When you run github-backup, you will be asked whether you want to allow "
|
|
|
279
279
|
Github Rate-limit and Throttling
|
|
280
280
|
--------------------------------
|
|
281
281
|
|
|
282
|
-
"github-backup" will automatically throttle itself based on feedback from the Github API.
|
|
282
|
+
"github-backup" will automatically throttle itself based on feedback from the Github API.
|
|
283
283
|
|
|
284
284
|
Their API is usually rate-limited to 5000 calls per hour. The API will ask github-backup to pause until a specific time when the limit is reset again (at the start of the next hour). This continues until the backup is complete.
|
|
285
285
|
|
|
@@ -355,7 +355,12 @@ Gotchas / Known-issues
|
|
|
355
355
|
All is not everything
|
|
356
356
|
---------------------
|
|
357
357
|
|
|
358
|
-
The ``--all`` argument does not include: cloning private repos (``-P, --private``), cloning forks (``-F, --fork``), cloning starred repositories (``--all-starred``), ``--pull-details``, cloning LFS repositories (``--lfs``), cloning gists (``--gists``) or cloning starred gist repos (``--starred-gists``). See examples for more.
|
|
358
|
+
The ``--all`` argument does not include: downloading attachments from issue and pull request comments (``--attachments``), cloning private repos (``-P, --private``), cloning forks (``-F, --fork``), cloning starred repositories (``--all-starred``), ``--pull-details``, cloning LFS repositories (``--lfs``), cloning gists (``--gists``) or cloning starred gist repos (``--starred-gists``). See examples for more.
|
|
359
|
+
|
|
360
|
+
Saves nothing if no arguments are passed
|
|
361
|
+
----------------------------------------
|
|
362
|
+
|
|
363
|
+
At least one argument like ``--all`` or ``--repositories`` is needed for github-backup to actually save data. Without relevant arguments, github-backup fetches some data from GitHub but doesn't put any of it into files.
|
|
359
364
|
|
|
360
365
|
Starred repository size
|
|
361
366
|
-----------------------
|
|
@@ -393,9 +398,9 @@ This means any blocking errors on previous runs can cause missing data in backup
|
|
|
393
398
|
|
|
394
399
|
Using (``--incremental-by-files``) will request new data from the API **based on when the file was modified on filesystem**. e.g. if you modify the file yourself you may miss something.
|
|
395
400
|
|
|
396
|
-
Still
|
|
401
|
+
Still safer than the previous version.
|
|
397
402
|
|
|
398
|
-
|
|
403
|
+
Incremental backup only changes how issue and pull request data is fetched.
|
|
399
404
|
|
|
400
405
|
Known blocking errors
|
|
401
406
|
---------------------
|
|
@@ -459,12 +464,12 @@ Github Backup Examples
|
|
|
459
464
|
|
|
460
465
|
Backup all repositories, including private ones using a classic token::
|
|
461
466
|
|
|
462
|
-
|
|
467
|
+
ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
463
468
|
github-backup WhiteHouse --token $ACCESS_TOKEN --organization --output-directory /tmp/white-house --repositories --private
|
|
464
469
|
|
|
465
470
|
Use a fine-grained access token to backup a single organization repository with everything else (wiki, pull requests, comments, issues etc)::
|
|
466
471
|
|
|
467
|
-
|
|
472
|
+
FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
468
473
|
ORGANIZATION=docker
|
|
469
474
|
REPO=cli
|
|
470
475
|
# e.g. git@github.com:docker/cli.git
|
|
@@ -472,14 +477,14 @@ Use a fine-grained access token to backup a single organization repository with
|
|
|
472
477
|
|
|
473
478
|
Quietly and incrementally backup useful Github user data (public and private repos with SSH) including; all issues, pulls, all public starred repos and gists (omitting "hooks", "releases" and therefore "assets" to prevent blocking). *Great for a cron job.* ::
|
|
474
479
|
|
|
475
|
-
|
|
480
|
+
FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
476
481
|
GH_USER=YOUR-GITHUB-USER
|
|
477
482
|
|
|
478
483
|
github-backup -f $FINE_ACCESS_TOKEN --prefer-ssh -o ~/github-backup/ -l error -P -i --all-starred --starred --watched --followers --following --issues --issue-comments --issue-events --pulls --pull-comments --pull-reviews --pull-commits --labels --milestones --security-advisories --discussions --repositories --wikis --releases --assets --attachments --pull-details --gists --starred-gists $GH_USER
|
|
479
|
-
|
|
484
|
+
|
|
480
485
|
Debug an error/block or incomplete backup into a temporary directory. Omit "incremental" to fill a previous incomplete backup. ::
|
|
481
486
|
|
|
482
|
-
|
|
487
|
+
FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
483
488
|
GH_USER=YOUR-GITHUB-USER
|
|
484
489
|
|
|
485
490
|
github-backup -f $FINE_ACCESS_TOKEN -o /tmp/github-backup/ -l debug -P --all-starred --starred --watched --followers --following --issues --issue-comments --issue-events --pulls --pull-comments --pull-reviews --pull-commits --labels --milestones --discussions --repositories --wikis --releases --assets --pull-details --gists --starred-gists $GH_USER
|
|
@@ -22,7 +22,7 @@ Using PIP via PyPI::
|
|
|
22
22
|
Using PIP via Github (more likely the latest version)::
|
|
23
23
|
|
|
24
24
|
pip install git+https://github.com/josegonzalez/python-github-backup.git#egg=github-backup
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
*Install note for python newcomers:*
|
|
27
27
|
|
|
28
28
|
Python scripts are unlikely to be included in your ``$PATH`` by default, this means it cannot be run directly in terminal with ``$ github-backup ...``, you can either add python's install path to your environments ``$PATH`` or call the script directly e.g. using ``$ ~/.local/bin/github-backup``.*
|
|
@@ -249,7 +249,7 @@ Note: When you run github-backup, you will be asked whether you want to allow "
|
|
|
249
249
|
Github Rate-limit and Throttling
|
|
250
250
|
--------------------------------
|
|
251
251
|
|
|
252
|
-
"github-backup" will automatically throttle itself based on feedback from the Github API.
|
|
252
|
+
"github-backup" will automatically throttle itself based on feedback from the Github API.
|
|
253
253
|
|
|
254
254
|
Their API is usually rate-limited to 5000 calls per hour. The API will ask github-backup to pause until a specific time when the limit is reset again (at the start of the next hour). This continues until the backup is complete.
|
|
255
255
|
|
|
@@ -325,7 +325,12 @@ Gotchas / Known-issues
|
|
|
325
325
|
All is not everything
|
|
326
326
|
---------------------
|
|
327
327
|
|
|
328
|
-
The ``--all`` argument does not include: cloning private repos (``-P, --private``), cloning forks (``-F, --fork``), cloning starred repositories (``--all-starred``), ``--pull-details``, cloning LFS repositories (``--lfs``), cloning gists (``--gists``) or cloning starred gist repos (``--starred-gists``). See examples for more.
|
|
328
|
+
The ``--all`` argument does not include: downloading attachments from issue and pull request comments (``--attachments``), cloning private repos (``-P, --private``), cloning forks (``-F, --fork``), cloning starred repositories (``--all-starred``), ``--pull-details``, cloning LFS repositories (``--lfs``), cloning gists (``--gists``) or cloning starred gist repos (``--starred-gists``). See examples for more.
|
|
329
|
+
|
|
330
|
+
Saves nothing if no arguments are passed
|
|
331
|
+
----------------------------------------
|
|
332
|
+
|
|
333
|
+
At least one argument like ``--all`` or ``--repositories`` is needed for github-backup to actually save data. Without relevant arguments, github-backup fetches some data from GitHub but doesn't put any of it into files.
|
|
329
334
|
|
|
330
335
|
Starred repository size
|
|
331
336
|
-----------------------
|
|
@@ -363,9 +368,9 @@ This means any blocking errors on previous runs can cause missing data in backup
|
|
|
363
368
|
|
|
364
369
|
Using (``--incremental-by-files``) will request new data from the API **based on when the file was modified on filesystem**. e.g. if you modify the file yourself you may miss something.
|
|
365
370
|
|
|
366
|
-
Still
|
|
371
|
+
Still safer than the previous version.
|
|
367
372
|
|
|
368
|
-
|
|
373
|
+
Incremental backup only changes how issue and pull request data is fetched.
|
|
369
374
|
|
|
370
375
|
Known blocking errors
|
|
371
376
|
---------------------
|
|
@@ -429,12 +434,12 @@ Github Backup Examples
|
|
|
429
434
|
|
|
430
435
|
Backup all repositories, including private ones using a classic token::
|
|
431
436
|
|
|
432
|
-
|
|
437
|
+
ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
433
438
|
github-backup WhiteHouse --token $ACCESS_TOKEN --organization --output-directory /tmp/white-house --repositories --private
|
|
434
439
|
|
|
435
440
|
Use a fine-grained access token to backup a single organization repository with everything else (wiki, pull requests, comments, issues etc)::
|
|
436
441
|
|
|
437
|
-
|
|
442
|
+
FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
438
443
|
ORGANIZATION=docker
|
|
439
444
|
REPO=cli
|
|
440
445
|
# e.g. git@github.com:docker/cli.git
|
|
@@ -442,14 +447,14 @@ Use a fine-grained access token to backup a single organization repository with
|
|
|
442
447
|
|
|
443
448
|
Quietly and incrementally backup useful Github user data (public and private repos with SSH) including; all issues, pulls, all public starred repos and gists (omitting "hooks", "releases" and therefore "assets" to prevent blocking). *Great for a cron job.* ::
|
|
444
449
|
|
|
445
|
-
|
|
450
|
+
FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
446
451
|
GH_USER=YOUR-GITHUB-USER
|
|
447
452
|
|
|
448
453
|
github-backup -f $FINE_ACCESS_TOKEN --prefer-ssh -o ~/github-backup/ -l error -P -i --all-starred --starred --watched --followers --following --issues --issue-comments --issue-events --pulls --pull-comments --pull-reviews --pull-commits --labels --milestones --security-advisories --discussions --repositories --wikis --releases --assets --attachments --pull-details --gists --starred-gists $GH_USER
|
|
449
|
-
|
|
454
|
+
|
|
450
455
|
Debug an error/block or incomplete backup into a temporary directory. Omit "incremental" to fill a previous incomplete backup. ::
|
|
451
456
|
|
|
452
|
-
|
|
457
|
+
FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
453
458
|
GH_USER=YOUR-GITHUB-USER
|
|
454
459
|
|
|
455
460
|
github-backup -f $FINE_ACCESS_TOKEN -o /tmp/github-backup/ -l debug -P --all-starred --starred --watched --followers --following --issues --issue-comments --issue-events --pulls --pull-comments --pull-reviews --pull-commits --labels --milestones --discussions --repositories --wikis --releases --assets --pull-details --gists --starred-gists $GH_USER
|
|
@@ -22,7 +22,7 @@ Using PIP via PyPI::
|
|
|
22
22
|
Using PIP via Github (more likely the latest version)::
|
|
23
23
|
|
|
24
24
|
pip install git+https://github.com/josegonzalez/python-github-backup.git#egg=github-backup
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
*Install note for python newcomers:*
|
|
27
27
|
|
|
28
28
|
Python scripts are unlikely to be included in your ``$PATH`` by default, this means it cannot be run directly in terminal with ``$ github-backup ...``, you can either add python's install path to your environments ``$PATH`` or call the script directly e.g. using ``$ ~/.local/bin/github-backup``.*
|
|
@@ -249,7 +249,7 @@ Note: When you run github-backup, you will be asked whether you want to allow "
|
|
|
249
249
|
Github Rate-limit and Throttling
|
|
250
250
|
--------------------------------
|
|
251
251
|
|
|
252
|
-
"github-backup" will automatically throttle itself based on feedback from the Github API.
|
|
252
|
+
"github-backup" will automatically throttle itself based on feedback from the Github API.
|
|
253
253
|
|
|
254
254
|
Their API is usually rate-limited to 5000 calls per hour. The API will ask github-backup to pause until a specific time when the limit is reset again (at the start of the next hour). This continues until the backup is complete.
|
|
255
255
|
|
|
@@ -325,7 +325,12 @@ Gotchas / Known-issues
|
|
|
325
325
|
All is not everything
|
|
326
326
|
---------------------
|
|
327
327
|
|
|
328
|
-
The ``--all`` argument does not include: cloning private repos (``-P, --private``), cloning forks (``-F, --fork``), cloning starred repositories (``--all-starred``), ``--pull-details``, cloning LFS repositories (``--lfs``), cloning gists (``--gists``) or cloning starred gist repos (``--starred-gists``). See examples for more.
|
|
328
|
+
The ``--all`` argument does not include: downloading attachments from issue and pull request comments (``--attachments``), cloning private repos (``-P, --private``), cloning forks (``-F, --fork``), cloning starred repositories (``--all-starred``), ``--pull-details``, cloning LFS repositories (``--lfs``), cloning gists (``--gists``) or cloning starred gist repos (``--starred-gists``). See examples for more.
|
|
329
|
+
|
|
330
|
+
Saves nothing if no arguments are passed
|
|
331
|
+
----------------------------------------
|
|
332
|
+
|
|
333
|
+
At least one argument like ``--all`` or ``--repositories`` is needed for github-backup to actually save data. Without relevant arguments, github-backup fetches some data from GitHub but doesn't put any of it into files.
|
|
329
334
|
|
|
330
335
|
Starred repository size
|
|
331
336
|
-----------------------
|
|
@@ -363,9 +368,9 @@ This means any blocking errors on previous runs can cause missing data in backup
|
|
|
363
368
|
|
|
364
369
|
Using (``--incremental-by-files``) will request new data from the API **based on when the file was modified on filesystem**. e.g. if you modify the file yourself you may miss something.
|
|
365
370
|
|
|
366
|
-
Still
|
|
371
|
+
Still safer than the previous version.
|
|
367
372
|
|
|
368
|
-
|
|
373
|
+
Incremental backup only changes how issue and pull request data is fetched.
|
|
369
374
|
|
|
370
375
|
Known blocking errors
|
|
371
376
|
---------------------
|
|
@@ -429,12 +434,12 @@ Github Backup Examples
|
|
|
429
434
|
|
|
430
435
|
Backup all repositories, including private ones using a classic token::
|
|
431
436
|
|
|
432
|
-
|
|
437
|
+
ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
433
438
|
github-backup WhiteHouse --token $ACCESS_TOKEN --organization --output-directory /tmp/white-house --repositories --private
|
|
434
439
|
|
|
435
440
|
Use a fine-grained access token to backup a single organization repository with everything else (wiki, pull requests, comments, issues etc)::
|
|
436
441
|
|
|
437
|
-
|
|
442
|
+
FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
438
443
|
ORGANIZATION=docker
|
|
439
444
|
REPO=cli
|
|
440
445
|
# e.g. git@github.com:docker/cli.git
|
|
@@ -442,14 +447,14 @@ Use a fine-grained access token to backup a single organization repository with
|
|
|
442
447
|
|
|
443
448
|
Quietly and incrementally backup useful Github user data (public and private repos with SSH) including; all issues, pulls, all public starred repos and gists (omitting "hooks", "releases" and therefore "assets" to prevent blocking). *Great for a cron job.* ::
|
|
444
449
|
|
|
445
|
-
|
|
450
|
+
FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
446
451
|
GH_USER=YOUR-GITHUB-USER
|
|
447
452
|
|
|
448
453
|
github-backup -f $FINE_ACCESS_TOKEN --prefer-ssh -o ~/github-backup/ -l error -P -i --all-starred --starred --watched --followers --following --issues --issue-comments --issue-events --pulls --pull-comments --pull-reviews --pull-commits --labels --milestones --security-advisories --discussions --repositories --wikis --releases --assets --attachments --pull-details --gists --starred-gists $GH_USER
|
|
449
|
-
|
|
454
|
+
|
|
450
455
|
Debug an error/block or incomplete backup into a temporary directory. Omit "incremental" to fill a previous incomplete backup. ::
|
|
451
456
|
|
|
452
|
-
|
|
457
|
+
FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
453
458
|
GH_USER=YOUR-GITHUB-USER
|
|
454
459
|
|
|
455
460
|
github-backup -f $FINE_ACCESS_TOKEN -o /tmp/github-backup/ -l debug -P --all-starred --starred --watched --followers --following --issues --issue-comments --issue-events --pulls --pull-comments --pull-reviews --pull-commits --labels --milestones --discussions --repositories --wikis --releases --assets --pull-details --gists --starred-gists $GH_USER
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.63.0"
|
|
@@ -6,7 +6,6 @@ import argparse
|
|
|
6
6
|
import base64
|
|
7
7
|
import calendar
|
|
8
8
|
import codecs
|
|
9
|
-
import errno
|
|
10
9
|
import json
|
|
11
10
|
import logging
|
|
12
11
|
import os
|
|
@@ -40,7 +39,6 @@ from .graphql_queries import (
|
|
|
40
39
|
DISCUSSION_REPLIES_QUERY,
|
|
41
40
|
)
|
|
42
41
|
|
|
43
|
-
FNULL = open(os.devnull, "w")
|
|
44
42
|
FILE_URI_PREFIX = "file://"
|
|
45
43
|
logger = logging.getLogger(__name__)
|
|
46
44
|
|
|
@@ -128,13 +126,7 @@ def logging_subprocess(
|
|
|
128
126
|
|
|
129
127
|
def mkdir_p(*args):
|
|
130
128
|
for path in args:
|
|
131
|
-
|
|
132
|
-
os.makedirs(path)
|
|
133
|
-
except OSError as exc: # Python >2.5
|
|
134
|
-
if exc.errno == errno.EEXIST and os.path.isdir(path):
|
|
135
|
-
pass
|
|
136
|
-
else:
|
|
137
|
-
raise
|
|
129
|
+
os.makedirs(path, exist_ok=True)
|
|
138
130
|
|
|
139
131
|
|
|
140
132
|
def mask_password(url, secret="*****"):
|
|
@@ -488,7 +480,7 @@ def parse_args(args=None):
|
|
|
488
480
|
"--attachments",
|
|
489
481
|
action="store_true",
|
|
490
482
|
dest="include_attachments",
|
|
491
|
-
help="download user-attachments from issues, pull requests, and discussions",
|
|
483
|
+
help="download user-attachments from issues, pull requests, and discussions [*]",
|
|
492
484
|
)
|
|
493
485
|
parser.add_argument(
|
|
494
486
|
"--throttle-limit",
|
|
@@ -529,19 +521,18 @@ def get_auth(args, encode=True, for_git_cli=False):
|
|
|
529
521
|
if platform.system() != "Darwin":
|
|
530
522
|
raise Exception("Keychain arguments are only supported on Mac OSX")
|
|
531
523
|
try:
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
).strip()
|
|
524
|
+
token = subprocess.check_output(
|
|
525
|
+
[
|
|
526
|
+
"security",
|
|
527
|
+
"find-generic-password",
|
|
528
|
+
"-s",
|
|
529
|
+
args.osx_keychain_item_name,
|
|
530
|
+
"-a",
|
|
531
|
+
args.osx_keychain_item_account,
|
|
532
|
+
"-w",
|
|
533
|
+
],
|
|
534
|
+
stderr=subprocess.DEVNULL,
|
|
535
|
+
).strip()
|
|
545
536
|
token = token.decode("utf-8")
|
|
546
537
|
auth = token + ":" + "x-oauth-basic"
|
|
547
538
|
except subprocess.SubprocessError:
|
|
@@ -554,7 +545,7 @@ def get_auth(args, encode=True, for_git_cli=False):
|
|
|
554
545
|
)
|
|
555
546
|
elif args.token_fine:
|
|
556
547
|
if args.token_fine.startswith(FILE_URI_PREFIX):
|
|
557
|
-
args.token_fine =
|
|
548
|
+
args.token_fine = read_first_line(args.token_fine)
|
|
558
549
|
|
|
559
550
|
if args.token_fine.startswith("github_pat_"):
|
|
560
551
|
auth = args.token_fine
|
|
@@ -570,7 +561,7 @@ def get_auth(args, encode=True, for_git_cli=False):
|
|
|
570
561
|
)
|
|
571
562
|
args.token_classic = read_token_from_gh_cli(args)
|
|
572
563
|
elif args.token_classic.startswith(FILE_URI_PREFIX):
|
|
573
|
-
args.token_classic =
|
|
564
|
+
args.token_classic = read_first_line(args.token_classic)
|
|
574
565
|
|
|
575
566
|
if not args.as_app:
|
|
576
567
|
auth = args.token_classic + ":" + "x-oauth-basic"
|
|
@@ -632,8 +623,9 @@ def get_github_host(args):
|
|
|
632
623
|
return host
|
|
633
624
|
|
|
634
625
|
|
|
635
|
-
def
|
|
636
|
-
|
|
626
|
+
def read_first_line(file_uri):
|
|
627
|
+
with open(file_uri[len(FILE_URI_PREFIX) :], "rt") as f:
|
|
628
|
+
return f.readline().strip()
|
|
637
629
|
|
|
638
630
|
|
|
639
631
|
def read_token_from_gh_cli(args):
|
|
@@ -806,6 +798,7 @@ def retrieve_data(args, template, query_args=None, paginated=True, lazy=False):
|
|
|
806
798
|
response = json.loads(http_response.read().decode("utf-8"))
|
|
807
799
|
break # Exit retry loop and handle the data returned
|
|
808
800
|
except (
|
|
801
|
+
ConnectionError,
|
|
809
802
|
IncompleteRead,
|
|
810
803
|
json.decoder.JSONDecodeError,
|
|
811
804
|
TimeoutError,
|
|
@@ -1297,7 +1290,7 @@ def get_jwt_signed_url_via_markdown_api(url, token, repo_context):
|
|
|
1297
1290
|
request.add_header("Content-Type", "application/json")
|
|
1298
1291
|
request.add_header("Accept", "application/vnd.github+json")
|
|
1299
1292
|
|
|
1300
|
-
html = urlopen(request, timeout=30).read().decode("utf-8")
|
|
1293
|
+
html = urlopen(request, context=https_ctx, timeout=30).read().decode("utf-8")
|
|
1301
1294
|
|
|
1302
1295
|
# Parse JWT-signed URL from HTML response
|
|
1303
1296
|
# Format: <img src="https://private-user-images.githubusercontent.com/...?jwt=..." ...>
|
|
@@ -1781,7 +1774,10 @@ def get_authenticated_user(args):
|
|
|
1781
1774
|
|
|
1782
1775
|
|
|
1783
1776
|
def check_git_lfs_install():
|
|
1784
|
-
exit_code = subprocess.call(
|
|
1777
|
+
exit_code = subprocess.call(
|
|
1778
|
+
["git", "lfs", "version"], stdin=subprocess.DEVNULL,
|
|
1779
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
1780
|
+
)
|
|
1785
1781
|
if exit_code != 0:
|
|
1786
1782
|
raise Exception(
|
|
1787
1783
|
"The argument --lfs requires you to have Git LFS installed.\nYou can get it from https://git-lfs.github.com."
|
|
@@ -1969,10 +1965,11 @@ def read_legacy_last_update(args, output_directory):
|
|
|
1969
1965
|
return None, None
|
|
1970
1966
|
|
|
1971
1967
|
last_update_path = os.path.join(output_directory, INCREMENTAL_LAST_UPDATE_FILENAME)
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1968
|
+
try:
|
|
1969
|
+
with open(last_update_path) as f:
|
|
1970
|
+
return last_update_path, f.read().strip()
|
|
1971
|
+
except FileNotFoundError:
|
|
1972
|
+
return last_update_path, None
|
|
1976
1973
|
|
|
1977
1974
|
|
|
1978
1975
|
def read_resource_last_update(args, resource_cwd, legacy_last_update=None):
|
|
@@ -1980,13 +1977,13 @@ def read_resource_last_update(args, resource_cwd, legacy_last_update=None):
|
|
|
1980
1977
|
return None
|
|
1981
1978
|
|
|
1982
1979
|
last_update_path = os.path.join(resource_cwd, INCREMENTAL_LAST_UPDATE_FILENAME)
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1980
|
+
try:
|
|
1981
|
+
with open(last_update_path) as f:
|
|
1982
|
+
return f.read().strip()
|
|
1983
|
+
except FileNotFoundError:
|
|
1984
|
+
if legacy_last_update and resource_backup_exists(resource_cwd):
|
|
1985
|
+
return legacy_last_update
|
|
1986
|
+
return None
|
|
1990
1987
|
|
|
1991
1988
|
|
|
1992
1989
|
def write_resource_last_update(args, resource_cwd, repository):
|
|
@@ -1995,7 +1992,8 @@ def write_resource_last_update(args, resource_cwd, repository):
|
|
|
1995
1992
|
|
|
1996
1993
|
mkdir_p(resource_cwd)
|
|
1997
1994
|
last_update_path = os.path.join(resource_cwd, INCREMENTAL_LAST_UPDATE_FILENAME)
|
|
1998
|
-
open(last_update_path, "w")
|
|
1995
|
+
with open(last_update_path, "w") as f:
|
|
1996
|
+
f.write(get_repository_checkpoint_time(repository))
|
|
1999
1997
|
|
|
2000
1998
|
|
|
2001
1999
|
def iter_incremental_resource_dirs(output_directory):
|
|
@@ -2383,7 +2381,8 @@ def backup_discussions(args, repo_cwd, repository):
|
|
|
2383
2381
|
discussions_since = None
|
|
2384
2382
|
discussion_last_update_path = os.path.join(discussion_cwd, "last_update")
|
|
2385
2383
|
if args.incremental and os.path.exists(discussion_last_update_path):
|
|
2386
|
-
|
|
2384
|
+
with open(discussion_last_update_path) as f:
|
|
2385
|
+
discussions_since = f.read().strip()
|
|
2387
2386
|
|
|
2388
2387
|
logger.info("Retrieving {0} discussions".format(repository["full_name"]))
|
|
2389
2388
|
try:
|
|
@@ -2469,7 +2468,8 @@ def backup_discussions(args, repo_cwd, repository):
|
|
|
2469
2468
|
and newest_seen
|
|
2470
2469
|
and (not discussions_since or newest_seen > discussions_since)
|
|
2471
2470
|
):
|
|
2472
|
-
open(discussion_last_update_path, "w")
|
|
2471
|
+
with open(discussion_last_update_path, "w") as f:
|
|
2472
|
+
f.write(newest_seen)
|
|
2473
2473
|
|
|
2474
2474
|
attempted_count = len(summaries) - skipped_count
|
|
2475
2475
|
if not summaries:
|
|
@@ -2606,7 +2606,8 @@ def get_pull_reviews_since(args, pulls_cwd):
|
|
|
2606
2606
|
# repository-level checkpoint would otherwise skip old PRs forever.
|
|
2607
2607
|
return None, None, reviews_last_update_path
|
|
2608
2608
|
|
|
2609
|
-
|
|
2609
|
+
with open(reviews_last_update_path) as f:
|
|
2610
|
+
reviews_since = f.read().strip()
|
|
2610
2611
|
if args_since and reviews_since:
|
|
2611
2612
|
return min(args_since, reviews_since), reviews_since, reviews_last_update_path
|
|
2612
2613
|
|
|
@@ -2656,10 +2657,20 @@ def backup_pulls(args, repo_cwd, repository, repos_template):
|
|
|
2656
2657
|
def pull_is_due_for_repository_checkpoint(pull):
|
|
2657
2658
|
return not repository_since or pull["updated_at"] > repository_since
|
|
2658
2659
|
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2660
|
+
try:
|
|
2661
|
+
if not args.include_pull_details:
|
|
2662
|
+
pull_states = ["open", "closed"]
|
|
2663
|
+
for pull_state in pull_states:
|
|
2664
|
+
query_args["state"] = pull_state
|
|
2665
|
+
for pull in retrieve_data(
|
|
2666
|
+
args, _pulls_template, query_args=query_args, lazy=True
|
|
2667
|
+
):
|
|
2668
|
+
track_newest_pull_update(pull)
|
|
2669
|
+
if pulls_since and pull["updated_at"] <= pulls_since:
|
|
2670
|
+
break
|
|
2671
|
+
if not pulls_since or pull["updated_at"] > pulls_since:
|
|
2672
|
+
pulls[pull["number"]] = pull
|
|
2673
|
+
else:
|
|
2663
2674
|
for pull in retrieve_data(
|
|
2664
2675
|
args, _pulls_template, query_args=query_args, lazy=True
|
|
2665
2676
|
):
|
|
@@ -2668,22 +2679,29 @@ def backup_pulls(args, repo_cwd, repository, repos_template):
|
|
|
2668
2679
|
break
|
|
2669
2680
|
if not pulls_since or pull["updated_at"] > pulls_since:
|
|
2670
2681
|
pulls[pull["number"]] = pull
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2682
|
+
except HTTPError as e:
|
|
2683
|
+
# Repositories with pull requests disabled return HTTP 404 for the
|
|
2684
|
+
# listing endpoint; treat that as "no pull requests" rather than a
|
|
2685
|
+
# fatal error. retrieve_data is lazy here, so the 404 surfaces during
|
|
2686
|
+
# iteration. Only the listing call is guarded — a 404 on an individual
|
|
2687
|
+
# pull's detail fetch below is a different condition and must propagate.
|
|
2688
|
+
if e.code == 404:
|
|
2689
|
+
logger.info("Pull requests are disabled for this repository, skipping")
|
|
2690
|
+
return
|
|
2691
|
+
raise
|
|
2692
|
+
|
|
2693
|
+
if args.include_pull_details:
|
|
2694
|
+
# Replace the summary payload with the full pull detail. Done outside
|
|
2695
|
+
# the disabled-feature guard above so a 404 on a single pull (e.g. one
|
|
2696
|
+
# deleted between listing and fetch) is not mistaken for the whole
|
|
2697
|
+
# feature being disabled, which would discard every collected pull.
|
|
2698
|
+
for number in list(pulls.keys()):
|
|
2699
|
+
if pull_is_due_for_repository_checkpoint(pulls[number]):
|
|
2700
|
+
pulls[number] = retrieve_data(
|
|
2701
|
+
args,
|
|
2702
|
+
_pulls_template + "/{}".format(number),
|
|
2703
|
+
paginated=False,
|
|
2704
|
+
)[0]
|
|
2687
2705
|
|
|
2688
2706
|
logger.info("Saving {0} pull requests to disk".format(len(list(pulls.keys()))))
|
|
2689
2707
|
# Comments from pulls API are only _review_ comments
|
|
@@ -2758,7 +2776,8 @@ def backup_pulls(args, repo_cwd, repository, repos_template):
|
|
|
2758
2776
|
and not pull_review_errors
|
|
2759
2777
|
and (not pull_reviews_since or newest_pull_update > pull_reviews_since)
|
|
2760
2778
|
):
|
|
2761
|
-
open(pull_reviews_last_update_path, "w")
|
|
2779
|
+
with open(pull_reviews_last_update_path, "w") as f:
|
|
2780
|
+
f.write(newest_pull_update)
|
|
2762
2781
|
|
|
2763
2782
|
|
|
2764
2783
|
def backup_milestones(args, repo_cwd, repository, repos_template):
|
|
@@ -2811,8 +2830,8 @@ def backup_security_advisories(args, repo_cwd, repository, repos_template):
|
|
|
2811
2830
|
|
|
2812
2831
|
try:
|
|
2813
2832
|
_advisories = retrieve_data(args, template)
|
|
2814
|
-
except
|
|
2815
|
-
if
|
|
2833
|
+
except HTTPError as e:
|
|
2834
|
+
if e.code == 404:
|
|
2816
2835
|
logger.info("Security advisories are not available for this repository, skipping")
|
|
2817
2836
|
return
|
|
2818
2837
|
raise
|
|
@@ -2859,8 +2878,8 @@ def backup_hooks(args, repo_cwd, repository, repos_template):
|
|
|
2859
2878
|
template = "{0}/{1}/hooks".format(repos_template, repository["full_name"])
|
|
2860
2879
|
try:
|
|
2861
2880
|
_backup_data(args, "hooks", template, output_file, hook_cwd)
|
|
2862
|
-
except
|
|
2863
|
-
if
|
|
2881
|
+
except HTTPError as e:
|
|
2882
|
+
if e.code == 404:
|
|
2864
2883
|
logger.info("Unable to read hooks, skipping")
|
|
2865
2884
|
else:
|
|
2866
2885
|
raise e
|
|
@@ -2980,7 +2999,8 @@ def fetch_repository(
|
|
|
2980
2999
|
masked_remote_url = mask_password(remote_url)
|
|
2981
3000
|
|
|
2982
3001
|
initialized = subprocess.call(
|
|
2983
|
-
"git ls-remote
|
|
3002
|
+
["git", "ls-remote", remote_url], stdin=subprocess.DEVNULL,
|
|
3003
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
2984
3004
|
)
|
|
2985
3005
|
if initialized == 128:
|
|
2986
3006
|
if ".wiki.git" in remote_url:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-backup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.63.0
|
|
4
4
|
Summary: backup a github user or organization
|
|
5
5
|
Home-page: http://github.com/josegonzalez/python-github-backup
|
|
6
6
|
Author: Jose Diaz-Gonzalez
|
|
@@ -52,7 +52,7 @@ Using PIP via PyPI::
|
|
|
52
52
|
Using PIP via Github (more likely the latest version)::
|
|
53
53
|
|
|
54
54
|
pip install git+https://github.com/josegonzalez/python-github-backup.git#egg=github-backup
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
*Install note for python newcomers:*
|
|
57
57
|
|
|
58
58
|
Python scripts are unlikely to be included in your ``$PATH`` by default, this means it cannot be run directly in terminal with ``$ github-backup ...``, you can either add python's install path to your environments ``$PATH`` or call the script directly e.g. using ``$ ~/.local/bin/github-backup``.*
|
|
@@ -279,7 +279,7 @@ Note: When you run github-backup, you will be asked whether you want to allow "
|
|
|
279
279
|
Github Rate-limit and Throttling
|
|
280
280
|
--------------------------------
|
|
281
281
|
|
|
282
|
-
"github-backup" will automatically throttle itself based on feedback from the Github API.
|
|
282
|
+
"github-backup" will automatically throttle itself based on feedback from the Github API.
|
|
283
283
|
|
|
284
284
|
Their API is usually rate-limited to 5000 calls per hour. The API will ask github-backup to pause until a specific time when the limit is reset again (at the start of the next hour). This continues until the backup is complete.
|
|
285
285
|
|
|
@@ -355,7 +355,12 @@ Gotchas / Known-issues
|
|
|
355
355
|
All is not everything
|
|
356
356
|
---------------------
|
|
357
357
|
|
|
358
|
-
The ``--all`` argument does not include: cloning private repos (``-P, --private``), cloning forks (``-F, --fork``), cloning starred repositories (``--all-starred``), ``--pull-details``, cloning LFS repositories (``--lfs``), cloning gists (``--gists``) or cloning starred gist repos (``--starred-gists``). See examples for more.
|
|
358
|
+
The ``--all`` argument does not include: downloading attachments from issue and pull request comments (``--attachments``), cloning private repos (``-P, --private``), cloning forks (``-F, --fork``), cloning starred repositories (``--all-starred``), ``--pull-details``, cloning LFS repositories (``--lfs``), cloning gists (``--gists``) or cloning starred gist repos (``--starred-gists``). See examples for more.
|
|
359
|
+
|
|
360
|
+
Saves nothing if no arguments are passed
|
|
361
|
+
----------------------------------------
|
|
362
|
+
|
|
363
|
+
At least one argument like ``--all`` or ``--repositories`` is needed for github-backup to actually save data. Without relevant arguments, github-backup fetches some data from GitHub but doesn't put any of it into files.
|
|
359
364
|
|
|
360
365
|
Starred repository size
|
|
361
366
|
-----------------------
|
|
@@ -393,9 +398,9 @@ This means any blocking errors on previous runs can cause missing data in backup
|
|
|
393
398
|
|
|
394
399
|
Using (``--incremental-by-files``) will request new data from the API **based on when the file was modified on filesystem**. e.g. if you modify the file yourself you may miss something.
|
|
395
400
|
|
|
396
|
-
Still
|
|
401
|
+
Still safer than the previous version.
|
|
397
402
|
|
|
398
|
-
|
|
403
|
+
Incremental backup only changes how issue and pull request data is fetched.
|
|
399
404
|
|
|
400
405
|
Known blocking errors
|
|
401
406
|
---------------------
|
|
@@ -459,12 +464,12 @@ Github Backup Examples
|
|
|
459
464
|
|
|
460
465
|
Backup all repositories, including private ones using a classic token::
|
|
461
466
|
|
|
462
|
-
|
|
467
|
+
ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
463
468
|
github-backup WhiteHouse --token $ACCESS_TOKEN --organization --output-directory /tmp/white-house --repositories --private
|
|
464
469
|
|
|
465
470
|
Use a fine-grained access token to backup a single organization repository with everything else (wiki, pull requests, comments, issues etc)::
|
|
466
471
|
|
|
467
|
-
|
|
472
|
+
FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
468
473
|
ORGANIZATION=docker
|
|
469
474
|
REPO=cli
|
|
470
475
|
# e.g. git@github.com:docker/cli.git
|
|
@@ -472,14 +477,14 @@ Use a fine-grained access token to backup a single organization repository with
|
|
|
472
477
|
|
|
473
478
|
Quietly and incrementally backup useful Github user data (public and private repos with SSH) including; all issues, pulls, all public starred repos and gists (omitting "hooks", "releases" and therefore "assets" to prevent blocking). *Great for a cron job.* ::
|
|
474
479
|
|
|
475
|
-
|
|
480
|
+
FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
476
481
|
GH_USER=YOUR-GITHUB-USER
|
|
477
482
|
|
|
478
483
|
github-backup -f $FINE_ACCESS_TOKEN --prefer-ssh -o ~/github-backup/ -l error -P -i --all-starred --starred --watched --followers --following --issues --issue-comments --issue-events --pulls --pull-comments --pull-reviews --pull-commits --labels --milestones --security-advisories --discussions --repositories --wikis --releases --assets --attachments --pull-details --gists --starred-gists $GH_USER
|
|
479
|
-
|
|
484
|
+
|
|
480
485
|
Debug an error/block or incomplete backup into a temporary directory. Omit "incremental" to fill a previous incomplete backup. ::
|
|
481
486
|
|
|
482
|
-
|
|
487
|
+
FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
|
|
483
488
|
GH_USER=YOUR-GITHUB-USER
|
|
484
489
|
|
|
485
490
|
github-backup -f $FINE_ACCESS_TOKEN -o /tmp/github-backup/ -l debug -P --all-starred --starred --watched --followers --following --issues --issue-comments --issue-events --pulls --pull-comments --pull-reviews --pull-commits --labels --milestones --discussions --repositories --wikis --releases --assets --pull-details --gists --starred-gists $GH_USER
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Tests for repositories with pull requests disabled (GitHub issue #511).
|
|
2
|
+
|
|
3
|
+
When pull requests are disabled on a repository, the ``/pulls`` endpoint
|
|
4
|
+
responds with HTTP 404. The backup should treat that as "no pull requests"
|
|
5
|
+
and continue, rather than aborting the whole run. (The ``/issues`` endpoint
|
|
6
|
+
returns 200 with an empty list when issues are disabled, so it needs no such
|
|
7
|
+
handling.)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from urllib.error import HTTPError
|
|
12
|
+
|
|
13
|
+
from github_backup import github_backup
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _http_error(template, code, reason):
|
|
17
|
+
return HTTPError(template, code, reason, hdrs=None, fp=None)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_backup_pulls_skips_on_404(create_args, tmp_path, monkeypatch, caplog):
|
|
21
|
+
args = create_args(include_pulls=True, since=None)
|
|
22
|
+
repository = {"full_name": "owner/repo"}
|
|
23
|
+
|
|
24
|
+
def fake_retrieve_data(passed_args, template, query_args=None, lazy=False, **kwargs):
|
|
25
|
+
# retrieve_data is called lazily for the pull listing; the 404 must
|
|
26
|
+
# surface while iterating the returned generator.
|
|
27
|
+
def gen():
|
|
28
|
+
raise _http_error(template, 404, "Not Found")
|
|
29
|
+
yield # pragma: no cover - generator marker
|
|
30
|
+
|
|
31
|
+
if lazy:
|
|
32
|
+
return gen()
|
|
33
|
+
raise _http_error(template, 404, "Not Found")
|
|
34
|
+
|
|
35
|
+
monkeypatch.setattr(github_backup, "retrieve_data", fake_retrieve_data)
|
|
36
|
+
|
|
37
|
+
# Should not raise, and should log that it skipped.
|
|
38
|
+
with caplog.at_level(logging.INFO, logger=github_backup.logger.name):
|
|
39
|
+
github_backup.backup_pulls(
|
|
40
|
+
args, tmp_path, repository, "https://api.github.com/repos"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
assert "Pull requests are disabled for this repository, skipping" in caplog.text
|
|
44
|
+
|
|
45
|
+
pulls_dir = tmp_path / "pulls"
|
|
46
|
+
if pulls_dir.exists():
|
|
47
|
+
assert not [p for p in pulls_dir.iterdir() if p.suffix == ".json"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_backup_pulls_reraises_non_404(create_args, tmp_path, monkeypatch):
|
|
51
|
+
args = create_args(include_pulls=True, since=None)
|
|
52
|
+
repository = {"full_name": "owner/repo"}
|
|
53
|
+
|
|
54
|
+
def fake_retrieve_data(passed_args, template, query_args=None, lazy=False, **kwargs):
|
|
55
|
+
def gen():
|
|
56
|
+
raise _http_error(template, 500, "Server Error")
|
|
57
|
+
yield # pragma: no cover - generator marker
|
|
58
|
+
|
|
59
|
+
if lazy:
|
|
60
|
+
return gen()
|
|
61
|
+
raise _http_error(template, 500, "Server Error")
|
|
62
|
+
|
|
63
|
+
monkeypatch.setattr(github_backup, "retrieve_data", fake_retrieve_data)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
github_backup.backup_pulls(
|
|
67
|
+
args, tmp_path, repository, "https://api.github.com/repos"
|
|
68
|
+
)
|
|
69
|
+
except HTTPError as exc:
|
|
70
|
+
assert exc.code == 500
|
|
71
|
+
else:
|
|
72
|
+
raise AssertionError("Expected non-404 error to propagate")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_backup_pulls_details_individual_404_propagates(
|
|
76
|
+
create_args, tmp_path, monkeypatch
|
|
77
|
+
):
|
|
78
|
+
# With --pull-details the listing succeeds (feature is enabled), but an
|
|
79
|
+
# individual pull's detail fetch 404s (e.g. deleted mid-backup). That must
|
|
80
|
+
# propagate, NOT be swallowed as "pull requests are disabled".
|
|
81
|
+
args = create_args(include_pulls=True, include_pull_details=True, since=None)
|
|
82
|
+
repository = {"full_name": "owner/repo"}
|
|
83
|
+
|
|
84
|
+
listing = "https://api.github.com/repos/owner/repo/pulls"
|
|
85
|
+
|
|
86
|
+
def fake_retrieve_data(passed_args, template, query_args=None, lazy=False, **kwargs):
|
|
87
|
+
if template == listing:
|
|
88
|
+
# Listing works fine -> pull requests are clearly not disabled.
|
|
89
|
+
def gen():
|
|
90
|
+
yield {"number": 1, "updated_at": "2026-02-01T00:00:00Z"}
|
|
91
|
+
|
|
92
|
+
return gen()
|
|
93
|
+
# Individual pull detail fetch (.../pulls/1) is missing.
|
|
94
|
+
raise _http_error(template, 404, "Not Found")
|
|
95
|
+
|
|
96
|
+
monkeypatch.setattr(github_backup, "retrieve_data", fake_retrieve_data)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
github_backup.backup_pulls(
|
|
100
|
+
args, tmp_path, repository, "https://api.github.com/repos"
|
|
101
|
+
)
|
|
102
|
+
except HTTPError as exc:
|
|
103
|
+
assert exc.code == 404
|
|
104
|
+
else:
|
|
105
|
+
raise AssertionError("Expected individual-pull 404 to propagate")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.62.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|