Mercurial > september
annotate september.py @ 4:bdfc8a4a335d draft
Expose more information to the command formatting.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Mon, 06 Apr 2015 20:03:11 -0700 |
parents | 6bbebb01f614 |
children | 9c6605b1619b |
rev | line source |
---|---|
0 | 1 import os |
2 import re | |
3 import sys | |
4 import json | |
5 import os.path | |
6 import logging | |
1 | 7 import hashlib |
0 | 8 import argparse |
1 | 9 import tempfile |
0 | 10 import subprocess |
11 import configparser | |
12 from urllib.parse import urlparse | |
13 | |
14 | |
15 logger = logging.getLogger(__name__) | |
16 | |
17 | |
18 class IRepo(object): | |
19 def clone(self, repo_url, repo_path): | |
20 raise NotImplementedError() | |
21 | |
1 | 22 def pull(self, repo_path, remote): |
0 | 23 raise NotImplementedError() |
24 | |
25 def getTags(self, repo_path): | |
26 raise NotImplementedError() | |
27 | |
28 def update(self, repo_path, rev_id): | |
29 raise NotImplementedError() | |
30 | |
31 | |
32 class GitRepo(object): | |
33 def clone(self, repo_url, repo_path): | |
34 subprocess.check_call(['git', 'clone', repo_url, repo_path]) | |
35 | |
1 | 36 def pull(self, repo_path, remote): |
37 subprocess.check_call(['git', '-C', repo_path, | |
38 'pull', remote, 'master']) | |
0 | 39 |
40 def getTags(self, repo_path): | |
1 | 41 output = subprocess.check_output(['git', '-C', repo_path, |
42 'show-ref', '--tags']) | |
0 | 43 pat = re.compile(r'^(?P<id>[0-9a-f]+) (?P<tag>.+)$') |
44 for line in output.split('\n'): | |
45 m = pat.match(line) | |
46 if m: | |
47 yield (m.group('tag'), m.group('id')) | |
48 | |
49 def update(self, repo_path, rev_id): | |
50 rev_id = rev_id or 'master' | |
1 | 51 subprocess.check_call(['git', '-C', repo_path, 'checkout', rev_id]) |
0 | 52 |
53 | |
54 class MercurialRepo(object): | |
55 def clone(self, repo_url, repo_path): | |
56 subprocess.check_call(['hg', 'clone', repo_url, repo_path]) | |
57 | |
1 | 58 def pull(self, repo_path, remote): |
59 subprocess.check_call(['hg', '-R', repo_path, 'pull', remote], | |
0 | 60 stderr=subprocess.STDOUT) |
61 | |
62 def getTags(self, repo_path): | |
63 output = subprocess.check_output( | |
1 | 64 ('hg -R "' + repo_path + |
65 '" log -r "tag()" --template "{tags} {node}\\n"'), | |
0 | 66 stderr=subprocess.STDOUT, |
67 universal_newlines=True, | |
68 shell=True) | |
69 pat = re.compile(r'^(?P<tag>.+) (?P<id>[0-9a-f]+)$') | |
70 for line in output.split('\n'): | |
71 m = pat.match(line) | |
72 if m: | |
73 yield (m.group('tag'), m.group('id')) | |
74 | |
75 def update(self, repo_path, rev_id): | |
76 rev_id = rev_id or 'default' | |
1 | 77 subprocess.check_call(['hg', '-R', repo_path, 'update', rev_id], |
0 | 78 stderr=subprocess.STDOUT) |
79 | |
80 | |
81 repo_class = { | |
82 'git': GitRepo, | |
83 'hg': MercurialRepo} | |
84 | |
85 | |
86 def guess_repo_type(repo): | |
87 # Parse repo as an URL: scheme://netloc/path;parameters?query#fragment | |
88 scheme, netloc, path, params, query, fragment = urlparse(repo) | |
89 if scheme == 'ssh': | |
90 if netloc.startswith('git@'): | |
91 return 'git' | |
92 if netloc.startswith('hg@'): | |
93 return 'hg' | |
94 elif scheme == 'https': | |
95 if path.endswith('.git'): | |
96 return 'git' | |
97 elif scheme == '' and netloc == '' and os.path.isdir(path): | |
98 if os.path.isdir(os.path.join(path, '.git')): | |
99 return 'git' | |
100 if os.path.isdir(os.path.join(path, '.hg')): | |
101 return 'hg' | |
102 return None | |
103 | |
104 | |
105 def main(): | |
106 # Setup the argument parser. | |
107 parser = argparse.ArgumentParser( | |
108 prog='september', | |
109 description=("An utility that goes back in time and does " | |
110 "something in the background.")) | |
111 parser.add_argument( | |
112 'repo', | |
1 | 113 nargs='?', |
0 | 114 help="The repository to observe and process") |
115 parser.add_argument( | |
116 '-t', '--tmp-dir', | |
117 help="The temporary directory in which to clone the repository.") | |
118 parser.add_argument( | |
119 '--scm', | |
120 default='guess', | |
121 choices=['guess', 'git', 'mercurial'], | |
122 help="The type of source control system handling the repository.") | |
123 parser.add_argument( | |
124 '--config', | |
125 help="The configuration file to use.") | |
126 parser.add_argument( | |
127 '--command', | |
128 help="The command to run on each tag.") | |
1 | 129 parser.add_argument( |
130 '--scan-only', | |
131 action='store_true', | |
132 help=("Only scan the repository history. Don't update or run the " | |
133 "command")) | |
134 parser.add_argument( | |
135 '--status', | |
136 action='store_true', | |
137 help="See September's status for the given repository.") | |
0 | 138 |
1 | 139 # Parse arguments. |
0 | 140 res = parser.parse_args() |
1 | 141 repo_dir = res.repo or os.getcwd() |
4
bdfc8a4a335d
Expose more information to the command formatting.
Ludovic Chabant <ludovic@chabant.com>
parents:
1
diff
changeset
|
142 work_dir = os.getcwd() |
1 | 143 |
144 # Guess the repo type. | |
0 | 145 repo_type = res.scm |
146 if not repo_type or repo_type == 'guess': | |
1 | 147 repo_type = guess_repo_type(repo_dir) |
0 | 148 if not repo_type: |
149 logger.error("Can't guess the repository type. Please use the " | |
150 "--scm option to specify it.") | |
151 sys.exit(1) | |
152 if repo_type not in repo_class: | |
153 logger.error("Unknown repository type: %s" % repo_type) | |
154 sys.exit(1) | |
155 | |
1 | 156 # Find the configuration file. |
4
bdfc8a4a335d
Expose more information to the command formatting.
Ludovic Chabant <ludovic@chabant.com>
parents:
1
diff
changeset
|
157 config_file_dir = None |
1 | 158 config_file = res.config or os.path.join(repo_dir, '.september.cfg') |
0 | 159 config = configparser.ConfigParser(interpolation=None) |
160 if os.path.exists(config_file): | |
161 logger.info("Loading configuration file: %s" % config_file) | |
162 config.read(config_file) | |
4
bdfc8a4a335d
Expose more information to the command formatting.
Ludovic Chabant <ludovic@chabant.com>
parents:
1
diff
changeset
|
163 config_file_dir = os.path.dirname(config_file) |
0 | 164 |
1 | 165 # Validate the configuration. |
0 | 166 if not config.has_section('september'): |
167 config.add_section('september') | |
168 config_sec = config['september'] | |
169 if res.command: | |
170 config_sec['command'] = res.command | |
1 | 171 if res.tmp_dir: |
172 config_sec['tmp_dir'] = res.tmp_dir | |
0 | 173 |
174 if not config.has_option('september', 'command'): | |
175 logger.error("There is no 'command' configuration setting under the " | |
176 "'september' section, and no command was passed as an " | |
177 "option.") | |
178 sys.exit(1) | |
179 | |
1 | 180 # Get the temp dir. |
181 tmp_dir = config_sec.get('tmp_dir', None) | |
182 if not tmp_dir: | |
183 tmp_name = 'september_%s' % hashlib.md5( | |
184 repo_dir.encode('utf8')).hexdigest() | |
185 tmp_dir = os.path.join(tempfile.gettempdir(), tmp_name) | |
186 | |
0 | 187 # Find the cache file in the temp directory. |
1 | 188 cache_file = os.path.join(tmp_dir, 'september.json') |
0 | 189 if os.path.exists(cache_file): |
190 with open(cache_file, 'r') as fp: | |
191 cache = json.load(fp) | |
192 else: | |
193 cache = {'tags': {}} | |
194 | |
1 | 195 # See if we just need to show the status: |
196 if res.status: | |
197 logger.info("Status for '%s':" % repo_dir) | |
198 for t, v in cache['tags'].items(): | |
199 logger.info("- %s" % t) | |
200 logger.info(" commit ID : %s" % v['id']) | |
201 logger.info(" processed? : %s" % v['processed']) | |
202 return | |
203 | |
204 # Create the repo handler. | |
205 repo = repo_class[repo_type]() | |
206 | |
0 | 207 # Update the cache: get any new/moved tags. |
208 first_tag = config_sec.get('first_tag', None) | |
209 tag_pattern = config_sec.get('tag_pattern', None) | |
210 tag_re = None | |
211 if tag_pattern: | |
212 tag_re = re.compile(tag_pattern) | |
213 | |
1 | 214 reached_first_tag = not bool(first_tag) |
0 | 215 previous_tags = cache['tags'] |
1 | 216 tags = repo.getTags(repo_dir) |
0 | 217 for t, i in tags: |
1 | 218 if not reached_first_tag and first_tag == t: |
219 reached_first_tag = True | |
220 | |
221 if not reached_first_tag: | |
222 if t in previous_tags: | |
223 logger.info("Removing tag '%s'." % t) | |
224 del previous_tags[t] | |
225 continue | |
226 | |
0 | 227 if not tag_re or tag_re.search(t): |
228 if t not in previous_tags: | |
229 logger.info("Adding tag '%s'." % t) | |
230 previous_tags[t] = {'id': i, 'processed': False} | |
231 elif previous_tags[t]['id'] != i: | |
232 logger.info("Moving tag '%s'." % t) | |
233 previous_tags[t] = {'id': i, 'processed': False} | |
234 | |
235 logger.info("Updating cache file '%s'." % cache_file) | |
236 with open(cache_file, 'w') as fp: | |
237 json.dump(cache, fp) | |
238 | |
1 | 239 if res.scan_only: |
240 return | |
241 | |
242 # Clone or update/checkout the repository in the temp directory. | |
243 clone_dir = os.path.join(tmp_dir, 'clone') | |
244 if not os.path.exists(clone_dir): | |
245 logger.info("Cloning '%s' into: %s" % (repo_dir, clone_dir)) | |
246 repo.clone(repo_dir, clone_dir) | |
247 else: | |
248 logger.info("Pulling changes from '%s'." % repo_dir) | |
249 repo.pull(clone_dir, repo_dir) | |
250 repo.update(clone_dir, None) | |
251 | |
0 | 252 # Process tags! |
253 use_shell = config_sec.get('use_shell') in ['1', 'yes', 'true'] | |
254 for tn, ti in cache['tags'].items(): | |
255 if ti['processed']: | |
256 logger.info("Skipping '%s'." % tn) | |
257 continue | |
258 | |
259 logger.info("Updating repo to '%s'." % tn) | |
260 repo.update(clone_dir, ti['id']) | |
261 | |
262 command = config_sec['command'] % { | |
263 'rev_id': ti['id'], | |
264 'root_dir': clone_dir, | |
4
bdfc8a4a335d
Expose more information to the command formatting.
Ludovic Chabant <ludovic@chabant.com>
parents:
1
diff
changeset
|
265 'config_dir': config_file_dir, |
bdfc8a4a335d
Expose more information to the command formatting.
Ludovic Chabant <ludovic@chabant.com>
parents:
1
diff
changeset
|
266 'work_dir': work_dir, |
0 | 267 'tag': tn} |
268 logger.info("Running: %s" % command) | |
269 subprocess.check_call(command, shell=use_shell) | |
270 | |
271 ti['processed'] = True | |
272 with open(cache_file, 'w') as fp: | |
273 json.dump(cache, fp) | |
274 | |
275 | |
276 if __name__ == '__main__': | |
277 logging.basicConfig(level=logging.INFO, format='%(message)s') | |
278 main() | |
279 |