diff --git a/.github/workflows/changelog-enforcer.yml b/.github/workflows/changelog-enforcer.yml index b2009fb3..cd31258b 100644 --- a/.github/workflows/changelog-enforcer.yml +++ b/.github/workflows/changelog-enforcer.yml @@ -8,8 +8,7 @@ jobs: changelog: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: dangoslen/changelog-enforcer@v2 + - uses: dangoslen/changelog-enforcer@v3 with: changeLogPath: 'CHANGELOG.md' skipLabels: 'Skip Changelog,0 diff trivial,automatic' diff --git a/.github/workflows/mepo.yaml b/.github/workflows/mepo.yaml index 14a81704..287f02f9 100644 --- a/.github/workflows/mepo.yaml +++ b/.github/workflows/mepo.yaml @@ -8,14 +8,14 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - python-version: [3.x, pypy-3.8] + python-version: ['3.9', '3.10', '3.11', 'pypy-3.9'] name: Python ${{ matrix.python-version }} on ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} cache: 'pip' diff --git a/CHANGELOG.md b/CHANGELOG.md index eef1fd61..4196add1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +## [1.48.0] - 2022-12-09 + +### Added + +- Added new `reset` command to reset a mepo clone + +### Changed + +- Updated GitHub Actions + ## [1.47.0] - 2022-11-14 ### Added diff --git a/mepo.d/cmdline/parser.py b/mepo.d/cmdline/parser.py index 48936a36..0a810eb4 100644 --- a/mepo.d/cmdline/parser.py +++ b/mepo.d/cmdline/parser.py @@ -36,6 +36,7 @@ def parse(self): self.__pull() self.__pull_all() self.__compare() + self.__reset() self.__whereis() self.__stage() self.__unstage() @@ -336,6 +337,26 @@ def __compare(self): action = 'store_true', help = 'Tells command to ignore terminal size and wrap') + def __reset(self): + reset = self.subparsers.add_parser( + 'reset', + description = 'Reset the current mepo clone to the original state. ' + 'This will delete all subrepos and does not check for uncommitted changes! ' + 'Must be run in the root of the mepo clone.', + aliases=mepoconfig.get_command_alias('reset')) + reset.add_argument( + '-f','--force', + action = 'store_true', + help = 'Force action.') + reset.add_argument( + '--reclone', + action = 'store_true', + help = 'Reclone repos after reset.') + reset.add_argument( + '-n','--dry-run', + action = 'store_true', + help = 'Dry-run only') + def __whereis(self): whereis = self.subparsers.add_parser( 'whereis', diff --git a/mepo.d/command/reset/reset.py b/mepo.d/command/reset/reset.py new file mode 100644 index 00000000..41cf79ef --- /dev/null +++ b/mepo.d/command/reset/reset.py @@ -0,0 +1,87 @@ +import os +import shutil + +from state.state import MepoState +from state.exceptions import NotInRootDirError + +from command.clone import clone as mepo_clone + +# This command will "reset" the mepo clone. This will delete all +# the subrepos, remove the .mepo directory, and then re-clone all the +# subrepos. This is useful if you want to start over with a fresh clone +# of the project. + +def run(args): + allcomps = MepoState.read_state() + + # Check to see that we are in the root directory of the project + ## First get the root directory of the project + rootdir = MepoState.get_root_dir() + ## Then get the current directory + curdir = os.getcwd() + ## Then check that they are the same, if they are not, then throw a NotInRootDirError + if rootdir != curdir: + raise NotInRootDirError('Error! As a safety precaution, you must be in the root directory of the project to reset') + + # If we get this far, then we are in the root directory of the project + + # If a user has called this command without the force flag, we + # will ask them to confirm that they want to reset the project + if not args.force and not args.dry_run: + print(f"Are you sure you want to reset the project? If so, type 'yes' and press enter.", end=' ') + answer = input() + if answer != "yes": + print("Reset cancelled.") + return + + # First, we need to delete all the subrepos + # Loop over all the components in reverse (since we are deleting them) + for comp in reversed(allcomps): + # If the component is the Fixture, then skip it + if comp.fixture: + continue + else: + # Get the relative path to the component + relpath = _get_relative_path(comp.local) + print(f'Removing {relpath}', end='...') + # Remove the component if not dry run + if not args.dry_run: + shutil.rmtree(relpath) + print('done.') + else: + print(f'dry-run only. Not removing {relpath}') + + # Next, we need to remove the .mepo directory + print(f'Removing .mepo', end='...') + if not args.dry_run: + shutil.rmtree('.mepo') + print('done.') + else: + print(f'dry-run only. Not removing .mepo') + + # If they pass in the --reclone flag, then we will re-clone all the subrepos + if args.reclone: + + # mepo_clone requires args which is an Argparse Namespace object + # We will create a new Namespace object with the correct arguments + # for mepo_clone + clone_args = type('Namespace', (object,), {'repo_url': None, 'directory': None, 'branch': None, 'config': None, 'allrepos': False, 'style': None}) + if not args.dry_run: + print('Re-cloning all subrepos') + mepo_clone.run(clone_args) + print('Recloning done.') + else: + print(f'Dry-run only. Not re-cloning all subrepos') + +def _get_relative_path(local_path): + """ + Get the relative path when given a local path. + + local_path: The path to a subrepo as known by mepo (relative to the .mepo directory) + """ + + # This creates a full path on the disk from the root of mepo and the local_path + full_local_path=os.path.join(MepoState.get_root_dir(),local_path) + + # We return the path relative to where we currently are + return os.path.relpath(full_local_path, os.getcwd()) diff --git a/mepo.d/state/exceptions.py b/mepo.d/state/exceptions.py index 02ed1529..a6e0aa04 100644 --- a/mepo.d/state/exceptions.py +++ b/mepo.d/state/exceptions.py @@ -17,3 +17,7 @@ class ConfigFileNotFoundError(FileNotFoundError): class SuffixNotRecognizedError(RuntimeError): """Raised when the config suffix is not recognized""" pass + +class NotInRootDirError(SystemExit): + """Raised when a command is run not in the root directory""" + pass