comparison september.py @ 0:ee98303e24b8

Initial commit.
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 04 Mar 2015 23:33:07 -0800
parents
children 6bbebb01f614
comparison
equal deleted inserted replaced
-1:000000000000 0:ee98303e24b8
1 import os
2 import re
3 import sys
4 import json
5 import os.path
6 import logging
7 import argparse
8 import subprocess
9 import configparser
10 from urllib.parse import urlparse
11
12
13 logger = logging.getLogger(__name__)
14
15
16 class IRepo(object):
17 def clone(self, repo_url, repo_path):
18 raise NotImplementedError()
19
20 def pull(self, repo_path):
21 raise NotImplementedError()
22
23 def getTags(self, repo_path):
24 raise NotImplementedError()
25
26 def update(self, repo_path, rev_id):
27 raise NotImplementedError()
28
29
30 class GitRepo(object):
31 def clone(self, repo_url, repo_path):
32 subprocess.check_call(['git', 'clone', repo_url, repo_path])
33
34 def pull(self, repo_path):
35 subprocess.check_call(['git', 'pull', 'origin', 'master'])
36
37 def getTags(self, repo_path):
38 output = subprocess.check_output(['git', 'show-ref', '--tags'])
39 pat = re.compile(r'^(?P<id>[0-9a-f]+) (?P<tag>.+)$')
40 for line in output.split('\n'):
41 m = pat.match(line)
42 if m:
43 yield (m.group('tag'), m.group('id'))
44
45 def update(self, repo_path, rev_id):
46 rev_id = rev_id or 'master'
47 subprocess.check_call(['git', 'checkout', rev_id])
48
49
50 class MercurialRepo(object):
51 def clone(self, repo_url, repo_path):
52 subprocess.check_call(['hg', 'clone', repo_url, repo_path])
53
54 def pull(self, repo_path):
55 subprocess.check_call(['hg', 'pull'],
56 stderr=subprocess.STDOUT)
57
58 def getTags(self, repo_path):
59 output = subprocess.check_output(
60 'hg log -r "tag()" --template "{tags} {node}\\n"',
61 stderr=subprocess.STDOUT,
62 universal_newlines=True,
63 shell=True)
64 pat = re.compile(r'^(?P<tag>.+) (?P<id>[0-9a-f]+)$')
65 for line in output.split('\n'):
66 m = pat.match(line)
67 if m:
68 yield (m.group('tag'), m.group('id'))
69
70 def update(self, repo_path, rev_id):
71 rev_id = rev_id or 'default'
72 subprocess.check_call(['hg', 'update', rev_id],
73 stderr=subprocess.STDOUT)
74
75
76 repo_class = {
77 'git': GitRepo,
78 'hg': MercurialRepo}
79
80
81 def guess_repo_type(repo):
82 # Parse repo as an URL: scheme://netloc/path;parameters?query#fragment
83 scheme, netloc, path, params, query, fragment = urlparse(repo)
84 if scheme == 'ssh':
85 if netloc.startswith('git@'):
86 return 'git'
87 if netloc.startswith('hg@'):
88 return 'hg'
89 elif scheme == 'https':
90 if path.endswith('.git'):
91 return 'git'
92 elif scheme == '' and netloc == '' and os.path.isdir(path):
93 if os.path.isdir(os.path.join(path, '.git')):
94 return 'git'
95 if os.path.isdir(os.path.join(path, '.hg')):
96 return 'hg'
97 return None
98
99
100 def main():
101 # Setup the argument parser.
102 parser = argparse.ArgumentParser(
103 prog='september',
104 description=("An utility that goes back in time and does "
105 "something in the background."))
106 parser.add_argument(
107 'repo',
108 help="The repository to observe and process")
109 parser.add_argument(
110 '-t', '--tmp-dir',
111 help="The temporary directory in which to clone the repository.")
112 parser.add_argument(
113 '--scm',
114 default='guess',
115 choices=['guess', 'git', 'mercurial'],
116 help="The type of source control system handling the repository.")
117 parser.add_argument(
118 '--config',
119 help="The configuration file to use.")
120 parser.add_argument(
121 '--command',
122 help="The command to run on each tag.")
123
124 # Parse arguments, guess repo type.
125 res = parser.parse_args()
126 repo_type = res.scm
127 if not repo_type or repo_type == 'guess':
128 repo_type = guess_repo_type(res.repo)
129 if not repo_type:
130 logger.error("Can't guess the repository type. Please use the "
131 "--scm option to specify it.")
132 sys.exit(1)
133 if repo_type not in repo_class:
134 logger.error("Unknown repository type: %s" % repo_type)
135 sys.exit(1)
136
137 # Create the repo handler.
138 repo = repo_class[repo_type]()
139
140 # Clone or update/checkout the repository in the temp directory.
141 clone_dir = os.path.join(res.tmp_dir, 'clone')
142 if not os.path.exists(clone_dir):
143 logger.info("Cloning '%s' into: %s" % (res.repo, clone_dir))
144 repo.clone(res.repo, clone_dir)
145 else:
146 os.chdir(clone_dir)
147 logger.info("Pulling changes from '%s'." % res.repo)
148 repo.update(res.repo, None)
149
150 os.chdir(clone_dir)
151
152 # Find the configuration file in the repository clone.
153 config_file = res.config or os.path.join(clone_dir, '.september.yml')
154 config = configparser.ConfigParser(interpolation=None)
155 if os.path.exists(config_file):
156 logger.info("Loading configuration file: %s" % config_file)
157 config.read(config_file)
158
159 if not config.has_section('september'):
160 config.add_section('september')
161 config_sec = config['september']
162 if res.command:
163 config_sec['command'] = res.command
164
165 if not config.has_option('september', 'command'):
166 logger.error("There is no 'command' configuration setting under the "
167 "'september' section, and no command was passed as an "
168 "option.")
169 sys.exit(1)
170
171 # Find the cache file in the temp directory.
172 cache_file = os.path.join(res.tmp_dir, 'september.json')
173 if os.path.exists(cache_file):
174 with open(cache_file, 'r') as fp:
175 cache = json.load(fp)
176 else:
177 cache = {'tags': {}}
178
179 # Update the cache: get any new/moved tags.
180 first_tag = config_sec.get('first_tag', None)
181 tag_pattern = config_sec.get('tag_pattern', None)
182 tag_re = None
183 if tag_pattern:
184 tag_re = re.compile(tag_pattern)
185
186 previous_tags = cache['tags']
187 tags = repo.getTags(clone_dir)
188 for t, i in tags:
189 if not tag_re or tag_re.search(t):
190 if t not in previous_tags:
191 logger.info("Adding tag '%s'." % t)
192 previous_tags[t] = {'id': i, 'processed': False}
193 elif previous_tags[t]['id'] != i:
194 logger.info("Moving tag '%s'." % t)
195 previous_tags[t] = {'id': i, 'processed': False}
196
197 if first_tag and first_tag == t:
198 break
199
200 logger.info("Updating cache file '%s'." % cache_file)
201 with open(cache_file, 'w') as fp:
202 json.dump(cache, fp)
203
204 # Process tags!
205 use_shell = config_sec.get('use_shell') in ['1', 'yes', 'true']
206 for tn, ti in cache['tags'].items():
207 if ti['processed']:
208 logger.info("Skipping '%s'." % tn)
209 continue
210
211 logger.info("Updating repo to '%s'." % tn)
212 repo.update(clone_dir, ti['id'])
213
214 command = config_sec['command'] % {
215 'rev_id': ti['id'],
216 'root_dir': clone_dir,
217 'tag': tn}
218 logger.info("Running: %s" % command)
219 subprocess.check_call(command, shell=use_shell)
220
221 ti['processed'] = True
222 with open(cache_file, 'w') as fp:
223 json.dump(cache, fp)
224
225
226 if __name__ == '__main__':
227 logging.basicConfig(level=logging.INFO, format='%(message)s')
228 main()
229