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.
Files changed (38) hide show
  1. {github_backup-0.62.0 → github_backup-0.63.0}/CHANGES.rst +103 -1
  2. {github_backup-0.62.0/github_backup.egg-info → github_backup-0.63.0}/PKG-INFO +16 -11
  3. {github_backup-0.62.0 → github_backup-0.63.0}/README +15 -10
  4. {github_backup-0.62.0 → github_backup-0.63.0}/README.rst +15 -10
  5. github_backup-0.63.0/github_backup/__init__.py +1 -0
  6. {github_backup-0.62.0 → github_backup-0.63.0}/github_backup/github_backup.py +90 -70
  7. {github_backup-0.62.0 → github_backup-0.63.0/github_backup.egg-info}/PKG-INFO +16 -11
  8. {github_backup-0.62.0 → github_backup-0.63.0}/github_backup.egg-info/SOURCES.txt +1 -0
  9. {github_backup-0.62.0 → github_backup-0.63.0}/release-requirements.txt +2 -2
  10. github_backup-0.63.0/tests/test_disabled_issues_pulls.py +105 -0
  11. github_backup-0.62.0/github_backup/__init__.py +0 -1
  12. {github_backup-0.62.0 → github_backup-0.63.0}/LICENSE.txt +0 -0
  13. {github_backup-0.62.0 → github_backup-0.63.0}/MANIFEST.in +0 -0
  14. {github_backup-0.62.0 → github_backup-0.63.0}/github_backup/__main__.py +0 -0
  15. {github_backup-0.62.0 → github_backup-0.63.0}/github_backup/cli.py +0 -0
  16. {github_backup-0.62.0 → github_backup-0.63.0}/github_backup/graphql_queries.py +0 -0
  17. {github_backup-0.62.0 → github_backup-0.63.0}/github_backup.egg-info/dependency_links.txt +0 -0
  18. {github_backup-0.62.0 → github_backup-0.63.0}/github_backup.egg-info/entry_points.txt +0 -0
  19. {github_backup-0.62.0 → github_backup-0.63.0}/github_backup.egg-info/top_level.txt +0 -0
  20. {github_backup-0.62.0 → github_backup-0.63.0}/github_backup.egg-info/zip-safe +0 -0
  21. {github_backup-0.62.0 → github_backup-0.63.0}/requirements.txt +0 -0
  22. {github_backup-0.62.0 → github_backup-0.63.0}/setup.cfg +0 -0
  23. {github_backup-0.62.0 → github_backup-0.63.0}/setup.py +0 -0
  24. {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_all_starred.py +0 -0
  25. {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_attachments.py +0 -0
  26. {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_auth.py +0 -0
  27. {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_case_sensitivity.py +0 -0
  28. {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_discussions.py +0 -0
  29. {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_http_451.py +0 -0
  30. {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_incremental_per_repository.py +0 -0
  31. {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_json_dump_if_changed.py +0 -0
  32. {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_pagination.py +0 -0
  33. {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_pull_incremental_pagination.py +0 -0
  34. {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_pull_reviews.py +0 -0
  35. {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_releases.py +0 -0
  36. {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_retrieve_data.py +0 -0
  37. {github_backup-0.62.0 → github_backup-0.63.0}/tests/test_skip_assets_on.py +0 -0
  38. {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.62.0 (2026-04-29)
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.62.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 saver than the previous version.
401
+ Still safer than the previous version.
397
402
 
398
- Specifically, issues and pull requests are handled like this.
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
- export ACCESS_TOKEN=SOME-GITHUB-TOKEN
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
- export FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
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
- export FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
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
- export FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
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 saver than the previous version.
371
+ Still safer than the previous version.
367
372
 
368
- Specifically, issues and pull requests are handled like this.
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
- export ACCESS_TOKEN=SOME-GITHUB-TOKEN
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
- export FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
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
- export FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
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
- export FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
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 saver than the previous version.
371
+ Still safer than the previous version.
367
372
 
368
- Specifically, issues and pull requests are handled like this.
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
- export ACCESS_TOKEN=SOME-GITHUB-TOKEN
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
- export FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
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
- export FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
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
- export FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
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
- try:
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
- with open(os.devnull, "w") as devnull:
533
- token = subprocess.check_output(
534
- [
535
- "security",
536
- "find-generic-password",
537
- "-s",
538
- args.osx_keychain_item_name,
539
- "-a",
540
- args.osx_keychain_item_account,
541
- "-w",
542
- ],
543
- stderr=devnull,
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 = read_file_contents(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 = read_file_contents(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 read_file_contents(file_uri):
636
- return open(file_uri[len(FILE_URI_PREFIX) :], "rt").readline().strip()
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(["git", "lfs", "version"])
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
- if os.path.exists(last_update_path):
1973
- return last_update_path, open(last_update_path).read().strip()
1974
-
1975
- return last_update_path, None
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
- if os.path.exists(last_update_path):
1984
- return open(last_update_path).read().strip()
1985
-
1986
- if legacy_last_update and resource_backup_exists(resource_cwd):
1987
- return legacy_last_update
1988
-
1989
- return None
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").write(get_repository_checkpoint_time(repository))
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
- discussions_since = open(discussion_last_update_path).read().strip()
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").write(newest_seen)
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
- reviews_since = open(reviews_last_update_path).read().strip()
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
- if not args.include_pull_details:
2660
- pull_states = ["open", "closed"]
2661
- for pull_state in pull_states:
2662
- query_args["state"] = pull_state
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
- else:
2672
- for pull in retrieve_data(
2673
- args, _pulls_template, query_args=query_args, lazy=True
2674
- ):
2675
- track_newest_pull_update(pull)
2676
- if pulls_since and pull["updated_at"] <= pulls_since:
2677
- break
2678
- if not pulls_since or pull["updated_at"] > pulls_since:
2679
- if pull_is_due_for_repository_checkpoint(pull):
2680
- pulls[pull["number"]] = retrieve_data(
2681
- args,
2682
- _pulls_template + "/{}".format(pull["number"]),
2683
- paginated=False,
2684
- )[0]
2685
- else:
2686
- pulls[pull["number"]] = pull
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").write(newest_pull_update)
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 Exception as e:
2815
- if "404" in str(e):
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 Exception as e:
2863
- if "404" in str(e):
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 " + remote_url, stdout=FNULL, stderr=FNULL, shell=True
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.62.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 saver than the previous version.
401
+ Still safer than the previous version.
397
402
 
398
- Specifically, issues and pull requests are handled like this.
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
- export ACCESS_TOKEN=SOME-GITHUB-TOKEN
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
- export FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
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
- export FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
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
- export FINE_ACCESS_TOKEN=SOME-GITHUB-TOKEN
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
@@ -21,6 +21,7 @@ tests/test_all_starred.py
21
21
  tests/test_attachments.py
22
22
  tests/test_auth.py
23
23
  tests/test_case_sensitivity.py
24
+ tests/test_disabled_issues_pulls.py
24
25
  tests/test_discussions.py
25
26
  tests/test_http_451.py
26
27
  tests/test_incremental_per_repository.py
@@ -1,10 +1,10 @@
1
1
  # Linting & Formatting
2
2
  autopep8==2.3.2
3
- black==26.3.1
3
+ black==26.5.1
4
4
  flake8==7.3.0
5
5
 
6
6
  # Testing
7
- pytest==9.0.3
7
+ pytest==9.1.0
8
8
 
9
9
  # Release & Publishing
10
10
  twine==6.2.0
@@ -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