mirror of
https://github.com/josegonzalez/python-github-backup.git
synced 2025-12-22 15:41:10 +01:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81a72ac8af | ||
|
|
3edbfc777c | ||
|
|
3c43e0f481 | ||
|
|
875f09eeaf | ||
|
|
db36c3c137 |
19
CHANGES.rst
19
CHANGES.rst
@@ -1,9 +1,26 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
0.58.0 (2025-12-16)
|
||||
0.59.0 (2025-12-21)
|
||||
-------------------
|
||||
------------------------
|
||||
- Add --starred-skip-size-over flag to limit starred repo size (#108)
|
||||
[Rodos]
|
||||
|
||||
Allow users to skip starred repositories exceeding a size threshold
|
||||
when using --all-starred. Size is specified in MB and checked against
|
||||
the GitHub API's repository size field.
|
||||
|
||||
- Only affects starred repos; user's own repos always included
|
||||
- Logs each skipped repo with name and size
|
||||
|
||||
Closes #108
|
||||
- Chore: remove deprecated -u/-p password authentication options.
|
||||
[Rodos]
|
||||
|
||||
|
||||
0.58.0 (2025-12-16)
|
||||
-------------------
|
||||
- Fix retry logic for HTTP 5xx errors and network failures. [Rodos]
|
||||
|
||||
Refactors error handling to retry all 5xx errors (not just 502), network errors (URLError, socket.error, IncompleteRead), and JSON parse errors with exponential backoff and jitter. Respects retry-after and rate limit headers per GitHub API requirements. Consolidates retry logic into make_request_with_retry() wrapper and adds clear logging for retry attempts and failures. Removes dead code from 2016 (errors list, _request_http_error, _request_url_error) that was intentionally disabled in commit 1e5a9048 to fix #29.
|
||||
|
||||
108
README.rst
108
README.rst
@@ -36,23 +36,26 @@ Show the CLI help output::
|
||||
|
||||
CLI Help output::
|
||||
|
||||
github-backup [-h] [-u USERNAME] [-p PASSWORD] [-t TOKEN_CLASSIC]
|
||||
[-f TOKEN_FINE] [--as-app] [-o OUTPUT_DIRECTORY]
|
||||
[-l LOG_LEVEL] [-i] [--starred] [--all-starred]
|
||||
[--watched] [--followers] [--following] [--all] [--issues]
|
||||
[--issue-comments] [--issue-events] [--pulls]
|
||||
github-backup [-h] [-t TOKEN_CLASSIC] [-f TOKEN_FINE] [-q] [--as-app]
|
||||
[-o OUTPUT_DIRECTORY] [-l LOG_LEVEL] [-i]
|
||||
[--incremental-by-files]
|
||||
[--starred] [--all-starred] [--starred-skip-size-over MB]
|
||||
[--watched] [--followers] [--following] [--all]
|
||||
[--issues] [--issue-comments] [--issue-events] [--pulls]
|
||||
[--pull-comments] [--pull-commits] [--pull-details]
|
||||
[--labels] [--hooks] [--milestones] [--repositories]
|
||||
[--bare] [--lfs] [--wikis] [--gists] [--starred-gists]
|
||||
[--skip-archived] [--skip-existing] [-L [LANGUAGES ...]]
|
||||
[-N NAME_REGEX] [-H GITHUB_HOST] [-O] [-R REPOSITORY]
|
||||
[-P] [-F] [--prefer-ssh] [-v]
|
||||
[--bare] [--no-prune] [--lfs] [--wikis] [--gists]
|
||||
[--starred-gists] [--skip-archived] [--skip-existing]
|
||||
[-L [LANGUAGES ...]] [-N NAME_REGEX] [-H GITHUB_HOST]
|
||||
[-O] [-R REPOSITORY] [-P] [-F] [--prefer-ssh] [-v]
|
||||
[--keychain-name OSX_KEYCHAIN_ITEM_NAME]
|
||||
[--keychain-account OSX_KEYCHAIN_ITEM_ACCOUNT]
|
||||
[--releases] [--latest-releases NUMBER_OF_LATEST_RELEASES]
|
||||
[--skip-prerelease] [--assets] [--skip-assets-on [REPO ...]]
|
||||
[--attachments] [--exclude [REPOSITORY [REPOSITORY ...]]
|
||||
[--throttle-limit THROTTLE_LIMIT] [--throttle-pause THROTTLE_PAUSE]
|
||||
[--skip-prerelease] [--assets]
|
||||
[--skip-assets-on [SKIP_ASSETS_ON ...]] [--attachments]
|
||||
[--throttle-limit THROTTLE_LIMIT]
|
||||
[--throttle-pause THROTTLE_PAUSE]
|
||||
[--exclude [EXCLUDE ...]]
|
||||
USER
|
||||
|
||||
Backup a github account
|
||||
@@ -60,29 +63,29 @@ CLI Help output::
|
||||
positional arguments:
|
||||
USER github username
|
||||
|
||||
optional arguments:
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
-u USERNAME, --username USERNAME
|
||||
username for basic auth
|
||||
-p PASSWORD, --password PASSWORD
|
||||
password for basic auth. If a username is given but
|
||||
not a password, the password will be prompted for.
|
||||
-f TOKEN_FINE, --token-fine TOKEN_FINE
|
||||
fine-grained personal access token or path to token
|
||||
(file://...)
|
||||
-t TOKEN_CLASSIC, --token TOKEN_CLASSIC
|
||||
-t, --token TOKEN_CLASSIC
|
||||
personal access, OAuth, or JSON Web token, or path to
|
||||
token (file://...)
|
||||
-f, --token-fine TOKEN_FINE
|
||||
fine-grained personal access token (github_pat_....),
|
||||
or path to token (file://...)
|
||||
-q, --quiet supress log messages less severe than warning, e.g.
|
||||
info
|
||||
--as-app authenticate as github app instead of as a user.
|
||||
-o OUTPUT_DIRECTORY, --output-directory OUTPUT_DIRECTORY
|
||||
-o, --output-directory OUTPUT_DIRECTORY
|
||||
directory at which to backup the repositories
|
||||
-l LOG_LEVEL, --log-level LOG_LEVEL
|
||||
-l, --log-level LOG_LEVEL
|
||||
log level to use (default: info, possible levels:
|
||||
debug, info, warning, error, critical)
|
||||
-i, --incremental incremental backup
|
||||
--incremental-by-files incremental backup using modified time of files
|
||||
--incremental-by-files
|
||||
incremental backup based on modification date of files
|
||||
--starred include JSON output of starred repositories in backup
|
||||
--all-starred include starred repositories in backup [*]
|
||||
--starred-skip-size-over MB
|
||||
skip starred repositories larger than this size in MB
|
||||
--watched include JSON output of watched repositories in backup
|
||||
--followers include JSON output of followers in backup
|
||||
--following include JSON output of following users in backup
|
||||
@@ -100,20 +103,22 @@ CLI Help output::
|
||||
--milestones include milestones in backup
|
||||
--repositories include repository clone in backup
|
||||
--bare clone bare repositories
|
||||
--no-prune disable prune option for git fetch
|
||||
--lfs clone LFS repositories (requires Git LFS to be
|
||||
installed, https://git-lfs.github.com) [*]
|
||||
--wikis include wiki clone in backup
|
||||
--gists include gists in backup [*]
|
||||
--starred-gists include starred gists in backup [*]
|
||||
--skip-archived skip project if it is archived
|
||||
--skip-existing skip project if a backup directory exists
|
||||
-L [LANGUAGES [LANGUAGES ...]], --languages [LANGUAGES [LANGUAGES ...]]
|
||||
-L, --languages [LANGUAGES ...]
|
||||
only allow these languages
|
||||
-N NAME_REGEX, --name-regex NAME_REGEX
|
||||
-N, --name-regex NAME_REGEX
|
||||
python regex to match names against
|
||||
-H GITHUB_HOST, --github-host GITHUB_HOST
|
||||
-H, --github-host GITHUB_HOST
|
||||
GitHub Enterprise hostname
|
||||
-O, --organization whether or not this is an organization user
|
||||
-R REPOSITORY, --repository REPOSITORY
|
||||
-R, --repository REPOSITORY
|
||||
name of repository to limit backup to
|
||||
-P, --private include private repositories [*]
|
||||
-F, --fork include forked repositories [*]
|
||||
@@ -128,19 +133,16 @@ CLI Help output::
|
||||
--releases include release information, not including assets or
|
||||
binaries
|
||||
--latest-releases NUMBER_OF_LATEST_RELEASES
|
||||
include certain number of the latest releases;
|
||||
only applies if including releases
|
||||
--skip-prerelease skip prerelease and draft versions; only applies if including releases
|
||||
include certain number of the latest releases; only
|
||||
applies if including releases
|
||||
--skip-prerelease skip prerelease and draft versions; only applies if
|
||||
including releases
|
||||
--assets include assets alongside release information; only
|
||||
applies if including releases
|
||||
--skip-assets-on [REPO ...]
|
||||
skip asset downloads for these repositories (e.g.
|
||||
--skip-assets-on repo1 owner/repo2)
|
||||
--attachments download user-attachments from issues and pull requests
|
||||
to issues/attachments/{issue_number}/ and
|
||||
pulls/attachments/{pull_number}/ directories
|
||||
--exclude [REPOSITORY [REPOSITORY ...]]
|
||||
names of repositories to exclude from backup.
|
||||
--skip-assets-on [SKIP_ASSETS_ON ...]
|
||||
skip asset downloads for these repositories
|
||||
--attachments download user-attachments from issues and pull
|
||||
requests
|
||||
--throttle-limit THROTTLE_LIMIT
|
||||
start throttling of GitHub API requests after this
|
||||
amount of API requests remain
|
||||
@@ -148,6 +150,8 @@ CLI Help output::
|
||||
wait this amount of seconds when API request
|
||||
throttling is active (default: 30.0, requires
|
||||
--throttle-limit to be set)
|
||||
--exclude [EXCLUDE ...]
|
||||
names of repositories to exclude
|
||||
|
||||
|
||||
Usage Details
|
||||
@@ -156,13 +160,13 @@ Usage Details
|
||||
Authentication
|
||||
--------------
|
||||
|
||||
**Password-based authentication** will fail if you have two-factor authentication enabled, and will `be deprecated <https://github.blog/2023-03-09-raising-the-bar-for-software-security-github-2fa-begins-march-13/>`_ by 2023 EOY.
|
||||
GitHub requires token-based authentication for API access. Password authentication was `removed in November 2020 <https://developer.github.com/changes/2020-02-14-deprecating-password-auth/>`_.
|
||||
|
||||
``--username`` is used for basic password authentication and separate from the positional argument ``USER``, which specifies the user account you wish to back up.
|
||||
The positional argument ``USER`` specifies the user or organization account you wish to back up.
|
||||
|
||||
**Classic tokens** are `slightly less secure <https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#personal-access-tokens-classic>`_ as they provide very coarse-grained permissions.
|
||||
**Fine-grained tokens** (``-f TOKEN_FINE``) are recommended for most use cases, especially long-running backups (e.g. cron jobs), as they provide precise permission control.
|
||||
|
||||
If you need authentication for long-running backups (e.g. for a cron job) it is recommended to use **fine-grained personal access token** ``-f TOKEN_FINE``.
|
||||
**Classic tokens** (``-t TOKEN``) are `slightly less secure <https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#personal-access-tokens-classic>`_ as they provide very coarse-grained permissions.
|
||||
|
||||
|
||||
Fine Tokens
|
||||
@@ -290,10 +294,20 @@ All is not everything
|
||||
|
||||
The ``--all`` argument does not include: cloning private repos (``-P, --private``), cloning forks (``-F, --fork``), cloning starred repositories (``--all-starred``), ``--pull-details``, cloning LFS repositories (``--lfs``), cloning gists (``--gists``) or cloning starred gist repos (``--starred-gists``). See examples for more.
|
||||
|
||||
Cloning all starred size
|
||||
------------------------
|
||||
Starred repository size
|
||||
-----------------------
|
||||
|
||||
Using the ``--all-starred`` argument to clone all starred repositories may use a large amount of storage space, especially if ``--all`` or more arguments are used. e.g. commonly starred repos can have tens of thousands of issues, many large assets and the repo itself etc. Consider just storing links to starred repos in JSON format with ``--starred``.
|
||||
Using the ``--all-starred`` argument to clone all starred repositories may use a large amount of storage space.
|
||||
|
||||
To see your starred repositories sorted by size (requires `GitHub CLI <https://cli.github.com>`_)::
|
||||
|
||||
gh api user/starred --paginate --jq 'sort_by(-.size)[]|"\(.full_name) \(.size/1024|round)MB"'
|
||||
|
||||
To limit which starred repositories are cloned, use ``--starred-skip-size-over SIZE`` where SIZE is in MB. For example, ``--starred-skip-size-over 500`` will skip any starred repository where the git repository size (code and history) exceeds 500 MB. Note that this size limit only applies to the repository itself, not issues, release assets or other metadata. This filter only affects starred repositories; your own repositories are always included regardless of size.
|
||||
|
||||
For finer control, avoid using ``--assets`` with starred repos, or use ``--skip-assets-on`` for specific repositories with large release binaries.
|
||||
|
||||
Alternatively, consider just storing links to starred repos in JSON format with ``--starred``.
|
||||
|
||||
Incremental Backup
|
||||
------------------
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.58.0"
|
||||
__version__ = "0.59.0"
|
||||
|
||||
@@ -43,7 +43,7 @@ def main():
|
||||
if args.private and not get_auth(args):
|
||||
logger.warning(
|
||||
"The --private flag has no effect without authentication. "
|
||||
"Use -t/--token, -f/--token-fine, or -u/--username to authenticate."
|
||||
"Use -t/--token or -f/--token-fine to authenticate."
|
||||
)
|
||||
|
||||
if args.quiet:
|
||||
|
||||
@@ -7,7 +7,6 @@ import base64
|
||||
import calendar
|
||||
import codecs
|
||||
import errno
|
||||
import getpass
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -24,7 +23,6 @@ from collections.abc import Generator
|
||||
from datetime import datetime
|
||||
from http.client import IncompleteRead
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import quote as urlquote
|
||||
from urllib.parse import urlencode, urlparse
|
||||
from urllib.request import HTTPRedirectHandler, Request, build_opener, urlopen
|
||||
|
||||
@@ -149,17 +147,6 @@ def mask_password(url, secret="*****"):
|
||||
def parse_args(args=None):
|
||||
parser = argparse.ArgumentParser(description="Backup a github account")
|
||||
parser.add_argument("user", metavar="USER", type=str, help="github username")
|
||||
parser.add_argument(
|
||||
"-u", "--username", dest="username", help="username for basic auth"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--password",
|
||||
dest="password",
|
||||
help="password for basic auth. "
|
||||
"If a username is given but not a password, the "
|
||||
"password will be prompted for.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--token",
|
||||
@@ -224,6 +211,13 @@ def parse_args(args=None):
|
||||
dest="all_starred",
|
||||
help="include starred repositories in backup [*]",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--starred-skip-size-over",
|
||||
type=int,
|
||||
metavar="MB",
|
||||
dest="starred_skip_size_over",
|
||||
help="skip starred repositories larger than this size in MB",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--watched",
|
||||
action="store_true",
|
||||
@@ -533,16 +527,6 @@ def get_auth(args, encode=True, for_git_cli=False):
|
||||
auth = args.token_classic
|
||||
else:
|
||||
auth = "x-access-token:" + args.token_classic
|
||||
elif args.username:
|
||||
if not args.password:
|
||||
args.password = getpass.getpass()
|
||||
if encode:
|
||||
password = args.password
|
||||
else:
|
||||
password = urlquote(args.password)
|
||||
auth = args.username + ":" + password
|
||||
elif args.password:
|
||||
raise Exception("You must specify a username for basic auth")
|
||||
|
||||
if not auth:
|
||||
return None
|
||||
@@ -1593,6 +1577,25 @@ def filter_repositories(args, unfiltered_repositories):
|
||||
]
|
||||
if args.skip_archived:
|
||||
repositories = [r for r in repositories if not r.get("archived")]
|
||||
if args.starred_skip_size_over is not None:
|
||||
if args.starred_skip_size_over <= 0:
|
||||
logger.warning(
|
||||
"--starred-skip-size-over must be greater than 0, ignoring"
|
||||
)
|
||||
else:
|
||||
size_limit_kb = args.starred_skip_size_over * 1024
|
||||
filtered = []
|
||||
for r in repositories:
|
||||
if r.get("is_starred") and r.get("size", 0) > size_limit_kb:
|
||||
size_mb = r.get("size", 0) / 1024
|
||||
logger.info(
|
||||
"Skipping starred repo {0} ({1:.0f} MB) due to --starred-skip-size-over {2}".format(
|
||||
r.get("full_name", r.get("name")), size_mb, args.starred_skip_size_over
|
||||
)
|
||||
)
|
||||
else:
|
||||
filtered.append(r)
|
||||
repositories = filtered
|
||||
if args.exclude:
|
||||
repositories = [
|
||||
r for r in repositories if "name" not in r or r["name"] not in args.exclude
|
||||
|
||||
@@ -46,8 +46,6 @@ class TestAllStarredCloning:
|
||||
args.prefer_ssh = False
|
||||
args.token_classic = None
|
||||
args.token_fine = None
|
||||
args.username = None
|
||||
args.password = None
|
||||
args.as_app = False
|
||||
args.osx_keychain_item_name = None
|
||||
args.osx_keychain_item_account = None
|
||||
|
||||
@@ -24,8 +24,6 @@ def attachment_test_setup(tmp_path):
|
||||
args.as_app = False
|
||||
args.token_fine = None
|
||||
args.token_classic = None
|
||||
args.username = None
|
||||
args.password = None
|
||||
args.osx_keychain_item_name = None
|
||||
args.osx_keychain_item_account = None
|
||||
args.user = "testuser"
|
||||
|
||||
@@ -26,6 +26,8 @@ class TestCaseSensitivity:
|
||||
args.private = False
|
||||
args.public = False
|
||||
args.all = True
|
||||
args.skip_archived = False
|
||||
args.starred_skip_size_over = None
|
||||
|
||||
# Simulate GitHub API returning canonical case
|
||||
repos = [
|
||||
@@ -65,6 +67,8 @@ class TestCaseSensitivity:
|
||||
args.private = False
|
||||
args.public = False
|
||||
args.all = True
|
||||
args.skip_archived = False
|
||||
args.starred_skip_size_over = None
|
||||
|
||||
repos = [
|
||||
{
|
||||
@@ -93,6 +97,8 @@ class TestCaseSensitivity:
|
||||
args.private = False
|
||||
args.public = False
|
||||
args.all = True
|
||||
args.skip_archived = False
|
||||
args.starred_skip_size_over = None
|
||||
|
||||
repos = [
|
||||
{"name": "repo1", "owner": {"login": "test-user"}, "private": False, "fork": False},
|
||||
|
||||
@@ -17,8 +17,6 @@ class TestHTTP451Exception:
|
||||
args.as_app = False
|
||||
args.token_fine = None
|
||||
args.token_classic = None
|
||||
args.username = None
|
||||
args.password = None
|
||||
args.osx_keychain_item_name = None
|
||||
args.osx_keychain_item_account = None
|
||||
args.throttle_limit = None
|
||||
@@ -52,8 +50,6 @@ class TestHTTP451Exception:
|
||||
args.as_app = False
|
||||
args.token_fine = None
|
||||
args.token_classic = None
|
||||
args.username = None
|
||||
args.password = None
|
||||
args.osx_keychain_item_name = None
|
||||
args.osx_keychain_item_account = None
|
||||
args.throttle_limit = None
|
||||
@@ -78,8 +74,6 @@ class TestHTTP451Exception:
|
||||
args.as_app = False
|
||||
args.token_fine = None
|
||||
args.token_classic = None
|
||||
args.username = None
|
||||
args.password = None
|
||||
args.osx_keychain_item_name = None
|
||||
args.osx_keychain_item_account = None
|
||||
args.throttle_limit = None
|
||||
|
||||
@@ -45,8 +45,6 @@ def mock_args():
|
||||
args.as_app = False
|
||||
args.token_fine = None
|
||||
args.token_classic = "fake_token"
|
||||
args.username = None
|
||||
args.password = None
|
||||
args.osx_keychain_item_name = None
|
||||
args.osx_keychain_item_account = None
|
||||
args.throttle_limit = None
|
||||
|
||||
@@ -70,8 +70,6 @@ class TestRetrieveDataRetry:
|
||||
args.as_app = False
|
||||
args.token_fine = None
|
||||
args.token_classic = "fake_token"
|
||||
args.username = None
|
||||
args.password = None
|
||||
args.osx_keychain_item_name = None
|
||||
args.osx_keychain_item_account = None
|
||||
args.throttle_limit = None
|
||||
@@ -313,8 +311,6 @@ class TestRetrieveDataThrottling:
|
||||
args.as_app = False
|
||||
args.token_fine = None
|
||||
args.token_classic = "fake_token"
|
||||
args.username = None
|
||||
args.password = None
|
||||
args.osx_keychain_item_name = None
|
||||
args.osx_keychain_item_account = None
|
||||
args.throttle_limit = 10 # Throttle when remaining <= 10
|
||||
@@ -344,8 +340,6 @@ class TestRetrieveDataSingleItem:
|
||||
args.as_app = False
|
||||
args.token_fine = None
|
||||
args.token_classic = "fake_token"
|
||||
args.username = None
|
||||
args.password = None
|
||||
args.osx_keychain_item_name = None
|
||||
args.osx_keychain_item_account = None
|
||||
args.throttle_limit = None
|
||||
|
||||
@@ -48,8 +48,6 @@ class TestSkipAssetsOn:
|
||||
args.prefer_ssh = False
|
||||
args.token_classic = "test-token"
|
||||
args.token_fine = None
|
||||
args.username = None
|
||||
args.password = None
|
||||
args.as_app = False
|
||||
args.osx_keychain_item_name = None
|
||||
args.osx_keychain_item_account = None
|
||||
|
||||
224
tests/test_starred_skip_size_over.py
Normal file
224
tests/test_starred_skip_size_over.py
Normal file
@@ -0,0 +1,224 @@
|
||||
"""Tests for --starred-skip-size-over flag behavior (issue #108)."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from github_backup import github_backup
|
||||
|
||||
|
||||
class TestStarredSkipSizeOver:
|
||||
"""Test suite for --starred-skip-size-over flag.
|
||||
|
||||
Issue #108: Allow restricting size of starred repositories before cloning.
|
||||
The size is based on the GitHub API's 'size' field (in KB), but the CLI
|
||||
argument accepts MB for user convenience.
|
||||
"""
|
||||
|
||||
def _create_mock_args(self, **overrides):
|
||||
"""Create a mock args object with sensible defaults."""
|
||||
args = Mock()
|
||||
args.user = "testuser"
|
||||
args.repository = None
|
||||
args.name_regex = None
|
||||
args.languages = None
|
||||
args.fork = False
|
||||
args.private = False
|
||||
args.skip_archived = False
|
||||
args.starred_skip_size_over = None
|
||||
args.exclude = None
|
||||
|
||||
for key, value in overrides.items():
|
||||
setattr(args, key, value)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
class TestStarredSkipSizeOverArgumentParsing(TestStarredSkipSizeOver):
|
||||
"""Tests for --starred-skip-size-over argument parsing."""
|
||||
|
||||
def test_starred_skip_size_over_not_set_defaults_to_none(self):
|
||||
"""When --starred-skip-size-over is not specified, it should default to None."""
|
||||
args = github_backup.parse_args(["testuser"])
|
||||
assert args.starred_skip_size_over is None
|
||||
|
||||
def test_starred_skip_size_over_accepts_integer(self):
|
||||
"""--starred-skip-size-over should accept an integer value."""
|
||||
args = github_backup.parse_args(["testuser", "--starred-skip-size-over", "500"])
|
||||
assert args.starred_skip_size_over == 500
|
||||
|
||||
def test_starred_skip_size_over_rejects_non_integer(self):
|
||||
"""--starred-skip-size-over should reject non-integer values."""
|
||||
with pytest.raises(SystemExit):
|
||||
github_backup.parse_args(["testuser", "--starred-skip-size-over", "abc"])
|
||||
|
||||
|
||||
class TestStarredSkipSizeOverFiltering(TestStarredSkipSizeOver):
|
||||
"""Tests for --starred-skip-size-over filtering behavior."""
|
||||
|
||||
def test_starred_repo_under_limit_is_kept(self):
|
||||
"""Starred repos under the size limit should be kept."""
|
||||
args = self._create_mock_args(starred_skip_size_over=500)
|
||||
|
||||
repos = [
|
||||
{
|
||||
"name": "small-repo",
|
||||
"owner": {"login": "otheruser"},
|
||||
"size": 100 * 1024, # 100 MB in KB
|
||||
"is_starred": True,
|
||||
}
|
||||
]
|
||||
|
||||
result = github_backup.filter_repositories(args, repos)
|
||||
assert len(result) == 1
|
||||
assert result[0]["name"] == "small-repo"
|
||||
|
||||
def test_starred_repo_over_limit_is_filtered(self):
|
||||
"""Starred repos over the size limit should be filtered out."""
|
||||
args = self._create_mock_args(starred_skip_size_over=500)
|
||||
|
||||
repos = [
|
||||
{
|
||||
"name": "huge-repo",
|
||||
"owner": {"login": "otheruser"},
|
||||
"size": 600 * 1024, # 600 MB in KB
|
||||
"is_starred": True,
|
||||
}
|
||||
]
|
||||
|
||||
result = github_backup.filter_repositories(args, repos)
|
||||
assert len(result) == 0
|
||||
|
||||
def test_own_repo_over_limit_is_kept(self):
|
||||
"""User's own repos should not be affected by the size limit."""
|
||||
args = self._create_mock_args(starred_skip_size_over=500)
|
||||
|
||||
repos = [
|
||||
{
|
||||
"name": "my-huge-repo",
|
||||
"owner": {"login": "testuser"},
|
||||
"size": 600 * 1024, # 600 MB in KB
|
||||
# No is_starred flag - this is the user's own repo
|
||||
}
|
||||
]
|
||||
|
||||
result = github_backup.filter_repositories(args, repos)
|
||||
assert len(result) == 1
|
||||
assert result[0]["name"] == "my-huge-repo"
|
||||
|
||||
def test_starred_repo_at_exact_limit_is_kept(self):
|
||||
"""Starred repos at exactly the size limit should be kept."""
|
||||
args = self._create_mock_args(starred_skip_size_over=500)
|
||||
|
||||
repos = [
|
||||
{
|
||||
"name": "exact-limit-repo",
|
||||
"owner": {"login": "otheruser"},
|
||||
"size": 500 * 1024, # Exactly 500 MB in KB
|
||||
"is_starred": True,
|
||||
}
|
||||
]
|
||||
|
||||
result = github_backup.filter_repositories(args, repos)
|
||||
assert len(result) == 1
|
||||
assert result[0]["name"] == "exact-limit-repo"
|
||||
|
||||
def test_mixed_repos_filtered_correctly(self):
|
||||
"""Mix of own and starred repos should be filtered correctly."""
|
||||
args = self._create_mock_args(starred_skip_size_over=500)
|
||||
|
||||
repos = [
|
||||
{
|
||||
"name": "my-huge-repo",
|
||||
"owner": {"login": "testuser"},
|
||||
"size": 1000 * 1024, # 1 GB - own repo, should be kept
|
||||
},
|
||||
{
|
||||
"name": "starred-small",
|
||||
"owner": {"login": "otheruser"},
|
||||
"size": 100 * 1024, # 100 MB - under limit
|
||||
"is_starred": True,
|
||||
},
|
||||
{
|
||||
"name": "starred-huge",
|
||||
"owner": {"login": "anotheruser"},
|
||||
"size": 2000 * 1024, # 2 GB - over limit
|
||||
"is_starred": True,
|
||||
},
|
||||
]
|
||||
|
||||
result = github_backup.filter_repositories(args, repos)
|
||||
assert len(result) == 2
|
||||
names = [r["name"] for r in result]
|
||||
assert "my-huge-repo" in names
|
||||
assert "starred-small" in names
|
||||
assert "starred-huge" not in names
|
||||
|
||||
def test_no_size_limit_keeps_all_starred(self):
|
||||
"""When no size limit is set, all starred repos should be kept."""
|
||||
args = self._create_mock_args(starred_skip_size_over=None)
|
||||
|
||||
repos = [
|
||||
{
|
||||
"name": "huge-starred-repo",
|
||||
"owner": {"login": "otheruser"},
|
||||
"size": 10000 * 1024, # 10 GB
|
||||
"is_starred": True,
|
||||
}
|
||||
]
|
||||
|
||||
result = github_backup.filter_repositories(args, repos)
|
||||
assert len(result) == 1
|
||||
|
||||
def test_repo_without_size_field_is_kept(self):
|
||||
"""Repos without a size field should be kept (size defaults to 0)."""
|
||||
args = self._create_mock_args(starred_skip_size_over=500)
|
||||
|
||||
repos = [
|
||||
{
|
||||
"name": "no-size-repo",
|
||||
"owner": {"login": "otheruser"},
|
||||
"is_starred": True,
|
||||
# No size field
|
||||
}
|
||||
]
|
||||
|
||||
result = github_backup.filter_repositories(args, repos)
|
||||
assert len(result) == 1
|
||||
|
||||
def test_zero_value_warns_and_is_ignored(self, caplog):
|
||||
"""Zero value should warn and keep all repos."""
|
||||
args = self._create_mock_args(starred_skip_size_over=0)
|
||||
|
||||
repos = [
|
||||
{
|
||||
"name": "huge-starred-repo",
|
||||
"owner": {"login": "otheruser"},
|
||||
"size": 10000 * 1024, # 10 GB
|
||||
"is_starred": True,
|
||||
}
|
||||
]
|
||||
|
||||
result = github_backup.filter_repositories(args, repos)
|
||||
assert len(result) == 1
|
||||
assert "must be greater than 0" in caplog.text
|
||||
|
||||
def test_negative_value_warns_and_is_ignored(self, caplog):
|
||||
"""Negative value should warn and keep all repos."""
|
||||
args = self._create_mock_args(starred_skip_size_over=-5)
|
||||
|
||||
repos = [
|
||||
{
|
||||
"name": "huge-starred-repo",
|
||||
"owner": {"login": "otheruser"},
|
||||
"size": 10000 * 1024, # 10 GB
|
||||
"is_starred": True,
|
||||
}
|
||||
]
|
||||
|
||||
result = github_backup.filter_repositories(args, repos)
|
||||
assert len(result) == 1
|
||||
assert "must be greater than 0" in caplog.text
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user