Mercurial > september
changeset 0:ee98303e24b8
Initial commit.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Wed, 04 Mar 2015 23:33:07 -0800 |
parents | |
children | 6bbebb01f614 |
files | september.py |
diffstat | 1 files changed, 229 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/september.py Wed Mar 04 23:33:07 2015 -0800 @@ -0,0 +1,229 @@ +import os +import re +import sys +import json +import os.path +import logging +import argparse +import subprocess +import configparser +from urllib.parse import urlparse + + +logger = logging.getLogger(__name__) + + +class IRepo(object): + def clone(self, repo_url, repo_path): + raise NotImplementedError() + + def pull(self, repo_path): + raise NotImplementedError() + + def getTags(self, repo_path): + raise NotImplementedError() + + def update(self, repo_path, rev_id): + raise NotImplementedError() + + +class GitRepo(object): + def clone(self, repo_url, repo_path): + subprocess.check_call(['git', 'clone', repo_url, repo_path]) + + def pull(self, repo_path): + subprocess.check_call(['git', 'pull', 'origin', 'master']) + + def getTags(self, repo_path): + output = subprocess.check_output(['git', 'show-ref', '--tags']) + pat = re.compile(r'^(?P<id>[0-9a-f]+) (?P<tag>.+)$') + for line in output.split('\n'): + m = pat.match(line) + if m: + yield (m.group('tag'), m.group('id')) + + def update(self, repo_path, rev_id): + rev_id = rev_id or 'master' + subprocess.check_call(['git', 'checkout', rev_id]) + + +class MercurialRepo(object): + def clone(self, repo_url, repo_path): + subprocess.check_call(['hg', 'clone', repo_url, repo_path]) + + def pull(self, repo_path): + subprocess.check_call(['hg', 'pull'], + stderr=subprocess.STDOUT) + + def getTags(self, repo_path): + output = subprocess.check_output( + 'hg log -r "tag()" --template "{tags} {node}\\n"', + stderr=subprocess.STDOUT, + universal_newlines=True, + shell=True) + pat = re.compile(r'^(?P<tag>.+) (?P<id>[0-9a-f]+)$') + for line in output.split('\n'): + m = pat.match(line) + if m: + yield (m.group('tag'), m.group('id')) + + def update(self, repo_path, rev_id): + rev_id = rev_id or 'default' + subprocess.check_call(['hg', 'update', rev_id], + stderr=subprocess.STDOUT) + + +repo_class = { + 'git': GitRepo, + 'hg': MercurialRepo} + + +def guess_repo_type(repo): + # Parse repo as an URL: scheme://netloc/path;parameters?query#fragment + scheme, netloc, path, params, query, fragment = urlparse(repo) + if scheme == 'ssh': + if netloc.startswith('git@'): + return 'git' + if netloc.startswith('hg@'): + return 'hg' + elif scheme == 'https': + if path.endswith('.git'): + return 'git' + elif scheme == '' and netloc == '' and os.path.isdir(path): + if os.path.isdir(os.path.join(path, '.git')): + return 'git' + if os.path.isdir(os.path.join(path, '.hg')): + return 'hg' + return None + + +def main(): + # Setup the argument parser. + parser = argparse.ArgumentParser( + prog='september', + description=("An utility that goes back in time and does " + "something in the background.")) + parser.add_argument( + 'repo', + help="The repository to observe and process") + parser.add_argument( + '-t', '--tmp-dir', + help="The temporary directory in which to clone the repository.") + parser.add_argument( + '--scm', + default='guess', + choices=['guess', 'git', 'mercurial'], + help="The type of source control system handling the repository.") + parser.add_argument( + '--config', + help="The configuration file to use.") + parser.add_argument( + '--command', + help="The command to run on each tag.") + + # Parse arguments, guess repo type. + res = parser.parse_args() + repo_type = res.scm + if not repo_type or repo_type == 'guess': + repo_type = guess_repo_type(res.repo) + if not repo_type: + logger.error("Can't guess the repository type. Please use the " + "--scm option to specify it.") + sys.exit(1) + if repo_type not in repo_class: + logger.error("Unknown repository type: %s" % repo_type) + sys.exit(1) + + # Create the repo handler. + repo = repo_class[repo_type]() + + # Clone or update/checkout the repository in the temp directory. + clone_dir = os.path.join(res.tmp_dir, 'clone') + if not os.path.exists(clone_dir): + logger.info("Cloning '%s' into: %s" % (res.repo, clone_dir)) + repo.clone(res.repo, clone_dir) + else: + os.chdir(clone_dir) + logger.info("Pulling changes from '%s'." % res.repo) + repo.update(res.repo, None) + + os.chdir(clone_dir) + + # Find the configuration file in the repository clone. + config_file = res.config or os.path.join(clone_dir, '.september.yml') + config = configparser.ConfigParser(interpolation=None) + if os.path.exists(config_file): + logger.info("Loading configuration file: %s" % config_file) + config.read(config_file) + + if not config.has_section('september'): + config.add_section('september') + config_sec = config['september'] + if res.command: + config_sec['command'] = res.command + + if not config.has_option('september', 'command'): + logger.error("There is no 'command' configuration setting under the " + "'september' section, and no command was passed as an " + "option.") + sys.exit(1) + + # Find the cache file in the temp directory. + cache_file = os.path.join(res.tmp_dir, 'september.json') + if os.path.exists(cache_file): + with open(cache_file, 'r') as fp: + cache = json.load(fp) + else: + cache = {'tags': {}} + + # Update the cache: get any new/moved tags. + first_tag = config_sec.get('first_tag', None) + tag_pattern = config_sec.get('tag_pattern', None) + tag_re = None + if tag_pattern: + tag_re = re.compile(tag_pattern) + + previous_tags = cache['tags'] + tags = repo.getTags(clone_dir) + for t, i in tags: + if not tag_re or tag_re.search(t): + if t not in previous_tags: + logger.info("Adding tag '%s'." % t) + previous_tags[t] = {'id': i, 'processed': False} + elif previous_tags[t]['id'] != i: + logger.info("Moving tag '%s'." % t) + previous_tags[t] = {'id': i, 'processed': False} + + if first_tag and first_tag == t: + break + + logger.info("Updating cache file '%s'." % cache_file) + with open(cache_file, 'w') as fp: + json.dump(cache, fp) + + # Process tags! + use_shell = config_sec.get('use_shell') in ['1', 'yes', 'true'] + for tn, ti in cache['tags'].items(): + if ti['processed']: + logger.info("Skipping '%s'." % tn) + continue + + logger.info("Updating repo to '%s'." % tn) + repo.update(clone_dir, ti['id']) + + command = config_sec['command'] % { + 'rev_id': ti['id'], + 'root_dir': clone_dir, + 'tag': tn} + logger.info("Running: %s" % command) + subprocess.check_call(command, shell=use_shell) + + ti['processed'] = True + with open(cache_file, 'w') as fp: + json.dump(cache, fp) + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO, format='%(message)s') + main() +