github-backup 0.64.0__tar.gz → 0.64.2__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.64.0 → github_backup-0.64.2}/CHANGES.rst +13 -1
- {github_backup-0.64.0/github_backup.egg-info → github_backup-0.64.2}/PKG-INFO +1 -1
- github_backup-0.64.2/github_backup/__init__.py +1 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/github_backup/github_backup.py +82 -10
- {github_backup-0.64.0 → github_backup-0.64.2/github_backup.egg-info}/PKG-INFO +1 -1
- {github_backup-0.64.0 → github_backup-0.64.2}/github_backup.egg-info/SOURCES.txt +1 -0
- github_backup-0.64.2/tests/test_gists.py +227 -0
- github_backup-0.64.0/github_backup/__init__.py +0 -1
- {github_backup-0.64.0 → github_backup-0.64.2}/LICENSE.txt +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/MANIFEST.in +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/README +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/README.rst +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/github_backup/__main__.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/github_backup/cli.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/github_backup/graphql_queries.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/github_backup.egg-info/dependency_links.txt +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/github_backup.egg-info/entry_points.txt +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/github_backup.egg-info/top_level.txt +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/github_backup.egg-info/zip-safe +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/release-requirements.txt +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/requirements.txt +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/setup.cfg +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/setup.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/tests/test_all_starred.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/tests/test_attachments.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/tests/test_auth.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/tests/test_case_sensitivity.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/tests/test_disabled_issues_pulls.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/tests/test_discussions.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/tests/test_http_451.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/tests/test_incremental_per_repository.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/tests/test_json_dump_if_changed.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/tests/test_logging_subprocess.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/tests/test_pagination.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/tests/test_pull_incremental_pagination.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/tests/test_pull_reviews.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/tests/test_releases.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/tests/test_retrieve_data.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/tests/test_skip_assets_on.py +0 -0
- {github_backup-0.64.0 → github_backup-0.64.2}/tests/test_starred_skip_size_over.py +0 -0
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
=========
|
|
3
3
|
|
|
4
|
-
0.64.
|
|
4
|
+
0.64.2 (2026-07-18)
|
|
5
5
|
-------------------
|
|
6
6
|
------------------------
|
|
7
|
+
- Add tests. [Duncan Ogilvie]
|
|
8
|
+
- Skip fetching gists that have not been updated. [Duncan Ogilvie]
|
|
9
|
+
- Skip retrieving repositories when not necessary. [Duncan Ogilvie]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
0.64.1 (2026-07-17)
|
|
13
|
+
-------------------
|
|
14
|
+
- Fix exception when DMCA'd/empty gists were backed up. [Duncan Ogilvie]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
0.64.0 (2026-07-07)
|
|
18
|
+
-------------------
|
|
7
19
|
|
|
8
20
|
Fix
|
|
9
21
|
~~~
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.64.2"
|
|
@@ -1789,7 +1789,39 @@ def check_git_lfs_install():
|
|
|
1789
1789
|
)
|
|
1790
1790
|
|
|
1791
1791
|
|
|
1792
|
+
# Resources fetched from their own endpoints, not from the repository listing
|
|
1793
|
+
# (gists and account-level data). Every other include_* flag needs the listing.
|
|
1794
|
+
NON_REPOSITORY_RESOURCES = frozenset(
|
|
1795
|
+
{
|
|
1796
|
+
"include_gists",
|
|
1797
|
+
"include_starred_gists",
|
|
1798
|
+
"include_starred",
|
|
1799
|
+
"include_watched",
|
|
1800
|
+
"include_followers",
|
|
1801
|
+
"include_following",
|
|
1802
|
+
}
|
|
1803
|
+
)
|
|
1804
|
+
|
|
1805
|
+
|
|
1806
|
+
def repository_list_needed(args):
|
|
1807
|
+
"""The base repository listing is only needed when a per-repository
|
|
1808
|
+
resource was requested; e.g. a gists-only backup can skip it entirely.
|
|
1809
|
+
Flags not listed in NON_REPOSITORY_RESOURCES default to needing it, so a
|
|
1810
|
+
newly added resource can never be silently skipped."""
|
|
1811
|
+
if args.repository:
|
|
1812
|
+
return True
|
|
1813
|
+
return any(
|
|
1814
|
+
value
|
|
1815
|
+
for name, value in vars(args).items()
|
|
1816
|
+
if name.startswith("include_") and name not in NON_REPOSITORY_RESOURCES
|
|
1817
|
+
)
|
|
1818
|
+
|
|
1819
|
+
|
|
1792
1820
|
def retrieve_repositories(args, authenticated_user):
|
|
1821
|
+
if not repository_list_needed(args):
|
|
1822
|
+
repos = []
|
|
1823
|
+
return retrieve_additional_repositories(args, authenticated_user, repos)
|
|
1824
|
+
|
|
1793
1825
|
logger.info("Retrieving repositories")
|
|
1794
1826
|
paginated = True
|
|
1795
1827
|
if args.user == authenticated_user["login"]:
|
|
@@ -1825,6 +1857,12 @@ def retrieve_repositories(args, authenticated_user):
|
|
|
1825
1857
|
logger.warning(f"Legal notice: {e.legal_url}")
|
|
1826
1858
|
return []
|
|
1827
1859
|
|
|
1860
|
+
return retrieve_additional_repositories(args, authenticated_user, repos)
|
|
1861
|
+
|
|
1862
|
+
|
|
1863
|
+
def retrieve_additional_repositories(args, authenticated_user, repos):
|
|
1864
|
+
"""Append starred repositories, gists and starred gists to the base
|
|
1865
|
+
repository list when requested."""
|
|
1828
1866
|
if args.all_starred:
|
|
1829
1867
|
starred_template = "https://{0}/users/{1}/starred".format(
|
|
1830
1868
|
get_github_api_host(args), args.user
|
|
@@ -2058,6 +2096,24 @@ def remove_legacy_last_update_if_migrated(
|
|
|
2058
2096
|
)
|
|
2059
2097
|
|
|
2060
2098
|
|
|
2099
|
+
def gist_backup_is_current(repository, repo_cwd, repo_dir):
|
|
2100
|
+
"""Whether fetching a gist can be skipped because a clone exists and the
|
|
2101
|
+
stored gist.json matches the listing's updated_at. Any edit to a gist —
|
|
2102
|
+
including a git push — bumps updated_at, so this can never miss a content
|
|
2103
|
+
change; at worst a comment bumps it and causes a harmless refetch."""
|
|
2104
|
+
if not os.path.exists(repo_dir):
|
|
2105
|
+
return False
|
|
2106
|
+
updated_at = repository.get("updated_at")
|
|
2107
|
+
if not updated_at:
|
|
2108
|
+
return False
|
|
2109
|
+
try:
|
|
2110
|
+
with codecs.open(os.path.join(repo_cwd, "gist.json"), encoding="utf-8") as f:
|
|
2111
|
+
stored = json.load(f)
|
|
2112
|
+
except (OSError, ValueError):
|
|
2113
|
+
return False
|
|
2114
|
+
return stored.get("updated_at") == updated_at
|
|
2115
|
+
|
|
2116
|
+
|
|
2061
2117
|
def backup_repositories(args, output_directory, repositories):
|
|
2062
2118
|
logger.info("Backing up repositories")
|
|
2063
2119
|
repos_template = "https://{0}/repos".format(get_github_api_host(args))
|
|
@@ -2065,6 +2121,7 @@ def backup_repositories(args, output_directory, repositories):
|
|
|
2065
2121
|
args, output_directory
|
|
2066
2122
|
)
|
|
2067
2123
|
incremental_resource_work_attempted = False
|
|
2124
|
+
skipped_unchanged_gists = 0
|
|
2068
2125
|
|
|
2069
2126
|
for repository in repositories:
|
|
2070
2127
|
if repository.get("is_gist"):
|
|
@@ -2098,18 +2155,30 @@ def backup_repositories(args, output_directory, repositories):
|
|
|
2098
2155
|
if not repository.get("is_gist")
|
|
2099
2156
|
else repository.get("id")
|
|
2100
2157
|
)
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2158
|
+
if repository.get("is_gist") and gist_backup_is_current(
|
|
2159
|
+
repository, repo_cwd, repo_dir
|
|
2160
|
+
):
|
|
2161
|
+
logger.debug(
|
|
2162
|
+
"Skipping gist {0} (unchanged since last backup)".format(repo_name)
|
|
2163
|
+
)
|
|
2164
|
+
skipped_unchanged_gists += 1
|
|
2165
|
+
else:
|
|
2166
|
+
fetch_repository(
|
|
2167
|
+
repo_name,
|
|
2168
|
+
repo_url,
|
|
2169
|
+
repo_dir,
|
|
2170
|
+
skip_existing=args.skip_existing,
|
|
2171
|
+
bare_clone=args.bare_clone,
|
|
2172
|
+
lfs_clone=args.lfs_clone,
|
|
2173
|
+
no_prune=args.no_prune,
|
|
2174
|
+
)
|
|
2110
2175
|
|
|
2111
2176
|
if repository.get("is_gist"):
|
|
2112
|
-
# dump gist information to a file as well
|
|
2177
|
+
# dump gist information to a file as well, even when the fetch
|
|
2178
|
+
# was skipped, so listing metadata (fork counts etc.) stays
|
|
2179
|
+
# fresh; the clone may also have been skipped entirely (e.g.
|
|
2180
|
+
# DMCA-blocked or empty gist), so make sure the directory exists
|
|
2181
|
+
mkdir_p(repo_cwd)
|
|
2113
2182
|
output_file = "{0}/gist.json".format(repo_cwd)
|
|
2114
2183
|
with codecs.open(output_file, "w", encoding="utf-8") as f:
|
|
2115
2184
|
json_dump(repository, f)
|
|
@@ -2178,6 +2247,9 @@ def backup_repositories(args, output_directory, repositories):
|
|
|
2178
2247
|
logger.info(f"Skipping remaining resources for {repository['full_name']}")
|
|
2179
2248
|
continue
|
|
2180
2249
|
|
|
2250
|
+
if skipped_unchanged_gists:
|
|
2251
|
+
logger.info("Skipped {0} unchanged gists".format(skipped_unchanged_gists))
|
|
2252
|
+
|
|
2181
2253
|
if incremental_resource_work_attempted:
|
|
2182
2254
|
remove_legacy_last_update_if_migrated(
|
|
2183
2255
|
args, output_directory, legacy_last_update_path
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Tests for gist backup behavior: skipping the repository listing for
|
|
2
|
+
gists-only backups, skipping fetches for unchanged gists, and writing
|
|
3
|
+
gist.json even when the clone was skipped (e.g. DMCA-blocked gists)."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
|
|
11
|
+
from github_backup.github_backup import (
|
|
12
|
+
backup_repositories,
|
|
13
|
+
gist_backup_is_current,
|
|
14
|
+
repository_list_needed,
|
|
15
|
+
retrieve_repositories,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestRepositoryListNeeded:
|
|
20
|
+
"""The base /user/repos listing should only be fetched when a
|
|
21
|
+
per-repository resource was requested."""
|
|
22
|
+
|
|
23
|
+
def test_gists_only_does_not_need_listing(self, create_args):
|
|
24
|
+
args = create_args(include_gists=True, include_starred_gists=True)
|
|
25
|
+
assert not repository_list_needed(args)
|
|
26
|
+
|
|
27
|
+
def test_account_level_flags_do_not_need_listing(self, create_args):
|
|
28
|
+
args = create_args(
|
|
29
|
+
include_starred=True,
|
|
30
|
+
include_watched=True,
|
|
31
|
+
include_followers=True,
|
|
32
|
+
include_following=True,
|
|
33
|
+
)
|
|
34
|
+
assert not repository_list_needed(args)
|
|
35
|
+
|
|
36
|
+
def test_all_starred_does_not_need_listing(self, create_args):
|
|
37
|
+
# starred repos come from /users/{user}/starred, not /user/repos
|
|
38
|
+
args = create_args(all_starred=True)
|
|
39
|
+
assert not repository_list_needed(args)
|
|
40
|
+
|
|
41
|
+
@pytest.mark.parametrize(
|
|
42
|
+
"flag",
|
|
43
|
+
[
|
|
44
|
+
"include_repository",
|
|
45
|
+
"include_everything",
|
|
46
|
+
"include_wiki",
|
|
47
|
+
"include_issues",
|
|
48
|
+
"include_pulls",
|
|
49
|
+
"include_releases",
|
|
50
|
+
"include_attachments",
|
|
51
|
+
],
|
|
52
|
+
)
|
|
53
|
+
def test_repository_scoped_flags_need_listing(self, create_args, flag):
|
|
54
|
+
args = create_args(**{flag: True})
|
|
55
|
+
assert repository_list_needed(args)
|
|
56
|
+
|
|
57
|
+
def test_single_repository_needs_listing(self, create_args):
|
|
58
|
+
args = create_args(repository="some-repo")
|
|
59
|
+
assert repository_list_needed(args)
|
|
60
|
+
|
|
61
|
+
def test_unknown_include_flag_defaults_to_needing_listing(self, create_args):
|
|
62
|
+
# Fail-safe: a future include_* flag that nobody registered in
|
|
63
|
+
# NON_REPOSITORY_RESOURCES must not silently skip the listing.
|
|
64
|
+
args = create_args(include_some_future_resource=True)
|
|
65
|
+
assert repository_list_needed(args)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TestRetrieveRepositoriesSkipsListing:
|
|
69
|
+
"""retrieve_repositories should not hit the repository endpoints for a
|
|
70
|
+
gists-only backup, but must still fetch gists and starred gists."""
|
|
71
|
+
|
|
72
|
+
@patch("github_backup.github_backup.retrieve_data")
|
|
73
|
+
def test_gists_only_skips_repo_listing(self, mock_retrieve, create_args):
|
|
74
|
+
args = create_args(include_gists=True, include_starred_gists=True)
|
|
75
|
+
mock_retrieve.return_value = [{"id": "abc123"}]
|
|
76
|
+
|
|
77
|
+
repos = retrieve_repositories(args, {"login": "testuser"})
|
|
78
|
+
|
|
79
|
+
requested = [call.args[1] for call in mock_retrieve.call_args_list]
|
|
80
|
+
assert not any("/repos" in url for url in requested), requested
|
|
81
|
+
assert any(url.endswith("/users/testuser/gists") for url in requested)
|
|
82
|
+
assert any(url.endswith("/gists/starred") for url in requested)
|
|
83
|
+
assert all(r["is_gist"] for r in repos)
|
|
84
|
+
|
|
85
|
+
@patch("github_backup.github_backup.retrieve_data")
|
|
86
|
+
def test_repository_backup_still_fetches_listing(self, mock_retrieve, create_args):
|
|
87
|
+
args = create_args(include_repository=True)
|
|
88
|
+
mock_retrieve.return_value = []
|
|
89
|
+
|
|
90
|
+
retrieve_repositories(args, {"login": "testuser"})
|
|
91
|
+
|
|
92
|
+
requested = [call.args[1] for call in mock_retrieve.call_args_list]
|
|
93
|
+
assert any(url.endswith("/user/repos") for url in requested), requested
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TestGistBackupIsCurrent:
|
|
97
|
+
"""A gist fetch is skipped only when a clone exists and the stored
|
|
98
|
+
gist.json updated_at matches the listing."""
|
|
99
|
+
|
|
100
|
+
GIST = {"id": "abc123", "is_gist": True, "updated_at": "2026-07-17T10:00:00Z"}
|
|
101
|
+
|
|
102
|
+
def _write_backup(self, repo_cwd, updated_at="2026-07-17T10:00:00Z", clone=True):
|
|
103
|
+
if clone:
|
|
104
|
+
os.makedirs(os.path.join(repo_cwd, "repository"))
|
|
105
|
+
else:
|
|
106
|
+
os.makedirs(repo_cwd)
|
|
107
|
+
with open(os.path.join(repo_cwd, "gist.json"), "w") as f:
|
|
108
|
+
json.dump({"id": "abc123", "updated_at": updated_at}, f)
|
|
109
|
+
|
|
110
|
+
def test_current_when_unchanged(self, tmp_path):
|
|
111
|
+
repo_cwd = str(tmp_path / "gists" / "abc123")
|
|
112
|
+
self._write_backup(repo_cwd)
|
|
113
|
+
assert gist_backup_is_current(
|
|
114
|
+
self.GIST, repo_cwd, os.path.join(repo_cwd, "repository")
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def test_not_current_when_updated(self, tmp_path):
|
|
118
|
+
repo_cwd = str(tmp_path / "gists" / "abc123")
|
|
119
|
+
self._write_backup(repo_cwd, updated_at="2020-01-01T00:00:00Z")
|
|
120
|
+
assert not gist_backup_is_current(
|
|
121
|
+
self.GIST, repo_cwd, os.path.join(repo_cwd, "repository")
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def test_not_current_without_clone(self, tmp_path):
|
|
125
|
+
# gist.json alone is not enough; e.g. the clone was skipped because
|
|
126
|
+
# the gist was inaccessible, so keep retrying the fetch
|
|
127
|
+
repo_cwd = str(tmp_path / "gists" / "abc123")
|
|
128
|
+
self._write_backup(repo_cwd, clone=False)
|
|
129
|
+
assert not gist_backup_is_current(
|
|
130
|
+
self.GIST, repo_cwd, os.path.join(repo_cwd, "repository")
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def test_not_current_with_corrupt_gist_json(self, tmp_path):
|
|
134
|
+
repo_cwd = str(tmp_path / "gists" / "abc123")
|
|
135
|
+
os.makedirs(os.path.join(repo_cwd, "repository"))
|
|
136
|
+
with open(os.path.join(repo_cwd, "gist.json"), "w") as f:
|
|
137
|
+
f.write("{not json")
|
|
138
|
+
assert not gist_backup_is_current(
|
|
139
|
+
self.GIST, repo_cwd, os.path.join(repo_cwd, "repository")
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def test_not_current_without_updated_at(self, tmp_path):
|
|
143
|
+
repo_cwd = str(tmp_path / "gists" / "abc123")
|
|
144
|
+
self._write_backup(repo_cwd)
|
|
145
|
+
assert not gist_backup_is_current(
|
|
146
|
+
{"id": "abc123", "is_gist": True},
|
|
147
|
+
repo_cwd,
|
|
148
|
+
os.path.join(repo_cwd, "repository"),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TestGistBackup:
|
|
153
|
+
"""End-to-end behavior of backup_repositories for gists."""
|
|
154
|
+
|
|
155
|
+
def _gist(self, **extra):
|
|
156
|
+
gist = {
|
|
157
|
+
"id": "abc123",
|
|
158
|
+
"is_gist": True,
|
|
159
|
+
"updated_at": "2026-07-17T10:00:00Z",
|
|
160
|
+
"git_pull_url": "https://gist.github.com/abc123.git",
|
|
161
|
+
}
|
|
162
|
+
gist.update(extra)
|
|
163
|
+
return gist
|
|
164
|
+
|
|
165
|
+
@patch("github_backup.github_backup.fetch_repository")
|
|
166
|
+
def test_new_gist_is_fetched_and_metadata_written(
|
|
167
|
+
self, mock_fetch, create_args, tmp_path
|
|
168
|
+
):
|
|
169
|
+
args = create_args(include_gists=True)
|
|
170
|
+
|
|
171
|
+
backup_repositories(args, str(tmp_path), [self._gist()])
|
|
172
|
+
|
|
173
|
+
mock_fetch.assert_called_once()
|
|
174
|
+
gist_json = tmp_path / "gists" / "abc123" / "gist.json"
|
|
175
|
+
assert json.loads(gist_json.read_text())["id"] == "abc123"
|
|
176
|
+
|
|
177
|
+
@patch("github_backup.github_backup.fetch_repository")
|
|
178
|
+
def test_unchanged_gist_skips_fetch_but_refreshes_metadata(
|
|
179
|
+
self, mock_fetch, create_args, tmp_path
|
|
180
|
+
):
|
|
181
|
+
args = create_args(include_gists=True)
|
|
182
|
+
repo_cwd = tmp_path / "gists" / "abc123"
|
|
183
|
+
(repo_cwd / "repository").mkdir(parents=True)
|
|
184
|
+
(repo_cwd / "gist.json").write_text(
|
|
185
|
+
json.dumps({"id": "abc123", "updated_at": "2026-07-17T10:00:00Z"})
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
backup_repositories(args, str(tmp_path), [self._gist(forks=5)])
|
|
189
|
+
|
|
190
|
+
assert not mock_fetch.called, "unchanged gist should not be fetched"
|
|
191
|
+
# gist.json is rewritten from the listing so metadata that changes
|
|
192
|
+
# without bumping updated_at (fork counts etc.) stays fresh
|
|
193
|
+
assert json.loads((repo_cwd / "gist.json").read_text())["forks"] == 5
|
|
194
|
+
|
|
195
|
+
@patch("github_backup.github_backup.fetch_repository")
|
|
196
|
+
def test_updated_gist_is_fetched(self, mock_fetch, create_args, tmp_path):
|
|
197
|
+
args = create_args(include_gists=True)
|
|
198
|
+
repo_cwd = tmp_path / "gists" / "abc123"
|
|
199
|
+
(repo_cwd / "repository").mkdir(parents=True)
|
|
200
|
+
(repo_cwd / "gist.json").write_text(
|
|
201
|
+
json.dumps({"id": "abc123", "updated_at": "2020-01-01T00:00:00Z"})
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
backup_repositories(args, str(tmp_path), [self._gist()])
|
|
205
|
+
|
|
206
|
+
mock_fetch.assert_called_once()
|
|
207
|
+
|
|
208
|
+
@patch("github_backup.github_backup.fetch_repository")
|
|
209
|
+
def test_inaccessible_gist_does_not_crash_backup(
|
|
210
|
+
self, mock_fetch, create_args, tmp_path
|
|
211
|
+
):
|
|
212
|
+
"""Regression test: fetch_repository skips inaccessible gists (e.g.
|
|
213
|
+
DMCA-blocked) without creating the clone directory; writing gist.json
|
|
214
|
+
must not crash and the next gist must still be processed."""
|
|
215
|
+
args = create_args(include_gists=True)
|
|
216
|
+
# mock fetch never creates the directory, like a skipped clone
|
|
217
|
+
blocked = self._gist(id="deadbeef")
|
|
218
|
+
second = self._gist()
|
|
219
|
+
|
|
220
|
+
backup_repositories(args, str(tmp_path), [blocked, second])
|
|
221
|
+
|
|
222
|
+
assert (tmp_path / "gists" / "deadbeef" / "gist.json").is_file()
|
|
223
|
+
assert (tmp_path / "gists" / "abc123" / "gist.json").is_file()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
if __name__ == "__main__":
|
|
227
|
+
pytest.main([__file__, "-v"])
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.64.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|