github-backup 0.52.0__py3-none-any.whl → 0.54.0__py3-none-any.whl
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/__init__.py +1 -1
- github_backup/github_backup.py +88 -12
- {github_backup-0.52.0.data → github_backup-0.54.0.data}/scripts/github-backup +14 -3
- {github_backup-0.52.0.dist-info → github_backup-0.54.0.dist-info}/METADATA +26 -2
- github_backup-0.54.0.dist-info/RECORD +9 -0
- github_backup-0.52.0.dist-info/RECORD +0 -9
- {github_backup-0.52.0.dist-info → github_backup-0.54.0.dist-info}/WHEEL +0 -0
- {github_backup-0.52.0.dist-info → github_backup-0.54.0.dist-info}/licenses/LICENSE.txt +0 -0
- {github_backup-0.52.0.dist-info → github_backup-0.54.0.dist-info}/top_level.txt +0 -0
- {github_backup-0.52.0.dist-info → github_backup-0.54.0.dist-info}/zip-safe +0 -0
github_backup/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.54.0"
|
github_backup/github_backup.py
CHANGED
|
@@ -1587,7 +1587,9 @@ def filter_repositories(args, unfiltered_repositories):
|
|
|
1587
1587
|
repositories = []
|
|
1588
1588
|
for r in unfiltered_repositories:
|
|
1589
1589
|
# gists can be anonymous, so need to safely check owner
|
|
1590
|
-
|
|
1590
|
+
# Use case-insensitive comparison to match GitHub's case-insensitive username behavior
|
|
1591
|
+
owner_login = r.get("owner", {}).get("login", "")
|
|
1592
|
+
if owner_login.lower() == args.user.lower() or r.get("is_starred"):
|
|
1591
1593
|
repositories.append(r)
|
|
1592
1594
|
|
|
1593
1595
|
name_regex = None
|
|
@@ -1898,11 +1900,21 @@ def backup_milestones(args, repo_cwd, repository, repos_template):
|
|
|
1898
1900
|
for milestone in _milestones:
|
|
1899
1901
|
milestones[milestone["number"]] = milestone
|
|
1900
1902
|
|
|
1901
|
-
|
|
1903
|
+
written_count = 0
|
|
1902
1904
|
for number, milestone in list(milestones.items()):
|
|
1903
1905
|
milestone_file = "{0}/{1}.json".format(milestone_cwd, number)
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
+
if json_dump_if_changed(milestone, milestone_file):
|
|
1907
|
+
written_count += 1
|
|
1908
|
+
|
|
1909
|
+
total = len(milestones)
|
|
1910
|
+
if written_count == total:
|
|
1911
|
+
logger.info("Saved {0} milestones to disk".format(total))
|
|
1912
|
+
elif written_count == 0:
|
|
1913
|
+
logger.info("{0} milestones unchanged, skipped write".format(total))
|
|
1914
|
+
else:
|
|
1915
|
+
logger.info("Saved {0} of {1} milestones to disk ({2} unchanged)".format(
|
|
1916
|
+
written_count, total, total - written_count
|
|
1917
|
+
))
|
|
1906
1918
|
|
|
1907
1919
|
|
|
1908
1920
|
def backup_labels(args, repo_cwd, repository, repos_template):
|
|
@@ -1955,19 +1967,17 @@ def backup_releases(args, repo_cwd, repository, repos_template, include_assets=F
|
|
|
1955
1967
|
reverse=True,
|
|
1956
1968
|
)
|
|
1957
1969
|
releases = releases[: args.number_of_latest_releases]
|
|
1958
|
-
logger.info("Saving the latest {0} releases to disk".format(len(releases)))
|
|
1959
|
-
else:
|
|
1960
|
-
logger.info("Saving {0} releases to disk".format(len(releases)))
|
|
1961
1970
|
|
|
1962
1971
|
# for each release, store it
|
|
1972
|
+
written_count = 0
|
|
1963
1973
|
for release in releases:
|
|
1964
1974
|
release_name = release["tag_name"]
|
|
1965
1975
|
release_name_safe = release_name.replace("/", "__")
|
|
1966
1976
|
output_filepath = os.path.join(
|
|
1967
1977
|
release_cwd, "{0}.json".format(release_name_safe)
|
|
1968
1978
|
)
|
|
1969
|
-
|
|
1970
|
-
|
|
1979
|
+
if json_dump_if_changed(release, output_filepath):
|
|
1980
|
+
written_count += 1
|
|
1971
1981
|
|
|
1972
1982
|
if include_assets:
|
|
1973
1983
|
assets = retrieve_data(args, release["assets_url"])
|
|
@@ -1984,6 +1994,17 @@ def backup_releases(args, repo_cwd, repository, repos_template, include_assets=F
|
|
|
1984
1994
|
fine=True if args.token_fine is not None else False,
|
|
1985
1995
|
)
|
|
1986
1996
|
|
|
1997
|
+
# Log the results
|
|
1998
|
+
total = len(releases)
|
|
1999
|
+
if written_count == total:
|
|
2000
|
+
logger.info("Saved {0} releases to disk".format(total))
|
|
2001
|
+
elif written_count == 0:
|
|
2002
|
+
logger.info("{0} releases unchanged, skipped write".format(total))
|
|
2003
|
+
else:
|
|
2004
|
+
logger.info("Saved {0} of {1} releases to disk ({2} unchanged)".format(
|
|
2005
|
+
written_count, total, total - written_count
|
|
2006
|
+
))
|
|
2007
|
+
|
|
1987
2008
|
|
|
1988
2009
|
def fetch_repository(
|
|
1989
2010
|
name,
|
|
@@ -2108,9 +2129,10 @@ def _backup_data(args, name, template, output_file, output_directory):
|
|
|
2108
2129
|
mkdir_p(output_directory)
|
|
2109
2130
|
data = retrieve_data(args, template)
|
|
2110
2131
|
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2132
|
+
if json_dump_if_changed(data, output_file):
|
|
2133
|
+
logger.info("Saved {0} {1} to disk".format(len(data), name))
|
|
2134
|
+
else:
|
|
2135
|
+
logger.info("{0} {1} unchanged, skipped write".format(len(data), name))
|
|
2114
2136
|
|
|
2115
2137
|
|
|
2116
2138
|
def json_dump(data, output_file):
|
|
@@ -2122,3 +2144,57 @@ def json_dump(data, output_file):
|
|
|
2122
2144
|
indent=4,
|
|
2123
2145
|
separators=(",", ": "),
|
|
2124
2146
|
)
|
|
2147
|
+
|
|
2148
|
+
|
|
2149
|
+
def json_dump_if_changed(data, output_file_path):
|
|
2150
|
+
"""
|
|
2151
|
+
Write JSON data to file only if content has changed.
|
|
2152
|
+
|
|
2153
|
+
Compares the serialized JSON data with the existing file content
|
|
2154
|
+
and only writes if different. This prevents unnecessary file
|
|
2155
|
+
modification timestamp updates and disk writes.
|
|
2156
|
+
|
|
2157
|
+
Uses atomic writes (temp file + rename) to prevent corruption
|
|
2158
|
+
if the process is interrupted during the write.
|
|
2159
|
+
|
|
2160
|
+
Args:
|
|
2161
|
+
data: The data to serialize as JSON
|
|
2162
|
+
output_file_path: The path to the output file
|
|
2163
|
+
|
|
2164
|
+
Returns:
|
|
2165
|
+
True if file was written (content changed or new file)
|
|
2166
|
+
False if write was skipped (content unchanged)
|
|
2167
|
+
"""
|
|
2168
|
+
# Serialize new data with consistent formatting matching json_dump()
|
|
2169
|
+
new_content = json.dumps(
|
|
2170
|
+
data,
|
|
2171
|
+
ensure_ascii=False,
|
|
2172
|
+
sort_keys=True,
|
|
2173
|
+
indent=4,
|
|
2174
|
+
separators=(",", ": "),
|
|
2175
|
+
)
|
|
2176
|
+
|
|
2177
|
+
# Check if file exists and compare content
|
|
2178
|
+
if os.path.exists(output_file_path):
|
|
2179
|
+
try:
|
|
2180
|
+
with codecs.open(output_file_path, "r", encoding="utf-8") as f:
|
|
2181
|
+
existing_content = f.read()
|
|
2182
|
+
if existing_content == new_content:
|
|
2183
|
+
logger.debug(
|
|
2184
|
+
"Content unchanged, skipping write: {0}".format(output_file_path)
|
|
2185
|
+
)
|
|
2186
|
+
return False
|
|
2187
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
2188
|
+
# If we can't read the existing file, write the new one
|
|
2189
|
+
logger.debug(
|
|
2190
|
+
"Error reading existing file {0}, will overwrite: {1}".format(
|
|
2191
|
+
output_file_path, e
|
|
2192
|
+
)
|
|
2193
|
+
)
|
|
2194
|
+
|
|
2195
|
+
# Write the file atomically using temp file + rename
|
|
2196
|
+
temp_file = output_file_path + ".temp"
|
|
2197
|
+
with codecs.open(temp_file, "w", encoding="utf-8") as f:
|
|
2198
|
+
f.write(new_content)
|
|
2199
|
+
os.rename(temp_file, output_file_path) # Atomic on POSIX systems
|
|
2200
|
+
return True
|
|
@@ -16,12 +16,23 @@ from github_backup.github_backup import (
|
|
|
16
16
|
retrieve_repositories,
|
|
17
17
|
)
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
# INFO and DEBUG go to stdout, WARNING and above go to stderr
|
|
20
|
+
log_format = logging.Formatter(
|
|
21
|
+
fmt="%(asctime)s.%(msecs)03d: %(message)s",
|
|
21
22
|
datefmt="%Y-%m-%dT%H:%M:%S",
|
|
22
|
-
level=logging.INFO,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
+
stdout_handler = logging.StreamHandler(sys.stdout)
|
|
26
|
+
stdout_handler.setLevel(logging.DEBUG)
|
|
27
|
+
stdout_handler.addFilter(lambda r: r.levelno < logging.WARNING)
|
|
28
|
+
stdout_handler.setFormatter(log_format)
|
|
29
|
+
|
|
30
|
+
stderr_handler = logging.StreamHandler(sys.stderr)
|
|
31
|
+
stderr_handler.setLevel(logging.WARNING)
|
|
32
|
+
stderr_handler.setFormatter(log_format)
|
|
33
|
+
|
|
34
|
+
logging.basicConfig(level=logging.INFO, handlers=[stdout_handler, stderr_handler])
|
|
35
|
+
|
|
25
36
|
|
|
26
37
|
def main():
|
|
27
38
|
args = parse_args()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-backup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.54.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
|
|
@@ -338,6 +338,25 @@ Skip existing on incomplete backups
|
|
|
338
338
|
The ``--skip-existing`` argument will skip a backup if the directory already exists, even if the backup in that directory failed (perhaps due to a blocking error). This may result in unexpected missing data in a regular backup.
|
|
339
339
|
|
|
340
340
|
|
|
341
|
+
Updates use fetch, not pull
|
|
342
|
+
---------------------------
|
|
343
|
+
|
|
344
|
+
When updating an existing repository backup, ``github-backup`` uses ``git fetch`` rather than ``git pull``. This is intentional - a backup tool should reliably download data without risk of failure. Using ``git pull`` would require handling merge conflicts, which adds complexity and could cause backups to fail unexpectedly.
|
|
345
|
+
|
|
346
|
+
With fetch, **all branches and commits are downloaded** safely into remote-tracking branches. The working directory files won't change, but your backup is complete.
|
|
347
|
+
|
|
348
|
+
If you look at files directly (e.g., ``cat README.md``), you'll see the old content. The new data is in the remote-tracking branches (confusingly named "remote" but stored locally). To view or use the latest files::
|
|
349
|
+
|
|
350
|
+
git show origin/main:README.md # view a file
|
|
351
|
+
git merge origin/main # update working directory
|
|
352
|
+
|
|
353
|
+
All branches are backed up as remote refs (``origin/main``, ``origin/feature-branch``, etc.).
|
|
354
|
+
|
|
355
|
+
If you want to browse files directly without merging, consider using ``--bare`` which skips the working directory entirely - the backup is just the git data.
|
|
356
|
+
|
|
357
|
+
See `#269 <https://github.com/josegonzalez/python-github-backup/issues/269>`_ for more discussion.
|
|
358
|
+
|
|
359
|
+
|
|
341
360
|
Github Backup Examples
|
|
342
361
|
======================
|
|
343
362
|
|
|
@@ -387,7 +406,12 @@ A huge thanks to all the contibuters!
|
|
|
387
406
|
Testing
|
|
388
407
|
-------
|
|
389
408
|
|
|
390
|
-
|
|
409
|
+
To run the test suite::
|
|
410
|
+
|
|
411
|
+
pip install pytest
|
|
412
|
+
pytest
|
|
413
|
+
|
|
414
|
+
To run linting::
|
|
391
415
|
|
|
392
416
|
pip install flake8
|
|
393
417
|
flake8 --ignore=E501
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
github_backup/__init__.py,sha256=GL361gEztfNayz_pZjsGe3Es4PhpuAOve1lCqPCbSZs,23
|
|
2
|
+
github_backup/github_backup.py,sha256=-7oiAGDfavMxSiqWgaiyS5vYq31kFxOXgEH0Vw5MTIo,80149
|
|
3
|
+
github_backup-0.54.0.data/scripts/github-backup,sha256=hC59RgEKx2RbHKC-eQkKfB3JXfiNIvIO4VAXiodSkLw,2004
|
|
4
|
+
github_backup-0.54.0.dist-info/licenses/LICENSE.txt,sha256=ML-pLyn1qtAKo_DHMcQ8-p7k3f8cp-IR-mf5B49R2rk,1085
|
|
5
|
+
github_backup-0.54.0.dist-info/METADATA,sha256=CJyhg2JeoygmRFtyGbyjs0PLOoy04IkdC4mLnu1WOnM,22810
|
|
6
|
+
github_backup-0.54.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
github_backup-0.54.0.dist-info/top_level.txt,sha256=Za6fKRAlxZ0b3ZBH_osPqklLe14LetdNAkvAQANT2AA,14
|
|
8
|
+
github_backup-0.54.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
9
|
+
github_backup-0.54.0.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
github_backup/__init__.py,sha256=uXy9jtvBCb6cwZyNng8d7tehPtkx2Xcdr7Lm3SEzEF8,23
|
|
2
|
-
github_backup/github_backup.py,sha256=eo8P69vIgeP_ntzY4DdGrzFnEKPIoqBrgBX3Dw_2lIo,77487
|
|
3
|
-
github_backup-0.52.0.data/scripts/github-backup,sha256=I79TNA67phytRmI9nm_LlD9h2zauXFOsOn9tYyAy9pU,1546
|
|
4
|
-
github_backup-0.52.0.dist-info/licenses/LICENSE.txt,sha256=ML-pLyn1qtAKo_DHMcQ8-p7k3f8cp-IR-mf5B49R2rk,1085
|
|
5
|
-
github_backup-0.52.0.dist-info/METADATA,sha256=92rNj52N87cGNzBGRJYCyaotJIEgZ5ZgZ-DUugpuvlI,21544
|
|
6
|
-
github_backup-0.52.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
github_backup-0.52.0.dist-info/top_level.txt,sha256=Za6fKRAlxZ0b3ZBH_osPqklLe14LetdNAkvAQANT2AA,14
|
|
8
|
-
github_backup-0.52.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
9
|
-
github_backup-0.52.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|