Mercurial > september
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 |