github-backup 0.65.1__tar.gz → 0.66.1__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 (43) hide show
  1. {github_backup-0.65.1 → github_backup-0.66.1}/CHANGES.rst +153 -1
  2. {github_backup-0.65.1/github_backup.egg-info → github_backup-0.66.1}/PKG-INFO +5 -5
  3. {github_backup-0.65.1 → github_backup-0.66.1}/README +4 -4
  4. {github_backup-0.65.1 → github_backup-0.66.1}/README.rst +4 -4
  5. github_backup-0.66.1/github_backup/__init__.py +1 -0
  6. {github_backup-0.65.1 → github_backup-0.66.1}/github_backup/github_backup.py +24 -1
  7. {github_backup-0.65.1 → github_backup-0.66.1/github_backup.egg-info}/PKG-INFO +5 -5
  8. {github_backup-0.65.1 → github_backup-0.66.1}/github_backup.egg-info/SOURCES.txt +2 -0
  9. {github_backup-0.65.1 → github_backup-0.66.1}/release-requirements.txt +4 -4
  10. github_backup-0.66.1/tests/test_app_repositories.py +71 -0
  11. github_backup-0.66.1/tests/test_hooks.py +96 -0
  12. github_backup-0.65.1/github_backup/__init__.py +0 -1
  13. {github_backup-0.65.1 → github_backup-0.66.1}/LICENSE.txt +0 -0
  14. {github_backup-0.65.1 → github_backup-0.66.1}/MANIFEST.in +0 -0
  15. {github_backup-0.65.1 → github_backup-0.66.1}/github_backup/__main__.py +0 -0
  16. {github_backup-0.65.1 → github_backup-0.66.1}/github_backup/cli.py +0 -0
  17. {github_backup-0.65.1 → github_backup-0.66.1}/github_backup/graphql_queries.py +0 -0
  18. {github_backup-0.65.1 → github_backup-0.66.1}/github_backup.egg-info/dependency_links.txt +0 -0
  19. {github_backup-0.65.1 → github_backup-0.66.1}/github_backup.egg-info/entry_points.txt +0 -0
  20. {github_backup-0.65.1 → github_backup-0.66.1}/github_backup.egg-info/top_level.txt +0 -0
  21. {github_backup-0.65.1 → github_backup-0.66.1}/github_backup.egg-info/zip-safe +0 -0
  22. {github_backup-0.65.1 → github_backup-0.66.1}/requirements.txt +0 -0
  23. {github_backup-0.65.1 → github_backup-0.66.1}/setup.cfg +0 -0
  24. {github_backup-0.65.1 → github_backup-0.66.1}/setup.py +0 -0
  25. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_all_starred.py +0 -0
  26. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_attachments.py +0 -0
  27. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_auth.py +0 -0
  28. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_case_sensitivity.py +0 -0
  29. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_disabled_issues_pulls.py +0 -0
  30. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_discussions.py +0 -0
  31. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_gists.py +0 -0
  32. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_http_451.py +0 -0
  33. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_incremental_per_repository.py +0 -0
  34. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_issue_timeline.py +0 -0
  35. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_json_dump_if_changed.py +0 -0
  36. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_logging_subprocess.py +0 -0
  37. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_pagination.py +0 -0
  38. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_pull_incremental_pagination.py +0 -0
  39. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_pull_reviews.py +0 -0
  40. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_releases.py +0 -0
  41. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_retrieve_data.py +0 -0
  42. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_skip_assets_on.py +0 -0
  43. {github_backup-0.65.1 → github_backup-0.66.1}/tests/test_starred_skip_size_over.py +0 -0
@@ -1,9 +1,161 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
- 0.65.1 (2026-08-01)
4
+ 0.66.1 (2026-11-03)
5
5
  -------------------
6
6
  ------------------------
7
+ - Skip hooks for repositories without admin access instead of logging a
8
+ 404 error (#532) [Rodos]
9
+
10
+ Reading hooks needs admin access, and GitHub returns 404 without it. When the
11
+ repository listing says the token isn't an admin, skip hooks without making the
12
+ request. The 404 skip message now names the repository and the permission it
13
+ needs, and the README note about hooks blocking starred backups is updated.
14
+ - Chore(deps): bump pytest. [dependabot[bot]]
15
+
16
+ Bumps the python-packages group with 1 update: [pytest](https://github.com/pytest-dev/pytest).
17
+
18
+
19
+ Updates `pytest` from 9.1.1 to 9.2.0
20
+ - [Release notes](https://github.com/pytest-dev/pytest/releases)
21
+ - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
22
+ - [Commits](https://github.com/pytest-dev/pytest/compare/9.1.1...9.2.0)
23
+
24
+ ---
25
+ updated-dependencies:
26
+ - dependency-name: pytest
27
+ dependency-version: 9.2.0
28
+ dependency-type: direct:production
29
+ update-type: version-update:semver-minor
30
+ dependency-group: python-packages
31
+ ...
32
+
33
+
34
+ 0.66.0 (2026-10-28)
35
+ -------------------
36
+ - List a user's private repositories when authenticated as a GitHub App.
37
+ [AC Capehart, Claude Opus 5.5]
38
+
39
+ With --as-app there is no authenticated user, so a user-account backup
40
+ fell through to /users/{user}/repos, which only returns public
41
+ repositories. --private silently backed up nothing private.
42
+
43
+ An installation token can list everything the installation can access
44
+ via /installation/repositories, private repositories included. Org
45
+ backups keep using /orgs/{org}/repos, and -R still fetches the single
46
+ repository directly.
47
+ - Chore(deps): bump setuptools. [dependabot[bot]]
48
+
49
+ Bumps the python-packages group with 1 update: [setuptools](https://github.com/pypa/setuptools).
50
+
51
+
52
+ Updates `setuptools` from 84.2.0 to 84.2.1
53
+ - [Release notes](https://github.com/pypa/setuptools/releases)
54
+ - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
55
+ - [Commits](https://github.com/pypa/setuptools/compare/v84.2.0...v84.2.1)
56
+
57
+ ---
58
+ updated-dependencies:
59
+ - dependency-name: setuptools
60
+ dependency-version: 84.2.1
61
+ dependency-type: direct:production
62
+ update-type: version-update:semver-patch
63
+ dependency-group: python-packages
64
+ ...
65
+ - Chore(deps): bump setuptools. [dependabot[bot]]
66
+
67
+ Bumps the python-packages group with 1 update: [setuptools](https://github.com/pypa/setuptools).
68
+
69
+
70
+ Updates `setuptools` from 84.0.0 to 84.2.0
71
+ - [Release notes](https://github.com/pypa/setuptools/releases)
72
+ - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
73
+ - [Commits](https://github.com/pypa/setuptools/compare/v84.0.0...v84.2.0)
74
+
75
+ ---
76
+ updated-dependencies:
77
+ - dependency-name: setuptools
78
+ dependency-version: 84.2.0
79
+ dependency-type: direct:production
80
+ update-type: version-update:semver-minor
81
+ dependency-group: python-packages
82
+ ...
83
+ - Chore(deps): bump black. [dependabot[bot]]
84
+
85
+ Bumps the python-packages group with 1 update: [black](https://github.com/psf/black).
86
+
87
+
88
+ Updates `black` from 26.10.0 to 26.10.1
89
+ - [Release notes](https://github.com/psf/black/releases)
90
+ - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
91
+ - [Commits](https://github.com/psf/black/compare/26.10.0...26.10.1)
92
+
93
+ ---
94
+ updated-dependencies:
95
+ - dependency-name: black
96
+ dependency-version: 26.10.1
97
+ dependency-type: direct:production
98
+ update-type: version-update:semver-patch
99
+ dependency-group: python-packages
100
+ ...
101
+ - Chore(deps): bump black. [dependabot[bot]]
102
+
103
+ Bumps the python-packages group with 1 update: [black](https://github.com/psf/black).
104
+
105
+
106
+ Updates `black` from 26.5.1 to 26.10.0
107
+ - [Release notes](https://github.com/psf/black/releases)
108
+ - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
109
+ - [Commits](https://github.com/psf/black/compare/26.5.1...26.10.0)
110
+
111
+ ---
112
+ updated-dependencies:
113
+ - dependency-name: black
114
+ dependency-version: 26.10.0
115
+ dependency-type: direct:production
116
+ update-type: version-update:semver-minor
117
+ dependency-group: python-packages
118
+ ...
119
+ - Chore(deps): bump flake8 in the python-packages group.
120
+ [dependabot[bot]]
121
+
122
+ Bumps the python-packages group with 1 update: [flake8](https://github.com/pycqa/flake8).
123
+
124
+
125
+ Updates `flake8` from 7.3.0 to 7.4.1
126
+ - [Commits](https://github.com/pycqa/flake8/compare/7.3.0...7.4.1)
127
+
128
+ ---
129
+ updated-dependencies:
130
+ - dependency-name: flake8
131
+ dependency-version: 7.4.1
132
+ dependency-type: direct:production
133
+ update-type: version-update:semver-minor
134
+ dependency-group: python-packages
135
+ ...
136
+ - Chore(deps): bump setuptools in the python-packages group.
137
+ [dependabot[bot]]
138
+
139
+ Bumps the python-packages group with 1 update: [setuptools](https://github.com/pypa/setuptools).
140
+
141
+
142
+ Updates `setuptools` from 83.0.0 to 84.0.0
143
+ - [Release notes](https://github.com/pypa/setuptools/releases)
144
+ - [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
145
+ - [Commits](https://github.com/pypa/setuptools/compare/v83.0.0...v84.0.0)
146
+
147
+ ---
148
+ updated-dependencies:
149
+ - dependency-name: setuptools
150
+ dependency-version: 84.0.0
151
+ dependency-type: direct:production
152
+ update-type: version-update:semver-major
153
+ dependency-group: python-packages
154
+ ...
155
+
156
+
157
+ 0.65.1 (2026-08-01)
158
+ -------------------
7
159
  - Fix timeline not being properly incremental. [Duncan Ogilvie]
8
160
  - Chore(deps): bump twine from 6.2.0 to 7.0.0 in the python-packages
9
161
  group. [dependabot[bot]]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-backup
3
- Version: 0.65.1
3
+ Version: 0.66.1
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
@@ -229,6 +229,8 @@ GitHub Apps
229
229
 
230
230
  GitHub Apps are ideal for organization backups in CI/CD. Tokens are scoped to specific repositories and expire after 1 hour.
231
231
 
232
+ For a user account (no ``--organization``), ``--as-app`` lists the repositories the installation can access, so private repositories are included.
233
+
232
234
  **One-time setup:**
233
235
 
234
236
  1. Create a GitHub App at *Settings -> Developer Settings -> GitHub Apps -> New GitHub App*
@@ -435,11 +437,9 @@ If the incremental argument is used, per-resource checkpoints are only advanced
435
437
 
436
438
  It's therefore recommended to only use the incremental argument if the output/result is being actively monitored, or complimented with periodic full non-incremental runs, to avoid unexpected missing data in a regular backup runs.
437
439
 
438
- **Starred public repo hooks blocking**
439
-
440
- Since the ``--all`` argument includes ``--hooks``, if you use ``--all`` and ``--all-starred`` together to clone a users starred public repositories, the backup will likely error and block the backup continuing.
440
+ **Hooks need admin access**
441
441
 
442
- This is due to needing the correct permission for ``--hooks`` on public repos.
442
+ Reading a repository's hooks requires admin access to it, and fine-grained tokens also need the Webhooks read permission. Since the ``--all`` argument includes ``--hooks``, repositories you don't administer, such as starred repositories with ``--all-starred``, are skipped for hooks and the rest of the backup continues.
443
443
 
444
444
 
445
445
  "bare" is actually "mirror"
@@ -199,6 +199,8 @@ GitHub Apps
199
199
 
200
200
  GitHub Apps are ideal for organization backups in CI/CD. Tokens are scoped to specific repositories and expire after 1 hour.
201
201
 
202
+ For a user account (no ``--organization``), ``--as-app`` lists the repositories the installation can access, so private repositories are included.
203
+
202
204
  **One-time setup:**
203
205
 
204
206
  1. Create a GitHub App at *Settings -> Developer Settings -> GitHub Apps -> New GitHub App*
@@ -405,11 +407,9 @@ If the incremental argument is used, per-resource checkpoints are only advanced
405
407
 
406
408
  It's therefore recommended to only use the incremental argument if the output/result is being actively monitored, or complimented with periodic full non-incremental runs, to avoid unexpected missing data in a regular backup runs.
407
409
 
408
- **Starred public repo hooks blocking**
409
-
410
- Since the ``--all`` argument includes ``--hooks``, if you use ``--all`` and ``--all-starred`` together to clone a users starred public repositories, the backup will likely error and block the backup continuing.
410
+ **Hooks need admin access**
411
411
 
412
- This is due to needing the correct permission for ``--hooks`` on public repos.
412
+ Reading a repository's hooks requires admin access to it, and fine-grained tokens also need the Webhooks read permission. Since the ``--all`` argument includes ``--hooks``, repositories you don't administer, such as starred repositories with ``--all-starred``, are skipped for hooks and the rest of the backup continues.
413
413
 
414
414
 
415
415
  "bare" is actually "mirror"
@@ -199,6 +199,8 @@ GitHub Apps
199
199
 
200
200
  GitHub Apps are ideal for organization backups in CI/CD. Tokens are scoped to specific repositories and expire after 1 hour.
201
201
 
202
+ For a user account (no ``--organization``), ``--as-app`` lists the repositories the installation can access, so private repositories are included.
203
+
202
204
  **One-time setup:**
203
205
 
204
206
  1. Create a GitHub App at *Settings -> Developer Settings -> GitHub Apps -> New GitHub App*
@@ -405,11 +407,9 @@ If the incremental argument is used, per-resource checkpoints are only advanced
405
407
 
406
408
  It's therefore recommended to only use the incremental argument if the output/result is being actively monitored, or complimented with periodic full non-incremental runs, to avoid unexpected missing data in a regular backup runs.
407
409
 
408
- **Starred public repo hooks blocking**
409
-
410
- Since the ``--all`` argument includes ``--hooks``, if you use ``--all`` and ``--all-starred`` together to clone a users starred public repositories, the backup will likely error and block the backup continuing.
410
+ **Hooks need admin access**
411
411
 
412
- This is due to needing the correct permission for ``--hooks`` on public repos.
412
+ Reading a repository's hooks requires admin access to it, and fine-grained tokens also need the Webhooks read permission. Since the ``--all`` argument includes ``--hooks``, repositories you don't administer, such as starred repositories with ``--all-starred``, are skipped for hooks and the rest of the backup continues.
413
413
 
414
414
 
415
415
  "bare" is actually "mirror"
@@ -0,0 +1 @@
1
+ __version__ = "0.66.1"
@@ -1838,6 +1838,11 @@ def retrieve_repositories(args, authenticated_user):
1838
1838
  if args.user == authenticated_user["login"]:
1839
1839
  # we must use the /user/repos API to be able to access private repos
1840
1840
  template = "https://{0}/user/repos".format(get_github_api_host(args))
1841
+ elif args.as_app and not args.organization:
1842
+ # /users/{user}/repos is public-only; the installation sees private repos too
1843
+ template = "https://{0}/installation/repositories".format(
1844
+ get_github_api_host(args)
1845
+ )
1841
1846
  else:
1842
1847
  if args.private and not args.organization:
1843
1848
  logger.warning(
@@ -1868,6 +1873,10 @@ def retrieve_repositories(args, authenticated_user):
1868
1873
  logger.warning(f"Legal notice: {e.legal_url}")
1869
1874
  return []
1870
1875
 
1876
+ if template.endswith("/installation/repositories"):
1877
+ # Each page is {"total_count": ..., "repositories": [...]}
1878
+ repos = [repo for page in repos for repo in page["repositories"]]
1879
+
1871
1880
  return retrieve_additional_repositories(args, authenticated_user, repos)
1872
1881
 
1873
1882
 
@@ -3146,6 +3155,15 @@ def backup_hooks(args, repo_cwd, repository, repos_template):
3146
3155
  if not auth:
3147
3156
  logger.info("Skipping hooks since no authentication provided")
3148
3157
  return
3158
+ # Reading hooks needs admin access, and GitHub returns 404 without it.
3159
+ # Skip the request when the repository listing says we aren't an admin.
3160
+ if repository.get("permissions", {}).get("admin") is False:
3161
+ logger.info(
3162
+ "Skipping hooks for {0} since admin access is required".format(
3163
+ repository["full_name"]
3164
+ )
3165
+ )
3166
+ return
3149
3167
  hook_cwd = os.path.join(repo_cwd, "hooks")
3150
3168
  output_file = "{0}/hooks.json".format(hook_cwd)
3151
3169
  template = "{0}/{1}/hooks".format(repos_template, repository["full_name"])
@@ -3153,7 +3171,12 @@ def backup_hooks(args, repo_cwd, repository, repos_template):
3153
3171
  _backup_data(args, "hooks", template, output_file, hook_cwd)
3154
3172
  except HTTPError as e:
3155
3173
  if e.code == 404:
3156
- logger.info("Unable to read hooks, skipping")
3174
+ logger.info(
3175
+ "Unable to read hooks for {0}, skipping (requires admin access, "
3176
+ "or Webhooks read permission for fine-grained tokens)".format(
3177
+ repository["full_name"]
3178
+ )
3179
+ )
3157
3180
  else:
3158
3181
  raise e
3159
3182
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-backup
3
- Version: 0.65.1
3
+ Version: 0.66.1
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
@@ -229,6 +229,8 @@ GitHub Apps
229
229
 
230
230
  GitHub Apps are ideal for organization backups in CI/CD. Tokens are scoped to specific repositories and expire after 1 hour.
231
231
 
232
+ For a user account (no ``--organization``), ``--as-app`` lists the repositories the installation can access, so private repositories are included.
233
+
232
234
  **One-time setup:**
233
235
 
234
236
  1. Create a GitHub App at *Settings -> Developer Settings -> GitHub Apps -> New GitHub App*
@@ -435,11 +437,9 @@ If the incremental argument is used, per-resource checkpoints are only advanced
435
437
 
436
438
  It's therefore recommended to only use the incremental argument if the output/result is being actively monitored, or complimented with periodic full non-incremental runs, to avoid unexpected missing data in a regular backup runs.
437
439
 
438
- **Starred public repo hooks blocking**
439
-
440
- Since the ``--all`` argument includes ``--hooks``, if you use ``--all`` and ``--all-starred`` together to clone a users starred public repositories, the backup will likely error and block the backup continuing.
440
+ **Hooks need admin access**
441
441
 
442
- This is due to needing the correct permission for ``--hooks`` on public repos.
442
+ Reading a repository's hooks requires admin access to it, and fine-grained tokens also need the Webhooks read permission. Since the ``--all`` argument includes ``--hooks``, repositories you don't administer, such as starred repositories with ``--all-starred``, are skipped for hooks and the rest of the backup continues.
443
443
 
444
444
 
445
445
  "bare" is actually "mirror"
@@ -18,12 +18,14 @@ github_backup.egg-info/entry_points.txt
18
18
  github_backup.egg-info/top_level.txt
19
19
  github_backup.egg-info/zip-safe
20
20
  tests/test_all_starred.py
21
+ tests/test_app_repositories.py
21
22
  tests/test_attachments.py
22
23
  tests/test_auth.py
23
24
  tests/test_case_sensitivity.py
24
25
  tests/test_disabled_issues_pulls.py
25
26
  tests/test_discussions.py
26
27
  tests/test_gists.py
28
+ tests/test_hooks.py
27
29
  tests/test_http_451.py
28
30
  tests/test_incremental_per_repository.py
29
31
  tests/test_issue_timeline.py
@@ -1,15 +1,15 @@
1
1
  # Linting & Formatting
2
2
  autopep8==2.3.2
3
- black==26.5.1
4
- flake8==7.3.0
3
+ black==26.10.1
4
+ flake8==7.4.1
5
5
 
6
6
  # Testing
7
- pytest==9.1.1
7
+ pytest==9.2.0
8
8
 
9
9
  # Release & Publishing
10
10
  twine==7.0.0
11
11
  gitchangelog==3.0.4
12
- setuptools==83.0.0
12
+ setuptools==84.2.1
13
13
 
14
14
  # Documentation
15
15
  restructuredtext-lint==2.0.2
@@ -0,0 +1,71 @@
1
+ """Tests for listing a user's repositories with a GitHub App installation token."""
2
+
3
+ from unittest.mock import patch
4
+
5
+ import pytest
6
+
7
+ from github_backup import github_backup
8
+
9
+ NO_USER = {"login": None}
10
+
11
+
12
+ def page(*names):
13
+ return {
14
+ "total_count": 3,
15
+ "repositories": [{"name": name, "private": True} for name in names],
16
+ }
17
+
18
+
19
+ @pytest.fixture
20
+ def mock_retrieve_data():
21
+ with patch("github_backup.github_backup.retrieve_data") as mock:
22
+ yield mock
23
+
24
+
25
+ def test_app_lists_user_repositories_from_installation(create_args, mock_retrieve_data):
26
+ args = create_args(as_app=True, include_repository=True)
27
+ mock_retrieve_data.return_value = [page("a", "b"), page("c")]
28
+
29
+ repos = github_backup.retrieve_repositories(args, NO_USER)
30
+
31
+ assert mock_retrieve_data.call_args.args[1] == (
32
+ "https://api.github.com/installation/repositories"
33
+ )
34
+ assert [r["name"] for r in repos] == ["a", "b", "c"]
35
+
36
+
37
+ def test_app_installation_listing_uses_ghes_host(create_args, mock_retrieve_data):
38
+ args = create_args(
39
+ as_app=True, include_repository=True, github_host="ghe.example.com"
40
+ )
41
+ mock_retrieve_data.return_value = [page("a")]
42
+
43
+ github_backup.retrieve_repositories(args, NO_USER)
44
+
45
+ assert mock_retrieve_data.call_args.args[1] == (
46
+ "https://ghe.example.com/api/v3/installation/repositories"
47
+ )
48
+
49
+
50
+ def test_app_organization_still_lists_org_repositories(create_args, mock_retrieve_data):
51
+ args = create_args(as_app=True, include_repository=True, organization=True)
52
+ mock_retrieve_data.return_value = [{"name": "a"}]
53
+
54
+ repos = github_backup.retrieve_repositories(args, NO_USER)
55
+
56
+ assert mock_retrieve_data.call_args.args[1] == (
57
+ "https://api.github.com/orgs/testuser/repos"
58
+ )
59
+ assert repos == [{"name": "a"}]
60
+
61
+
62
+ def test_app_single_repository_is_fetched_directly(create_args, mock_retrieve_data):
63
+ args = create_args(as_app=True, repository="dotfiles")
64
+ mock_retrieve_data.return_value = [{"name": "dotfiles"}]
65
+
66
+ repos = github_backup.retrieve_repositories(args, NO_USER)
67
+
68
+ assert mock_retrieve_data.call_args.args[1] == (
69
+ "https://api.github.com/repos/testuser/dotfiles"
70
+ )
71
+ assert repos == [{"name": "dotfiles"}]
@@ -0,0 +1,96 @@
1
+ """Tests for hooks backup permissions (GitHub issue #532).
2
+
3
+ Reading ``/repos/{owner}/{repo}/hooks`` needs admin access to the repository.
4
+ Without it GitHub responds with HTTP 404 rather than 403, which used to log an
5
+ API error for every repository the user doesn't administer.
6
+ """
7
+
8
+ import logging
9
+ from urllib.error import HTTPError
10
+
11
+ import pytest
12
+
13
+ from github_backup import github_backup
14
+
15
+ REPOS_TEMPLATE = "https://api.github.com/repos"
16
+
17
+
18
+ @pytest.fixture
19
+ def hook_args(create_args):
20
+ return create_args(include_hooks=True, token_classic="ghp_token")
21
+
22
+
23
+ def _repository(**fields):
24
+ return {"full_name": "owner/repo", **fields}
25
+
26
+
27
+ def test_hooks_skipped_without_request_when_not_admin(
28
+ hook_args, tmp_path, monkeypatch, caplog
29
+ ):
30
+ def fail_retrieve_data(*args, **kwargs):
31
+ raise AssertionError("hooks should not be requested without admin access")
32
+
33
+ monkeypatch.setattr(github_backup, "retrieve_data", fail_retrieve_data)
34
+
35
+ with caplog.at_level(logging.INFO, logger=github_backup.logger.name):
36
+ github_backup.backup_hooks(
37
+ hook_args,
38
+ tmp_path,
39
+ _repository(permissions={"admin": False, "push": True, "pull": True}),
40
+ REPOS_TEMPLATE,
41
+ )
42
+
43
+ assert "Skipping hooks for owner/repo since admin access is required" in caplog.text
44
+ assert not (tmp_path / "hooks").exists()
45
+
46
+
47
+ @pytest.mark.parametrize(
48
+ "repository",
49
+ [
50
+ _repository(permissions={"admin": True, "push": True, "pull": True}),
51
+ _repository(),
52
+ ],
53
+ ids=["admin", "no-permissions-field"],
54
+ )
55
+ def test_hooks_requested_when_admin_or_permissions_unknown(
56
+ hook_args, tmp_path, monkeypatch, repository
57
+ ):
58
+ requested = []
59
+
60
+ def fake_retrieve_data(passed_args, template, *args, **kwargs):
61
+ requested.append(template)
62
+ return [{"id": 1, "name": "web"}]
63
+
64
+ monkeypatch.setattr(github_backup, "retrieve_data", fake_retrieve_data)
65
+
66
+ github_backup.backup_hooks(hook_args, tmp_path, repository, REPOS_TEMPLATE)
67
+
68
+ assert requested == ["https://api.github.com/repos/owner/repo/hooks"]
69
+ assert (tmp_path / "hooks" / "hooks.json").exists()
70
+
71
+
72
+ def test_hooks_404_is_skipped_with_reason(hook_args, tmp_path, monkeypatch, caplog):
73
+ def fake_retrieve_data(passed_args, template, *args, **kwargs):
74
+ raise HTTPError(template, 404, "Not Found", hdrs=None, fp=None)
75
+
76
+ monkeypatch.setattr(github_backup, "retrieve_data", fake_retrieve_data)
77
+
78
+ with caplog.at_level(logging.INFO, logger=github_backup.logger.name):
79
+ github_backup.backup_hooks(
80
+ hook_args,
81
+ tmp_path,
82
+ _repository(permissions={"admin": True}),
83
+ REPOS_TEMPLATE,
84
+ )
85
+
86
+ assert "Unable to read hooks for owner/repo, skipping" in caplog.text
87
+
88
+
89
+ def test_hooks_non_404_error_propagates(hook_args, tmp_path, monkeypatch):
90
+ def fake_retrieve_data(passed_args, template, *args, **kwargs):
91
+ raise HTTPError(template, 500, "Server Error", hdrs=None, fp=None)
92
+
93
+ monkeypatch.setattr(github_backup, "retrieve_data", fake_retrieve_data)
94
+
95
+ with pytest.raises(HTTPError):
96
+ github_backup.backup_hooks(hook_args, tmp_path, _repository(), REPOS_TEMPLATE)
@@ -1 +0,0 @@
1
- __version__ = "0.65.1"
File without changes
File without changes