github-backup 0.56.0__tar.gz → 0.57.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.56.0 → github_backup-0.57.0}/CHANGES.rst +32 -1
- {github_backup-0.56.0/github_backup.egg-info → github_backup-0.57.0}/PKG-INFO +40 -3
- {github_backup-0.56.0 → github_backup-0.57.0}/README +39 -2
- {github_backup-0.56.0 → github_backup-0.57.0}/README.rst +39 -2
- github_backup-0.57.0/github_backup/__init__.py +1 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/github_backup/github_backup.py +72 -33
- {github_backup-0.56.0 → github_backup-0.57.0/github_backup.egg-info}/PKG-INFO +40 -3
- {github_backup-0.56.0 → github_backup-0.57.0}/github_backup.egg-info/SOURCES.txt +2 -1
- github_backup-0.57.0/tests/test_skip_assets_on.py +320 -0
- github_backup-0.56.0/github_backup/__init__.py +0 -1
- {github_backup-0.56.0 → github_backup-0.57.0}/LICENSE.txt +0 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/MANIFEST.in +0 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/github_backup/__main__.py +0 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/github_backup/cli.py +0 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/github_backup.egg-info/dependency_links.txt +0 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/github_backup.egg-info/entry_points.txt +0 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/github_backup.egg-info/top_level.txt +0 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/github_backup.egg-info/zip-safe +0 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/release-requirements.txt +0 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/requirements.txt +0 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/setup.cfg +0 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/setup.py +0 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/tests/test_all_starred.py +0 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/tests/test_attachments.py +0 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/tests/test_case_sensitivity.py +0 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/tests/test_http_451.py +0 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/tests/test_json_dump_if_changed.py +0 -0
- {github_backup-0.56.0 → github_backup-0.57.0}/tests/test_pagination.py +0 -0
|
@@ -1,9 +1,40 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
-
0.
|
|
4
|
+
0.57.0 (2025-12-12)
|
|
5
5
|
-------------------
|
|
6
6
|
------------------------
|
|
7
|
+
- Add GitHub Apps documentation and remove outdated header. [Rodos]
|
|
8
|
+
|
|
9
|
+
- Add GitHub Apps authentication section with setup steps
|
|
10
|
+
and CI/CD workflow example using actions/create-github-app-token
|
|
11
|
+
- Remove outdated machine-man-preview header (graduated 2020)
|
|
12
|
+
|
|
13
|
+
Closes #189
|
|
14
|
+
- Docs: add stdin token example to README. [Rodos]
|
|
15
|
+
|
|
16
|
+
Add example showing how to pipe a token from stdin using
|
|
17
|
+
file:///dev/stdin to avoid storing tokens in environment
|
|
18
|
+
variables or command history.
|
|
19
|
+
|
|
20
|
+
Closes #187
|
|
21
|
+
- Add --skip-assets-on flag to skip release asset downloads (#135)
|
|
22
|
+
[Rodos]
|
|
23
|
+
|
|
24
|
+
Allow users to skip downloading release assets for specific repositories
|
|
25
|
+
while still backing up release metadata. Useful for starred repos with
|
|
26
|
+
large assets (e.g. syncthing with 27GB+).
|
|
27
|
+
|
|
28
|
+
Usage: --skip-assets-on repo1 repo2 owner/repo3
|
|
29
|
+
|
|
30
|
+
Features:
|
|
31
|
+
- Space-separated repos (consistent with --exclude)
|
|
32
|
+
- Case-insensitive matching
|
|
33
|
+
- Supports both repo name and owner/repo format
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
0.56.0 (2025-12-11)
|
|
37
|
+
-------------------
|
|
7
38
|
|
|
8
39
|
Fix
|
|
9
40
|
~~~
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-backup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.57.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
|
|
@@ -80,8 +80,8 @@ CLI Help output::
|
|
|
80
80
|
[--keychain-name OSX_KEYCHAIN_ITEM_NAME]
|
|
81
81
|
[--keychain-account OSX_KEYCHAIN_ITEM_ACCOUNT]
|
|
82
82
|
[--releases] [--latest-releases NUMBER_OF_LATEST_RELEASES]
|
|
83
|
-
[--skip-prerelease] [--assets] [--
|
|
84
|
-
[--exclude [REPOSITORY [REPOSITORY ...]]
|
|
83
|
+
[--skip-prerelease] [--assets] [--skip-assets-on [REPO ...]]
|
|
84
|
+
[--attachments] [--exclude [REPOSITORY [REPOSITORY ...]]
|
|
85
85
|
[--throttle-limit THROTTLE_LIMIT] [--throttle-pause THROTTLE_PAUSE]
|
|
86
86
|
USER
|
|
87
87
|
|
|
@@ -163,6 +163,9 @@ CLI Help output::
|
|
|
163
163
|
--skip-prerelease skip prerelease and draft versions; only applies if including releases
|
|
164
164
|
--assets include assets alongside release information; only
|
|
165
165
|
applies if including releases
|
|
166
|
+
--skip-assets-on [REPO ...]
|
|
167
|
+
skip asset downloads for these repositories (e.g.
|
|
168
|
+
--skip-assets-on repo1 owner/repo2)
|
|
166
169
|
--attachments download user-attachments from issues and pull requests
|
|
167
170
|
to issues/attachments/{issue_number}/ and
|
|
168
171
|
pulls/attachments/{pull_number}/ directories
|
|
@@ -204,6 +207,37 @@ Customise the permissions for your use case, but for a personal account full bac
|
|
|
204
207
|
**Repository permissions**: Read access to contents, issues, metadata, pull requests, and webhooks.
|
|
205
208
|
|
|
206
209
|
|
|
210
|
+
GitHub Apps
|
|
211
|
+
~~~~~~~~~~~
|
|
212
|
+
|
|
213
|
+
GitHub Apps are ideal for organization backups in CI/CD. Tokens are scoped to specific repositories and expire after 1 hour.
|
|
214
|
+
|
|
215
|
+
**One-time setup:**
|
|
216
|
+
|
|
217
|
+
1. Create a GitHub App at *Settings -> Developer Settings -> GitHub Apps -> New GitHub App*
|
|
218
|
+
2. Set a name and homepage URL (can be any URL)
|
|
219
|
+
3. Uncheck "Webhook > Active" (not needed for backups)
|
|
220
|
+
4. Set permissions (same as fine-grained tokens above)
|
|
221
|
+
5. Click "Create GitHub App", then note the **App ID** shown on the next page
|
|
222
|
+
6. Under "Private keys", click "Generate a private key" and save the downloaded file
|
|
223
|
+
7. Go to *Install App* in your app's settings
|
|
224
|
+
8. Select the account/organization and which repositories to back up
|
|
225
|
+
|
|
226
|
+
**CI/CD usage with GitHub Actions:**
|
|
227
|
+
|
|
228
|
+
Store the App ID as a repository variable and the private key contents as a secret, then use ``actions/create-github-app-token``::
|
|
229
|
+
|
|
230
|
+
- uses: actions/create-github-app-token@v1
|
|
231
|
+
id: app-token
|
|
232
|
+
with:
|
|
233
|
+
app-id: ${{ vars.APP_ID }}
|
|
234
|
+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
235
|
+
|
|
236
|
+
- run: github-backup myorg -t ${{ steps.app-token.outputs.token }} --as-app -o ./backup --all
|
|
237
|
+
|
|
238
|
+
Note: Installation tokens expire after 1 hour. For long-running backups, use a fine-grained personal access token instead.
|
|
239
|
+
|
|
240
|
+
|
|
207
241
|
Prefer SSH
|
|
208
242
|
~~~~~~~~~~
|
|
209
243
|
|
|
@@ -391,6 +425,9 @@ Debug an error/block or incomplete backup into a temporary directory. Omit "incr
|
|
|
391
425
|
|
|
392
426
|
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-commits --labels --milestones --repositories --wikis --releases --assets --pull-details --gists --starred-gists $GH_USER
|
|
393
427
|
|
|
428
|
+
Pipe a token from stdin to avoid storing it in environment variables or command history (Unix-like systems only)::
|
|
429
|
+
|
|
430
|
+
my-secret-manager get github-token | github-backup user -t file:///dev/stdin -o /backup --repositories
|
|
394
431
|
|
|
395
432
|
Restoring from Backup
|
|
396
433
|
=====================
|
|
@@ -50,8 +50,8 @@ CLI Help output::
|
|
|
50
50
|
[--keychain-name OSX_KEYCHAIN_ITEM_NAME]
|
|
51
51
|
[--keychain-account OSX_KEYCHAIN_ITEM_ACCOUNT]
|
|
52
52
|
[--releases] [--latest-releases NUMBER_OF_LATEST_RELEASES]
|
|
53
|
-
[--skip-prerelease] [--assets] [--
|
|
54
|
-
[--exclude [REPOSITORY [REPOSITORY ...]]
|
|
53
|
+
[--skip-prerelease] [--assets] [--skip-assets-on [REPO ...]]
|
|
54
|
+
[--attachments] [--exclude [REPOSITORY [REPOSITORY ...]]
|
|
55
55
|
[--throttle-limit THROTTLE_LIMIT] [--throttle-pause THROTTLE_PAUSE]
|
|
56
56
|
USER
|
|
57
57
|
|
|
@@ -133,6 +133,9 @@ CLI Help output::
|
|
|
133
133
|
--skip-prerelease skip prerelease and draft versions; only applies if including releases
|
|
134
134
|
--assets include assets alongside release information; only
|
|
135
135
|
applies if including releases
|
|
136
|
+
--skip-assets-on [REPO ...]
|
|
137
|
+
skip asset downloads for these repositories (e.g.
|
|
138
|
+
--skip-assets-on repo1 owner/repo2)
|
|
136
139
|
--attachments download user-attachments from issues and pull requests
|
|
137
140
|
to issues/attachments/{issue_number}/ and
|
|
138
141
|
pulls/attachments/{pull_number}/ directories
|
|
@@ -174,6 +177,37 @@ Customise the permissions for your use case, but for a personal account full bac
|
|
|
174
177
|
**Repository permissions**: Read access to contents, issues, metadata, pull requests, and webhooks.
|
|
175
178
|
|
|
176
179
|
|
|
180
|
+
GitHub Apps
|
|
181
|
+
~~~~~~~~~~~
|
|
182
|
+
|
|
183
|
+
GitHub Apps are ideal for organization backups in CI/CD. Tokens are scoped to specific repositories and expire after 1 hour.
|
|
184
|
+
|
|
185
|
+
**One-time setup:**
|
|
186
|
+
|
|
187
|
+
1. Create a GitHub App at *Settings -> Developer Settings -> GitHub Apps -> New GitHub App*
|
|
188
|
+
2. Set a name and homepage URL (can be any URL)
|
|
189
|
+
3. Uncheck "Webhook > Active" (not needed for backups)
|
|
190
|
+
4. Set permissions (same as fine-grained tokens above)
|
|
191
|
+
5. Click "Create GitHub App", then note the **App ID** shown on the next page
|
|
192
|
+
6. Under "Private keys", click "Generate a private key" and save the downloaded file
|
|
193
|
+
7. Go to *Install App* in your app's settings
|
|
194
|
+
8. Select the account/organization and which repositories to back up
|
|
195
|
+
|
|
196
|
+
**CI/CD usage with GitHub Actions:**
|
|
197
|
+
|
|
198
|
+
Store the App ID as a repository variable and the private key contents as a secret, then use ``actions/create-github-app-token``::
|
|
199
|
+
|
|
200
|
+
- uses: actions/create-github-app-token@v1
|
|
201
|
+
id: app-token
|
|
202
|
+
with:
|
|
203
|
+
app-id: ${{ vars.APP_ID }}
|
|
204
|
+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
205
|
+
|
|
206
|
+
- run: github-backup myorg -t ${{ steps.app-token.outputs.token }} --as-app -o ./backup --all
|
|
207
|
+
|
|
208
|
+
Note: Installation tokens expire after 1 hour. For long-running backups, use a fine-grained personal access token instead.
|
|
209
|
+
|
|
210
|
+
|
|
177
211
|
Prefer SSH
|
|
178
212
|
~~~~~~~~~~
|
|
179
213
|
|
|
@@ -361,6 +395,9 @@ Debug an error/block or incomplete backup into a temporary directory. Omit "incr
|
|
|
361
395
|
|
|
362
396
|
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-commits --labels --milestones --repositories --wikis --releases --assets --pull-details --gists --starred-gists $GH_USER
|
|
363
397
|
|
|
398
|
+
Pipe a token from stdin to avoid storing it in environment variables or command history (Unix-like systems only)::
|
|
399
|
+
|
|
400
|
+
my-secret-manager get github-token | github-backup user -t file:///dev/stdin -o /backup --repositories
|
|
364
401
|
|
|
365
402
|
Restoring from Backup
|
|
366
403
|
=====================
|
|
@@ -50,8 +50,8 @@ CLI Help output::
|
|
|
50
50
|
[--keychain-name OSX_KEYCHAIN_ITEM_NAME]
|
|
51
51
|
[--keychain-account OSX_KEYCHAIN_ITEM_ACCOUNT]
|
|
52
52
|
[--releases] [--latest-releases NUMBER_OF_LATEST_RELEASES]
|
|
53
|
-
[--skip-prerelease] [--assets] [--
|
|
54
|
-
[--exclude [REPOSITORY [REPOSITORY ...]]
|
|
53
|
+
[--skip-prerelease] [--assets] [--skip-assets-on [REPO ...]]
|
|
54
|
+
[--attachments] [--exclude [REPOSITORY [REPOSITORY ...]]
|
|
55
55
|
[--throttle-limit THROTTLE_LIMIT] [--throttle-pause THROTTLE_PAUSE]
|
|
56
56
|
USER
|
|
57
57
|
|
|
@@ -133,6 +133,9 @@ CLI Help output::
|
|
|
133
133
|
--skip-prerelease skip prerelease and draft versions; only applies if including releases
|
|
134
134
|
--assets include assets alongside release information; only
|
|
135
135
|
applies if including releases
|
|
136
|
+
--skip-assets-on [REPO ...]
|
|
137
|
+
skip asset downloads for these repositories (e.g.
|
|
138
|
+
--skip-assets-on repo1 owner/repo2)
|
|
136
139
|
--attachments download user-attachments from issues and pull requests
|
|
137
140
|
to issues/attachments/{issue_number}/ and
|
|
138
141
|
pulls/attachments/{pull_number}/ directories
|
|
@@ -174,6 +177,37 @@ Customise the permissions for your use case, but for a personal account full bac
|
|
|
174
177
|
**Repository permissions**: Read access to contents, issues, metadata, pull requests, and webhooks.
|
|
175
178
|
|
|
176
179
|
|
|
180
|
+
GitHub Apps
|
|
181
|
+
~~~~~~~~~~~
|
|
182
|
+
|
|
183
|
+
GitHub Apps are ideal for organization backups in CI/CD. Tokens are scoped to specific repositories and expire after 1 hour.
|
|
184
|
+
|
|
185
|
+
**One-time setup:**
|
|
186
|
+
|
|
187
|
+
1. Create a GitHub App at *Settings -> Developer Settings -> GitHub Apps -> New GitHub App*
|
|
188
|
+
2. Set a name and homepage URL (can be any URL)
|
|
189
|
+
3. Uncheck "Webhook > Active" (not needed for backups)
|
|
190
|
+
4. Set permissions (same as fine-grained tokens above)
|
|
191
|
+
5. Click "Create GitHub App", then note the **App ID** shown on the next page
|
|
192
|
+
6. Under "Private keys", click "Generate a private key" and save the downloaded file
|
|
193
|
+
7. Go to *Install App* in your app's settings
|
|
194
|
+
8. Select the account/organization and which repositories to back up
|
|
195
|
+
|
|
196
|
+
**CI/CD usage with GitHub Actions:**
|
|
197
|
+
|
|
198
|
+
Store the App ID as a repository variable and the private key contents as a secret, then use ``actions/create-github-app-token``::
|
|
199
|
+
|
|
200
|
+
- uses: actions/create-github-app-token@v1
|
|
201
|
+
id: app-token
|
|
202
|
+
with:
|
|
203
|
+
app-id: ${{ vars.APP_ID }}
|
|
204
|
+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
205
|
+
|
|
206
|
+
- run: github-backup myorg -t ${{ steps.app-token.outputs.token }} --as-app -o ./backup --all
|
|
207
|
+
|
|
208
|
+
Note: Installation tokens expire after 1 hour. For long-running backups, use a fine-grained personal access token instead.
|
|
209
|
+
|
|
210
|
+
|
|
177
211
|
Prefer SSH
|
|
178
212
|
~~~~~~~~~~
|
|
179
213
|
|
|
@@ -361,6 +395,9 @@ Debug an error/block or incomplete backup into a temporary directory. Omit "incr
|
|
|
361
395
|
|
|
362
396
|
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-commits --labels --milestones --repositories --wikis --releases --assets --pull-details --gists --starred-gists $GH_USER
|
|
363
397
|
|
|
398
|
+
Pipe a token from stdin to avoid storing it in environment variables or command history (Unix-like systems only)::
|
|
399
|
+
|
|
400
|
+
my-secret-manager get github-token | github-backup user -t file:///dev/stdin -o /backup --repositories
|
|
364
401
|
|
|
365
402
|
Restoring from Backup
|
|
366
403
|
=====================
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.57.0"
|
|
@@ -440,6 +440,12 @@ def parse_args(args=None):
|
|
|
440
440
|
dest="include_assets",
|
|
441
441
|
help="include assets alongside release information; only applies if including releases",
|
|
442
442
|
)
|
|
443
|
+
parser.add_argument(
|
|
444
|
+
"--skip-assets-on",
|
|
445
|
+
dest="skip_assets_on",
|
|
446
|
+
nargs="*",
|
|
447
|
+
help="skip asset downloads for these repositories",
|
|
448
|
+
)
|
|
443
449
|
parser.add_argument(
|
|
444
450
|
"--attachments",
|
|
445
451
|
action="store_true",
|
|
@@ -561,7 +567,7 @@ def get_github_host(args):
|
|
|
561
567
|
|
|
562
568
|
|
|
563
569
|
def read_file_contents(file_uri):
|
|
564
|
-
return open(file_uri[len(FILE_URI_PREFIX):], "rt").readline().strip()
|
|
570
|
+
return open(file_uri[len(FILE_URI_PREFIX) :], "rt").readline().strip()
|
|
565
571
|
|
|
566
572
|
|
|
567
573
|
def get_github_repo_url(args, repository):
|
|
@@ -631,7 +637,7 @@ def retrieve_data_gen(args, template, query_args=None, single_request=False):
|
|
|
631
637
|
pass
|
|
632
638
|
raise RepositoryUnavailableError(
|
|
633
639
|
"Repository unavailable due to legal reasons (HTTP 451)",
|
|
634
|
-
dmca_url=dmca_url
|
|
640
|
+
dmca_url=dmca_url,
|
|
635
641
|
)
|
|
636
642
|
|
|
637
643
|
# Check if we got correct data
|
|
@@ -709,7 +715,7 @@ def retrieve_data_gen(args, template, query_args=None, single_request=False):
|
|
|
709
715
|
# Parse Link header: <https://api.github.com/...?per_page=100&after=cursor>; rel="next"
|
|
710
716
|
for link in link_header.split(","):
|
|
711
717
|
if 'rel="next"' in link:
|
|
712
|
-
next_url = link[link.find("<") + 1:link.find(">")]
|
|
718
|
+
next_url = link[link.find("<") + 1 : link.find(">")]
|
|
713
719
|
break
|
|
714
720
|
if not next_url:
|
|
715
721
|
break
|
|
@@ -763,9 +769,7 @@ def _get_response(request, auth, template):
|
|
|
763
769
|
return r, errors
|
|
764
770
|
|
|
765
771
|
|
|
766
|
-
def _construct_request(
|
|
767
|
-
per_page, query_args, template, auth, as_app=None, fine=False
|
|
768
|
-
):
|
|
772
|
+
def _construct_request(per_page, query_args, template, auth, as_app=None, fine=False):
|
|
769
773
|
# If template is already a full URL with query params (from Link header), use it directly
|
|
770
774
|
if "?" in template and template.startswith("http"):
|
|
771
775
|
request_url = template
|
|
@@ -796,9 +800,6 @@ def _construct_request(
|
|
|
796
800
|
else:
|
|
797
801
|
auth = auth.encode("ascii")
|
|
798
802
|
request.add_header("Authorization", "token ".encode("ascii") + auth)
|
|
799
|
-
request.add_header(
|
|
800
|
-
"Accept", "application/vnd.github.machine-man-preview+json"
|
|
801
|
-
)
|
|
802
803
|
|
|
803
804
|
log_url = template if "?" not in template else template.split("?")[0]
|
|
804
805
|
if querystring:
|
|
@@ -1480,9 +1481,11 @@ def download_attachments(
|
|
|
1480
1481
|
manifest = {
|
|
1481
1482
|
"issue_number": number,
|
|
1482
1483
|
"issue_type": item_type,
|
|
1483
|
-
"repository":
|
|
1484
|
-
|
|
1485
|
-
|
|
1484
|
+
"repository": (
|
|
1485
|
+
f"{args.user}/{args.repository}"
|
|
1486
|
+
if hasattr(args, "repository") and args.repository
|
|
1487
|
+
else args.user
|
|
1488
|
+
),
|
|
1486
1489
|
"manifest_updated_at": datetime.now(timezone.utc).isoformat(),
|
|
1487
1490
|
"attachments": attachment_metadata_list,
|
|
1488
1491
|
}
|
|
@@ -1538,9 +1541,7 @@ def retrieve_repositories(args, authenticated_user):
|
|
|
1538
1541
|
else:
|
|
1539
1542
|
repo_path = "{0}/{1}".format(args.user, args.repository)
|
|
1540
1543
|
single_request = True
|
|
1541
|
-
template = "https://{0}/repos/{1}".format(
|
|
1542
|
-
get_github_api_host(args), repo_path
|
|
1543
|
-
)
|
|
1544
|
+
template = "https://{0}/repos/{1}".format(get_github_api_host(args), repo_path)
|
|
1544
1545
|
|
|
1545
1546
|
repos = retrieve_data(args, template, single_request=single_request)
|
|
1546
1547
|
|
|
@@ -1565,7 +1566,10 @@ def retrieve_repositories(args, authenticated_user):
|
|
|
1565
1566
|
repos.extend(gists)
|
|
1566
1567
|
|
|
1567
1568
|
if args.include_starred_gists:
|
|
1568
|
-
if
|
|
1569
|
+
if (
|
|
1570
|
+
not authenticated_user.get("login")
|
|
1571
|
+
or args.user.lower() != authenticated_user["login"].lower()
|
|
1572
|
+
):
|
|
1569
1573
|
logger.warning(
|
|
1570
1574
|
"Cannot retrieve starred gists for '%s'. GitHub only allows access to the authenticated user's starred gists.",
|
|
1571
1575
|
args.user,
|
|
@@ -1673,9 +1677,11 @@ def backup_repositories(args, output_directory, repositories):
|
|
|
1673
1677
|
|
|
1674
1678
|
include_gists = args.include_gists or args.include_starred_gists
|
|
1675
1679
|
include_starred = args.all_starred and repository.get("is_starred")
|
|
1676
|
-
if (
|
|
1677
|
-
|
|
1678
|
-
|
|
1680
|
+
if (
|
|
1681
|
+
(args.include_repository or args.include_everything)
|
|
1682
|
+
or (include_gists and repository.get("is_gist"))
|
|
1683
|
+
or include_starred
|
|
1684
|
+
):
|
|
1679
1685
|
repo_name = (
|
|
1680
1686
|
repository.get("name")
|
|
1681
1687
|
if not repository.get("is_gist")
|
|
@@ -1735,7 +1741,9 @@ def backup_repositories(args, output_directory, repositories):
|
|
|
1735
1741
|
include_assets=args.include_assets or args.include_everything,
|
|
1736
1742
|
)
|
|
1737
1743
|
except RepositoryUnavailableError as e:
|
|
1738
|
-
logger.warning(
|
|
1744
|
+
logger.warning(
|
|
1745
|
+
f"Repository {repository['full_name']} is unavailable (HTTP 451)"
|
|
1746
|
+
)
|
|
1739
1747
|
if e.dmca_url:
|
|
1740
1748
|
logger.warning(f"DMCA notice: {e.dmca_url}")
|
|
1741
1749
|
logger.info(f"Skipping remaining resources for {repository['full_name']}")
|
|
@@ -1795,7 +1803,11 @@ def backup_issues(args, repo_cwd, repository, repos_template):
|
|
|
1795
1803
|
modified = os.path.getmtime(issue_file)
|
|
1796
1804
|
modified = datetime.fromtimestamp(modified).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
1797
1805
|
if modified > issue["updated_at"]:
|
|
1798
|
-
logger.info(
|
|
1806
|
+
logger.info(
|
|
1807
|
+
"Skipping issue {0} because it wasn't modified since last backup".format(
|
|
1808
|
+
number
|
|
1809
|
+
)
|
|
1810
|
+
)
|
|
1799
1811
|
continue
|
|
1800
1812
|
|
|
1801
1813
|
if args.include_issue_comments or args.include_everything:
|
|
@@ -1869,7 +1881,11 @@ def backup_pulls(args, repo_cwd, repository, repos_template):
|
|
|
1869
1881
|
modified = os.path.getmtime(pull_file)
|
|
1870
1882
|
modified = datetime.fromtimestamp(modified).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
1871
1883
|
if modified > pull["updated_at"]:
|
|
1872
|
-
logger.info(
|
|
1884
|
+
logger.info(
|
|
1885
|
+
"Skipping pull request {0} because it wasn't modified since last backup".format(
|
|
1886
|
+
number
|
|
1887
|
+
)
|
|
1888
|
+
)
|
|
1873
1889
|
continue
|
|
1874
1890
|
if args.include_pull_comments or args.include_everything:
|
|
1875
1891
|
template = comments_regular_template.format(number)
|
|
@@ -1919,9 +1935,11 @@ def backup_milestones(args, repo_cwd, repository, repos_template):
|
|
|
1919
1935
|
elif written_count == 0:
|
|
1920
1936
|
logger.info("{0} milestones unchanged, skipped write".format(total))
|
|
1921
1937
|
else:
|
|
1922
|
-
logger.info(
|
|
1923
|
-
|
|
1924
|
-
|
|
1938
|
+
logger.info(
|
|
1939
|
+
"Saved {0} of {1} milestones to disk ({2} unchanged)".format(
|
|
1940
|
+
written_count, total, total - written_count
|
|
1941
|
+
)
|
|
1942
|
+
)
|
|
1925
1943
|
|
|
1926
1944
|
|
|
1927
1945
|
def backup_labels(args, repo_cwd, repository, repos_template):
|
|
@@ -1975,6 +1993,20 @@ def backup_releases(args, repo_cwd, repository, repos_template, include_assets=F
|
|
|
1975
1993
|
)
|
|
1976
1994
|
releases = releases[: args.number_of_latest_releases]
|
|
1977
1995
|
|
|
1996
|
+
# Check if this repo should skip asset downloads (case-insensitive)
|
|
1997
|
+
skip_assets = False
|
|
1998
|
+
if include_assets:
|
|
1999
|
+
repo_name = repository.get("name", "").lower()
|
|
2000
|
+
repo_full_name = repository.get("full_name", "").lower()
|
|
2001
|
+
skip_repos = [r.lower() for r in (args.skip_assets_on or [])]
|
|
2002
|
+
skip_assets = repo_name in skip_repos or repo_full_name in skip_repos
|
|
2003
|
+
if skip_assets:
|
|
2004
|
+
logger.info(
|
|
2005
|
+
"Skipping assets for {0} ({1} releases) due to --skip-assets-on".format(
|
|
2006
|
+
repository.get("name"), len(releases)
|
|
2007
|
+
)
|
|
2008
|
+
)
|
|
2009
|
+
|
|
1978
2010
|
# for each release, store it
|
|
1979
2011
|
written_count = 0
|
|
1980
2012
|
for release in releases:
|
|
@@ -1986,7 +2018,7 @@ def backup_releases(args, repo_cwd, repository, repos_template, include_assets=F
|
|
|
1986
2018
|
if json_dump_if_changed(release, output_filepath):
|
|
1987
2019
|
written_count += 1
|
|
1988
2020
|
|
|
1989
|
-
if include_assets:
|
|
2021
|
+
if include_assets and not skip_assets:
|
|
1990
2022
|
assets = retrieve_data(args, release["assets_url"])
|
|
1991
2023
|
if len(assets) > 0:
|
|
1992
2024
|
# give release asset files somewhere to live & download them (not including source archives)
|
|
@@ -2008,9 +2040,11 @@ def backup_releases(args, repo_cwd, repository, repos_template, include_assets=F
|
|
|
2008
2040
|
elif written_count == 0:
|
|
2009
2041
|
logger.info("{0} releases unchanged, skipped write".format(total))
|
|
2010
2042
|
else:
|
|
2011
|
-
logger.info(
|
|
2012
|
-
|
|
2013
|
-
|
|
2043
|
+
logger.info(
|
|
2044
|
+
"Saved {0} of {1} releases to disk ({2} unchanged)".format(
|
|
2045
|
+
written_count, total, total - written_count
|
|
2046
|
+
)
|
|
2047
|
+
)
|
|
2014
2048
|
|
|
2015
2049
|
|
|
2016
2050
|
def fetch_repository(
|
|
@@ -2024,9 +2058,12 @@ def fetch_repository(
|
|
|
2024
2058
|
):
|
|
2025
2059
|
if bare_clone:
|
|
2026
2060
|
if os.path.exists(local_dir):
|
|
2027
|
-
clone_exists =
|
|
2028
|
-
|
|
2029
|
-
|
|
2061
|
+
clone_exists = (
|
|
2062
|
+
subprocess.check_output(
|
|
2063
|
+
["git", "rev-parse", "--is-bare-repository"], cwd=local_dir
|
|
2064
|
+
)
|
|
2065
|
+
== b"true\n"
|
|
2066
|
+
)
|
|
2030
2067
|
else:
|
|
2031
2068
|
clone_exists = False
|
|
2032
2069
|
else:
|
|
@@ -2047,7 +2084,9 @@ def fetch_repository(
|
|
|
2047
2084
|
)
|
|
2048
2085
|
else:
|
|
2049
2086
|
logger.info(
|
|
2050
|
-
"Skipping {0} (repository not accessible - may be empty, private, or credentials invalid)".format(
|
|
2087
|
+
"Skipping {0} (repository not accessible - may be empty, private, or credentials invalid)".format(
|
|
2088
|
+
name
|
|
2089
|
+
)
|
|
2051
2090
|
)
|
|
2052
2091
|
return
|
|
2053
2092
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-backup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.57.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
|
|
@@ -80,8 +80,8 @@ CLI Help output::
|
|
|
80
80
|
[--keychain-name OSX_KEYCHAIN_ITEM_NAME]
|
|
81
81
|
[--keychain-account OSX_KEYCHAIN_ITEM_ACCOUNT]
|
|
82
82
|
[--releases] [--latest-releases NUMBER_OF_LATEST_RELEASES]
|
|
83
|
-
[--skip-prerelease] [--assets] [--
|
|
84
|
-
[--exclude [REPOSITORY [REPOSITORY ...]]
|
|
83
|
+
[--skip-prerelease] [--assets] [--skip-assets-on [REPO ...]]
|
|
84
|
+
[--attachments] [--exclude [REPOSITORY [REPOSITORY ...]]
|
|
85
85
|
[--throttle-limit THROTTLE_LIMIT] [--throttle-pause THROTTLE_PAUSE]
|
|
86
86
|
USER
|
|
87
87
|
|
|
@@ -163,6 +163,9 @@ CLI Help output::
|
|
|
163
163
|
--skip-prerelease skip prerelease and draft versions; only applies if including releases
|
|
164
164
|
--assets include assets alongside release information; only
|
|
165
165
|
applies if including releases
|
|
166
|
+
--skip-assets-on [REPO ...]
|
|
167
|
+
skip asset downloads for these repositories (e.g.
|
|
168
|
+
--skip-assets-on repo1 owner/repo2)
|
|
166
169
|
--attachments download user-attachments from issues and pull requests
|
|
167
170
|
to issues/attachments/{issue_number}/ and
|
|
168
171
|
pulls/attachments/{pull_number}/ directories
|
|
@@ -204,6 +207,37 @@ Customise the permissions for your use case, but for a personal account full bac
|
|
|
204
207
|
**Repository permissions**: Read access to contents, issues, metadata, pull requests, and webhooks.
|
|
205
208
|
|
|
206
209
|
|
|
210
|
+
GitHub Apps
|
|
211
|
+
~~~~~~~~~~~
|
|
212
|
+
|
|
213
|
+
GitHub Apps are ideal for organization backups in CI/CD. Tokens are scoped to specific repositories and expire after 1 hour.
|
|
214
|
+
|
|
215
|
+
**One-time setup:**
|
|
216
|
+
|
|
217
|
+
1. Create a GitHub App at *Settings -> Developer Settings -> GitHub Apps -> New GitHub App*
|
|
218
|
+
2. Set a name and homepage URL (can be any URL)
|
|
219
|
+
3. Uncheck "Webhook > Active" (not needed for backups)
|
|
220
|
+
4. Set permissions (same as fine-grained tokens above)
|
|
221
|
+
5. Click "Create GitHub App", then note the **App ID** shown on the next page
|
|
222
|
+
6. Under "Private keys", click "Generate a private key" and save the downloaded file
|
|
223
|
+
7. Go to *Install App* in your app's settings
|
|
224
|
+
8. Select the account/organization and which repositories to back up
|
|
225
|
+
|
|
226
|
+
**CI/CD usage with GitHub Actions:**
|
|
227
|
+
|
|
228
|
+
Store the App ID as a repository variable and the private key contents as a secret, then use ``actions/create-github-app-token``::
|
|
229
|
+
|
|
230
|
+
- uses: actions/create-github-app-token@v1
|
|
231
|
+
id: app-token
|
|
232
|
+
with:
|
|
233
|
+
app-id: ${{ vars.APP_ID }}
|
|
234
|
+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
|
235
|
+
|
|
236
|
+
- run: github-backup myorg -t ${{ steps.app-token.outputs.token }} --as-app -o ./backup --all
|
|
237
|
+
|
|
238
|
+
Note: Installation tokens expire after 1 hour. For long-running backups, use a fine-grained personal access token instead.
|
|
239
|
+
|
|
240
|
+
|
|
207
241
|
Prefer SSH
|
|
208
242
|
~~~~~~~~~~
|
|
209
243
|
|
|
@@ -391,6 +425,9 @@ Debug an error/block or incomplete backup into a temporary directory. Omit "incr
|
|
|
391
425
|
|
|
392
426
|
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-commits --labels --milestones --repositories --wikis --releases --assets --pull-details --gists --starred-gists $GH_USER
|
|
393
427
|
|
|
428
|
+
Pipe a token from stdin to avoid storing it in environment variables or command history (Unix-like systems only)::
|
|
429
|
+
|
|
430
|
+
my-secret-manager get github-token | github-backup user -t file:///dev/stdin -o /backup --repositories
|
|
394
431
|
|
|
395
432
|
Restoring from Backup
|
|
396
433
|
=====================
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Tests for --skip-assets-on flag behavior (issue #135)."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import Mock, patch
|
|
5
|
+
|
|
6
|
+
from github_backup import github_backup
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestSkipAssetsOn:
|
|
10
|
+
"""Test suite for --skip-assets-on flag.
|
|
11
|
+
|
|
12
|
+
Issue #135: Allow skipping asset downloads for specific repositories
|
|
13
|
+
while still backing up release metadata.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def _create_mock_args(self, **overrides):
|
|
17
|
+
"""Create a mock args object with sensible defaults."""
|
|
18
|
+
args = Mock()
|
|
19
|
+
args.user = "testuser"
|
|
20
|
+
args.output_directory = "/tmp/backup"
|
|
21
|
+
args.include_repository = False
|
|
22
|
+
args.include_everything = False
|
|
23
|
+
args.include_gists = False
|
|
24
|
+
args.include_starred_gists = False
|
|
25
|
+
args.all_starred = False
|
|
26
|
+
args.skip_existing = False
|
|
27
|
+
args.bare_clone = False
|
|
28
|
+
args.lfs_clone = False
|
|
29
|
+
args.no_prune = False
|
|
30
|
+
args.include_wiki = False
|
|
31
|
+
args.include_issues = False
|
|
32
|
+
args.include_issue_comments = False
|
|
33
|
+
args.include_issue_events = False
|
|
34
|
+
args.include_pulls = False
|
|
35
|
+
args.include_pull_comments = False
|
|
36
|
+
args.include_pull_commits = False
|
|
37
|
+
args.include_pull_details = False
|
|
38
|
+
args.include_labels = False
|
|
39
|
+
args.include_hooks = False
|
|
40
|
+
args.include_milestones = False
|
|
41
|
+
args.include_releases = True
|
|
42
|
+
args.include_assets = True
|
|
43
|
+
args.skip_assets_on = []
|
|
44
|
+
args.include_attachments = False
|
|
45
|
+
args.incremental = False
|
|
46
|
+
args.incremental_by_files = False
|
|
47
|
+
args.github_host = None
|
|
48
|
+
args.prefer_ssh = False
|
|
49
|
+
args.token_classic = "test-token"
|
|
50
|
+
args.token_fine = None
|
|
51
|
+
args.username = None
|
|
52
|
+
args.password = None
|
|
53
|
+
args.as_app = False
|
|
54
|
+
args.osx_keychain_item_name = None
|
|
55
|
+
args.osx_keychain_item_account = None
|
|
56
|
+
args.skip_prerelease = False
|
|
57
|
+
args.number_of_latest_releases = None
|
|
58
|
+
|
|
59
|
+
for key, value in overrides.items():
|
|
60
|
+
setattr(args, key, value)
|
|
61
|
+
|
|
62
|
+
return args
|
|
63
|
+
|
|
64
|
+
def _create_mock_repository(self, name="test-repo", owner="testuser"):
|
|
65
|
+
"""Create a mock repository object."""
|
|
66
|
+
return {
|
|
67
|
+
"name": name,
|
|
68
|
+
"full_name": f"{owner}/{name}",
|
|
69
|
+
"owner": {"login": owner},
|
|
70
|
+
"private": False,
|
|
71
|
+
"fork": False,
|
|
72
|
+
"has_wiki": False,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def _create_mock_release(self, tag="v1.0.0"):
|
|
76
|
+
"""Create a mock release object."""
|
|
77
|
+
return {
|
|
78
|
+
"tag_name": tag,
|
|
79
|
+
"name": tag,
|
|
80
|
+
"prerelease": False,
|
|
81
|
+
"draft": False,
|
|
82
|
+
"assets_url": f"https://api.github.com/repos/testuser/test-repo/releases/{tag}/assets",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def _create_mock_asset(self, name="asset.zip"):
|
|
86
|
+
"""Create a mock asset object."""
|
|
87
|
+
return {
|
|
88
|
+
"name": name,
|
|
89
|
+
"url": f"https://api.github.com/repos/testuser/test-repo/releases/assets/{name}",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TestSkipAssetsOnArgumentParsing(TestSkipAssetsOn):
|
|
94
|
+
"""Tests for --skip-assets-on argument parsing."""
|
|
95
|
+
|
|
96
|
+
def test_skip_assets_on_not_set_defaults_to_none(self):
|
|
97
|
+
"""When --skip-assets-on is not specified, it should default to None."""
|
|
98
|
+
args = github_backup.parse_args(["testuser"])
|
|
99
|
+
assert args.skip_assets_on is None
|
|
100
|
+
|
|
101
|
+
def test_skip_assets_on_single_repo(self):
|
|
102
|
+
"""Single --skip-assets-on should create list with one item."""
|
|
103
|
+
args = github_backup.parse_args(["testuser", "--skip-assets-on", "big-repo"])
|
|
104
|
+
assert args.skip_assets_on == ["big-repo"]
|
|
105
|
+
|
|
106
|
+
def test_skip_assets_on_multiple_repos(self):
|
|
107
|
+
"""Multiple repos can be specified space-separated (like --exclude)."""
|
|
108
|
+
args = github_backup.parse_args(
|
|
109
|
+
[
|
|
110
|
+
"testuser",
|
|
111
|
+
"--skip-assets-on",
|
|
112
|
+
"big-repo",
|
|
113
|
+
"another-repo",
|
|
114
|
+
"owner/third-repo",
|
|
115
|
+
]
|
|
116
|
+
)
|
|
117
|
+
assert args.skip_assets_on == ["big-repo", "another-repo", "owner/third-repo"]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TestSkipAssetsOnBehavior(TestSkipAssetsOn):
|
|
121
|
+
"""Tests for --skip-assets-on behavior in backup_releases."""
|
|
122
|
+
|
|
123
|
+
@patch("github_backup.github_backup.download_file")
|
|
124
|
+
@patch("github_backup.github_backup.retrieve_data")
|
|
125
|
+
@patch("github_backup.github_backup.mkdir_p")
|
|
126
|
+
@patch("github_backup.github_backup.json_dump_if_changed")
|
|
127
|
+
def test_assets_downloaded_when_not_skipped(
|
|
128
|
+
self, mock_json_dump, mock_mkdir, mock_retrieve, mock_download
|
|
129
|
+
):
|
|
130
|
+
"""Assets should be downloaded when repo is not in skip list."""
|
|
131
|
+
args = self._create_mock_args(skip_assets_on=[])
|
|
132
|
+
repository = self._create_mock_repository(name="normal-repo")
|
|
133
|
+
release = self._create_mock_release()
|
|
134
|
+
asset = self._create_mock_asset()
|
|
135
|
+
|
|
136
|
+
mock_json_dump.return_value = True
|
|
137
|
+
mock_retrieve.side_effect = [
|
|
138
|
+
[release], # First call: get releases
|
|
139
|
+
[asset], # Second call: get assets
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
with patch("os.path.join", side_effect=lambda *args: "/".join(args)):
|
|
143
|
+
github_backup.backup_releases(
|
|
144
|
+
args,
|
|
145
|
+
"/tmp/backup/repositories/normal-repo",
|
|
146
|
+
repository,
|
|
147
|
+
"https://api.github.com/repos/{owner}/{repo}",
|
|
148
|
+
include_assets=True,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# download_file should have been called for the asset
|
|
152
|
+
mock_download.assert_called_once()
|
|
153
|
+
|
|
154
|
+
@patch("github_backup.github_backup.download_file")
|
|
155
|
+
@patch("github_backup.github_backup.retrieve_data")
|
|
156
|
+
@patch("github_backup.github_backup.mkdir_p")
|
|
157
|
+
@patch("github_backup.github_backup.json_dump_if_changed")
|
|
158
|
+
def test_assets_skipped_when_repo_name_matches(
|
|
159
|
+
self, mock_json_dump, mock_mkdir, mock_retrieve, mock_download
|
|
160
|
+
):
|
|
161
|
+
"""Assets should be skipped when repo name is in skip list."""
|
|
162
|
+
args = self._create_mock_args(skip_assets_on=["big-repo"])
|
|
163
|
+
repository = self._create_mock_repository(name="big-repo")
|
|
164
|
+
release = self._create_mock_release()
|
|
165
|
+
|
|
166
|
+
mock_json_dump.return_value = True
|
|
167
|
+
mock_retrieve.return_value = [release]
|
|
168
|
+
|
|
169
|
+
github_backup.backup_releases(
|
|
170
|
+
args,
|
|
171
|
+
"/tmp/backup/repositories/big-repo",
|
|
172
|
+
repository,
|
|
173
|
+
"https://api.github.com/repos/{owner}/{repo}",
|
|
174
|
+
include_assets=True,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# download_file should NOT have been called
|
|
178
|
+
mock_download.assert_not_called()
|
|
179
|
+
|
|
180
|
+
@patch("github_backup.github_backup.download_file")
|
|
181
|
+
@patch("github_backup.github_backup.retrieve_data")
|
|
182
|
+
@patch("github_backup.github_backup.mkdir_p")
|
|
183
|
+
@patch("github_backup.github_backup.json_dump_if_changed")
|
|
184
|
+
def test_assets_skipped_when_full_name_matches(
|
|
185
|
+
self, mock_json_dump, mock_mkdir, mock_retrieve, mock_download
|
|
186
|
+
):
|
|
187
|
+
"""Assets should be skipped when owner/repo format matches."""
|
|
188
|
+
args = self._create_mock_args(skip_assets_on=["otheruser/big-repo"])
|
|
189
|
+
repository = self._create_mock_repository(name="big-repo", owner="otheruser")
|
|
190
|
+
release = self._create_mock_release()
|
|
191
|
+
|
|
192
|
+
mock_json_dump.return_value = True
|
|
193
|
+
mock_retrieve.return_value = [release]
|
|
194
|
+
|
|
195
|
+
github_backup.backup_releases(
|
|
196
|
+
args,
|
|
197
|
+
"/tmp/backup/repositories/big-repo",
|
|
198
|
+
repository,
|
|
199
|
+
"https://api.github.com/repos/{owner}/{repo}",
|
|
200
|
+
include_assets=True,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# download_file should NOT have been called
|
|
204
|
+
mock_download.assert_not_called()
|
|
205
|
+
|
|
206
|
+
@patch("github_backup.github_backup.download_file")
|
|
207
|
+
@patch("github_backup.github_backup.retrieve_data")
|
|
208
|
+
@patch("github_backup.github_backup.mkdir_p")
|
|
209
|
+
@patch("github_backup.github_backup.json_dump_if_changed")
|
|
210
|
+
def test_case_insensitive_matching(
|
|
211
|
+
self, mock_json_dump, mock_mkdir, mock_retrieve, mock_download
|
|
212
|
+
):
|
|
213
|
+
"""Skip matching should be case-insensitive."""
|
|
214
|
+
# User types uppercase, repo name is lowercase
|
|
215
|
+
args = self._create_mock_args(skip_assets_on=["BIG-REPO"])
|
|
216
|
+
repository = self._create_mock_repository(name="big-repo")
|
|
217
|
+
release = self._create_mock_release()
|
|
218
|
+
|
|
219
|
+
mock_json_dump.return_value = True
|
|
220
|
+
mock_retrieve.return_value = [release]
|
|
221
|
+
|
|
222
|
+
github_backup.backup_releases(
|
|
223
|
+
args,
|
|
224
|
+
"/tmp/backup/repositories/big-repo",
|
|
225
|
+
repository,
|
|
226
|
+
"https://api.github.com/repos/{owner}/{repo}",
|
|
227
|
+
include_assets=True,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# download_file should NOT have been called (case-insensitive match)
|
|
231
|
+
assert not mock_download.called
|
|
232
|
+
|
|
233
|
+
@patch("github_backup.github_backup.download_file")
|
|
234
|
+
@patch("github_backup.github_backup.retrieve_data")
|
|
235
|
+
@patch("github_backup.github_backup.mkdir_p")
|
|
236
|
+
@patch("github_backup.github_backup.json_dump_if_changed")
|
|
237
|
+
def test_multiple_skip_repos(
|
|
238
|
+
self, mock_json_dump, mock_mkdir, mock_retrieve, mock_download
|
|
239
|
+
):
|
|
240
|
+
"""Multiple repos in skip list should all be skipped."""
|
|
241
|
+
args = self._create_mock_args(skip_assets_on=["repo1", "repo2", "repo3"])
|
|
242
|
+
repository = self._create_mock_repository(name="repo2")
|
|
243
|
+
release = self._create_mock_release()
|
|
244
|
+
|
|
245
|
+
mock_json_dump.return_value = True
|
|
246
|
+
mock_retrieve.return_value = [release]
|
|
247
|
+
|
|
248
|
+
github_backup.backup_releases(
|
|
249
|
+
args,
|
|
250
|
+
"/tmp/backup/repositories/repo2",
|
|
251
|
+
repository,
|
|
252
|
+
"https://api.github.com/repos/{owner}/{repo}",
|
|
253
|
+
include_assets=True,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# download_file should NOT have been called
|
|
257
|
+
mock_download.assert_not_called()
|
|
258
|
+
|
|
259
|
+
@patch("github_backup.github_backup.download_file")
|
|
260
|
+
@patch("github_backup.github_backup.retrieve_data")
|
|
261
|
+
@patch("github_backup.github_backup.mkdir_p")
|
|
262
|
+
@patch("github_backup.github_backup.json_dump_if_changed")
|
|
263
|
+
def test_release_metadata_still_saved_when_assets_skipped(
|
|
264
|
+
self, mock_json_dump, mock_mkdir, mock_retrieve, mock_download
|
|
265
|
+
):
|
|
266
|
+
"""Release JSON should still be saved even when assets are skipped."""
|
|
267
|
+
args = self._create_mock_args(skip_assets_on=["big-repo"])
|
|
268
|
+
repository = self._create_mock_repository(name="big-repo")
|
|
269
|
+
release = self._create_mock_release()
|
|
270
|
+
|
|
271
|
+
mock_json_dump.return_value = True
|
|
272
|
+
mock_retrieve.return_value = [release]
|
|
273
|
+
|
|
274
|
+
github_backup.backup_releases(
|
|
275
|
+
args,
|
|
276
|
+
"/tmp/backup/repositories/big-repo",
|
|
277
|
+
repository,
|
|
278
|
+
"https://api.github.com/repos/{owner}/{repo}",
|
|
279
|
+
include_assets=True,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# json_dump_if_changed should have been called for release metadata
|
|
283
|
+
mock_json_dump.assert_called_once()
|
|
284
|
+
# But download_file should NOT have been called
|
|
285
|
+
mock_download.assert_not_called()
|
|
286
|
+
|
|
287
|
+
@patch("github_backup.github_backup.download_file")
|
|
288
|
+
@patch("github_backup.github_backup.retrieve_data")
|
|
289
|
+
@patch("github_backup.github_backup.mkdir_p")
|
|
290
|
+
@patch("github_backup.github_backup.json_dump_if_changed")
|
|
291
|
+
def test_non_matching_repo_still_downloads_assets(
|
|
292
|
+
self, mock_json_dump, mock_mkdir, mock_retrieve, mock_download
|
|
293
|
+
):
|
|
294
|
+
"""Repos not in skip list should still download assets."""
|
|
295
|
+
args = self._create_mock_args(skip_assets_on=["other-repo"])
|
|
296
|
+
repository = self._create_mock_repository(name="normal-repo")
|
|
297
|
+
release = self._create_mock_release()
|
|
298
|
+
asset = self._create_mock_asset()
|
|
299
|
+
|
|
300
|
+
mock_json_dump.return_value = True
|
|
301
|
+
mock_retrieve.side_effect = [
|
|
302
|
+
[release], # First call: get releases
|
|
303
|
+
[asset], # Second call: get assets
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
with patch("os.path.join", side_effect=lambda *args: "/".join(args)):
|
|
307
|
+
github_backup.backup_releases(
|
|
308
|
+
args,
|
|
309
|
+
"/tmp/backup/repositories/normal-repo",
|
|
310
|
+
repository,
|
|
311
|
+
"https://api.github.com/repos/{owner}/{repo}",
|
|
312
|
+
include_assets=True,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# download_file SHOULD have been called
|
|
316
|
+
mock_download.assert_called_once()
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if __name__ == "__main__":
|
|
320
|
+
pytest.main([__file__, "-v"])
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.56.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
|