mirror of
https://github.com/avatao-content/baseimage-tutorial-framework
synced 2025-06-28 13:25:12 +00:00
Simplify package structure
This commit is contained in:
1
tfw/components/snapshots/__init__.py
Normal file
1
tfw/components/snapshots/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .snapshot_handler import SnapshotHandler
|
86
tfw/components/snapshots/snapshot_handler.py
Normal file
86
tfw/components/snapshots/snapshot_handler.py
Normal file
@ -0,0 +1,86 @@
|
||||
import logging
|
||||
from os.path import join as joinpath
|
||||
from os.path import basename
|
||||
from os import makedirs
|
||||
from datetime import datetime
|
||||
|
||||
from dateutil import parser as dateparser
|
||||
|
||||
from tfw.internals.networking import Scope
|
||||
|
||||
from .snapshot_provider import SnapshotProvider
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SnapshotHandler:
|
||||
keys = ['snapshot']
|
||||
|
||||
def __init__(self, *, directories, snapshots_dir, exclude_unix_patterns=None):
|
||||
self._snapshots_dir = snapshots_dir
|
||||
self.snapshot_providers = {}
|
||||
self._exclude_unix_patterns = exclude_unix_patterns
|
||||
self.init_snapshot_providers(directories)
|
||||
|
||||
self.command_handlers = {
|
||||
'take_snapshot': self.handle_take_snapshot,
|
||||
'restore_snapshot': self.handle_restore_snapshot,
|
||||
'exclude': self.handle_exclude
|
||||
}
|
||||
|
||||
def init_snapshot_providers(self, directories):
|
||||
for index, directory in enumerate(directories):
|
||||
git_dir = self.init_git_dir(index, directory)
|
||||
self.snapshot_providers[directory] = SnapshotProvider(
|
||||
directory,
|
||||
git_dir,
|
||||
self._exclude_unix_patterns
|
||||
)
|
||||
|
||||
def init_git_dir(self, index, directory):
|
||||
git_dir = joinpath(
|
||||
self._snapshots_dir,
|
||||
f'{basename(directory)}-{index}'
|
||||
)
|
||||
makedirs(git_dir, exist_ok=True)
|
||||
return git_dir
|
||||
|
||||
def handle_event(self, message, server_connector):
|
||||
try:
|
||||
data = message['data']
|
||||
message['data'] = self.command_handlers[data['command']](data)
|
||||
server_connector.send_message(message, scope=Scope.WEBSOCKET)
|
||||
except KeyError:
|
||||
LOG.error('IGNORING MESSAGE: Invalid message received: %s', message)
|
||||
|
||||
def handle_take_snapshot(self, data):
|
||||
LOG.debug('Taking snapshots of directories %s', self.snapshot_providers.keys())
|
||||
for provider in self.snapshot_providers.values():
|
||||
provider.take_snapshot()
|
||||
return data
|
||||
|
||||
def handle_restore_snapshot(self, data):
|
||||
date = dateparser.parse(
|
||||
data.get(
|
||||
'value',
|
||||
datetime.now().isoformat()
|
||||
)
|
||||
)
|
||||
LOG.debug(
|
||||
'Restoring snapshots (@ %s) of directories %s',
|
||||
date,
|
||||
self.snapshot_providers.keys()
|
||||
)
|
||||
for provider in self.snapshot_providers.values():
|
||||
provider.restore_snapshot(date)
|
||||
return data
|
||||
|
||||
def handle_exclude(self, data):
|
||||
exclude_unix_patterns = data['value']
|
||||
if not isinstance(exclude_unix_patterns, list):
|
||||
raise KeyError
|
||||
|
||||
for provider in self.snapshot_providers.values():
|
||||
provider.exclude = exclude_unix_patterns
|
||||
return data
|
221
tfw/components/snapshots/snapshot_provider.py
Normal file
221
tfw/components/snapshots/snapshot_provider.py
Normal file
@ -0,0 +1,221 @@
|
||||
import re
|
||||
from subprocess import run, CalledProcessError, PIPE
|
||||
from getpass import getuser
|
||||
from os.path import isdir
|
||||
from os.path import join as joinpath
|
||||
from uuid import uuid4
|
||||
|
||||
from dateutil import parser as dateparser
|
||||
|
||||
|
||||
class SnapshotProvider:
|
||||
def __init__(self, directory, git_dir, exclude_unix_patterns=None):
|
||||
self._classname = self.__class__.__name__
|
||||
author = f'{getuser()} via TFW {self._classname}'
|
||||
self.gitenv = {
|
||||
'GIT_DIR': git_dir,
|
||||
'GIT_WORK_TREE': directory,
|
||||
'GIT_AUTHOR_NAME': author,
|
||||
'GIT_AUTHOR_EMAIL': '',
|
||||
'GIT_COMMITTER_NAME': author,
|
||||
'GIT_COMMITTER_EMAIL': '',
|
||||
'GIT_PAGER': 'cat'
|
||||
}
|
||||
|
||||
self._init_repo()
|
||||
self.__last_valid_branch = self._branch
|
||||
if exclude_unix_patterns:
|
||||
self.exclude = exclude_unix_patterns
|
||||
|
||||
def _init_repo(self):
|
||||
self._check_environment()
|
||||
|
||||
if not self._repo_is_initialized:
|
||||
self._run(('git', 'init'))
|
||||
|
||||
if self._number_of_commits == 0:
|
||||
try:
|
||||
self._snapshot()
|
||||
except CalledProcessError:
|
||||
raise EnvironmentError(f'{self._classname} cannot init on empty directories!')
|
||||
|
||||
self._check_head_not_detached()
|
||||
|
||||
def _check_environment(self):
|
||||
if not isdir(self.gitenv['GIT_DIR']) or not isdir(self.gitenv['GIT_WORK_TREE']):
|
||||
raise EnvironmentError(f'{self._classname}: "directory" and "git_dir" must exist!')
|
||||
|
||||
@property
|
||||
def _repo_is_initialized(self):
|
||||
return self._run(
|
||||
('git', 'status'),
|
||||
check=False
|
||||
).returncode == 0
|
||||
|
||||
@property
|
||||
def _number_of_commits(self):
|
||||
return int(
|
||||
self._get_stdout((
|
||||
'git', 'rev-list',
|
||||
'--all',
|
||||
'--count'
|
||||
))
|
||||
)
|
||||
|
||||
def _snapshot(self):
|
||||
self._run((
|
||||
'git', 'add',
|
||||
'-A'
|
||||
))
|
||||
try:
|
||||
self._get_stdout((
|
||||
'git', 'commit',
|
||||
'-m', 'Snapshot'
|
||||
))
|
||||
except CalledProcessError as err:
|
||||
if b'nothing to commit, working tree clean' not in err.output:
|
||||
raise
|
||||
|
||||
def _check_head_not_detached(self):
|
||||
if self._head_detached:
|
||||
raise EnvironmentError(f'{self._classname} cannot init from detached HEAD state!')
|
||||
|
||||
@property
|
||||
def _head_detached(self):
|
||||
return self._branch == 'HEAD'
|
||||
|
||||
@property
|
||||
def _branch(self):
|
||||
return self._get_stdout((
|
||||
'git', 'rev-parse',
|
||||
'--abbrev-ref', 'HEAD'
|
||||
))
|
||||
|
||||
def _get_stdout(self, *args, **kwargs):
|
||||
kwargs['stdout'] = PIPE
|
||||
kwargs['stderr'] = PIPE
|
||||
stdout_bytes = self._run(*args, **kwargs).stdout
|
||||
return stdout_bytes.decode().rstrip('\n')
|
||||
|
||||
def _run(self, *args, **kwargs):
|
||||
if 'check' not in kwargs:
|
||||
kwargs['check'] = True
|
||||
if 'env' not in kwargs:
|
||||
kwargs['env'] = self.gitenv
|
||||
return run(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def exclude(self):
|
||||
with open(self._exclude_path, 'r') as ofile:
|
||||
return ofile.read()
|
||||
|
||||
@exclude.setter
|
||||
def exclude(self, exclude_patterns):
|
||||
with open(self._exclude_path, 'w') as ifile:
|
||||
ifile.write('\n'.join(exclude_patterns))
|
||||
|
||||
@property
|
||||
def _exclude_path(self):
|
||||
return joinpath(
|
||||
self.gitenv['GIT_DIR'],
|
||||
'info',
|
||||
'exclude'
|
||||
)
|
||||
|
||||
def take_snapshot(self):
|
||||
if self._head_detached:
|
||||
self._checkout_new_branch_from_head()
|
||||
self._snapshot()
|
||||
|
||||
def _checkout_new_branch_from_head(self):
|
||||
branch_name = str(uuid4())
|
||||
self._run((
|
||||
'git', 'branch',
|
||||
branch_name
|
||||
))
|
||||
self._checkout(branch_name)
|
||||
|
||||
def _checkout(self, what):
|
||||
self._run((
|
||||
'git', 'checkout',
|
||||
what
|
||||
))
|
||||
|
||||
def restore_snapshot(self, date):
|
||||
commit = self._get_commit_from_timestamp(date)
|
||||
branch = self._last_valid_branch
|
||||
if commit == self._latest_commit_on_branch(branch):
|
||||
commit = branch
|
||||
self._checkout(commit)
|
||||
|
||||
def _get_commit_from_timestamp(self, date):
|
||||
commit = self._get_stdout((
|
||||
'git', 'rev-list',
|
||||
'--date=iso',
|
||||
'-n', '1',
|
||||
f'--before="{date.isoformat()}"',
|
||||
self._last_valid_branch
|
||||
))
|
||||
if not commit:
|
||||
commit = self._get_oldest_parent_of_head()
|
||||
return commit
|
||||
|
||||
def _get_oldest_parent_of_head(self):
|
||||
return self._get_stdout((
|
||||
'git',
|
||||
'rev-list',
|
||||
'--max-parents=0',
|
||||
'HEAD'
|
||||
))
|
||||
|
||||
@property
|
||||
def _last_valid_branch(self):
|
||||
if not self._head_detached:
|
||||
self.__last_valid_branch = self._branch
|
||||
return self.__last_valid_branch
|
||||
|
||||
def _latest_commit_on_branch(self, branch):
|
||||
return self._get_stdout((
|
||||
'git', 'log',
|
||||
'-n', '1',
|
||||
'--pretty=format:%H',
|
||||
branch
|
||||
))
|
||||
|
||||
@property
|
||||
def all_timelines(self):
|
||||
return self._branches
|
||||
|
||||
@property
|
||||
def _branches(self):
|
||||
git_branch_output = self._get_stdout(('git', 'branch'))
|
||||
regex_pattern = re.compile(r'(?:[^\S\n]|[*])') # matches '*' and non-newline whitespace chars
|
||||
return re.sub(regex_pattern, '', git_branch_output).splitlines()
|
||||
|
||||
@property
|
||||
def timeline(self):
|
||||
return self._last_valid_branch
|
||||
|
||||
@timeline.setter
|
||||
def timeline(self, value):
|
||||
self._checkout(value)
|
||||
|
||||
@property
|
||||
def snapshots(self):
|
||||
return self._pretty_log_branch()
|
||||
|
||||
def _pretty_log_branch(self):
|
||||
git_log_output = self._get_stdout((
|
||||
'git', 'log',
|
||||
'--pretty=%H@%aI'
|
||||
))
|
||||
|
||||
commits = []
|
||||
for line in git_log_output.splitlines():
|
||||
commit_hash, timestamp = line.split('@')
|
||||
commits.append({
|
||||
'hash': commit_hash,
|
||||
'timestamp': dateparser.parse(timestamp)
|
||||
})
|
||||
|
||||
return commits
|
Reference in New Issue
Block a user