github-backup 0.54.0__tar.gz → 0.56.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.54.0 → github_backup-0.56.0}/CHANGES.rst +134 -1
- {github_backup-0.54.0/github_backup.egg-info → github_backup-0.56.0}/PKG-INFO +27 -4
- {github_backup-0.54.0 → github_backup-0.56.0}/README +26 -3
- {github_backup-0.54.0 → github_backup-0.56.0}/README.rst +26 -3
- github_backup-0.56.0/github_backup/__init__.py +1 -0
- github_backup-0.56.0/github_backup/__main__.py +13 -0
- github_backup-0.54.0/bin/github-backup → github_backup-0.56.0/github_backup/cli.py +9 -0
- {github_backup-0.54.0 → github_backup-0.56.0}/github_backup/github_backup.py +41 -32
- {github_backup-0.54.0 → github_backup-0.56.0/github_backup.egg-info}/PKG-INFO +27 -4
- {github_backup-0.54.0 → github_backup-0.56.0}/github_backup.egg-info/SOURCES.txt +5 -1
- github_backup-0.56.0/github_backup.egg-info/entry_points.txt +2 -0
- {github_backup-0.54.0 → github_backup-0.56.0}/release-requirements.txt +4 -4
- {github_backup-0.54.0 → github_backup-0.56.0}/setup.py +5 -1
- github_backup-0.56.0/tests/test_all_starred.py +161 -0
- github_backup-0.56.0/tests/test_case_sensitivity.py +112 -0
- github_backup-0.54.0/github_backup/__init__.py +0 -1
- {github_backup-0.54.0 → github_backup-0.56.0}/LICENSE.txt +0 -0
- {github_backup-0.54.0 → github_backup-0.56.0}/MANIFEST.in +0 -0
- {github_backup-0.54.0 → github_backup-0.56.0}/github_backup.egg-info/dependency_links.txt +0 -0
- {github_backup-0.54.0 → github_backup-0.56.0}/github_backup.egg-info/top_level.txt +0 -0
- {github_backup-0.54.0 → github_backup-0.56.0}/github_backup.egg-info/zip-safe +0 -0
- {github_backup-0.54.0 → github_backup-0.56.0}/requirements.txt +0 -0
- {github_backup-0.54.0 → github_backup-0.56.0}/setup.cfg +0 -0
- {github_backup-0.54.0 → github_backup-0.56.0}/tests/test_attachments.py +0 -0
- {github_backup-0.54.0 → github_backup-0.56.0}/tests/test_http_451.py +0 -0
- {github_backup-0.54.0 → github_backup-0.56.0}/tests/test_json_dump_if_changed.py +0 -0
- {github_backup-0.54.0 → github_backup-0.56.0}/tests/test_pagination.py +0 -0
|
@@ -1,10 +1,143 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
-
0.
|
|
4
|
+
0.56.0 (2025-12-11)
|
|
5
5
|
-------------------
|
|
6
6
|
------------------------
|
|
7
7
|
|
|
8
|
+
Fix
|
|
9
|
+
~~~
|
|
10
|
+
- Replace deprecated git lfs clone with git clone + git lfs fetch --all.
|
|
11
|
+
[Rodos]
|
|
12
|
+
|
|
13
|
+
git lfs clone is deprecated - modern git clone handles LFS automatically.
|
|
14
|
+
Using git lfs fetch --all ensures all LFS objects across all refs are
|
|
15
|
+
backed up, matching the existing bare clone behavior and providing
|
|
16
|
+
complete LFS backups.
|
|
17
|
+
|
|
18
|
+
Closes #379
|
|
19
|
+
- Add Windows support with entry_points and os.replace. [Rodos]
|
|
20
|
+
|
|
21
|
+
- Replace os.rename() with os.replace() for atomic file operations
|
|
22
|
+
on Windows (os.rename fails if destination exists on Windows)
|
|
23
|
+
- Add entry_points console_scripts for proper .exe generation on Windows
|
|
24
|
+
- Create github_backup/cli.py with main() entry point
|
|
25
|
+
- Add github_backup/__main__.py for python -m github_backup support
|
|
26
|
+
- Keep bin/github-backup as thin wrapper for backwards compatibility
|
|
27
|
+
|
|
28
|
+
Closes #112
|
|
29
|
+
|
|
30
|
+
Other
|
|
31
|
+
~~~~~
|
|
32
|
+
- Docs: add "Restoring from Backup" section to README. [Rodos]
|
|
33
|
+
|
|
34
|
+
Clarifies that this tool is backup-only with no inbuilt restore.
|
|
35
|
+
Documents that git repos can be pushed back, but issues/PRs have
|
|
36
|
+
GitHub API limitations affecting all backup tools.
|
|
37
|
+
|
|
38
|
+
Closes #246
|
|
39
|
+
- Chore(deps): bump urllib3 in the python-packages group.
|
|
40
|
+
[dependabot[bot]]
|
|
41
|
+
|
|
42
|
+
Bumps the python-packages group with 1 update: [urllib3](https://github.com/urllib3/urllib3).
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
Updates `urllib3` from 2.6.0 to 2.6.1
|
|
46
|
+
- [Release notes](https://github.com/urllib3/urllib3/releases)
|
|
47
|
+
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
|
|
48
|
+
- [Commits](https://github.com/urllib3/urllib3/compare/2.6.0...2.6.1)
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
updated-dependencies:
|
|
52
|
+
- dependency-name: urllib3
|
|
53
|
+
dependency-version: 2.6.1
|
|
54
|
+
dependency-type: direct:production
|
|
55
|
+
update-type: version-update:semver-patch
|
|
56
|
+
dependency-group: python-packages
|
|
57
|
+
...
|
|
58
|
+
- Chore(deps): bump the python-packages group with 3 updates.
|
|
59
|
+
[dependabot[bot]]
|
|
60
|
+
|
|
61
|
+
Bumps the python-packages group with 3 updates: [black](https://github.com/psf/black), [pytest](https://github.com/pytest-dev/pytest) and [platformdirs](https://github.com/tox-dev/platformdirs).
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
Updates `black` from 25.11.0 to 25.12.0
|
|
65
|
+
- [Release notes](https://github.com/psf/black/releases)
|
|
66
|
+
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
|
|
67
|
+
- [Commits](https://github.com/psf/black/compare/25.11.0...25.12.0)
|
|
68
|
+
|
|
69
|
+
Updates `pytest` from 9.0.1 to 9.0.2
|
|
70
|
+
- [Release notes](https://github.com/pytest-dev/pytest/releases)
|
|
71
|
+
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
|
|
72
|
+
- [Commits](https://github.com/pytest-dev/pytest/compare/9.0.1...9.0.2)
|
|
73
|
+
|
|
74
|
+
Updates `platformdirs` from 4.5.0 to 4.5.1
|
|
75
|
+
- [Release notes](https://github.com/tox-dev/platformdirs/releases)
|
|
76
|
+
- [Changelog](https://github.com/tox-dev/platformdirs/blob/main/CHANGES.rst)
|
|
77
|
+
- [Commits](https://github.com/tox-dev/platformdirs/compare/4.5.0...4.5.1)
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
updated-dependencies:
|
|
81
|
+
- dependency-name: black
|
|
82
|
+
dependency-version: 25.12.0
|
|
83
|
+
dependency-type: direct:production
|
|
84
|
+
update-type: version-update:semver-minor
|
|
85
|
+
dependency-group: python-packages
|
|
86
|
+
- dependency-name: pytest
|
|
87
|
+
dependency-version: 9.0.2
|
|
88
|
+
dependency-type: direct:production
|
|
89
|
+
update-type: version-update:semver-patch
|
|
90
|
+
dependency-group: python-packages
|
|
91
|
+
- dependency-name: platformdirs
|
|
92
|
+
dependency-version: 4.5.1
|
|
93
|
+
dependency-type: direct:production
|
|
94
|
+
update-type: version-update:semver-patch
|
|
95
|
+
dependency-group: python-packages
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
0.55.0 (2025-12-07)
|
|
100
|
+
-------------------
|
|
101
|
+
|
|
102
|
+
Fix
|
|
103
|
+
~~~
|
|
104
|
+
- Improve error messages for inaccessible repos and empty wikis. [Rodos]
|
|
105
|
+
- --all-starred now clones repos without --repositories. [Rodos]
|
|
106
|
+
- Warn when --private used without authentication. [Rodos]
|
|
107
|
+
- Warn and skip when --starred-gists used for different user. [Rodos]
|
|
108
|
+
|
|
109
|
+
GitHub's API only allows retrieving starred gists for the authenticated
|
|
110
|
+
user. Previously, using --starred-gists when backing up a different user
|
|
111
|
+
would silently return no relevant data.
|
|
112
|
+
|
|
113
|
+
Now warns and skips the retrieval entirely when the target user differs
|
|
114
|
+
from the authenticated user. Uses case-insensitive comparison to match
|
|
115
|
+
GitHub's username handling.
|
|
116
|
+
|
|
117
|
+
Fixes #93
|
|
118
|
+
|
|
119
|
+
Other
|
|
120
|
+
~~~~~
|
|
121
|
+
- Test: add missing test coverage for case sensitivity fix. [Rodos]
|
|
122
|
+
- Docs: fix RST formatting in Known blocking errors section. [Rodos]
|
|
123
|
+
- Chore(deps): bump urllib3 from 2.5.0 to 2.6.0. [dependabot[bot]]
|
|
124
|
+
|
|
125
|
+
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.5.0 to 2.6.0.
|
|
126
|
+
- [Release notes](https://github.com/urllib3/urllib3/releases)
|
|
127
|
+
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
|
|
128
|
+
- [Commits](https://github.com/urllib3/urllib3/compare/2.5.0...2.6.0)
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
updated-dependencies:
|
|
132
|
+
- dependency-name: urllib3
|
|
133
|
+
dependency-version: 2.6.0
|
|
134
|
+
dependency-type: direct:production
|
|
135
|
+
...
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
0.54.0 (2025-12-03)
|
|
139
|
+
-------------------
|
|
140
|
+
|
|
8
141
|
Fix
|
|
9
142
|
~~~
|
|
10
143
|
- Send INFO/DEBUG to stdout, WARNING/ERROR to stderr. [Rodos]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-backup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.56.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
|
|
@@ -245,6 +245,8 @@ When you use the ``--lfs`` option, you will need to make sure you have Git LFS i
|
|
|
245
245
|
|
|
246
246
|
Instructions on how to do this can be found on https://git-lfs.github.com.
|
|
247
247
|
|
|
248
|
+
LFS objects are fetched for all refs, not just the current checkout, ensuring a complete backup of all LFS content across all branches and history.
|
|
249
|
+
|
|
248
250
|
|
|
249
251
|
About Attachments
|
|
250
252
|
-----------------
|
|
@@ -311,11 +313,11 @@ If the incremental argument is used, this will result in the next backup only re
|
|
|
311
313
|
|
|
312
314
|
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.
|
|
313
315
|
|
|
314
|
-
|
|
316
|
+
**Starred public repo hooks blocking**
|
|
315
317
|
|
|
316
|
-
|
|
318
|
+
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.
|
|
317
319
|
|
|
318
|
-
|
|
320
|
+
This is due to needing the correct permission for ``--hooks`` on public repos.
|
|
319
321
|
|
|
320
322
|
|
|
321
323
|
"bare" is actually "mirror"
|
|
@@ -331,6 +333,8 @@ Starred gists vs starred repo behaviour
|
|
|
331
333
|
|
|
332
334
|
The starred normal repo cloning (``--all-starred``) argument stores starred repos separately to the users own repositories. However, using ``--starred-gists`` will store starred gists within the same directory as the users own gists ``--gists``. Also, all gist repo directory names are IDs not the gist's name.
|
|
333
335
|
|
|
336
|
+
Note: ``--starred-gists`` only retrieves starred gists for the authenticated user, not the target user, due to a GitHub API limitation.
|
|
337
|
+
|
|
334
338
|
|
|
335
339
|
Skip existing on incomplete backups
|
|
336
340
|
-----------------------------------
|
|
@@ -388,6 +392,25 @@ Debug an error/block or incomplete backup into a temporary directory. Omit "incr
|
|
|
388
392
|
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
|
|
389
393
|
|
|
390
394
|
|
|
395
|
+
Restoring from Backup
|
|
396
|
+
=====================
|
|
397
|
+
|
|
398
|
+
This tool creates backups only, there is no inbuilt restore command.
|
|
399
|
+
|
|
400
|
+
**Git repositories, wikis, and gists** can be restored by pushing them back to GitHub as you would any git repository. For example, to restore a bare repository backup::
|
|
401
|
+
|
|
402
|
+
cd /tmp/white-house/repositories/petitions/repository
|
|
403
|
+
git push --mirror git@github.com:WhiteHouse/petitions.git
|
|
404
|
+
|
|
405
|
+
**Issues, pull requests, comments, and other metadata** are saved as JSON files for archival purposes. The GitHub API does not support recreating this data faithfully, creating issues via the API has limitations:
|
|
406
|
+
|
|
407
|
+
- New issue/PR numbers are assigned (original numbers cannot be set)
|
|
408
|
+
- Timestamps reflect creation time (original dates cannot be set)
|
|
409
|
+
- The API caller becomes the author (original authors cannot be set)
|
|
410
|
+
- Cross-references between issues and PRs will break
|
|
411
|
+
|
|
412
|
+
These are GitHub API limitations that affect all backup and migration tools, not just this one. Recreating issues with these limitations via the GitHub API is an exercise for the reader. The JSON backups remain useful for searching, auditing, or manual reference.
|
|
413
|
+
|
|
391
414
|
|
|
392
415
|
Development
|
|
393
416
|
===========
|
|
@@ -215,6 +215,8 @@ When you use the ``--lfs`` option, you will need to make sure you have Git LFS i
|
|
|
215
215
|
|
|
216
216
|
Instructions on how to do this can be found on https://git-lfs.github.com.
|
|
217
217
|
|
|
218
|
+
LFS objects are fetched for all refs, not just the current checkout, ensuring a complete backup of all LFS content across all branches and history.
|
|
219
|
+
|
|
218
220
|
|
|
219
221
|
About Attachments
|
|
220
222
|
-----------------
|
|
@@ -281,11 +283,11 @@ If the incremental argument is used, this will result in the next backup only re
|
|
|
281
283
|
|
|
282
284
|
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.
|
|
283
285
|
|
|
284
|
-
|
|
286
|
+
**Starred public repo hooks blocking**
|
|
285
287
|
|
|
286
|
-
|
|
288
|
+
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.
|
|
287
289
|
|
|
288
|
-
|
|
290
|
+
This is due to needing the correct permission for ``--hooks`` on public repos.
|
|
289
291
|
|
|
290
292
|
|
|
291
293
|
"bare" is actually "mirror"
|
|
@@ -301,6 +303,8 @@ Starred gists vs starred repo behaviour
|
|
|
301
303
|
|
|
302
304
|
The starred normal repo cloning (``--all-starred``) argument stores starred repos separately to the users own repositories. However, using ``--starred-gists`` will store starred gists within the same directory as the users own gists ``--gists``. Also, all gist repo directory names are IDs not the gist's name.
|
|
303
305
|
|
|
306
|
+
Note: ``--starred-gists`` only retrieves starred gists for the authenticated user, not the target user, due to a GitHub API limitation.
|
|
307
|
+
|
|
304
308
|
|
|
305
309
|
Skip existing on incomplete backups
|
|
306
310
|
-----------------------------------
|
|
@@ -358,6 +362,25 @@ Debug an error/block or incomplete backup into a temporary directory. Omit "incr
|
|
|
358
362
|
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
|
|
359
363
|
|
|
360
364
|
|
|
365
|
+
Restoring from Backup
|
|
366
|
+
=====================
|
|
367
|
+
|
|
368
|
+
This tool creates backups only, there is no inbuilt restore command.
|
|
369
|
+
|
|
370
|
+
**Git repositories, wikis, and gists** can be restored by pushing them back to GitHub as you would any git repository. For example, to restore a bare repository backup::
|
|
371
|
+
|
|
372
|
+
cd /tmp/white-house/repositories/petitions/repository
|
|
373
|
+
git push --mirror git@github.com:WhiteHouse/petitions.git
|
|
374
|
+
|
|
375
|
+
**Issues, pull requests, comments, and other metadata** are saved as JSON files for archival purposes. The GitHub API does not support recreating this data faithfully, creating issues via the API has limitations:
|
|
376
|
+
|
|
377
|
+
- New issue/PR numbers are assigned (original numbers cannot be set)
|
|
378
|
+
- Timestamps reflect creation time (original dates cannot be set)
|
|
379
|
+
- The API caller becomes the author (original authors cannot be set)
|
|
380
|
+
- Cross-references between issues and PRs will break
|
|
381
|
+
|
|
382
|
+
These are GitHub API limitations that affect all backup and migration tools, not just this one. Recreating issues with these limitations via the GitHub API is an exercise for the reader. The JSON backups remain useful for searching, auditing, or manual reference.
|
|
383
|
+
|
|
361
384
|
|
|
362
385
|
Development
|
|
363
386
|
===========
|
|
@@ -215,6 +215,8 @@ When you use the ``--lfs`` option, you will need to make sure you have Git LFS i
|
|
|
215
215
|
|
|
216
216
|
Instructions on how to do this can be found on https://git-lfs.github.com.
|
|
217
217
|
|
|
218
|
+
LFS objects are fetched for all refs, not just the current checkout, ensuring a complete backup of all LFS content across all branches and history.
|
|
219
|
+
|
|
218
220
|
|
|
219
221
|
About Attachments
|
|
220
222
|
-----------------
|
|
@@ -281,11 +283,11 @@ If the incremental argument is used, this will result in the next backup only re
|
|
|
281
283
|
|
|
282
284
|
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.
|
|
283
285
|
|
|
284
|
-
|
|
286
|
+
**Starred public repo hooks blocking**
|
|
285
287
|
|
|
286
|
-
|
|
288
|
+
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.
|
|
287
289
|
|
|
288
|
-
|
|
290
|
+
This is due to needing the correct permission for ``--hooks`` on public repos.
|
|
289
291
|
|
|
290
292
|
|
|
291
293
|
"bare" is actually "mirror"
|
|
@@ -301,6 +303,8 @@ Starred gists vs starred repo behaviour
|
|
|
301
303
|
|
|
302
304
|
The starred normal repo cloning (``--all-starred``) argument stores starred repos separately to the users own repositories. However, using ``--starred-gists`` will store starred gists within the same directory as the users own gists ``--gists``. Also, all gist repo directory names are IDs not the gist's name.
|
|
303
305
|
|
|
306
|
+
Note: ``--starred-gists`` only retrieves starred gists for the authenticated user, not the target user, due to a GitHub API limitation.
|
|
307
|
+
|
|
304
308
|
|
|
305
309
|
Skip existing on incomplete backups
|
|
306
310
|
-----------------------------------
|
|
@@ -358,6 +362,25 @@ Debug an error/block or incomplete backup into a temporary directory. Omit "incr
|
|
|
358
362
|
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
|
|
359
363
|
|
|
360
364
|
|
|
365
|
+
Restoring from Backup
|
|
366
|
+
=====================
|
|
367
|
+
|
|
368
|
+
This tool creates backups only, there is no inbuilt restore command.
|
|
369
|
+
|
|
370
|
+
**Git repositories, wikis, and gists** can be restored by pushing them back to GitHub as you would any git repository. For example, to restore a bare repository backup::
|
|
371
|
+
|
|
372
|
+
cd /tmp/white-house/repositories/petitions/repository
|
|
373
|
+
git push --mirror git@github.com:WhiteHouse/petitions.git
|
|
374
|
+
|
|
375
|
+
**Issues, pull requests, comments, and other metadata** are saved as JSON files for archival purposes. The GitHub API does not support recreating this data faithfully, creating issues via the API has limitations:
|
|
376
|
+
|
|
377
|
+
- New issue/PR numbers are assigned (original numbers cannot be set)
|
|
378
|
+
- Timestamps reflect creation time (original dates cannot be set)
|
|
379
|
+
- The API caller becomes the author (original authors cannot be set)
|
|
380
|
+
- Cross-references between issues and PRs will break
|
|
381
|
+
|
|
382
|
+
These are GitHub API limitations that affect all backup and migration tools, not just this one. Recreating issues with these limitations via the GitHub API is an exercise for the reader. The JSON backups remain useful for searching, auditing, or manual reference.
|
|
383
|
+
|
|
361
384
|
|
|
362
385
|
Development
|
|
363
386
|
===========
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.56.0"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Allow running as: python -m github_backup"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from github_backup.cli import main
|
|
6
|
+
from github_backup.github_backup import logger
|
|
7
|
+
|
|
8
|
+
if __name__ == "__main__":
|
|
9
|
+
try:
|
|
10
|
+
main()
|
|
11
|
+
except Exception as e:
|
|
12
|
+
logger.error(str(e))
|
|
13
|
+
sys.exit(1)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python
|
|
2
|
+
"""Command-line interface for github-backup."""
|
|
2
3
|
|
|
3
4
|
import logging
|
|
4
5
|
import os
|
|
@@ -9,6 +10,7 @@ from github_backup.github_backup import (
|
|
|
9
10
|
backup_repositories,
|
|
10
11
|
check_git_lfs_install,
|
|
11
12
|
filter_repositories,
|
|
13
|
+
get_auth,
|
|
12
14
|
get_authenticated_user,
|
|
13
15
|
logger,
|
|
14
16
|
mkdir_p,
|
|
@@ -35,8 +37,15 @@ logging.basicConfig(level=logging.INFO, handlers=[stdout_handler, stderr_handler
|
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
def main():
|
|
40
|
+
"""Main entry point for github-backup CLI."""
|
|
38
41
|
args = parse_args()
|
|
39
42
|
|
|
43
|
+
if args.private and not get_auth(args):
|
|
44
|
+
logger.warning(
|
|
45
|
+
"The --private flag has no effect without authentication. "
|
|
46
|
+
"Use -t/--token, -f/--token-fine, or -u/--username to authenticate."
|
|
47
|
+
)
|
|
48
|
+
|
|
40
49
|
if args.quiet:
|
|
41
50
|
logger.setLevel(logging.WARNING)
|
|
42
51
|
|
|
@@ -561,7 +561,7 @@ def get_github_host(args):
|
|
|
561
561
|
|
|
562
562
|
|
|
563
563
|
def read_file_contents(file_uri):
|
|
564
|
-
return open(file_uri[len(FILE_URI_PREFIX)
|
|
564
|
+
return open(file_uri[len(FILE_URI_PREFIX):], "rt").readline().strip()
|
|
565
565
|
|
|
566
566
|
|
|
567
567
|
def get_github_repo_url(args, repository):
|
|
@@ -1038,7 +1038,7 @@ def download_attachment_file(url, path, auth, as_app=False, fine=False):
|
|
|
1038
1038
|
bytes_downloaded += len(chunk)
|
|
1039
1039
|
|
|
1040
1040
|
# Atomic rename to final location
|
|
1041
|
-
os.
|
|
1041
|
+
os.replace(temp_path, path)
|
|
1042
1042
|
|
|
1043
1043
|
metadata["size_bytes"] = bytes_downloaded
|
|
1044
1044
|
metadata["success"] = True
|
|
@@ -1459,7 +1459,7 @@ def download_attachments(
|
|
|
1459
1459
|
|
|
1460
1460
|
# Rename to add extension (already atomic from download)
|
|
1461
1461
|
try:
|
|
1462
|
-
os.
|
|
1462
|
+
os.replace(filepath, final_filepath)
|
|
1463
1463
|
metadata["saved_as"] = os.path.basename(final_filepath)
|
|
1464
1464
|
except Exception as e:
|
|
1465
1465
|
logger.warning(
|
|
@@ -1490,7 +1490,7 @@ def download_attachments(
|
|
|
1490
1490
|
manifest_path = os.path.join(attachments_dir, "manifest.json")
|
|
1491
1491
|
with open(manifest_path + ".temp", "w") as f:
|
|
1492
1492
|
json.dump(manifest, f, indent=2)
|
|
1493
|
-
|
|
1493
|
+
os.replace(manifest_path + ".temp", manifest_path) # Atomic write
|
|
1494
1494
|
logger.debug(
|
|
1495
1495
|
"Wrote manifest for {0} #{1}: {2} attachments".format(
|
|
1496
1496
|
item_type_display, number, len(attachment_metadata_list)
|
|
@@ -1565,16 +1565,22 @@ def retrieve_repositories(args, authenticated_user):
|
|
|
1565
1565
|
repos.extend(gists)
|
|
1566
1566
|
|
|
1567
1567
|
if args.include_starred_gists:
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1568
|
+
if not authenticated_user.get("login") or args.user.lower() != authenticated_user["login"].lower():
|
|
1569
|
+
logger.warning(
|
|
1570
|
+
"Cannot retrieve starred gists for '%s'. GitHub only allows access to the authenticated user's starred gists.",
|
|
1571
|
+
args.user,
|
|
1572
|
+
)
|
|
1573
|
+
else:
|
|
1574
|
+
starred_gists_template = "https://{0}/gists/starred".format(
|
|
1575
|
+
get_github_api_host(args)
|
|
1576
|
+
)
|
|
1577
|
+
starred_gists = retrieve_data(
|
|
1578
|
+
args, starred_gists_template, single_request=False
|
|
1579
|
+
)
|
|
1580
|
+
# flag each repo as a starred gist for downstream processing
|
|
1581
|
+
for item in starred_gists:
|
|
1582
|
+
item.update({"is_gist": True, "is_starred": True})
|
|
1583
|
+
repos.extend(starred_gists)
|
|
1578
1584
|
|
|
1579
1585
|
return repos
|
|
1580
1586
|
|
|
@@ -1666,9 +1672,10 @@ def backup_repositories(args, output_directory, repositories):
|
|
|
1666
1672
|
repo_url = get_github_repo_url(args, repository)
|
|
1667
1673
|
|
|
1668
1674
|
include_gists = args.include_gists or args.include_starred_gists
|
|
1675
|
+
include_starred = args.all_starred and repository.get("is_starred")
|
|
1669
1676
|
if (args.include_repository or args.include_everything) or (
|
|
1670
1677
|
include_gists and repository.get("is_gist")
|
|
1671
|
-
):
|
|
1678
|
+
) or include_starred:
|
|
1672
1679
|
repo_name = (
|
|
1673
1680
|
repository.get("name")
|
|
1674
1681
|
if not repository.get("is_gist")
|
|
@@ -1804,7 +1811,7 @@ def backup_issues(args, repo_cwd, repository, repos_template):
|
|
|
1804
1811
|
|
|
1805
1812
|
with codecs.open(issue_file + ".temp", "w", encoding="utf-8") as f:
|
|
1806
1813
|
json_dump(issue, f)
|
|
1807
|
-
|
|
1814
|
+
os.replace(issue_file + ".temp", issue_file) # Atomic write
|
|
1808
1815
|
|
|
1809
1816
|
|
|
1810
1817
|
def backup_pulls(args, repo_cwd, repository, repos_template):
|
|
@@ -1879,7 +1886,7 @@ def backup_pulls(args, repo_cwd, repository, repos_template):
|
|
|
1879
1886
|
|
|
1880
1887
|
with codecs.open(pull_file + ".temp", "w", encoding="utf-8") as f:
|
|
1881
1888
|
json_dump(pull, f)
|
|
1882
|
-
|
|
1889
|
+
os.replace(pull_file + ".temp", pull_file) # Atomic write
|
|
1883
1890
|
|
|
1884
1891
|
|
|
1885
1892
|
def backup_milestones(args, repo_cwd, repository, repos_template):
|
|
@@ -2017,12 +2024,9 @@ def fetch_repository(
|
|
|
2017
2024
|
):
|
|
2018
2025
|
if bare_clone:
|
|
2019
2026
|
if os.path.exists(local_dir):
|
|
2020
|
-
clone_exists = (
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
)
|
|
2024
|
-
== b"true\n"
|
|
2025
|
-
)
|
|
2027
|
+
clone_exists = subprocess.check_output(
|
|
2028
|
+
["git", "rev-parse", "--is-bare-repository"], cwd=local_dir
|
|
2029
|
+
) == b"true\n"
|
|
2026
2030
|
else:
|
|
2027
2031
|
clone_exists = False
|
|
2028
2032
|
else:
|
|
@@ -2037,11 +2041,14 @@ def fetch_repository(
|
|
|
2037
2041
|
"git ls-remote " + remote_url, stdout=FNULL, stderr=FNULL, shell=True
|
|
2038
2042
|
)
|
|
2039
2043
|
if initialized == 128:
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
name
|
|
2044
|
+
if ".wiki.git" in remote_url:
|
|
2045
|
+
logger.info(
|
|
2046
|
+
"Skipping {0} wiki (wiki is enabled but has no content)".format(name)
|
|
2047
|
+
)
|
|
2048
|
+
else:
|
|
2049
|
+
logger.info(
|
|
2050
|
+
"Skipping {0} (repository not accessible - may be empty, private, or credentials invalid)".format(name)
|
|
2043
2051
|
)
|
|
2044
|
-
)
|
|
2045
2052
|
return
|
|
2046
2053
|
|
|
2047
2054
|
if clone_exists:
|
|
@@ -2083,11 +2090,13 @@ def fetch_repository(
|
|
|
2083
2090
|
git_command.pop()
|
|
2084
2091
|
logging_subprocess(git_command, cwd=local_dir)
|
|
2085
2092
|
else:
|
|
2086
|
-
|
|
2087
|
-
git_command = ["git", "lfs", "clone", remote_url, local_dir]
|
|
2088
|
-
else:
|
|
2089
|
-
git_command = ["git", "clone", remote_url, local_dir]
|
|
2093
|
+
git_command = ["git", "clone", remote_url, local_dir]
|
|
2090
2094
|
logging_subprocess(git_command)
|
|
2095
|
+
if lfs_clone:
|
|
2096
|
+
git_command = ["git", "lfs", "fetch", "--all", "--prune"]
|
|
2097
|
+
if no_prune:
|
|
2098
|
+
git_command.pop()
|
|
2099
|
+
logging_subprocess(git_command, cwd=local_dir)
|
|
2091
2100
|
|
|
2092
2101
|
|
|
2093
2102
|
def backup_account(args, output_directory):
|
|
@@ -2196,5 +2205,5 @@ def json_dump_if_changed(data, output_file_path):
|
|
|
2196
2205
|
temp_file = output_file_path + ".temp"
|
|
2197
2206
|
with codecs.open(temp_file, "w", encoding="utf-8") as f:
|
|
2198
2207
|
f.write(new_content)
|
|
2199
|
-
os.
|
|
2208
|
+
os.replace(temp_file, output_file_path) # Atomic write
|
|
2200
2209
|
return True
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-backup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.56.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
|
|
@@ -245,6 +245,8 @@ When you use the ``--lfs`` option, you will need to make sure you have Git LFS i
|
|
|
245
245
|
|
|
246
246
|
Instructions on how to do this can be found on https://git-lfs.github.com.
|
|
247
247
|
|
|
248
|
+
LFS objects are fetched for all refs, not just the current checkout, ensuring a complete backup of all LFS content across all branches and history.
|
|
249
|
+
|
|
248
250
|
|
|
249
251
|
About Attachments
|
|
250
252
|
-----------------
|
|
@@ -311,11 +313,11 @@ If the incremental argument is used, this will result in the next backup only re
|
|
|
311
313
|
|
|
312
314
|
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.
|
|
313
315
|
|
|
314
|
-
|
|
316
|
+
**Starred public repo hooks blocking**
|
|
315
317
|
|
|
316
|
-
|
|
318
|
+
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.
|
|
317
319
|
|
|
318
|
-
|
|
320
|
+
This is due to needing the correct permission for ``--hooks`` on public repos.
|
|
319
321
|
|
|
320
322
|
|
|
321
323
|
"bare" is actually "mirror"
|
|
@@ -331,6 +333,8 @@ Starred gists vs starred repo behaviour
|
|
|
331
333
|
|
|
332
334
|
The starred normal repo cloning (``--all-starred``) argument stores starred repos separately to the users own repositories. However, using ``--starred-gists`` will store starred gists within the same directory as the users own gists ``--gists``. Also, all gist repo directory names are IDs not the gist's name.
|
|
333
335
|
|
|
336
|
+
Note: ``--starred-gists`` only retrieves starred gists for the authenticated user, not the target user, due to a GitHub API limitation.
|
|
337
|
+
|
|
334
338
|
|
|
335
339
|
Skip existing on incomplete backups
|
|
336
340
|
-----------------------------------
|
|
@@ -388,6 +392,25 @@ Debug an error/block or incomplete backup into a temporary directory. Omit "incr
|
|
|
388
392
|
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
|
|
389
393
|
|
|
390
394
|
|
|
395
|
+
Restoring from Backup
|
|
396
|
+
=====================
|
|
397
|
+
|
|
398
|
+
This tool creates backups only, there is no inbuilt restore command.
|
|
399
|
+
|
|
400
|
+
**Git repositories, wikis, and gists** can be restored by pushing them back to GitHub as you would any git repository. For example, to restore a bare repository backup::
|
|
401
|
+
|
|
402
|
+
cd /tmp/white-house/repositories/petitions/repository
|
|
403
|
+
git push --mirror git@github.com:WhiteHouse/petitions.git
|
|
404
|
+
|
|
405
|
+
**Issues, pull requests, comments, and other metadata** are saved as JSON files for archival purposes. The GitHub API does not support recreating this data faithfully, creating issues via the API has limitations:
|
|
406
|
+
|
|
407
|
+
- New issue/PR numbers are assigned (original numbers cannot be set)
|
|
408
|
+
- Timestamps reflect creation time (original dates cannot be set)
|
|
409
|
+
- The API caller becomes the author (original authors cannot be set)
|
|
410
|
+
- Cross-references between issues and PRs will break
|
|
411
|
+
|
|
412
|
+
These are GitHub API limitations that affect all backup and migration tools, not just this one. Recreating issues with these limitations via the GitHub API is an exercise for the reader. The JSON backups remain useful for searching, auditing, or manual reference.
|
|
413
|
+
|
|
391
414
|
|
|
392
415
|
Development
|
|
393
416
|
===========
|
|
@@ -6,15 +6,19 @@ README.rst
|
|
|
6
6
|
release-requirements.txt
|
|
7
7
|
requirements.txt
|
|
8
8
|
setup.py
|
|
9
|
-
bin/github-backup
|
|
10
9
|
github_backup/__init__.py
|
|
10
|
+
github_backup/__main__.py
|
|
11
|
+
github_backup/cli.py
|
|
11
12
|
github_backup/github_backup.py
|
|
12
13
|
github_backup.egg-info/PKG-INFO
|
|
13
14
|
github_backup.egg-info/SOURCES.txt
|
|
14
15
|
github_backup.egg-info/dependency_links.txt
|
|
16
|
+
github_backup.egg-info/entry_points.txt
|
|
15
17
|
github_backup.egg-info/top_level.txt
|
|
16
18
|
github_backup.egg-info/zip-safe
|
|
19
|
+
tests/test_all_starred.py
|
|
17
20
|
tests/test_attachments.py
|
|
21
|
+
tests/test_case_sensitivity.py
|
|
18
22
|
tests/test_http_451.py
|
|
19
23
|
tests/test_json_dump_if_changed.py
|
|
20
24
|
tests/test_pagination.py
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
autopep8==2.3.2
|
|
2
|
-
black==25.
|
|
2
|
+
black==25.12.0
|
|
3
3
|
bleach==6.3.0
|
|
4
4
|
certifi==2025.11.12
|
|
5
5
|
charset-normalizer==3.4.4
|
|
@@ -8,7 +8,7 @@ colorama==0.4.6
|
|
|
8
8
|
docutils==0.22.3
|
|
9
9
|
flake8==7.3.0
|
|
10
10
|
gitchangelog==3.0.4
|
|
11
|
-
pytest==9.0.
|
|
11
|
+
pytest==9.0.2
|
|
12
12
|
idna==3.11
|
|
13
13
|
importlib-metadata==8.7.0
|
|
14
14
|
jaraco.classes==3.4.0
|
|
@@ -21,7 +21,7 @@ mypy-extensions==1.1.0
|
|
|
21
21
|
packaging==25.0
|
|
22
22
|
pathspec==0.12.1
|
|
23
23
|
pkginfo==1.12.1.2
|
|
24
|
-
platformdirs==4.5.
|
|
24
|
+
platformdirs==4.5.1
|
|
25
25
|
pycodestyle==2.14.0
|
|
26
26
|
pyflakes==3.4.0
|
|
27
27
|
Pygments==2.19.2
|
|
@@ -35,6 +35,6 @@ setuptools==80.9.0
|
|
|
35
35
|
six==1.17.0
|
|
36
36
|
tqdm==4.67.1
|
|
37
37
|
twine==6.2.0
|
|
38
|
-
urllib3==2.
|
|
38
|
+
urllib3==2.6.1
|
|
39
39
|
webencodings==0.5.1
|
|
40
40
|
zipp==3.23.0
|
|
@@ -33,7 +33,11 @@ setup(
|
|
|
33
33
|
author="Jose Diaz-Gonzalez",
|
|
34
34
|
author_email="github-backup@josediazgonzalez.com",
|
|
35
35
|
packages=["github_backup"],
|
|
36
|
-
|
|
36
|
+
entry_points={
|
|
37
|
+
"console_scripts": [
|
|
38
|
+
"github-backup=github_backup.cli:main",
|
|
39
|
+
],
|
|
40
|
+
},
|
|
37
41
|
url="http://github.com/josegonzalez/python-github-backup",
|
|
38
42
|
license="MIT",
|
|
39
43
|
classifiers=[
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Tests for --all-starred flag behavior (issue #225)."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import Mock, patch
|
|
5
|
+
|
|
6
|
+
from github_backup import github_backup
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestAllStarredCloning:
|
|
10
|
+
"""Test suite for --all-starred repository cloning behavior.
|
|
11
|
+
|
|
12
|
+
Issue #225: --all-starred should clone starred repos without requiring --repositories.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def _create_mock_args(self, **overrides):
|
|
16
|
+
"""Create a mock args object with sensible defaults."""
|
|
17
|
+
args = Mock()
|
|
18
|
+
args.user = "testuser"
|
|
19
|
+
args.output_directory = "/tmp/backup"
|
|
20
|
+
args.include_repository = False
|
|
21
|
+
args.include_everything = False
|
|
22
|
+
args.include_gists = False
|
|
23
|
+
args.include_starred_gists = False
|
|
24
|
+
args.all_starred = False
|
|
25
|
+
args.skip_existing = False
|
|
26
|
+
args.bare_clone = False
|
|
27
|
+
args.lfs_clone = False
|
|
28
|
+
args.no_prune = False
|
|
29
|
+
args.include_wiki = False
|
|
30
|
+
args.include_issues = False
|
|
31
|
+
args.include_issue_comments = False
|
|
32
|
+
args.include_issue_events = False
|
|
33
|
+
args.include_pulls = False
|
|
34
|
+
args.include_pull_comments = False
|
|
35
|
+
args.include_pull_commits = False
|
|
36
|
+
args.include_pull_details = False
|
|
37
|
+
args.include_labels = False
|
|
38
|
+
args.include_hooks = False
|
|
39
|
+
args.include_milestones = False
|
|
40
|
+
args.include_releases = False
|
|
41
|
+
args.include_assets = False
|
|
42
|
+
args.include_attachments = False
|
|
43
|
+
args.incremental = False
|
|
44
|
+
args.incremental_by_files = False
|
|
45
|
+
args.github_host = None
|
|
46
|
+
args.prefer_ssh = False
|
|
47
|
+
args.token_classic = None
|
|
48
|
+
args.token_fine = None
|
|
49
|
+
args.username = None
|
|
50
|
+
args.password = None
|
|
51
|
+
args.as_app = False
|
|
52
|
+
args.osx_keychain_item_name = None
|
|
53
|
+
args.osx_keychain_item_account = None
|
|
54
|
+
|
|
55
|
+
for key, value in overrides.items():
|
|
56
|
+
setattr(args, key, value)
|
|
57
|
+
|
|
58
|
+
return args
|
|
59
|
+
|
|
60
|
+
@patch('github_backup.github_backup.fetch_repository')
|
|
61
|
+
@patch('github_backup.github_backup.get_github_repo_url')
|
|
62
|
+
def test_all_starred_clones_without_repositories_flag(self, mock_get_url, mock_fetch):
|
|
63
|
+
"""--all-starred should clone starred repos without --repositories flag.
|
|
64
|
+
|
|
65
|
+
This is the core fix for issue #225.
|
|
66
|
+
"""
|
|
67
|
+
args = self._create_mock_args(all_starred=True)
|
|
68
|
+
mock_get_url.return_value = "https://github.com/otheruser/awesome-project.git"
|
|
69
|
+
|
|
70
|
+
# A starred repository (is_starred flag set by retrieve_repositories)
|
|
71
|
+
starred_repo = {
|
|
72
|
+
"name": "awesome-project",
|
|
73
|
+
"full_name": "otheruser/awesome-project",
|
|
74
|
+
"owner": {"login": "otheruser"},
|
|
75
|
+
"private": False,
|
|
76
|
+
"fork": False,
|
|
77
|
+
"has_wiki": False,
|
|
78
|
+
"is_starred": True, # This flag is set for starred repos
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
with patch('github_backup.github_backup.mkdir_p'):
|
|
82
|
+
github_backup.backup_repositories(args, "/tmp/backup", [starred_repo])
|
|
83
|
+
|
|
84
|
+
# fetch_repository should be called for the starred repo
|
|
85
|
+
assert mock_fetch.called, "--all-starred should trigger repository cloning"
|
|
86
|
+
mock_fetch.assert_called_once()
|
|
87
|
+
call_args = mock_fetch.call_args
|
|
88
|
+
assert call_args[0][0] == "awesome-project" # repo name
|
|
89
|
+
|
|
90
|
+
@patch('github_backup.github_backup.fetch_repository')
|
|
91
|
+
@patch('github_backup.github_backup.get_github_repo_url')
|
|
92
|
+
def test_starred_repo_not_cloned_without_all_starred_flag(self, mock_get_url, mock_fetch):
|
|
93
|
+
"""Starred repos should NOT be cloned if --all-starred is not set."""
|
|
94
|
+
args = self._create_mock_args(all_starred=False)
|
|
95
|
+
mock_get_url.return_value = "https://github.com/otheruser/awesome-project.git"
|
|
96
|
+
|
|
97
|
+
starred_repo = {
|
|
98
|
+
"name": "awesome-project",
|
|
99
|
+
"full_name": "otheruser/awesome-project",
|
|
100
|
+
"owner": {"login": "otheruser"},
|
|
101
|
+
"private": False,
|
|
102
|
+
"fork": False,
|
|
103
|
+
"has_wiki": False,
|
|
104
|
+
"is_starred": True,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
with patch('github_backup.github_backup.mkdir_p'):
|
|
108
|
+
github_backup.backup_repositories(args, "/tmp/backup", [starred_repo])
|
|
109
|
+
|
|
110
|
+
# fetch_repository should NOT be called
|
|
111
|
+
assert not mock_fetch.called, "Starred repos should not be cloned without --all-starred"
|
|
112
|
+
|
|
113
|
+
@patch('github_backup.github_backup.fetch_repository')
|
|
114
|
+
@patch('github_backup.github_backup.get_github_repo_url')
|
|
115
|
+
def test_non_starred_repo_not_cloned_with_only_all_starred(self, mock_get_url, mock_fetch):
|
|
116
|
+
"""Non-starred repos should NOT be cloned when only --all-starred is set."""
|
|
117
|
+
args = self._create_mock_args(all_starred=True)
|
|
118
|
+
mock_get_url.return_value = "https://github.com/testuser/my-project.git"
|
|
119
|
+
|
|
120
|
+
# A regular (non-starred) repository
|
|
121
|
+
regular_repo = {
|
|
122
|
+
"name": "my-project",
|
|
123
|
+
"full_name": "testuser/my-project",
|
|
124
|
+
"owner": {"login": "testuser"},
|
|
125
|
+
"private": False,
|
|
126
|
+
"fork": False,
|
|
127
|
+
"has_wiki": False,
|
|
128
|
+
# No is_starred flag
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
with patch('github_backup.github_backup.mkdir_p'):
|
|
132
|
+
github_backup.backup_repositories(args, "/tmp/backup", [regular_repo])
|
|
133
|
+
|
|
134
|
+
# fetch_repository should NOT be called for non-starred repos
|
|
135
|
+
assert not mock_fetch.called, "Non-starred repos should not be cloned with only --all-starred"
|
|
136
|
+
|
|
137
|
+
@patch('github_backup.github_backup.fetch_repository')
|
|
138
|
+
@patch('github_backup.github_backup.get_github_repo_url')
|
|
139
|
+
def test_repositories_flag_still_works(self, mock_get_url, mock_fetch):
|
|
140
|
+
"""--repositories flag should still clone repos as before."""
|
|
141
|
+
args = self._create_mock_args(include_repository=True)
|
|
142
|
+
mock_get_url.return_value = "https://github.com/testuser/my-project.git"
|
|
143
|
+
|
|
144
|
+
regular_repo = {
|
|
145
|
+
"name": "my-project",
|
|
146
|
+
"full_name": "testuser/my-project",
|
|
147
|
+
"owner": {"login": "testuser"},
|
|
148
|
+
"private": False,
|
|
149
|
+
"fork": False,
|
|
150
|
+
"has_wiki": False,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
with patch('github_backup.github_backup.mkdir_p'):
|
|
154
|
+
github_backup.backup_repositories(args, "/tmp/backup", [regular_repo])
|
|
155
|
+
|
|
156
|
+
# fetch_repository should be called
|
|
157
|
+
assert mock_fetch.called, "--repositories should trigger repository cloning"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Tests for case-insensitive username/organization filtering."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from unittest.mock import Mock
|
|
5
|
+
|
|
6
|
+
from github_backup import github_backup
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestCaseSensitivity:
|
|
10
|
+
"""Test suite for case-insensitive username matching in filter_repositories."""
|
|
11
|
+
|
|
12
|
+
def test_filter_repositories_case_insensitive_user(self):
|
|
13
|
+
"""Should filter repositories case-insensitively for usernames.
|
|
14
|
+
|
|
15
|
+
Reproduces issue #198 where typing 'iamrodos' fails to match
|
|
16
|
+
repositories with owner.login='Iamrodos' (the canonical case from GitHub API).
|
|
17
|
+
"""
|
|
18
|
+
# Simulate user typing lowercase username
|
|
19
|
+
args = Mock()
|
|
20
|
+
args.user = "iamrodos" # lowercase (what user typed)
|
|
21
|
+
args.repository = None
|
|
22
|
+
args.name_regex = None
|
|
23
|
+
args.languages = None
|
|
24
|
+
args.exclude = None
|
|
25
|
+
args.fork = False
|
|
26
|
+
args.private = False
|
|
27
|
+
args.public = False
|
|
28
|
+
args.all = True
|
|
29
|
+
|
|
30
|
+
# Simulate GitHub API returning canonical case
|
|
31
|
+
repos = [
|
|
32
|
+
{
|
|
33
|
+
"name": "repo1",
|
|
34
|
+
"owner": {"login": "Iamrodos"}, # Capital I (canonical from API)
|
|
35
|
+
"private": False,
|
|
36
|
+
"fork": False,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "repo2",
|
|
40
|
+
"owner": {"login": "Iamrodos"},
|
|
41
|
+
"private": False,
|
|
42
|
+
"fork": False,
|
|
43
|
+
},
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
filtered = github_backup.filter_repositories(args, repos)
|
|
47
|
+
|
|
48
|
+
# Should match despite case difference
|
|
49
|
+
assert len(filtered) == 2
|
|
50
|
+
assert filtered[0]["name"] == "repo1"
|
|
51
|
+
assert filtered[1]["name"] == "repo2"
|
|
52
|
+
|
|
53
|
+
def test_filter_repositories_case_insensitive_org(self):
|
|
54
|
+
"""Should filter repositories case-insensitively for organizations.
|
|
55
|
+
|
|
56
|
+
Tests the example from issue #198 where 'prai-org' doesn't match 'PRAI-Org'.
|
|
57
|
+
"""
|
|
58
|
+
args = Mock()
|
|
59
|
+
args.user = "prai-org" # lowercase (what user typed)
|
|
60
|
+
args.repository = None
|
|
61
|
+
args.name_regex = None
|
|
62
|
+
args.languages = None
|
|
63
|
+
args.exclude = None
|
|
64
|
+
args.fork = False
|
|
65
|
+
args.private = False
|
|
66
|
+
args.public = False
|
|
67
|
+
args.all = True
|
|
68
|
+
|
|
69
|
+
repos = [
|
|
70
|
+
{
|
|
71
|
+
"name": "repo1",
|
|
72
|
+
"owner": {"login": "PRAI-Org"}, # Different case (canonical from API)
|
|
73
|
+
"private": False,
|
|
74
|
+
"fork": False,
|
|
75
|
+
},
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
filtered = github_backup.filter_repositories(args, repos)
|
|
79
|
+
|
|
80
|
+
# Should match despite case difference
|
|
81
|
+
assert len(filtered) == 1
|
|
82
|
+
assert filtered[0]["name"] == "repo1"
|
|
83
|
+
|
|
84
|
+
def test_filter_repositories_case_variations(self):
|
|
85
|
+
"""Should handle various case combinations correctly."""
|
|
86
|
+
args = Mock()
|
|
87
|
+
args.user = "TeSt-UsEr" # Mixed case
|
|
88
|
+
args.repository = None
|
|
89
|
+
args.name_regex = None
|
|
90
|
+
args.languages = None
|
|
91
|
+
args.exclude = None
|
|
92
|
+
args.fork = False
|
|
93
|
+
args.private = False
|
|
94
|
+
args.public = False
|
|
95
|
+
args.all = True
|
|
96
|
+
|
|
97
|
+
repos = [
|
|
98
|
+
{"name": "repo1", "owner": {"login": "test-user"}, "private": False, "fork": False},
|
|
99
|
+
{"name": "repo2", "owner": {"login": "TEST-USER"}, "private": False, "fork": False},
|
|
100
|
+
{"name": "repo3", "owner": {"login": "TeSt-UsEr"}, "private": False, "fork": False},
|
|
101
|
+
{"name": "repo4", "owner": {"login": "other-user"}, "private": False, "fork": False},
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
filtered = github_backup.filter_repositories(args, repos)
|
|
105
|
+
|
|
106
|
+
# Should match first 3 (all case variations of same user)
|
|
107
|
+
assert len(filtered) == 3
|
|
108
|
+
assert set(r["name"] for r in filtered) == {"repo1", "repo2", "repo3"}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
pytest.main([__file__, "-v"])
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.54.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
|