diff --git a/README.md b/README.md index 7907454..35a70c4 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,6 @@ list-old-packages ├── --list-debug └── repo_path* run -├── --delete-debug ├── --dry-run ├── --only-delete └── repo_path* @@ -45,11 +44,25 @@ Arguments: - `--dry-run`: Do a dry run - `--only-delete`: Only delete files, don't modify the repo files from them (default: false) - Without this argument, parcut will try to remove and add the relevant packages using `repo-add` and `repo-remove`, meaning it optionally depends on those programs. - - These are needed to run the repo anyways, so you *should* have them installed already. -- `--delete-debug`: Delete debug symbol packages (default: true) + - As long as you're on Arch, this will be installed, as it's part of pacman. Plus, if you're running a repo, you need these anyways. + +## Exit codes + +- `0`: Completed successfully +- `10`: Failed to remove old package from repo - probably missing write perms on the database. +- `11`: Permission denied when trying to delete a package - missing write perms on the package. +- `12`: Failed to add new package to repo - again, probably missing write perms on the database. ## Notes and credits This was inspired by [guydunigo/remove_old_arch_pkgs](https://github.com/guydunigo/remove_old_arch_pkgs), which I used at first, but ran into some bugs with. -I also ~~stole~~ borrowed a bit of code from [blend-os/blend](https://github.com/blend-os/blend) for coloring the terminal. +I also ~~stole~~ borrowed and modified a bit of code from [blend-os/blend](https://github.com/blend-os/blend) for coloring the terminal. + +## Notes and limitations + +This requires a repo db file to already exist; if the repo doesn't already exist, you can just create an empty file like this instead: `touch reponame.db` + +### Development + +This uses `unittest` for testing; tests can be run by running `python3 -m unittest`. Running `test.py` normally will generate data to test the program, which is also generated automatically at the start of every test. diff --git a/colors.py b/colors.py new file mode 100644 index 0000000..65dec9e --- /dev/null +++ b/colors.py @@ -0,0 +1,71 @@ +# copied from https://github.com/blend-os/blend, and modified +class colors: + reset = '\x1b[0m' + bold = '\x1b[01m' + disable = '\x1b[02m' + underline = '\x1b[04m' + reverse = '\x1b[07m' + strikethrough = '\x1b[09m' + invisible = '\x1b[08m' + + class fg: + black = '\x1b[30m' + red = '\x1b[31m' + green = '\x1b[32m' + yellow = '\x1b[33m' + blue = '\x1b[34m' + magenta = '\x1b[35m' + cyan = '\x1b[36m' + white = '\x1b[37m' + default = '\x1b[39m' + + # bright colors based off this: https://gist.github.com/nfejzic/6f3788b3841cb0d7ac11584c6b33b5b9 + class bright: + black = '\x1b[90m' + red = '\x1b[91m' + green = '\x1b[92m' + yellow = '\x1b[93m' + blue = '\x1b[94m' + magenta = '\x1b[95m' + cyan = '\x1b[96m' + white = '\x1b[97m' + + rainbow = [ + bright.red, + yellow, + bright.yellow, + bright.green, + bright.cyan, + blue, + magenta + ] + + class bg: + black = '\x1b[40m' + red = '\x1b[41m' + green = '\x1b[42m' + yellow = '\x1b[43m' + blue = '\x1b[44m' + magenta = '\x1b[45m' + cyan = '\x1b[46m' + white = '\x1b[47m' + + class bright: + black = '\x1b[40m' + red = '\x1b[41m' + green = '\x1b[42m' + yellow = '\x1b[43m' + blue = '\x1b[44m' + magenta = '\x1b[45m' + cyan = '\x1b[46m' + white = '\x1b[47m' + + rainbow = [ + bright.red, + yellow, + bright.yellow, + bright.green, + bright.cyan, + blue, + magenta + ] diff --git a/repo-cleanup.py b/repo-cleanup.py index 3f6cd4a..d42dc94 100755 --- a/repo-cleanup.py +++ b/repo-cleanup.py @@ -2,6 +2,9 @@ from natsort import natsorted import click import glob +from subprocess import getstatusoutput +from colors import colors +from os import remove @click.group('cli') @@ -17,15 +20,70 @@ def cli(): default=False, help="Only delete files, don't run repo-remove and repo-add on their old and new versions (default: false)" ) -@click.option( - '--delete-debug', - is_flag=True, - default=False, - help='Delete debug symbol packages (default: false)' -) @click.argument('repo_path') -def run(repo_path, dry_run=False, only_delete=False, delete_debug=False): - get_old_packages_runner(repo_path) +def run(repo_path, dry_run=False, only_delete=False): + if dry_run: + print( + f'{colors.bg.bright.blue}{colors.fg.black}Dry run mode enabled; no changes will be made{colors.reset}\n' + ) + old_packages = get_old_packages(repo_path) + repo_name = HelperFunctions.get_repo_name(repo_path) + + if not only_delete: + print('=== Removing old packages ===') + for pkg in old_packages: + print(f'Removing {pkg}...') + if not dry_run: + output = getstatusoutput( + f'repo-remove {repo_path}/{repo_name}.db.tar.zst {repo_path}/{pkg}' + ) + if output[0] != 0: + print( + f'{colors.bg.red}[ERROR]{colors.reset} failed to remove {pkg}' + ) + print(f'Exit code: {output[0]}') + print(f'Command output:\n{output[1]}') + exit(10) + + print( + f'{colors.fg.green}✅ Successfully removed old packages from repo{colors.reset}' + ) + print() + + print('=== Deleting old packages ===') + for pkg in old_packages: + print(f'Deleting {pkg}...') + if not dry_run: + try: + remove(f'{repo_path}/{pkg}') + except Exception as e: + print(f'{colors.bg.red}[ERROR]{colors.reset} failed to delete {pkg}') + print(f'Error: {e}') + exit(11) + + print(f'{colors.fg.green}✅ Successfully deleted packages{colors.reset}') + print() + + new_packages = get_new_packages(repo_path) + if not only_delete: + print('=== Adding new packages ===') + for pkg in new_packages: + print(f'Adding {pkg}...') + if not dry_run: + output = getstatusoutput( + f'repo-add {repo_path}/{repo_name}.db.tar.zst {repo_path}/{pkg}' + ) + if output[0] != 0: + print(f'{colors.bg.red}[ERROR]{colors.reset} failed to add {pkg}') + print(f'Exit code: {output[0]}') + print(f'Command output:\n{output[1]}') + exit(12) + print( + f'{colors.fg.green}✅ Successfully added new packages to repo{colors.reset}' + ) + print() + + print(f'✨ {colors.bg.green}Repo successfully updated{colors.reset} ✨') @cli.command('list-old-packages') @@ -60,11 +118,9 @@ def get_old_packages(repo_path, list_debug=True): old_no_debug = natsorted(no_debug)[:1] old_packages.extend(old_no_debug) for item in old_no_debug: - if ( - list_debug - and item.replace(pkg_name, f'{pkg_name}-debug') in pkg_map[pkg_name] - ): - old_packages.append(item.replace(pkg_name, f'{pkg_name}-debug')) + debug_pkg = item.replace(pkg_name, f'{pkg_name}-debug') + if list_debug and debug_pkg in pkg_map[pkg_name]: + old_packages.append(debug_pkg) return natsorted(old_packages) @@ -126,11 +182,11 @@ class HelperFunctions: Example: { - 'swayfx': [ - 'swayfx-0.4-3-x86_64.pkg.tar.zst', - 'swayfx-debug-0.4-3-x86_64.pkg.tar.zst', - 'swayfx-0.3.2-1-x86_64.pkg.tar.zst - ] + 'swayfx': [ + 'swayfx-0.4-3-x86_64.pkg.tar.zst', + 'swayfx-debug-0.4-3-x86_64.pkg.tar.zst', + 'swayfx-0.3.2-1-x86_64.pkg.tar.zst + ] } ''' files = HelperFunctions.get_package_list(repo_path) @@ -180,53 +236,14 @@ class HelperFunctions: files = [f[f.rfind('/') + 1 :] for f in files] repo_name = HelperFunctions.get_repo_name(repo_path) - files.remove(f'{repo_name}.db.tar.zst') - files.remove(f'{repo_name}.files.tar.zst') + try: + files.remove(f'{repo_name}.db.tar.zst') + files.remove(f'{repo_name}.files.tar.zst') + except FileNotFoundError: + pass return natsorted(files) if __name__ == '__main__': cli() - - -class colors: - reset = '\x1b[0m' - bold = '\x1b[01m' - disable = '\x1b[02m' - underline = '\x1b[04m' - reverse = '\x1b[07m' - strikethrough = '\x1b[09m' - invisible = '\x1b[08m' - - class fg: - black = '\x1b[30m' - red = '\x1b[31m' - green = '\x1b[32m' - orange = '\x1b[33m' - blue = '\x1b[34m' - purple = '\x1b[35m' - cyan = '\x1b[36m' - lightgrey = '\x1b[37m' - darkgrey = '\x1b[90m' - lightred = '\x1b[91m' - lightgreen = '\x1b[92m' - yellow = '\x1b[93m' - lightblue = '\x1b[94m' - pink = '\x1b[95m' - lightcyan = '\x1b[96m' - - rainbow = [lightred, orange, yellow, lightgreen, lightcyan, blue, purple] - - class bg: - black = '\x1b[40m' - red = '\x1b[41m' - green = '\x1b[42m' - orange = '\x1b[43m' - blue = '\x1b[44m' - purple = '\x1b[45m' - cyan = '\x1b[46m' - lightgrey = '\x1b[47m' - - -fg = colors.fg() diff --git a/test.py b/test.py index 2624e43..faaf3ff 100644 --- a/test.py +++ b/test.py @@ -1,6 +1,7 @@ import unittest import os from pathlib import Path +from natsort import natsorted repo_cleanup = __import__('repo-cleanup') repo_path = 'repo/' @@ -33,43 +34,100 @@ def create_test_data(): touch(f'{repo_path}/{f}') +# NOTE: Add lists are wrapped in natsorted() so that they can be easily manually edited, +# rather than having to run and update them manually each time a test is updated class Tests(unittest.TestCase): - def test_list_old(self): + def test_run(self): create_test_data() - result = repo_cleanup.get_old_packages(repo_path) - expected = [ - 'example-program-8.4.3-5-x86_64.pkg.tar.zst', - 'test-debug-r87.e176baf-1-x86_64.pkg.tar.zst', - 'test-r87.e176baf-1-x86_64.pkg.tar.zst' - ] - self.assertEqual(result, expected) - - def test_list_new(self): - create_test_data() - result = repo_cleanup.get_new_packages(repo_path) - expected = [ - 'example-program-10.2.1-2-x86_64.pkg.tar.zst', - 'example-program-debug-10.2.1-2-x86_64.pkg.tar.zst', - 'test-debug-r100.ab937ef-1-x86_64.pkg.tar.zst', - 'test-r100.ab937ef-1-x86_64.pkg.tar.zst' - ] - self.assertEqual(result, expected) - - def test_package_map(self): - create_test_data() - result = repo_cleanup.HelperFunctions.package_map(repo_path) - expected = { - 'example-program': [ + # to run a click command - https://stackoverflow.com/questions/48619517/call-a-click-command-from-code + repo_cleanup.run([repo_path, '--only-delete'], standalone_mode=False) + result = natsorted(os.listdir(repo_path)) + expected = natsorted( + [ 'example-program-8.4.3-5-x86_64.pkg.tar.zst', 'example-program-10.2.1-2-x86_64.pkg.tar.zst', - 'example-program-debug-10.2.1-2-x86_64.pkg.tar.zst' - ], - 'test': [ + 'example-program-debug-10.2.1-2-x86_64.pkg.tar.zst', + 'repo.db', + 'repo.db.tar.zst', + 'repo.files', + 'repo.files.tar.zst', 'test-debug-r87.e176baf-1-x86_64.pkg.tar.zst', 'test-debug-r100.ab937ef-1-x86_64.pkg.tar.zst', 'test-r87.e176baf-1-x86_64.pkg.tar.zst', 'test-r100.ab937ef-1-x86_64.pkg.tar.zst' ] + ) + + def test_list_old(self): + create_test_data() + result = repo_cleanup.get_old_packages(repo_path) + expected = natsorted( + [ + 'example-program-8.4.3-5-x86_64.pkg.tar.zst', + 'test-debug-r87.e176baf-1-x86_64.pkg.tar.zst', + 'test-r87.e176baf-1-x86_64.pkg.tar.zst' + ] + ) + self.assertEqual(result, expected) + + def test_list_old_no_debug(self): + create_test_data() + result = repo_cleanup.get_old_packages(repo_path, list_debug=False) + expected = natsorted( + [ + 'example-program-8.4.3-5-x86_64.pkg.tar.zst', + 'test-r87.e176baf-1-x86_64.pkg.tar.zst' + ] + ) + self.assertEqual(result, expected) + + def test_list_new(self): + create_test_data() + result = repo_cleanup.get_new_packages(repo_path) + expected = natsorted( + [ + 'example-program-10.2.1-2-x86_64.pkg.tar.zst', + 'example-program-debug-10.2.1-2-x86_64.pkg.tar.zst', + 'test-debug-r100.ab937ef-1-x86_64.pkg.tar.zst', + 'test-r100.ab937ef-1-x86_64.pkg.tar.zst' + ] + ) + self.assertEqual(result, expected) + + def test_list_new_no_debug(self): + create_test_data() + result = repo_cleanup.get_new_packages(repo_path) + expected = natsorted( + [ + 'example-program-10.2.1-2-x86_64.pkg.tar.zst', + 'example-program-debug-10.2.1-2-x86_64.pkg.tar.zst', + 'test-debug-r100.ab937ef-1-x86_64.pkg.tar.zst', + 'test-r100.ab937ef-1-x86_64.pkg.tar.zst' + ] + ) + self.assertEqual(result, expected) + + def test_package_map(self): + create_test_data() + result = repo_cleanup.HelperFunctions.package_map(repo_path) + # only the lists are sorted, not the keys, so it should be manually run then verified it's correct if the keys are updated + # you can change the items in the list safely, though + expected = { + 'example-program': natsorted( + [ + 'example-program-8.4.3-5-x86_64.pkg.tar.zst', + 'example-program-10.2.1-2-x86_64.pkg.tar.zst', + 'example-program-debug-10.2.1-2-x86_64.pkg.tar.zst' + ] + ), + 'test': natsorted( + [ + 'test-debug-r87.e176baf-1-x86_64.pkg.tar.zst', + 'test-debug-r100.ab937ef-1-x86_64.pkg.tar.zst', + 'test-r87.e176baf-1-x86_64.pkg.tar.zst', + 'test-r100.ab937ef-1-x86_64.pkg.tar.zst' + ] + ) } self.assertEqual(result, expected)