##// END OF EJS Templates
removed exception args indexing (not supported by py3k)...
Renato Cunha -
r11567:34cc8b84 default
parent child Browse files
Show More
@@ -1,441 +1,441
1 1 # bugzilla.py - bugzilla integration for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''hooks for integrating with the Bugzilla bug tracker
9 9
10 10 This hook extension adds comments on bugs in Bugzilla when changesets
11 11 that refer to bugs by Bugzilla ID are seen. The hook does not change
12 12 bug status.
13 13
14 14 The hook updates the Bugzilla database directly. Only Bugzilla
15 15 installations using MySQL are supported.
16 16
17 17 The hook relies on a Bugzilla script to send bug change notification
18 18 emails. That script changes between Bugzilla versions; the
19 19 'processmail' script used prior to 2.18 is replaced in 2.18 and
20 20 subsequent versions by 'config/sendbugmail.pl'. Note that these will
21 21 be run by Mercurial as the user pushing the change; you will need to
22 22 ensure the Bugzilla install file permissions are set appropriately.
23 23
24 24 The extension is configured through three different configuration
25 25 sections. These keys are recognized in the [bugzilla] section:
26 26
27 27 host
28 28 Hostname of the MySQL server holding the Bugzilla database.
29 29
30 30 db
31 31 Name of the Bugzilla database in MySQL. Default 'bugs'.
32 32
33 33 user
34 34 Username to use to access MySQL server. Default 'bugs'.
35 35
36 36 password
37 37 Password to use to access MySQL server.
38 38
39 39 timeout
40 40 Database connection timeout (seconds). Default 5.
41 41
42 42 version
43 43 Bugzilla version. Specify '3.0' for Bugzilla versions 3.0 and later,
44 44 '2.18' for Bugzilla versions from 2.18 and '2.16' for versions prior
45 45 to 2.18.
46 46
47 47 bzuser
48 48 Fallback Bugzilla user name to record comments with, if changeset
49 49 committer cannot be found as a Bugzilla user.
50 50
51 51 bzdir
52 52 Bugzilla install directory. Used by default notify. Default
53 53 '/var/www/html/bugzilla'.
54 54
55 55 notify
56 56 The command to run to get Bugzilla to send bug change notification
57 57 emails. Substitutes from a map with 3 keys, 'bzdir', 'id' (bug id)
58 58 and 'user' (committer bugzilla email). Default depends on version;
59 59 from 2.18 it is "cd %(bzdir)s && perl -T contrib/sendbugmail.pl
60 60 %(id)s %(user)s".
61 61
62 62 regexp
63 63 Regular expression to match bug IDs in changeset commit message.
64 64 Must contain one "()" group. The default expression matches 'Bug
65 65 1234', 'Bug no. 1234', 'Bug number 1234', 'Bugs 1234,5678', 'Bug
66 66 1234 and 5678' and variations thereof. Matching is case insensitive.
67 67
68 68 style
69 69 The style file to use when formatting comments.
70 70
71 71 template
72 72 Template to use when formatting comments. Overrides style if
73 73 specified. In addition to the usual Mercurial keywords, the
74 74 extension specifies::
75 75
76 76 {bug} The Bugzilla bug ID.
77 77 {root} The full pathname of the Mercurial repository.
78 78 {webroot} Stripped pathname of the Mercurial repository.
79 79 {hgweb} Base URL for browsing Mercurial repositories.
80 80
81 81 Default 'changeset {node|short} in repo {root} refers '
82 82 'to bug {bug}.\\ndetails:\\n\\t{desc|tabindent}'
83 83
84 84 strip
85 85 The number of slashes to strip from the front of {root} to produce
86 86 {webroot}. Default 0.
87 87
88 88 usermap
89 89 Path of file containing Mercurial committer ID to Bugzilla user ID
90 90 mappings. If specified, the file should contain one mapping per
91 91 line, "committer"="Bugzilla user". See also the [usermap] section.
92 92
93 93 The [usermap] section is used to specify mappings of Mercurial
94 94 committer ID to Bugzilla user ID. See also [bugzilla].usermap.
95 95 "committer"="Bugzilla user"
96 96
97 97 Finally, the [web] section supports one entry:
98 98
99 99 baseurl
100 100 Base URL for browsing Mercurial repositories. Reference from
101 101 templates as {hgweb}.
102 102
103 103 Activating the extension::
104 104
105 105 [extensions]
106 106 bugzilla =
107 107
108 108 [hooks]
109 109 # run bugzilla hook on every change pulled or pushed in here
110 110 incoming.bugzilla = python:hgext.bugzilla.hook
111 111
112 112 Example configuration:
113 113
114 114 This example configuration is for a collection of Mercurial
115 115 repositories in /var/local/hg/repos/ used with a local Bugzilla 3.2
116 116 installation in /opt/bugzilla-3.2. ::
117 117
118 118 [bugzilla]
119 119 host=localhost
120 120 password=XYZZY
121 121 version=3.0
122 122 bzuser=unknown@domain.com
123 123 bzdir=/opt/bugzilla-3.2
124 124 template=Changeset {node|short} in {root|basename}.
125 125 {hgweb}/{webroot}/rev/{node|short}\\n
126 126 {desc}\\n
127 127 strip=5
128 128
129 129 [web]
130 130 baseurl=http://dev.domain.com/hg
131 131
132 132 [usermap]
133 133 user@emaildomain.com=user.name@bugzilladomain.com
134 134
135 135 Commits add a comment to the Bugzilla bug record of the form::
136 136
137 137 Changeset 3b16791d6642 in repository-name.
138 138 http://dev.domain.com/hg/repository-name/rev/3b16791d6642
139 139
140 140 Changeset commit comment. Bug 1234.
141 141 '''
142 142
143 143 from mercurial.i18n import _
144 144 from mercurial.node import short
145 145 from mercurial import cmdutil, templater, util
146 146 import re, time
147 147
148 148 MySQLdb = None
149 149
150 150 def buglist(ids):
151 151 return '(' + ','.join(map(str, ids)) + ')'
152 152
153 153 class bugzilla_2_16(object):
154 154 '''support for bugzilla version 2.16.'''
155 155
156 156 def __init__(self, ui):
157 157 self.ui = ui
158 158 host = self.ui.config('bugzilla', 'host', 'localhost')
159 159 user = self.ui.config('bugzilla', 'user', 'bugs')
160 160 passwd = self.ui.config('bugzilla', 'password')
161 161 db = self.ui.config('bugzilla', 'db', 'bugs')
162 162 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
163 163 usermap = self.ui.config('bugzilla', 'usermap')
164 164 if usermap:
165 165 self.ui.readconfig(usermap, sections=['usermap'])
166 166 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
167 167 (host, db, user, '*' * len(passwd)))
168 168 self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd,
169 169 db=db, connect_timeout=timeout)
170 170 self.cursor = self.conn.cursor()
171 171 self.longdesc_id = self.get_longdesc_id()
172 172 self.user_ids = {}
173 173 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
174 174
175 175 def run(self, *args, **kwargs):
176 176 '''run a query.'''
177 177 self.ui.note(_('query: %s %s\n') % (args, kwargs))
178 178 try:
179 179 self.cursor.execute(*args, **kwargs)
180 180 except MySQLdb.MySQLError:
181 181 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
182 182 raise
183 183
184 184 def get_longdesc_id(self):
185 185 '''get identity of longdesc field'''
186 186 self.run('select fieldid from fielddefs where name = "longdesc"')
187 187 ids = self.cursor.fetchall()
188 188 if len(ids) != 1:
189 189 raise util.Abort(_('unknown database schema'))
190 190 return ids[0][0]
191 191
192 192 def filter_real_bug_ids(self, ids):
193 193 '''filter not-existing bug ids from list.'''
194 194 self.run('select bug_id from bugs where bug_id in %s' % buglist(ids))
195 195 return sorted([c[0] for c in self.cursor.fetchall()])
196 196
197 197 def filter_unknown_bug_ids(self, node, ids):
198 198 '''filter bug ids from list that already refer to this changeset.'''
199 199
200 200 self.run('''select bug_id from longdescs where
201 201 bug_id in %s and thetext like "%%%s%%"''' %
202 202 (buglist(ids), short(node)))
203 203 unknown = set(ids)
204 204 for (id,) in self.cursor.fetchall():
205 205 self.ui.status(_('bug %d already knows about changeset %s\n') %
206 206 (id, short(node)))
207 207 unknown.discard(id)
208 208 return sorted(unknown)
209 209
210 210 def notify(self, ids, committer):
211 211 '''tell bugzilla to send mail.'''
212 212
213 213 self.ui.status(_('telling bugzilla to send mail:\n'))
214 214 (user, userid) = self.get_bugzilla_user(committer)
215 215 for id in ids:
216 216 self.ui.status(_(' bug %s\n') % id)
217 217 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
218 218 bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
219 219 try:
220 220 # Backwards-compatible with old notify string, which
221 221 # took one string. This will throw with a new format
222 222 # string.
223 223 cmd = cmdfmt % id
224 224 except TypeError:
225 225 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
226 226 self.ui.note(_('running notify command %s\n') % cmd)
227 227 fp = util.popen('(%s) 2>&1' % cmd)
228 228 out = fp.read()
229 229 ret = fp.close()
230 230 if ret:
231 231 self.ui.warn(out)
232 232 raise util.Abort(_('bugzilla notify command %s') %
233 233 util.explain_exit(ret)[0])
234 234 self.ui.status(_('done\n'))
235 235
236 236 def get_user_id(self, user):
237 237 '''look up numeric bugzilla user id.'''
238 238 try:
239 239 return self.user_ids[user]
240 240 except KeyError:
241 241 try:
242 242 userid = int(user)
243 243 except ValueError:
244 244 self.ui.note(_('looking up user %s\n') % user)
245 245 self.run('''select userid from profiles
246 246 where login_name like %s''', user)
247 247 all = self.cursor.fetchall()
248 248 if len(all) != 1:
249 249 raise KeyError(user)
250 250 userid = int(all[0][0])
251 251 self.user_ids[user] = userid
252 252 return userid
253 253
254 254 def map_committer(self, user):
255 255 '''map name of committer to bugzilla user name.'''
256 256 for committer, bzuser in self.ui.configitems('usermap'):
257 257 if committer.lower() == user.lower():
258 258 return bzuser
259 259 return user
260 260
261 261 def get_bugzilla_user(self, committer):
262 262 '''see if committer is a registered bugzilla user. Return
263 263 bugzilla username and userid if so. If not, return default
264 264 bugzilla username and userid.'''
265 265 user = self.map_committer(committer)
266 266 try:
267 267 userid = self.get_user_id(user)
268 268 except KeyError:
269 269 try:
270 270 defaultuser = self.ui.config('bugzilla', 'bzuser')
271 271 if not defaultuser:
272 272 raise util.Abort(_('cannot find bugzilla user id for %s') %
273 273 user)
274 274 userid = self.get_user_id(defaultuser)
275 275 user = defaultuser
276 276 except KeyError:
277 277 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
278 278 (user, defaultuser))
279 279 return (user, userid)
280 280
281 281 def add_comment(self, bugid, text, committer):
282 282 '''add comment to bug. try adding comment as committer of
283 283 changeset, otherwise as default bugzilla user.'''
284 284 (user, userid) = self.get_bugzilla_user(committer)
285 285 now = time.strftime('%Y-%m-%d %H:%M:%S')
286 286 self.run('''insert into longdescs
287 287 (bug_id, who, bug_when, thetext)
288 288 values (%s, %s, %s, %s)''',
289 289 (bugid, userid, now, text))
290 290 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
291 291 values (%s, %s, %s, %s)''',
292 292 (bugid, userid, now, self.longdesc_id))
293 293 self.conn.commit()
294 294
295 295 class bugzilla_2_18(bugzilla_2_16):
296 296 '''support for bugzilla 2.18 series.'''
297 297
298 298 def __init__(self, ui):
299 299 bugzilla_2_16.__init__(self, ui)
300 300 self.default_notify = \
301 301 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
302 302
303 303 class bugzilla_3_0(bugzilla_2_18):
304 304 '''support for bugzilla 3.0 series.'''
305 305
306 306 def __init__(self, ui):
307 307 bugzilla_2_18.__init__(self, ui)
308 308
309 309 def get_longdesc_id(self):
310 310 '''get identity of longdesc field'''
311 311 self.run('select id from fielddefs where name = "longdesc"')
312 312 ids = self.cursor.fetchall()
313 313 if len(ids) != 1:
314 314 raise util.Abort(_('unknown database schema'))
315 315 return ids[0][0]
316 316
317 317 class bugzilla(object):
318 318 # supported versions of bugzilla. different versions have
319 319 # different schemas.
320 320 _versions = {
321 321 '2.16': bugzilla_2_16,
322 322 '2.18': bugzilla_2_18,
323 323 '3.0': bugzilla_3_0
324 324 }
325 325
326 326 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
327 327 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
328 328
329 329 _bz = None
330 330
331 331 def __init__(self, ui, repo):
332 332 self.ui = ui
333 333 self.repo = repo
334 334
335 335 def bz(self):
336 336 '''return object that knows how to talk to bugzilla version in
337 337 use.'''
338 338
339 339 if bugzilla._bz is None:
340 340 bzversion = self.ui.config('bugzilla', 'version')
341 341 try:
342 342 bzclass = bugzilla._versions[bzversion]
343 343 except KeyError:
344 344 raise util.Abort(_('bugzilla version %s not supported') %
345 345 bzversion)
346 346 bugzilla._bz = bzclass(self.ui)
347 347 return bugzilla._bz
348 348
349 349 def __getattr__(self, key):
350 350 return getattr(self.bz(), key)
351 351
352 352 _bug_re = None
353 353 _split_re = None
354 354
355 355 def find_bug_ids(self, ctx):
356 356 '''find valid bug ids that are referred to in changeset
357 357 comments and that do not already have references to this
358 358 changeset.'''
359 359
360 360 if bugzilla._bug_re is None:
361 361 bugzilla._bug_re = re.compile(
362 362 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
363 363 re.IGNORECASE)
364 364 bugzilla._split_re = re.compile(r'\D+')
365 365 start = 0
366 366 ids = set()
367 367 while True:
368 368 m = bugzilla._bug_re.search(ctx.description(), start)
369 369 if not m:
370 370 break
371 371 start = m.end()
372 372 for id in bugzilla._split_re.split(m.group(1)):
373 373 if not id:
374 374 continue
375 375 ids.add(int(id))
376 376 if ids:
377 377 ids = self.filter_real_bug_ids(ids)
378 378 if ids:
379 379 ids = self.filter_unknown_bug_ids(ctx.node(), ids)
380 380 return ids
381 381
382 382 def update(self, bugid, ctx):
383 383 '''update bugzilla bug with reference to changeset.'''
384 384
385 385 def webroot(root):
386 386 '''strip leading prefix of repo root and turn into
387 387 url-safe path.'''
388 388 count = int(self.ui.config('bugzilla', 'strip', 0))
389 389 root = util.pconvert(root)
390 390 while count > 0:
391 391 c = root.find('/')
392 392 if c == -1:
393 393 break
394 394 root = root[c + 1:]
395 395 count -= 1
396 396 return root
397 397
398 398 mapfile = self.ui.config('bugzilla', 'style')
399 399 tmpl = self.ui.config('bugzilla', 'template')
400 400 t = cmdutil.changeset_templater(self.ui, self.repo,
401 401 False, None, mapfile, False)
402 402 if not mapfile and not tmpl:
403 403 tmpl = _('changeset {node|short} in repo {root} refers '
404 404 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
405 405 if tmpl:
406 406 tmpl = templater.parsestring(tmpl, quoted=False)
407 407 t.use_template(tmpl)
408 408 self.ui.pushbuffer()
409 409 t.show(ctx, changes=ctx.changeset(),
410 410 bug=str(bugid),
411 411 hgweb=self.ui.config('web', 'baseurl'),
412 412 root=self.repo.root,
413 413 webroot=webroot(self.repo.root))
414 414 data = self.ui.popbuffer()
415 415 self.add_comment(bugid, data, util.email(ctx.user()))
416 416
417 417 def hook(ui, repo, hooktype, node=None, **kwargs):
418 418 '''add comment to bugzilla for each changeset that refers to a
419 419 bugzilla bug id. only add a comment once per bug, so same change
420 420 seen multiple times does not fill bug with duplicate data.'''
421 421 try:
422 422 import MySQLdb as mysql
423 423 global MySQLdb
424 424 MySQLdb = mysql
425 425 except ImportError, err:
426 426 raise util.Abort(_('python mysql support not available: %s') % err)
427 427
428 428 if node is None:
429 429 raise util.Abort(_('hook type %s does not pass a changeset id') %
430 430 hooktype)
431 431 try:
432 432 bz = bugzilla(ui, repo)
433 433 ctx = repo[node]
434 434 ids = bz.find_bug_ids(ctx)
435 435 if ids:
436 436 for id in ids:
437 437 bz.update(id, ctx)
438 438 bz.notify(ids, util.email(ctx.user()))
439 439 except MySQLdb.MySQLError, err:
440 raise util.Abort(_('database error: %s') % err[1])
440 raise util.Abort(_('database error: %s') % err.args[1])
441 441
@@ -1,174 +1,174
1 1 # client.py - inotify status client
2 2 #
3 3 # Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
4 4 # Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
5 5 # Copyright 2009 Nicolas Dumazet <nicdumz@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from mercurial.i18n import _
11 11 import common, server
12 12 import errno, os, socket, struct
13 13
14 14 class QueryFailed(Exception):
15 15 pass
16 16
17 17 def start_server(function):
18 18 """
19 19 Decorator.
20 20 Tries to call function, if it fails, try to (re)start inotify server.
21 21 Raise QueryFailed if something went wrong
22 22 """
23 23 def decorated_function(self, *args):
24 24 result = None
25 25 try:
26 26 return function(self, *args)
27 27 except (OSError, socket.error), err:
28 28 autostart = self.ui.configbool('inotify', 'autostart', True)
29 29
30 if err[0] == errno.ECONNREFUSED:
30 if err.args[0] == errno.ECONNREFUSED:
31 31 self.ui.warn(_('inotify-client: found dead inotify server '
32 32 'socket; removing it\n'))
33 33 os.unlink(os.path.join(self.root, '.hg', 'inotify.sock'))
34 if err[0] in (errno.ECONNREFUSED, errno.ENOENT) and autostart:
34 if err.args[0] in (errno.ECONNREFUSED, errno.ENOENT) and autostart:
35 35 self.ui.debug('(starting inotify server)\n')
36 36 try:
37 37 try:
38 38 server.start(self.ui, self.dirstate, self.root,
39 39 dict(daemon=True, daemon_pipefds=''))
40 40 except server.AlreadyStartedException, inst:
41 41 # another process may have started its own
42 42 # inotify server while this one was starting.
43 43 self.ui.debug(str(inst))
44 44 except Exception, inst:
45 45 self.ui.warn(_('inotify-client: could not start inotify '
46 46 'server: %s\n') % inst)
47 47 else:
48 48 try:
49 49 return function(self, *args)
50 50 except socket.error, err:
51 51 self.ui.warn(_('inotify-client: could not talk to new '
52 'inotify server: %s\n') % err[-1])
53 elif err[0] in (errno.ECONNREFUSED, errno.ENOENT):
52 'inotify server: %s\n') % err.args[-1])
53 elif err.args[0] in (errno.ECONNREFUSED, errno.ENOENT):
54 54 # silently ignore normal errors if autostart is False
55 55 self.ui.debug('(inotify server not running)\n')
56 56 else:
57 57 self.ui.warn(_('inotify-client: failed to contact inotify '
58 'server: %s\n') % err[-1])
58 'server: %s\n') % err.args[-1])
59 59
60 60 self.ui.traceback()
61 61 raise QueryFailed('inotify query failed')
62 62
63 63 return decorated_function
64 64
65 65
66 66 class client(object):
67 67 def __init__(self, ui, repo):
68 68 self.ui = ui
69 69 self.dirstate = repo.dirstate
70 70 self.root = repo.root
71 71 self.sock = socket.socket(socket.AF_UNIX)
72 72
73 73 def _connect(self):
74 74 sockpath = os.path.join(self.root, '.hg', 'inotify.sock')
75 75 try:
76 76 self.sock.connect(sockpath)
77 77 except socket.error, err:
78 if err[0] == "AF_UNIX path too long":
78 if err.args[0] == "AF_UNIX path too long":
79 79 sockpath = os.readlink(sockpath)
80 80 self.sock.connect(sockpath)
81 81 else:
82 82 raise
83 83
84 84 def _send(self, type, data):
85 85 """Sends protocol version number, and the data"""
86 86 self.sock.sendall(chr(common.version) + type + data)
87 87
88 88 self.sock.shutdown(socket.SHUT_WR)
89 89
90 90 def _receive(self, type):
91 91 """
92 92 Read data, check version number, extract headers,
93 93 and returns a tuple (data descriptor, header)
94 94 Raises QueryFailed on error
95 95 """
96 96 cs = common.recvcs(self.sock)
97 97 try:
98 98 version = ord(cs.read(1))
99 99 except TypeError:
100 100 # empty answer, assume the server crashed
101 101 self.ui.warn(_('inotify-client: received empty answer from inotify '
102 102 'server'))
103 103 raise QueryFailed('server crashed')
104 104
105 105 if version != common.version:
106 106 self.ui.warn(_('(inotify: received response from incompatible '
107 107 'server version %d)\n') % version)
108 108 raise QueryFailed('incompatible server version')
109 109
110 110 readtype = cs.read(4)
111 111 if readtype != type:
112 112 self.ui.warn(_('(inotify: received \'%s\' response when expecting'
113 113 ' \'%s\')\n') % (readtype, type))
114 114 raise QueryFailed('wrong response type')
115 115
116 116 hdrfmt = common.resphdrfmts[type]
117 117 hdrsize = common.resphdrsizes[type]
118 118 try:
119 119 resphdr = struct.unpack(hdrfmt, cs.read(hdrsize))
120 120 except struct.error:
121 121 raise QueryFailed('unable to retrieve query response headers')
122 122
123 123 return cs, resphdr
124 124
125 125 def query(self, type, req):
126 126 self._connect()
127 127
128 128 self._send(type, req)
129 129
130 130 return self._receive(type)
131 131
132 132 @start_server
133 133 def statusquery(self, names, match, ignored, clean, unknown=True):
134 134
135 135 def genquery():
136 136 for n in names:
137 137 yield n
138 138 states = 'almrx!'
139 139 if ignored:
140 140 raise ValueError('this is insanity')
141 141 if clean:
142 142 states += 'c'
143 143 if unknown:
144 144 states += '?'
145 145 yield states
146 146
147 147 req = '\0'.join(genquery())
148 148
149 149 cs, resphdr = self.query('STAT', req)
150 150
151 151 def readnames(nbytes):
152 152 if nbytes:
153 153 names = cs.read(nbytes)
154 154 if names:
155 155 return filter(match, names.split('\0'))
156 156 return []
157 157 results = map(readnames, resphdr[:-1])
158 158
159 159 if names:
160 160 nbytes = resphdr[-1]
161 161 vdirs = cs.read(nbytes)
162 162 if vdirs:
163 163 for vdir in vdirs.split('\0'):
164 164 match.dir(vdir)
165 165
166 166 return results
167 167
168 168 @start_server
169 169 def debugquery(self):
170 170 cs, resphdr = self.query('DBUG', '')
171 171
172 172 nbytes = resphdr[0]
173 173 names = cs.read(nbytes)
174 174 return names.split('\0')
@@ -1,441 +1,441
1 1 # linuxserver.py - inotify status server for linux
2 2 #
3 3 # Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
4 4 # Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from mercurial.i18n import _
10 10 from mercurial import osutil, util
11 11 import server
12 12 import errno, os, select, stat, sys, time
13 13
14 14 try:
15 15 import linux as inotify
16 16 from linux import watcher
17 17 except ImportError:
18 18 raise
19 19
20 20 def walkrepodirs(dirstate, absroot):
21 21 '''Iterate over all subdirectories of this repo.
22 22 Exclude the .hg directory, any nested repos, and ignored dirs.'''
23 23 def walkit(dirname, top):
24 24 fullpath = server.join(absroot, dirname)
25 25 try:
26 26 for name, kind in osutil.listdir(fullpath):
27 27 if kind == stat.S_IFDIR:
28 28 if name == '.hg':
29 29 if not top:
30 30 return
31 31 else:
32 32 d = server.join(dirname, name)
33 33 if dirstate._ignore(d):
34 34 continue
35 35 for subdir in walkit(d, False):
36 36 yield subdir
37 37 except OSError, err:
38 38 if err.errno not in server.walk_ignored_errors:
39 39 raise
40 40 yield fullpath
41 41
42 42 return walkit('', True)
43 43
44 44 def _explain_watch_limit(ui, dirstate, rootabs):
45 45 path = '/proc/sys/fs/inotify/max_user_watches'
46 46 try:
47 47 limit = int(file(path).read())
48 48 except IOError, err:
49 49 if err.errno != errno.ENOENT:
50 50 raise
51 51 raise util.Abort(_('this system does not seem to '
52 52 'support inotify'))
53 53 ui.warn(_('*** the current per-user limit on the number '
54 54 'of inotify watches is %s\n') % limit)
55 55 ui.warn(_('*** this limit is too low to watch every '
56 56 'directory in this repository\n'))
57 57 ui.warn(_('*** counting directories: '))
58 58 ndirs = len(list(walkrepodirs(dirstate, rootabs)))
59 59 ui.warn(_('found %d\n') % ndirs)
60 60 newlimit = min(limit, 1024)
61 61 while newlimit < ((limit + ndirs) * 1.1):
62 62 newlimit *= 2
63 63 ui.warn(_('*** to raise the limit from %d to %d (run as root):\n') %
64 64 (limit, newlimit))
65 65 ui.warn(_('*** echo %d > %s\n') % (newlimit, path))
66 66 raise util.Abort(_('cannot watch %s until inotify watch limit is raised')
67 67 % rootabs)
68 68
69 69 class pollable(object):
70 70 """
71 71 Interface to support polling.
72 72 The file descriptor returned by fileno() is registered to a polling
73 73 object.
74 74 Usage:
75 75 Every tick, check if an event has happened since the last tick:
76 76 * If yes, call handle_events
77 77 * If no, call handle_timeout
78 78 """
79 79 poll_events = select.POLLIN
80 80 instances = {}
81 81 poll = select.poll()
82 82
83 83 def fileno(self):
84 84 raise NotImplementedError
85 85
86 86 def handle_events(self, events):
87 87 raise NotImplementedError
88 88
89 89 def handle_timeout(self):
90 90 raise NotImplementedError
91 91
92 92 def shutdown(self):
93 93 raise NotImplementedError
94 94
95 95 def register(self, timeout):
96 96 fd = self.fileno()
97 97
98 98 pollable.poll.register(fd, pollable.poll_events)
99 99 pollable.instances[fd] = self
100 100
101 101 self.registered = True
102 102 self.timeout = timeout
103 103
104 104 def unregister(self):
105 105 pollable.poll.unregister(self)
106 106 self.registered = False
107 107
108 108 @classmethod
109 109 def run(cls):
110 110 while True:
111 111 timeout = None
112 112 timeobj = None
113 113 for obj in cls.instances.itervalues():
114 114 if obj.timeout is not None and (timeout is None
115 115 or obj.timeout < timeout):
116 116 timeout, timeobj = obj.timeout, obj
117 117 try:
118 118 events = cls.poll.poll(timeout)
119 119 except select.error, err:
120 if err[0] == errno.EINTR:
120 if err.args[0] == errno.EINTR:
121 121 continue
122 122 raise
123 123 if events:
124 124 by_fd = {}
125 125 for fd, event in events:
126 126 by_fd.setdefault(fd, []).append(event)
127 127
128 128 for fd, events in by_fd.iteritems():
129 129 cls.instances[fd].handle_pollevents(events)
130 130
131 131 elif timeobj:
132 132 timeobj.handle_timeout()
133 133
134 134 def eventaction(code):
135 135 """
136 136 Decorator to help handle events in repowatcher
137 137 """
138 138 def decorator(f):
139 139 def wrapper(self, wpath):
140 140 if code == 'm' and wpath in self.lastevent and \
141 141 self.lastevent[wpath] in 'cm':
142 142 return
143 143 self.lastevent[wpath] = code
144 144 self.timeout = 250
145 145
146 146 f(self, wpath)
147 147
148 148 wrapper.func_name = f.func_name
149 149 return wrapper
150 150 return decorator
151 151
152 152 class repowatcher(server.repowatcher, pollable):
153 153 """
154 154 Watches inotify events
155 155 """
156 156 mask = (
157 157 inotify.IN_ATTRIB |
158 158 inotify.IN_CREATE |
159 159 inotify.IN_DELETE |
160 160 inotify.IN_DELETE_SELF |
161 161 inotify.IN_MODIFY |
162 162 inotify.IN_MOVED_FROM |
163 163 inotify.IN_MOVED_TO |
164 164 inotify.IN_MOVE_SELF |
165 165 inotify.IN_ONLYDIR |
166 166 inotify.IN_UNMOUNT |
167 167 0)
168 168
169 169 def __init__(self, ui, dirstate, root):
170 170 server.repowatcher.__init__(self, ui, dirstate, root)
171 171
172 172 self.lastevent = {}
173 173 self.dirty = False
174 174 try:
175 175 self.watcher = watcher.watcher()
176 176 except OSError, err:
177 177 raise util.Abort(_('inotify service not available: %s') %
178 178 err.strerror)
179 179 self.threshold = watcher.threshold(self.watcher)
180 180 self.fileno = self.watcher.fileno
181 181 self.register(timeout=None)
182 182
183 183 self.handle_timeout()
184 184 self.scan()
185 185
186 186 def event_time(self):
187 187 last = self.last_event
188 188 now = time.time()
189 189 self.last_event = now
190 190
191 191 if last is None:
192 192 return 'start'
193 193 delta = now - last
194 194 if delta < 5:
195 195 return '+%.3f' % delta
196 196 if delta < 50:
197 197 return '+%.2f' % delta
198 198 return '+%.1f' % delta
199 199
200 200 def add_watch(self, path, mask):
201 201 if not path:
202 202 return
203 203 if self.watcher.path(path) is None:
204 204 if self.ui.debugflag:
205 205 self.ui.note(_('watching %r\n') % path[self.prefixlen:])
206 206 try:
207 207 self.watcher.add(path, mask)
208 208 except OSError, err:
209 209 if err.errno in (errno.ENOENT, errno.ENOTDIR):
210 210 return
211 211 if err.errno != errno.ENOSPC:
212 212 raise
213 213 _explain_watch_limit(self.ui, self.dirstate, self.wprefix)
214 214
215 215 def setup(self):
216 216 self.ui.note(_('watching directories under %r\n') % self.wprefix)
217 217 self.add_watch(self.wprefix + '.hg', inotify.IN_DELETE)
218 218
219 219 def scan(self, topdir=''):
220 220 ds = self.dirstate._map.copy()
221 221 self.add_watch(server.join(self.wprefix, topdir), self.mask)
222 222 for root, dirs, files in server.walk(self.dirstate, self.wprefix,
223 223 topdir):
224 224 for d in dirs:
225 225 self.add_watch(server.join(root, d), self.mask)
226 226 wroot = root[self.prefixlen:]
227 227 for fn in files:
228 228 wfn = server.join(wroot, fn)
229 229 self.updatefile(wfn, self.getstat(wfn))
230 230 ds.pop(wfn, None)
231 231 wtopdir = topdir
232 232 if wtopdir and wtopdir[-1] != '/':
233 233 wtopdir += '/'
234 234 for wfn, state in ds.iteritems():
235 235 if not wfn.startswith(wtopdir):
236 236 continue
237 237 try:
238 238 st = self.stat(wfn)
239 239 except OSError:
240 240 status = state[0]
241 241 self.deletefile(wfn, status)
242 242 else:
243 243 self.updatefile(wfn, st)
244 244 self.check_deleted('!')
245 245 self.check_deleted('r')
246 246
247 247 @eventaction('c')
248 248 def created(self, wpath):
249 249 if wpath == '.hgignore':
250 250 self.update_hgignore()
251 251 try:
252 252 st = self.stat(wpath)
253 253 if stat.S_ISREG(st[0]) or stat.S_ISLNK(st[0]):
254 254 self.updatefile(wpath, st)
255 255 except OSError:
256 256 pass
257 257
258 258 @eventaction('m')
259 259 def modified(self, wpath):
260 260 if wpath == '.hgignore':
261 261 self.update_hgignore()
262 262 try:
263 263 st = self.stat(wpath)
264 264 if stat.S_ISREG(st[0]):
265 265 if self.dirstate[wpath] in 'lmn':
266 266 self.updatefile(wpath, st)
267 267 except OSError:
268 268 pass
269 269
270 270 @eventaction('d')
271 271 def deleted(self, wpath):
272 272 if wpath == '.hgignore':
273 273 self.update_hgignore()
274 274 elif wpath.startswith('.hg/'):
275 275 return
276 276
277 277 self.deletefile(wpath, self.dirstate[wpath])
278 278
279 279 def process_create(self, wpath, evt):
280 280 if self.ui.debugflag:
281 281 self.ui.note(_('%s event: created %s\n') %
282 282 (self.event_time(), wpath))
283 283
284 284 if evt.mask & inotify.IN_ISDIR:
285 285 self.scan(wpath)
286 286 else:
287 287 self.created(wpath)
288 288
289 289 def process_delete(self, wpath, evt):
290 290 if self.ui.debugflag:
291 291 self.ui.note(_('%s event: deleted %s\n') %
292 292 (self.event_time(), wpath))
293 293
294 294 if evt.mask & inotify.IN_ISDIR:
295 295 tree = self.tree.dir(wpath)
296 296 todelete = [wfn for wfn, ignore in tree.walk('?')]
297 297 for fn in todelete:
298 298 self.deletefile(fn, '?')
299 299 self.scan(wpath)
300 300 else:
301 301 self.deleted(wpath)
302 302
303 303 def process_modify(self, wpath, evt):
304 304 if self.ui.debugflag:
305 305 self.ui.note(_('%s event: modified %s\n') %
306 306 (self.event_time(), wpath))
307 307
308 308 if not (evt.mask & inotify.IN_ISDIR):
309 309 self.modified(wpath)
310 310
311 311 def process_unmount(self, evt):
312 312 self.ui.warn(_('filesystem containing %s was unmounted\n') %
313 313 evt.fullpath)
314 314 sys.exit(0)
315 315
316 316 def handle_pollevents(self, events):
317 317 if self.ui.debugflag:
318 318 self.ui.note(_('%s readable: %d bytes\n') %
319 319 (self.event_time(), self.threshold.readable()))
320 320 if not self.threshold():
321 321 if self.registered:
322 322 if self.ui.debugflag:
323 323 self.ui.note(_('%s below threshold - unhooking\n') %
324 324 (self.event_time()))
325 325 self.unregister()
326 326 self.timeout = 250
327 327 else:
328 328 self.read_events()
329 329
330 330 def read_events(self, bufsize=None):
331 331 events = self.watcher.read(bufsize)
332 332 if self.ui.debugflag:
333 333 self.ui.note(_('%s reading %d events\n') %
334 334 (self.event_time(), len(events)))
335 335 for evt in events:
336 336 if evt.fullpath == self.wprefix[:-1]:
337 337 # events on the root of the repository
338 338 # itself, e.g. permission changes or repository move
339 339 continue
340 340 assert evt.fullpath.startswith(self.wprefix)
341 341 wpath = evt.fullpath[self.prefixlen:]
342 342
343 343 # paths have been normalized, wpath never ends with a '/'
344 344
345 345 if wpath.startswith('.hg/') and evt.mask & inotify.IN_ISDIR:
346 346 # ignore subdirectories of .hg/ (merge, patches...)
347 347 continue
348 348 if wpath == ".hg/wlock":
349 349 if evt.mask & inotify.IN_DELETE:
350 350 self.dirstate.invalidate()
351 351 self.dirty = False
352 352 self.scan()
353 353 elif evt.mask & inotify.IN_CREATE:
354 354 self.dirty = True
355 355 else:
356 356 if self.dirty:
357 357 continue
358 358
359 359 if evt.mask & inotify.IN_UNMOUNT:
360 360 self.process_unmount(wpath, evt)
361 361 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
362 362 self.process_modify(wpath, evt)
363 363 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
364 364 inotify.IN_MOVED_FROM):
365 365 self.process_delete(wpath, evt)
366 366 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
367 367 self.process_create(wpath, evt)
368 368
369 369 self.lastevent.clear()
370 370
371 371 def handle_timeout(self):
372 372 if not self.registered:
373 373 if self.ui.debugflag:
374 374 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
375 375 (self.event_time(), self.threshold.readable()))
376 376 self.read_events(0)
377 377 self.register(timeout=None)
378 378
379 379 self.timeout = None
380 380
381 381 def shutdown(self):
382 382 self.watcher.close()
383 383
384 384 def debug(self):
385 385 """
386 386 Returns a sorted list of relatives paths currently watched,
387 387 for debugging purposes.
388 388 """
389 389 return sorted(tuple[0][self.prefixlen:] for tuple in self.watcher)
390 390
391 391 class socketlistener(server.socketlistener, pollable):
392 392 """
393 393 Listens for client queries on unix socket inotify.sock
394 394 """
395 395 def __init__(self, ui, root, repowatcher, timeout):
396 396 server.socketlistener.__init__(self, ui, root, repowatcher, timeout)
397 397 self.register(timeout=timeout)
398 398
399 399 def handle_timeout(self):
400 400 raise server.TimeoutException
401 401
402 402 def handle_pollevents(self, events):
403 403 for e in events:
404 404 self.accept_connection()
405 405
406 406 def shutdown(self):
407 407 self.sock.close()
408 408 try:
409 409 os.unlink(self.sockpath)
410 410 if self.realsockpath:
411 411 os.unlink(self.realsockpath)
412 412 os.rmdir(os.path.dirname(self.realsockpath))
413 413 except OSError, err:
414 414 if err.errno != errno.ENOENT:
415 415 raise
416 416
417 417 def answer_stat_query(self, cs):
418 418 if self.repowatcher.timeout:
419 419 # We got a query while a rescan is pending. Make sure we
420 420 # rescan before responding, or we could give back a wrong
421 421 # answer.
422 422 self.repowatcher.handle_timeout()
423 423 return server.socketlistener.answer_stat_query(self, cs)
424 424
425 425 class master(object):
426 426 def __init__(self, ui, dirstate, root, timeout=None):
427 427 self.ui = ui
428 428 self.repowatcher = repowatcher(ui, dirstate, root)
429 429 self.socketlistener = socketlistener(ui, root, self.repowatcher,
430 430 timeout)
431 431
432 432 def shutdown(self):
433 433 for obj in pollable.instances.itervalues():
434 434 obj.shutdown()
435 435
436 436 def run(self):
437 437 self.repowatcher.setup()
438 438 self.ui.note(_('finished setup\n'))
439 439 if os.getenv('TIME_STARTUP'):
440 440 sys.exit(0)
441 441 pollable.run()
@@ -1,488 +1,488
1 1 # server.py - common entry point for inotify status server
2 2 #
3 3 # Copyright 2009 Nicolas Dumazet <nicdumz@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from mercurial.i18n import _
9 9 from mercurial import cmdutil, osutil, util
10 10 import common
11 11
12 12 import errno
13 13 import os
14 14 import socket
15 15 import stat
16 16 import struct
17 17 import sys
18 18 import tempfile
19 19
20 20 class AlreadyStartedException(Exception):
21 21 pass
22 22 class TimeoutException(Exception):
23 23 pass
24 24
25 25 def join(a, b):
26 26 if a:
27 27 if a[-1] == '/':
28 28 return a + b
29 29 return a + '/' + b
30 30 return b
31 31
32 32 def split(path):
33 33 c = path.rfind('/')
34 34 if c == -1:
35 35 return '', path
36 36 return path[:c], path[c + 1:]
37 37
38 38 walk_ignored_errors = (errno.ENOENT, errno.ENAMETOOLONG)
39 39
40 40 def walk(dirstate, absroot, root):
41 41 '''Like os.walk, but only yields regular files.'''
42 42
43 43 # This function is critical to performance during startup.
44 44
45 45 def walkit(root, reporoot):
46 46 files, dirs = [], []
47 47
48 48 try:
49 49 fullpath = join(absroot, root)
50 50 for name, kind in osutil.listdir(fullpath):
51 51 if kind == stat.S_IFDIR:
52 52 if name == '.hg':
53 53 if not reporoot:
54 54 return
55 55 else:
56 56 dirs.append(name)
57 57 path = join(root, name)
58 58 if dirstate._ignore(path):
59 59 continue
60 60 for result in walkit(path, False):
61 61 yield result
62 62 elif kind in (stat.S_IFREG, stat.S_IFLNK):
63 63 files.append(name)
64 64 yield fullpath, dirs, files
65 65
66 66 except OSError, err:
67 67 if err.errno == errno.ENOTDIR:
68 68 # fullpath was a directory, but has since been replaced
69 69 # by a file.
70 70 yield fullpath, dirs, files
71 71 elif err.errno not in walk_ignored_errors:
72 72 raise
73 73
74 74 return walkit(root, root == '')
75 75
76 76 class directory(object):
77 77 """
78 78 Representing a directory
79 79
80 80 * path is the relative path from repo root to this directory
81 81 * files is a dict listing the files in this directory
82 82 - keys are file names
83 83 - values are file status
84 84 * dirs is a dict listing the subdirectories
85 85 - key are subdirectories names
86 86 - values are directory objects
87 87 """
88 88 def __init__(self, relpath=''):
89 89 self.path = relpath
90 90 self.files = {}
91 91 self.dirs = {}
92 92
93 93 def dir(self, relpath):
94 94 """
95 95 Returns the directory contained at the relative path relpath.
96 96 Creates the intermediate directories if necessary.
97 97 """
98 98 if not relpath:
99 99 return self
100 100 l = relpath.split('/')
101 101 ret = self
102 102 while l:
103 103 next = l.pop(0)
104 104 try:
105 105 ret = ret.dirs[next]
106 106 except KeyError:
107 107 d = directory(join(ret.path, next))
108 108 ret.dirs[next] = d
109 109 ret = d
110 110 return ret
111 111
112 112 def walk(self, states, visited=None):
113 113 """
114 114 yield (filename, status) pairs for items in the trees
115 115 that have status in states.
116 116 filenames are relative to the repo root
117 117 """
118 118 for file, st in self.files.iteritems():
119 119 if st in states:
120 120 yield join(self.path, file), st
121 121 for dir in self.dirs.itervalues():
122 122 if visited is not None:
123 123 visited.add(dir.path)
124 124 for e in dir.walk(states):
125 125 yield e
126 126
127 127 def lookup(self, states, path, visited):
128 128 """
129 129 yield root-relative filenames that match path, and whose
130 130 status are in states:
131 131 * if path is a file, yield path
132 132 * if path is a directory, yield directory files
133 133 * if path is not tracked, yield nothing
134 134 """
135 135 if path[-1] == '/':
136 136 path = path[:-1]
137 137
138 138 paths = path.split('/')
139 139
140 140 # we need to check separately for last node
141 141 last = paths.pop()
142 142
143 143 tree = self
144 144 try:
145 145 for dir in paths:
146 146 tree = tree.dirs[dir]
147 147 except KeyError:
148 148 # path is not tracked
149 149 visited.add(tree.path)
150 150 return
151 151
152 152 try:
153 153 # if path is a directory, walk it
154 154 target = tree.dirs[last]
155 155 visited.add(target.path)
156 156 for file, st in target.walk(states, visited):
157 157 yield file
158 158 except KeyError:
159 159 try:
160 160 if tree.files[last] in states:
161 161 # path is a file
162 162 visited.add(tree.path)
163 163 yield path
164 164 except KeyError:
165 165 # path is not tracked
166 166 pass
167 167
168 168 class repowatcher(object):
169 169 """
170 170 Watches inotify events
171 171 """
172 172 statuskeys = 'almr!?'
173 173
174 174 def __init__(self, ui, dirstate, root):
175 175 self.ui = ui
176 176 self.dirstate = dirstate
177 177
178 178 self.wprefix = join(root, '')
179 179 self.prefixlen = len(self.wprefix)
180 180
181 181 self.tree = directory()
182 182 self.statcache = {}
183 183 self.statustrees = dict([(s, directory()) for s in self.statuskeys])
184 184
185 185 self.ds_info = self.dirstate_info()
186 186
187 187 self.last_event = None
188 188
189 189
190 190 def handle_timeout(self):
191 191 pass
192 192
193 193 def dirstate_info(self):
194 194 try:
195 195 st = os.lstat(self.wprefix + '.hg/dirstate')
196 196 return st.st_mtime, st.st_ino
197 197 except OSError, err:
198 198 if err.errno != errno.ENOENT:
199 199 raise
200 200 return 0, 0
201 201
202 202 def filestatus(self, fn, st):
203 203 try:
204 204 type_, mode, size, time = self.dirstate._map[fn][:4]
205 205 except KeyError:
206 206 type_ = '?'
207 207 if type_ == 'n':
208 208 st_mode, st_size, st_mtime = st
209 209 if size == -1:
210 210 return 'l'
211 211 if size and (size != st_size or (mode ^ st_mode) & 0100):
212 212 return 'm'
213 213 if time != int(st_mtime):
214 214 return 'l'
215 215 return 'n'
216 216 if type_ == '?' and self.dirstate._dirignore(fn):
217 217 # we must check not only if the file is ignored, but if any part
218 218 # of its path match an ignore pattern
219 219 return 'i'
220 220 return type_
221 221
222 222 def updatefile(self, wfn, osstat):
223 223 '''
224 224 update the file entry of an existing file.
225 225
226 226 osstat: (mode, size, time) tuple, as returned by os.lstat(wfn)
227 227 '''
228 228
229 229 self._updatestatus(wfn, self.filestatus(wfn, osstat))
230 230
231 231 def deletefile(self, wfn, oldstatus):
232 232 '''
233 233 update the entry of a file which has been deleted.
234 234
235 235 oldstatus: char in statuskeys, status of the file before deletion
236 236 '''
237 237 if oldstatus == 'r':
238 238 newstatus = 'r'
239 239 elif oldstatus in 'almn':
240 240 newstatus = '!'
241 241 else:
242 242 newstatus = None
243 243
244 244 self.statcache.pop(wfn, None)
245 245 self._updatestatus(wfn, newstatus)
246 246
247 247 def _updatestatus(self, wfn, newstatus):
248 248 '''
249 249 Update the stored status of a file.
250 250
251 251 newstatus: - char in (statuskeys + 'ni'), new status to apply.
252 252 - or None, to stop tracking wfn
253 253 '''
254 254 root, fn = split(wfn)
255 255 d = self.tree.dir(root)
256 256
257 257 oldstatus = d.files.get(fn)
258 258 # oldstatus can be either:
259 259 # - None : fn is new
260 260 # - a char in statuskeys: fn is a (tracked) file
261 261
262 262 if self.ui.debugflag and oldstatus != newstatus:
263 263 self.ui.note(_('status: %r %s -> %s\n') %
264 264 (wfn, oldstatus, newstatus))
265 265
266 266 if oldstatus and oldstatus in self.statuskeys \
267 267 and oldstatus != newstatus:
268 268 del self.statustrees[oldstatus].dir(root).files[fn]
269 269
270 270 if newstatus in (None, 'i'):
271 271 d.files.pop(fn, None)
272 272 elif oldstatus != newstatus:
273 273 d.files[fn] = newstatus
274 274 if newstatus != 'n':
275 275 self.statustrees[newstatus].dir(root).files[fn] = newstatus
276 276
277 277 def check_deleted(self, key):
278 278 # Files that had been deleted but were present in the dirstate
279 279 # may have vanished from the dirstate; we must clean them up.
280 280 nuke = []
281 281 for wfn, ignore in self.statustrees[key].walk(key):
282 282 if wfn not in self.dirstate:
283 283 nuke.append(wfn)
284 284 for wfn in nuke:
285 285 root, fn = split(wfn)
286 286 del self.statustrees[key].dir(root).files[fn]
287 287 del self.tree.dir(root).files[fn]
288 288
289 289 def update_hgignore(self):
290 290 # An update of the ignore file can potentially change the
291 291 # states of all unknown and ignored files.
292 292
293 293 # XXX If the user has other ignore files outside the repo, or
294 294 # changes their list of ignore files at run time, we'll
295 295 # potentially never see changes to them. We could get the
296 296 # client to report to us what ignore data they're using.
297 297 # But it's easier to do nothing than to open that can of
298 298 # worms.
299 299
300 300 if '_ignore' in self.dirstate.__dict__:
301 301 delattr(self.dirstate, '_ignore')
302 302 self.ui.note(_('rescanning due to .hgignore change\n'))
303 303 self.handle_timeout()
304 304 self.scan()
305 305
306 306 def getstat(self, wpath):
307 307 try:
308 308 return self.statcache[wpath]
309 309 except KeyError:
310 310 try:
311 311 return self.stat(wpath)
312 312 except OSError, err:
313 313 if err.errno != errno.ENOENT:
314 314 raise
315 315
316 316 def stat(self, wpath):
317 317 try:
318 318 st = os.lstat(join(self.wprefix, wpath))
319 319 ret = st.st_mode, st.st_size, st.st_mtime
320 320 self.statcache[wpath] = ret
321 321 return ret
322 322 except OSError:
323 323 self.statcache.pop(wpath, None)
324 324 raise
325 325
326 326 class socketlistener(object):
327 327 """
328 328 Listens for client queries on unix socket inotify.sock
329 329 """
330 330 def __init__(self, ui, root, repowatcher, timeout):
331 331 self.ui = ui
332 332 self.repowatcher = repowatcher
333 333 self.sock = socket.socket(socket.AF_UNIX)
334 334 self.sockpath = join(root, '.hg/inotify.sock')
335 335 self.realsockpath = None
336 336 try:
337 337 self.sock.bind(self.sockpath)
338 338 except socket.error, err:
339 if err[0] == errno.EADDRINUSE:
339 if err.args[0] == errno.EADDRINUSE:
340 340 raise AlreadyStartedException(_('cannot start: socket is '
341 341 'already bound'))
342 if err[0] == "AF_UNIX path too long":
342 if err.args[0] == "AF_UNIX path too long":
343 343 if os.path.islink(self.sockpath) and \
344 344 not os.path.exists(self.sockpath):
345 345 raise util.Abort('inotify-server: cannot start: '
346 346 '.hg/inotify.sock is a broken symlink')
347 347 tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
348 348 self.realsockpath = os.path.join(tempdir, "inotify.sock")
349 349 try:
350 350 self.sock.bind(self.realsockpath)
351 351 os.symlink(self.realsockpath, self.sockpath)
352 352 except (OSError, socket.error), inst:
353 353 try:
354 354 os.unlink(self.realsockpath)
355 355 except:
356 356 pass
357 357 os.rmdir(tempdir)
358 358 if inst.errno == errno.EEXIST:
359 359 raise AlreadyStartedException(_('cannot start: tried '
360 360 'linking .hg/inotify.sock to a temporary socket but'
361 361 ' .hg/inotify.sock already exists'))
362 362 raise
363 363 else:
364 364 raise
365 365 self.sock.listen(5)
366 366 self.fileno = self.sock.fileno
367 367
368 368 def answer_stat_query(self, cs):
369 369 names = cs.read().split('\0')
370 370
371 371 states = names.pop()
372 372
373 373 self.ui.note(_('answering query for %r\n') % states)
374 374
375 375 visited = set()
376 376 if not names:
377 377 def genresult(states, tree):
378 378 for fn, state in tree.walk(states):
379 379 yield fn
380 380 else:
381 381 def genresult(states, tree):
382 382 for fn in names:
383 383 for f in tree.lookup(states, fn, visited):
384 384 yield f
385 385
386 386 return ['\0'.join(r) for r in [
387 387 genresult('l', self.repowatcher.statustrees['l']),
388 388 genresult('m', self.repowatcher.statustrees['m']),
389 389 genresult('a', self.repowatcher.statustrees['a']),
390 390 genresult('r', self.repowatcher.statustrees['r']),
391 391 genresult('!', self.repowatcher.statustrees['!']),
392 392 '?' in states
393 393 and genresult('?', self.repowatcher.statustrees['?'])
394 394 or [],
395 395 [],
396 396 'c' in states and genresult('n', self.repowatcher.tree) or [],
397 397 visited
398 398 ]]
399 399
400 400 def answer_dbug_query(self):
401 401 return ['\0'.join(self.repowatcher.debug())]
402 402
403 403 def accept_connection(self):
404 404 sock, addr = self.sock.accept()
405 405
406 406 cs = common.recvcs(sock)
407 407 version = ord(cs.read(1))
408 408
409 409 if version != common.version:
410 410 self.ui.warn(_('received query from incompatible client '
411 411 'version %d\n') % version)
412 412 try:
413 413 # try to send back our version to the client
414 414 # this way, the client too is informed of the mismatch
415 415 sock.sendall(chr(common.version))
416 416 except:
417 417 pass
418 418 return
419 419
420 420 type = cs.read(4)
421 421
422 422 if type == 'STAT':
423 423 results = self.answer_stat_query(cs)
424 424 elif type == 'DBUG':
425 425 results = self.answer_dbug_query()
426 426 else:
427 427 self.ui.warn(_('unrecognized query type: %s\n') % type)
428 428 return
429 429
430 430 try:
431 431 try:
432 432 v = chr(common.version)
433 433
434 434 sock.sendall(v + type + struct.pack(common.resphdrfmts[type],
435 435 *map(len, results)))
436 436 sock.sendall(''.join(results))
437 437 finally:
438 438 sock.shutdown(socket.SHUT_WR)
439 439 except socket.error, err:
440 if err[0] != errno.EPIPE:
440 if err.args[0] != errno.EPIPE:
441 441 raise
442 442
443 443 if sys.platform == 'linux2':
444 444 import linuxserver as _server
445 445 else:
446 446 raise ImportError
447 447
448 448 master = _server.master
449 449
450 450 def start(ui, dirstate, root, opts):
451 451 timeout = opts.get('idle_timeout')
452 452 if timeout:
453 453 timeout = float(timeout) * 60000
454 454 else:
455 455 timeout = None
456 456
457 457 class service(object):
458 458 def init(self):
459 459 try:
460 460 self.master = master(ui, dirstate, root, timeout)
461 461 except AlreadyStartedException, inst:
462 462 raise util.Abort("inotify-server: %s" % inst)
463 463
464 464 def run(self):
465 465 try:
466 466 try:
467 467 self.master.run()
468 468 except TimeoutException:
469 469 pass
470 470 finally:
471 471 self.master.shutdown()
472 472
473 473 if 'inserve' not in sys.argv:
474 474 runargs = util.hgcmd() + ['inserve', '-R', root]
475 475 else:
476 476 runargs = util.hgcmd() + sys.argv[1:]
477 477
478 478 pidfile = ui.config('inotify', 'pidfile')
479 479 if opts['daemon'] and pidfile is not None and 'pid-file' not in runargs:
480 480 runargs.append("--pid-file=%s" % pidfile)
481 481
482 482 service = service()
483 483 logfile = ui.config('inotify', 'log')
484 484
485 485 appendpid = ui.configbool('inotify', 'appendpid', False)
486 486
487 487 cmdutil.service(opts, initfn=service.init, runfn=service.run,
488 488 logfile=logfile, runargs=runargs, appendpid=appendpid)
@@ -1,306 +1,306
1 1 # httprepo.py - HTTP repository proxy classes for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from node import bin, hex, nullid
10 10 from i18n import _
11 11 import repo, changegroup, statichttprepo, error, url, util, pushkey
12 12 import os, urllib, urllib2, urlparse, zlib, httplib
13 13 import errno, socket
14 14 import encoding
15 15
16 16 def zgenerator(f):
17 17 zd = zlib.decompressobj()
18 18 try:
19 19 for chunk in util.filechunkiter(f):
20 20 yield zd.decompress(chunk)
21 21 except httplib.HTTPException:
22 22 raise IOError(None, _('connection ended unexpectedly'))
23 23 yield zd.flush()
24 24
25 25 class httprepository(repo.repository):
26 26 def __init__(self, ui, path):
27 27 self.path = path
28 28 self.caps = None
29 29 self.handler = None
30 30 scheme, netloc, urlpath, query, frag = urlparse.urlsplit(path)
31 31 if query or frag:
32 32 raise util.Abort(_('unsupported URL component: "%s"') %
33 33 (query or frag))
34 34
35 35 # urllib cannot handle URLs with embedded user or passwd
36 36 self._url, authinfo = url.getauthinfo(path)
37 37
38 38 self.ui = ui
39 39 self.ui.debug('using %s\n' % self._url)
40 40
41 41 self.urlopener = url.opener(ui, authinfo)
42 42
43 43 def __del__(self):
44 44 for h in self.urlopener.handlers:
45 45 h.close()
46 46 if hasattr(h, "close_all"):
47 47 h.close_all()
48 48
49 49 def url(self):
50 50 return self.path
51 51
52 52 # look up capabilities only when needed
53 53
54 54 def get_caps(self):
55 55 if self.caps is None:
56 56 try:
57 57 self.caps = set(self.do_read('capabilities').split())
58 58 except error.RepoError:
59 59 self.caps = set()
60 60 self.ui.debug('capabilities: %s\n' %
61 61 (' '.join(self.caps or ['none'])))
62 62 return self.caps
63 63
64 64 capabilities = property(get_caps)
65 65
66 66 def lock(self):
67 67 raise util.Abort(_('operation not supported over http'))
68 68
69 69 def do_cmd(self, cmd, **args):
70 70 data = args.pop('data', None)
71 71 headers = args.pop('headers', {})
72 72 self.ui.debug("sending %s command\n" % cmd)
73 73 q = {"cmd": cmd}
74 74 q.update(args)
75 75 qs = '?%s' % urllib.urlencode(q)
76 76 cu = "%s%s" % (self._url, qs)
77 77 req = urllib2.Request(cu, data, headers)
78 78 if data is not None:
79 79 # len(data) is broken if data doesn't fit into Py_ssize_t
80 80 # add the header ourself to avoid OverflowError
81 81 size = data.__len__()
82 82 self.ui.debug("sending %s bytes\n" % size)
83 83 req.add_unredirected_header('Content-Length', '%d' % size)
84 84 try:
85 85 resp = self.urlopener.open(req)
86 86 except urllib2.HTTPError, inst:
87 87 if inst.code == 401:
88 88 raise util.Abort(_('authorization failed'))
89 89 raise
90 90 except httplib.HTTPException, inst:
91 91 self.ui.debug('http error while sending %s command\n' % cmd)
92 92 self.ui.traceback()
93 93 raise IOError(None, inst)
94 94 except IndexError:
95 95 # this only happens with Python 2.3, later versions raise URLError
96 96 raise util.Abort(_('http error, possibly caused by proxy setting'))
97 97 # record the url we got redirected to
98 98 resp_url = resp.geturl()
99 99 if resp_url.endswith(qs):
100 100 resp_url = resp_url[:-len(qs)]
101 101 if self._url.rstrip('/') != resp_url.rstrip('/'):
102 102 self.ui.status(_('real URL is %s\n') % resp_url)
103 103 self._url = resp_url
104 104 try:
105 105 proto = resp.getheader('content-type')
106 106 except AttributeError:
107 107 proto = resp.headers['content-type']
108 108
109 109 safeurl = url.hidepassword(self._url)
110 110 # accept old "text/plain" and "application/hg-changegroup" for now
111 111 if not (proto.startswith('application/mercurial-') or
112 112 proto.startswith('text/plain') or
113 113 proto.startswith('application/hg-changegroup')):
114 114 self.ui.debug("requested URL: '%s'\n" % url.hidepassword(cu))
115 115 raise error.RepoError(
116 116 _("'%s' does not appear to be an hg repository:\n"
117 117 "---%%<--- (%s)\n%s\n---%%<---\n")
118 118 % (safeurl, proto, resp.read()))
119 119
120 120 if proto.startswith('application/mercurial-'):
121 121 try:
122 122 version = proto.split('-', 1)[1]
123 123 version_info = tuple([int(n) for n in version.split('.')])
124 124 except ValueError:
125 125 raise error.RepoError(_("'%s' sent a broken Content-Type "
126 126 "header (%s)") % (safeurl, proto))
127 127 if version_info > (0, 1):
128 128 raise error.RepoError(_("'%s' uses newer protocol %s") %
129 129 (safeurl, version))
130 130
131 131 return resp
132 132
133 133 def do_read(self, cmd, **args):
134 134 fp = self.do_cmd(cmd, **args)
135 135 try:
136 136 return fp.read()
137 137 finally:
138 138 # if using keepalive, allow connection to be reused
139 139 fp.close()
140 140
141 141 def lookup(self, key):
142 142 self.requirecap('lookup', _('look up remote revision'))
143 143 d = self.do_cmd("lookup", key = key).read()
144 144 success, data = d[:-1].split(' ', 1)
145 145 if int(success):
146 146 return bin(data)
147 147 raise error.RepoError(data)
148 148
149 149 def heads(self):
150 150 d = self.do_read("heads")
151 151 try:
152 152 return map(bin, d[:-1].split(" "))
153 153 except:
154 154 raise error.ResponseError(_("unexpected response:"), d)
155 155
156 156 def branchmap(self):
157 157 d = self.do_read("branchmap")
158 158 try:
159 159 branchmap = {}
160 160 for branchpart in d.splitlines():
161 161 branchheads = branchpart.split(' ')
162 162 branchname = urllib.unquote(branchheads[0])
163 163 # Earlier servers (1.3.x) send branch names in (their) local
164 164 # charset. The best we can do is assume it's identical to our
165 165 # own local charset, in case it's not utf-8.
166 166 try:
167 167 branchname.decode('utf-8')
168 168 except UnicodeDecodeError:
169 169 branchname = encoding.fromlocal(branchname)
170 170 branchheads = [bin(x) for x in branchheads[1:]]
171 171 branchmap[branchname] = branchheads
172 172 return branchmap
173 173 except:
174 174 raise error.ResponseError(_("unexpected response:"), d)
175 175
176 176 def branches(self, nodes):
177 177 n = " ".join(map(hex, nodes))
178 178 d = self.do_read("branches", nodes=n)
179 179 try:
180 180 br = [tuple(map(bin, b.split(" "))) for b in d.splitlines()]
181 181 return br
182 182 except:
183 183 raise error.ResponseError(_("unexpected response:"), d)
184 184
185 185 def between(self, pairs):
186 186 batch = 8 # avoid giant requests
187 187 r = []
188 188 for i in xrange(0, len(pairs), batch):
189 189 n = " ".join(["-".join(map(hex, p)) for p in pairs[i:i + batch]])
190 190 d = self.do_read("between", pairs=n)
191 191 try:
192 192 r += [l and map(bin, l.split(" ")) or []
193 193 for l in d.splitlines()]
194 194 except:
195 195 raise error.ResponseError(_("unexpected response:"), d)
196 196 return r
197 197
198 198 def changegroup(self, nodes, kind):
199 199 n = " ".join(map(hex, nodes))
200 200 f = self.do_cmd("changegroup", roots=n)
201 201 return util.chunkbuffer(zgenerator(f))
202 202
203 203 def changegroupsubset(self, bases, heads, source):
204 204 self.requirecap('changegroupsubset', _('look up remote changes'))
205 205 baselst = " ".join([hex(n) for n in bases])
206 206 headlst = " ".join([hex(n) for n in heads])
207 207 f = self.do_cmd("changegroupsubset", bases=baselst, heads=headlst)
208 208 return util.chunkbuffer(zgenerator(f))
209 209
210 210 def unbundle(self, cg, heads, source):
211 211 '''Send cg (a readable file-like object representing the
212 212 changegroup to push, typically a chunkbuffer object) to the
213 213 remote server as a bundle. Return an integer response code:
214 214 non-zero indicates a successful push (see
215 215 localrepository.addchangegroup()), and zero indicates either
216 216 error or nothing to push.'''
217 217 # have to stream bundle to a temp file because we do not have
218 218 # http 1.1 chunked transfer.
219 219
220 220 type = ""
221 221 types = self.capable('unbundle')
222 222 # servers older than d1b16a746db6 will send 'unbundle' as a
223 223 # boolean capability
224 224 try:
225 225 types = types.split(',')
226 226 except AttributeError:
227 227 types = [""]
228 228 if types:
229 229 for x in types:
230 230 if x in changegroup.bundletypes:
231 231 type = x
232 232 break
233 233
234 234 tempname = changegroup.writebundle(cg, None, type)
235 235 fp = url.httpsendfile(tempname, "rb")
236 236 try:
237 237 try:
238 238 resp = self.do_read(
239 239 'unbundle', data=fp,
240 240 headers={'Content-Type': 'application/mercurial-0.1'},
241 241 heads=' '.join(map(hex, heads)))
242 242 resp_code, output = resp.split('\n', 1)
243 243 try:
244 244 ret = int(resp_code)
245 245 except ValueError, err:
246 246 raise error.ResponseError(
247 247 _('push failed (unexpected response):'), resp)
248 248 for l in output.splitlines(True):
249 249 self.ui.status(_('remote: '), l)
250 250 return ret
251 251 except socket.error, err:
252 if err[0] in (errno.ECONNRESET, errno.EPIPE):
253 raise util.Abort(_('push failed: %s') % err[1])
254 raise util.Abort(err[1])
252 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
253 raise util.Abort(_('push failed: %s') % err.args[1])
254 raise util.Abort(err.args[1])
255 255 finally:
256 256 fp.close()
257 257 os.unlink(tempname)
258 258
259 259 def stream_out(self):
260 260 return self.do_cmd('stream_out')
261 261
262 262 def pushkey(self, namespace, key, old, new):
263 263 if not self.capable('pushkey'):
264 264 return False
265 265 d = self.do_cmd("pushkey", data="", # force a POST
266 266 namespace=namespace, key=key, old=old, new=new).read()
267 267 code, output = d.split('\n', 1)
268 268 try:
269 269 ret = bool(int(code))
270 270 except ValueError, err:
271 271 raise error.ResponseError(
272 272 _('push failed (unexpected response):'), d)
273 273 for l in output.splitlines(True):
274 274 self.ui.status(_('remote: '), l)
275 275 return ret
276 276
277 277 def listkeys(self, namespace):
278 278 if not self.capable('pushkey'):
279 279 return {}
280 280 d = self.do_cmd("listkeys", namespace=namespace).read()
281 281 r = {}
282 282 for l in d.splitlines():
283 283 k, v = l.split('\t')
284 284 r[k.decode('string-escape')] = v.decode('string-escape')
285 285 return r
286 286
287 287 class httpsrepository(httprepository):
288 288 def __init__(self, ui, path):
289 289 if not url.has_https:
290 290 raise util.Abort(_('Python support for SSL and HTTPS '
291 291 'is not installed'))
292 292 httprepository.__init__(self, ui, path)
293 293
294 294 def instance(ui, path, create):
295 295 if create:
296 296 raise util.Abort(_('cannot create new http repository'))
297 297 try:
298 298 if path.startswith('https:'):
299 299 inst = httpsrepository(ui, path)
300 300 else:
301 301 inst = httprepository(ui, path)
302 302 inst.between([(nullid, nullid)])
303 303 return inst
304 304 except error.RepoError:
305 305 ui.note('(falling back to static-http)\n')
306 306 return statichttprepo.instance(ui, "static-" + path, create)
General Comments 0
You need to be logged in to leave comments. Login now