Mercurial > september
annotate september.py @ 5:9c6605b1619b draft default tip
Make directories absolute and expand tilde-paths.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Tue, 12 May 2015 23:55:27 -0700 |
parents | bdfc8a4a335d |
children |
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() |
5
9c6605b1619b
Make directories absolute and expand tilde-paths.
Ludovic Chabant <ludovic@chabant.com>
parents:
4
diff
changeset
|
142 repo_dir = os.path.abspath(repo_dir) |
4
bdfc8a4a335d
Expose more information to the command formatting.
Ludovic Chabant <ludovic@chabant.com>
parents:
1
diff
changeset
|
143 work_dir = os.getcwd() |
1 | 144 |
145 # Guess the repo type. | |
0 | 146 repo_type = res.scm |
147 if not repo_type or repo_type == 'guess': | |
1 | 148 repo_type = guess_repo_type(repo_dir) |
0 | 149 if not repo_type: |
150 logger.error("Can't guess the repository type. Please use the " | |
151 "--scm option to specify it.") | |
152 sys.exit(1) | |
153 if repo_type not in repo_class: | |
154 logger.error("Unknown repository type: %s" % repo_type) | |
155 sys.exit(1) | |
156 | |
1 | 157 # Find the configuration file. |
4
bdfc8a4a335d
Expose more information to the command formatting.
Ludovic Chabant <ludovic@chabant.com>
parents:
1
diff
changeset
|
158 config_file_dir = None |
1 | 159 config_file = res.config or os.path.join(repo_dir, '.september.cfg') |
5
9c6605b1619b
Make directories absolute and expand tilde-paths.
Ludovic Chabant <ludovic@chabant.com>
parents:
4
diff
changeset
|
160 config_file = os.path.abspath(config_file) |
0 | 161 config = configparser.ConfigParser(interpolation=None) |
162 if os.path.exists(config_file): | |
163 logger.info("Loading configuration file: %s" % config_file) | |
164 config.read(config_file) | |
4
bdfc8a4a335d
Expose more information to the command formatting.
Ludovic Chabant <ludovic@chabant.com>
parents:
1
diff
changeset
|
165 config_file_dir = os.path.dirname(config_file) |
0 | 166 |
1 | 167 # Validate the configuration. |
0 | 168 if not config.has_section('september'): |
169 config.add_section('september') | |
170 config_sec = config['september'] | |
171 if res.command: | |
172 config_sec['command'] = res.command | |
1 | 173 if res.tmp_dir: |
174 config_sec['tmp_dir'] = res.tmp_dir | |
0 | 175 |
176 if not config.has_option('september', 'command'): | |
177 logger.error("There is no 'command' configuration setting under the " | |
178 "'september' section, and no command was passed as an " | |
179 "option.") | |
180 sys.exit(1) | |
181 | |
1 | 182 # Get the temp dir. |
183 tmp_dir = config_sec.get('tmp_dir', None) | |
184 if not tmp_dir: | |
185 tmp_name = 'september_%s' % hashlib.md5( | |
186 repo_dir.encode('utf8')).hexdigest() | |
187 tmp_dir = os.path.join(tempfile.gettempdir(), tmp_name) | |
5
9c6605b1619b
Make directories absolute and expand tilde-paths.
Ludovic Chabant <ludovic@chabant.com>
parents:
4
diff
changeset
|
188 elif tmp_dir.startswith('~'): |
9c6605b1619b
Make directories absolute and expand tilde-paths.
Ludovic Chabant <ludovic@chabant.com>
parents:
4
diff
changeset
|
189 tmp_dir = os.path.expanduser(tmp_dir) |
1 | 190 |
0 | 191 # Find the cache file in the temp directory. |
1 | 192 cache_file = os.path.join(tmp_dir, 'september.json') |
0 | 193 if os.path.exists(cache_file): |
194 with open(cache_file, 'r') as fp: | |
195 cache = json.load(fp) | |
196 else: | |
197 cache = {'tags': {}} | |
198 | |
1 | 199 # See if we just need to show the status: |
200 if res.status: | |
201 logger.info("Status for '%s':" % repo_dir) | |
202 for t, v in cache['tags'].items(): | |
203 logger.info("- %s" % t) | |
204 logger.info(" commit ID : %s" % v['id']) | |
205 logger.info(" processed? : %s" % v['processed']) | |
206 return | |
207 | |
208 # Create the repo handler. | |
209 repo = repo_class[repo_type]() | |
210 | |
0 | 211 # Update the cache: get any new/moved tags. |
212 first_tag = config_sec.get('first_tag', None) | |
213 tag_pattern = config_sec.get('tag_pattern', None) | |
214 tag_re = None | |
215 if tag_pattern: | |
216 tag_re = re.compile(tag_pattern) | |
217 | |
1 | 218 reached_first_tag = not bool(first_tag) |
0 | 219 previous_tags = cache['tags'] |
1 | 220 tags = repo.getTags(repo_dir) |
0 | 221 for t, i in tags: |
1 | 222 if not reached_first_tag and first_tag == t: |
223 reached_first_tag = True | |
224 | |
225 if not reached_first_tag: | |
226 if t in previous_tags: | |
227 logger.info("Removing tag '%s'." % t) | |
228 del previous_tags[t] | |
229 continue | |
230 | |
0 | 231 if not tag_re or tag_re.search(t): |
232 if t not in previous_tags: | |
233 logger.info("Adding tag '%s'." % t) | |
234 previous_tags[t] = {'id': i, 'processed': False} | |
235 elif previous_tags[t]['id'] != i: | |
236 logger.info("Moving tag '%s'." % t) | |
237 previous_tags[t] = {'id': i, 'processed': False} | |
238 | |
239 logger.info("Updating cache file '%s'." % cache_file) | |
240 with open(cache_file, 'w') as fp: | |
241 json.dump(cache, fp) | |
242 | |
1 | 243 if res.scan_only: |
244 return | |
245 | |
246 # Clone or update/checkout the repository in the temp directory. | |
247 clone_dir = os.path.join(tmp_dir, 'clone') | |
248 if not os.path.exists(clone_dir): | |
249 logger.info("Cloning '%s' into: %s" % (repo_dir, clone_dir)) | |
250 repo.clone(repo_dir, clone_dir) | |
251 else: | |
252 logger.info("Pulling changes from '%s'." % repo_dir) | |
253 repo.pull(clone_dir, repo_dir) | |
254 repo.update(clone_dir, None) | |
255 | |
0 | 256 # Process tags! |
257 use_shell = config_sec.get('use_shell') in ['1', 'yes', 'true'] | |
258 for tn, ti in cache['tags'].items(): | |
259 if ti['processed']: | |
260 logger.info("Skipping '%s'." % tn) | |
261 continue | |
262 | |
263 logger.info("Updating repo to '%s'." % tn) | |
264 repo.update(clone_dir, ti['id']) | |
265 | |
266 command = config_sec['command'] % { | |
267 'rev_id': ti['id'], | |
268 'root_dir': clone_dir, | |
4
bdfc8a4a335d
Expose more information to the command formatting.
Ludovic Chabant <ludovic@chabant.com>
parents:
1
diff
changeset
|
269 'config_dir': config_file_dir, |
bdfc8a4a335d
Expose more information to the command formatting.
Ludovic Chabant <ludovic@chabant.com>
parents:
1
diff
changeset
|
270 'work_dir': work_dir, |
0 | 271 'tag': tn} |
272 logger.info("Running: %s" % command) | |
273 subprocess.check_call(command, shell=use_shell) | |
274 | |
275 ti['processed'] = True | |
276 with open(cache_file, 'w') as fp: | |
277 json.dump(cache, fp) | |
278 | |
279 | |
280 if __name__ == '__main__': | |
281 logging.basicConfig(level=logging.INFO, format='%(message)s') | |
282 main() | |
283 |