Left: | ||
Right: |
OLD | NEW |
---|---|
(Empty) | |
1 #!/usr/bin/env python | |
2 # coding: utf-8 | |
3 | |
4 # This file is part of the Adblock Plus build tools, | |
5 # Copyright (C) 2006-2014 Eyeo GmbH | |
6 # | |
7 # Adblock Plus is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU General Public License version 3 as | |
9 # published by the Free Software Foundation. | |
10 # | |
11 # Adblock Plus is distributed in the hope that it will be useful, | |
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 # GNU General Public License for more details. | |
15 # | |
16 # You should have received a copy of the GNU General Public License | |
17 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. | |
18 | |
19 import sys | |
20 import os | |
21 import re | |
22 import codecs | |
23 import errno | |
24 import subprocess | |
25 import urlparse | |
26 from collections import OrderedDict | |
27 | |
28 class Mercurial(): | |
Sebastian Noack
2014/09/02 08:12:30
Since Mercurial and Git only have static functions
Wladimir Palant
2014/09/02 14:48:03
I'd rather keep it all in one file. Keep in mind t
Sebastian Noack
2014/09/03 19:23:07
Agreed. But you could just initiate those classes
Felix Dahlke
2014/09/05 08:55:53
I actually preferred the static methods :P As it w
Sebastian Noack
2014/09/05 09:29:30
Think of those classes as singletons. There is sup
Felix Dahlke
2014/09/05 09:55:01
A singleton is arguably also the wrong choice when
Sebastian Noack
2014/09/05 10:47:06
We just need two objects, with different implement
Felix Dahlke
2014/09/05 14:01:54
Sure, you normally don't pass namespaces around, i
Sebastian Noack
2014/09/05 14:24:46
Not in Python. staticmethod is just a regular func
| |
29 @staticmethod | |
30 def istype(repodir): | |
31 return os.path.exists(os.path.join(repodir, ".hg")) | |
32 | |
33 @staticmethod | |
34 def clone(source, target): | |
35 if not source.endswith("/"): | |
36 source += "/" | |
37 subprocess.check_call(["hg", "clone", "--quiet", "--noupdate", source, targe t]) | |
38 | |
39 @staticmethod | |
40 def get_revision_id(repo, rev=None): | |
41 command = ["hg", "id", "--repository", repo, "--id"] | |
42 if rev: | |
43 command.extend(["--rev", rev]) | |
44 | |
45 result, dummy = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=sub process.PIPE).communicate() | |
Sebastian Noack
2014/09/02 08:12:30
Any reason, not using check_output() here?
Wladimir Palant
2014/09/02 14:48:03
Yes, I need to drop stderr output. "hg id" won't b
Sebastian Noack
2014/09/03 19:23:07
subprocess.check_output(stderr=open(os.devnull, "w
Wladimir Palant
2014/09/03 21:57:47
What about exceptions? There is no subprocess.outp
Sebastian Noack
2014/09/04 10:07:30
Sure, if you also have to ignore non-zero status c
| |
46 return result.strip() | |
47 | |
48 @staticmethod | |
49 def pull(repo): | |
50 subprocess.check_call(["hg", "pull", "--repository", repo, "--quiet"]) | |
51 | |
52 @staticmethod | |
53 def update(repo, rev): | |
54 subprocess.check_call(["hg", "update", "--repository", repo, "--quiet", "--c heck", "--rev", rev]) | |
55 | |
56 class Git(): | |
57 @staticmethod | |
58 def istype(repodir): | |
59 return os.path.exists(os.path.join(repodir, ".git")) | |
60 | |
61 @staticmethod | |
62 def clone(source, target): | |
63 source = source.rstrip("/") | |
64 if not source.endswith(".git"): | |
65 source += ".git" | |
66 subprocess.check_call(["git", "clone", "--quiet", "--no-checkout", source, t arget]) | |
67 | |
68 @staticmethod | |
69 def get_revision_id(repo, rev="HEAD"): | |
70 command = ["git", "-C", repo, "rev-parse", "--revs-only", rev] | |
71 result, dummy = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=sub process.PIPE).communicate() | |
Sebastian Noack
2014/09/02 08:12:30
Again, any reason you don't use check_output()?
Wladimir Palant
2014/09/02 14:48:03
With the --revs-only flag - no real reason, I can
| |
72 return result.strip() | |
73 | |
74 @staticmethod | |
75 def pull(repo): | |
76 subprocess.check_call(["git", "-C", repo, "pull", "--quiet", "--all"]) | |
77 | |
78 @staticmethod | |
79 def update(repo, rev): | |
80 subprocess.check_call(["git", "-C", repo, "checkout", "--quiet", rev]) | |
81 | |
82 repo_types = { | |
83 "hg": Mercurial, | |
84 "git": Git, | |
85 } | |
86 | |
87 def parse_spec(path, line): | |
88 if "=" not in line: | |
89 print >>sys.stderr, "Invalid line in file %s: %s" % (path, line) | |
Sebastian Noack
2014/09/02 08:12:30
I prefer to use the warnings module here, instead
Wladimir Palant
2014/09/02 14:48:03
I ended up overriding warning.showwarning() to avo
Sebastian Noack
2014/09/03 19:23:07
You should have a look at warnings.filterwarnings(
Wladimir Palant
2014/09/03 21:57:47
I already did, so what? I don't need to switch off
Sebastian Noack
2014/09/04 10:07:30
I thought you were talking about filtering out irr
Wladimir Palant
2014/09/04 13:32:12
Thank you, the logging module is indeed a lot bett
| |
90 return None, None | |
91 | |
92 key, value = line.split("=", 1) | |
93 key = key.strip() | |
94 items = value.split() | |
95 if not len(items): | |
96 print >>sys.stderr, "No value specified for key %s in file %s" % (key, path) | |
97 return key, None | |
98 | |
99 result = OrderedDict() | |
100 if not key.startswith("_"): | |
101 result["_source"] = items.pop(0) | |
102 | |
103 for item in items: | |
104 if ":" in item: | |
105 type, value = item.split(":", 1) | |
106 else: | |
107 type, value = ("*", item) | |
108 if type in result: | |
109 print >>sys.stderr, "Ignoring duplicate value for type %s (key %s in file %s)" % (type, key, path) | |
110 else: | |
111 result[type] = value | |
112 return key, result | |
113 | |
114 def read_deps(repodir): | |
115 result = {} | |
116 deps_path = os.path.join(repodir, ".sub") | |
117 try: | |
118 with codecs.open(deps_path, "r", encoding="utf-8") as handle: | |
119 for line in handle: | |
120 # Remove comments and whitespace | |
121 line = re.sub(r"#.*", "", line).strip() | |
122 if not line: | |
123 continue | |
124 | |
125 key, spec = parse_spec(deps_path, line) | |
126 if spec: | |
127 result[key] = spec | |
128 return result | |
129 except IOError, e: | |
130 if e.errno != errno.ENOENT: | |
131 raise | |
132 return None | |
133 | |
134 def safe_join(path, subpath): | |
135 return os.path.join(path, *filter(lambda f: f.count(".") != len(f), subpath.sp lit("/"))) | |
Sebastian Noack
2014/09/02 08:12:30
I prefer list comprehensions over filter(lambda: .
| |
136 | |
137 def get_repo_type(repo): | |
138 for name, repotype in repo_types.iteritems(): | |
139 if repotype.istype(repo): | |
140 return name | |
141 return None | |
142 | |
143 def ensure_repo(config, parentrepo, dir): | |
144 target = safe_join(parentrepo, dir) | |
145 if os.path.exists(target): | |
146 return | |
147 | |
148 parenttype = get_repo_type(parentrepo) | |
149 type = None | |
150 for key in config.get("_root", {}): | |
151 if key == parenttype or (key in repo_types and type == None): | |
Sebastian Noack
2014/09/02 08:12:30
Please use "is" operator when comparing to None.
| |
152 type = key | |
153 if type == None: | |
154 raise Exception("No valid source found to create %s" % dir) | |
155 | |
156 url = urlparse.urljoin(config["_root"][type], config[dir]["_source"]) | |
157 print "Cloning repository %s into %s" % (url, target) | |
158 repo_types[type].clone(url, target) | |
159 | |
160 def update_repo(config, parentrepo, dir): | |
161 target = safe_join(parentrepo, dir) | |
162 type = get_repo_type(target) | |
163 if type == None: | |
164 print >>sys.stderr, "Type of repository %s unknown, skipping update" % targe t | |
165 return | |
166 | |
167 if type in config[dir]: | |
168 revision = config[dir][type] | |
169 elif "*" in config[dir]: | |
170 revision = config[dir]["*"] | |
171 else: | |
172 print >>sys.stderr, "No revision specified for repository %s (type %s), skip ping update" % (target, type) | |
173 return | |
174 | |
175 resolved_revision = repo_types[type].get_revision_id(target, revision) | |
176 if not resolved_revision: | |
177 print "Revision %s is unknown, downloading remote changes" % revision | |
178 repo_types[type].pull(target) | |
179 resolved_revision = repo_types[type].get_revision_id(target, revision) | |
180 if not resolved_revision: | |
181 raise Exception("Failed to resolve revision %s" % revision) | |
182 | |
183 current_revision = repo_types[type].get_revision_id(target) | |
184 if resolved_revision != current_revision: | |
185 print "Updating repository %s to revision %s" % (target, resolved_revision) | |
186 repo_types[type].update(target, resolved_revision) | |
187 | |
188 def resolve_deps(repodir, level=0): | |
189 config = read_deps(repodir) | |
190 if config == None: | |
191 if level == 0: | |
192 print >>sys.stderr, "No .sub file in directory %s, nothing to do..." % rep odir | |
193 return | |
194 if level >= 10: | |
195 print >>sys.stderr, "Too much subrepository nesting, ignoring %s" % repo | |
196 | |
197 for dir in config: | |
198 if dir.startswith("_"): | |
199 continue | |
200 target = safe_join(repodir, dir) | |
201 ensure_repo(config, repodir, dir) | |
202 update_repo(config, repodir, dir) | |
203 resolve_deps(target, level + 1) | |
204 | |
205 if __name__ == "__main__": | |
206 repos = sys.argv[1:] | |
207 if not len(repos): | |
208 repos = [os.getcwd()] | |
209 for repo in repos: | |
210 resolve_deps(repo) | |
OLD | NEW |