##// END OF EJS Templates
merge with stable
Benoit Boissinot -
r9898:b5170b8b merge default
parent child Browse files
Show More
@@ -1,859 +1,864
1 1 # server.py - inotify status server
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, incorporated herein by reference.
8 8
9 9 from mercurial.i18n import _
10 10 from mercurial import cmdutil, osutil, util
11 11 import common
12 12 import errno, os, select, socket, stat, struct, sys, tempfile, 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 class AlreadyStartedException(Exception): pass
21 21
22 22 def join(a, b):
23 23 if a:
24 24 if a[-1] == '/':
25 25 return a + b
26 26 return a + '/' + b
27 27 return b
28 28
29 29 def split(path):
30 30 c = path.rfind('/')
31 31 if c == -1:
32 32 return '', path
33 33 return path[:c], path[c+1:]
34 34
35 35 walk_ignored_errors = (errno.ENOENT, errno.ENAMETOOLONG)
36 36
37 37 def walkrepodirs(dirstate, absroot):
38 38 '''Iterate over all subdirectories of this repo.
39 39 Exclude the .hg directory, any nested repos, and ignored dirs.'''
40 40 def walkit(dirname, top):
41 41 fullpath = join(absroot, dirname)
42 42 try:
43 43 for name, kind in osutil.listdir(fullpath):
44 44 if kind == stat.S_IFDIR:
45 45 if name == '.hg':
46 46 if not top:
47 47 return
48 48 else:
49 49 d = join(dirname, name)
50 50 if dirstate._ignore(d):
51 51 continue
52 52 for subdir in walkit(d, False):
53 53 yield subdir
54 54 except OSError, err:
55 55 if err.errno not in walk_ignored_errors:
56 56 raise
57 57 yield fullpath
58 58
59 59 return walkit('', True)
60 60
61 61 def walk(dirstate, absroot, root):
62 62 '''Like os.walk, but only yields regular files.'''
63 63
64 64 # This function is critical to performance during startup.
65 65
66 66 def walkit(root, reporoot):
67 67 files, dirs = [], []
68 68
69 69 try:
70 70 fullpath = join(absroot, root)
71 71 for name, kind in osutil.listdir(fullpath):
72 72 if kind == stat.S_IFDIR:
73 73 if name == '.hg':
74 74 if not reporoot:
75 75 return
76 76 else:
77 77 dirs.append(name)
78 78 path = join(root, name)
79 79 if dirstate._ignore(path):
80 80 continue
81 81 for result in walkit(path, False):
82 82 yield result
83 83 elif kind in (stat.S_IFREG, stat.S_IFLNK):
84 84 files.append(name)
85 85 yield fullpath, dirs, files
86 86
87 87 except OSError, err:
88 88 if err.errno == errno.ENOTDIR:
89 89 # fullpath was a directory, but has since been replaced
90 90 # by a file.
91 91 yield fullpath, dirs, files
92 92 elif err.errno not in walk_ignored_errors:
93 93 raise
94 94
95 95 return walkit(root, root == '')
96 96
97 97 def _explain_watch_limit(ui, dirstate, rootabs):
98 98 path = '/proc/sys/fs/inotify/max_user_watches'
99 99 try:
100 100 limit = int(file(path).read())
101 101 except IOError, err:
102 102 if err.errno != errno.ENOENT:
103 103 raise
104 104 raise util.Abort(_('this system does not seem to '
105 105 'support inotify'))
106 106 ui.warn(_('*** the current per-user limit on the number '
107 107 'of inotify watches is %s\n') % limit)
108 108 ui.warn(_('*** this limit is too low to watch every '
109 109 'directory in this repository\n'))
110 110 ui.warn(_('*** counting directories: '))
111 111 ndirs = len(list(walkrepodirs(dirstate, rootabs)))
112 112 ui.warn(_('found %d\n') % ndirs)
113 113 newlimit = min(limit, 1024)
114 114 while newlimit < ((limit + ndirs) * 1.1):
115 115 newlimit *= 2
116 116 ui.warn(_('*** to raise the limit from %d to %d (run as root):\n') %
117 117 (limit, newlimit))
118 118 ui.warn(_('*** echo %d > %s\n') % (newlimit, path))
119 119 raise util.Abort(_('cannot watch %s until inotify watch limit is raised')
120 120 % rootabs)
121 121
122 122 class pollable(object):
123 123 """
124 124 Interface to support polling.
125 125 The file descriptor returned by fileno() is registered to a polling
126 126 object.
127 127 Usage:
128 128 Every tick, check if an event has happened since the last tick:
129 129 * If yes, call handle_events
130 130 * If no, call handle_timeout
131 131 """
132 132 poll_events = select.POLLIN
133 133 instances = {}
134 134 poll = select.poll()
135 135
136 136 def fileno(self):
137 137 raise NotImplementedError
138 138
139 139 def handle_events(self, events):
140 140 raise NotImplementedError
141 141
142 142 def handle_timeout(self):
143 143 raise NotImplementedError
144 144
145 145 def shutdown(self):
146 146 raise NotImplementedError
147 147
148 148 def register(self, timeout):
149 149 fd = self.fileno()
150 150
151 151 pollable.poll.register(fd, pollable.poll_events)
152 152 pollable.instances[fd] = self
153 153
154 154 self.registered = True
155 155 self.timeout = timeout
156 156
157 157 def unregister(self):
158 158 pollable.poll.unregister(self)
159 159 self.registered = False
160 160
161 161 @classmethod
162 162 def run(cls):
163 163 while True:
164 164 timeout = None
165 165 timeobj = None
166 166 for obj in cls.instances.itervalues():
167 167 if obj.timeout is not None and (timeout is None or obj.timeout < timeout):
168 168 timeout, timeobj = obj.timeout, obj
169 169 try:
170 170 events = cls.poll.poll(timeout)
171 171 except select.error, err:
172 172 if err[0] == errno.EINTR:
173 173 continue
174 174 raise
175 175 if events:
176 176 by_fd = {}
177 177 for fd, event in events:
178 178 by_fd.setdefault(fd, []).append(event)
179 179
180 180 for fd, events in by_fd.iteritems():
181 181 cls.instances[fd].handle_pollevents(events)
182 182
183 183 elif timeobj:
184 184 timeobj.handle_timeout()
185 185
186 186 def eventaction(code):
187 187 """
188 188 Decorator to help handle events in repowatcher
189 189 """
190 190 def decorator(f):
191 191 def wrapper(self, wpath):
192 192 if code == 'm' and wpath in self.lastevent and \
193 193 self.lastevent[wpath] in 'cm':
194 194 return
195 195 self.lastevent[wpath] = code
196 196 self.timeout = 250
197 197
198 198 f(self, wpath)
199 199
200 200 wrapper.func_name = f.func_name
201 201 return wrapper
202 202 return decorator
203 203
204 204 class directory(object):
205 205 """
206 206 Representing a directory
207 207
208 208 * path is the relative path from repo root to this directory
209 209 * files is a dict listing the files in this directory
210 210 - keys are file names
211 211 - values are file status
212 212 * dirs is a dict listing the subdirectories
213 213 - key are subdirectories names
214 214 - values are directory objects
215 215 """
216 216 def __init__(self, relpath=''):
217 217 self.path = relpath
218 218 self.files = {}
219 219 self.dirs = {}
220 220
221 221 def dir(self, relpath):
222 222 """
223 223 Returns the directory contained at the relative path relpath.
224 224 Creates the intermediate directories if necessary.
225 225 """
226 226 if not relpath:
227 227 return self
228 228 l = relpath.split('/')
229 229 ret = self
230 230 while l:
231 231 next = l.pop(0)
232 232 try:
233 233 ret = ret.dirs[next]
234 234 except KeyError:
235 235 d = directory(join(ret.path, next))
236 236 ret.dirs[next] = d
237 237 ret = d
238 238 return ret
239 239
240 240 def walk(self, states, visited=None):
241 241 """
242 242 yield (filename, status) pairs for items in the trees
243 243 that have status in states.
244 244 filenames are relative to the repo root
245 245 """
246 246 for file, st in self.files.iteritems():
247 247 if st in states:
248 248 yield join(self.path, file), st
249 249 for dir in self.dirs.itervalues():
250 250 if visited is not None:
251 251 visited.add(dir.path)
252 252 for e in dir.walk(states):
253 253 yield e
254 254
255 255 def lookup(self, states, path, visited):
256 256 """
257 257 yield root-relative filenames that match path, and whose
258 258 status are in states:
259 259 * if path is a file, yield path
260 260 * if path is a directory, yield directory files
261 261 * if path is not tracked, yield nothing
262 262 """
263 263 if path[-1] == '/':
264 264 path = path[:-1]
265 265
266 266 paths = path.split('/')
267 267
268 268 # we need to check separately for last node
269 269 last = paths.pop()
270 270
271 271 tree = self
272 272 try:
273 273 for dir in paths:
274 274 tree = tree.dirs[dir]
275 275 except KeyError:
276 276 # path is not tracked
277 277 visited.add(tree.path)
278 278 return
279 279
280 280 try:
281 281 # if path is a directory, walk it
282 282 target = tree.dirs[last]
283 283 visited.add(target.path)
284 284 for file, st in target.walk(states, visited):
285 285 yield file
286 286 except KeyError:
287 287 try:
288 288 if tree.files[last] in states:
289 289 # path is a file
290 290 visited.add(tree.path)
291 291 yield path
292 292 except KeyError:
293 293 # path is not tracked
294 294 pass
295 295
296 296 class repowatcher(pollable):
297 297 """
298 298 Watches inotify events
299 299 """
300 300 statuskeys = 'almr!?'
301 301 mask = (
302 302 inotify.IN_ATTRIB |
303 303 inotify.IN_CREATE |
304 304 inotify.IN_DELETE |
305 305 inotify.IN_DELETE_SELF |
306 306 inotify.IN_MODIFY |
307 307 inotify.IN_MOVED_FROM |
308 308 inotify.IN_MOVED_TO |
309 309 inotify.IN_MOVE_SELF |
310 310 inotify.IN_ONLYDIR |
311 311 inotify.IN_UNMOUNT |
312 312 0)
313 313
314 314 def __init__(self, ui, dirstate, root):
315 315 self.ui = ui
316 316 self.dirstate = dirstate
317 317
318 318 self.wprefix = join(root, '')
319 319 self.prefixlen = len(self.wprefix)
320 320 try:
321 321 self.watcher = watcher.watcher()
322 322 except OSError, err:
323 323 raise util.Abort(_('inotify service not available: %s') %
324 324 err.strerror)
325 325 self.threshold = watcher.threshold(self.watcher)
326 326 self.fileno = self.watcher.fileno
327 327
328 328 self.tree = directory()
329 329 self.statcache = {}
330 330 self.statustrees = dict([(s, directory()) for s in self.statuskeys])
331 331
332 332 self.last_event = None
333 333
334 334 self.lastevent = {}
335 335
336 336 self.register(timeout=None)
337 337
338 338 self.ds_info = self.dirstate_info()
339 339 self.handle_timeout()
340 340 self.scan()
341 341
342 342 def event_time(self):
343 343 last = self.last_event
344 344 now = time.time()
345 345 self.last_event = now
346 346
347 347 if last is None:
348 348 return 'start'
349 349 delta = now - last
350 350 if delta < 5:
351 351 return '+%.3f' % delta
352 352 if delta < 50:
353 353 return '+%.2f' % delta
354 354 return '+%.1f' % delta
355 355
356 356 def dirstate_info(self):
357 357 try:
358 358 st = os.lstat(self.wprefix + '.hg/dirstate')
359 359 return st.st_mtime, st.st_ino
360 360 except OSError, err:
361 361 if err.errno != errno.ENOENT:
362 362 raise
363 363 return 0, 0
364 364
365 365 def add_watch(self, path, mask):
366 366 if not path:
367 367 return
368 368 if self.watcher.path(path) is None:
369 369 if self.ui.debugflag:
370 370 self.ui.note(_('watching %r\n') % path[self.prefixlen:])
371 371 try:
372 372 self.watcher.add(path, mask)
373 373 except OSError, err:
374 374 if err.errno in (errno.ENOENT, errno.ENOTDIR):
375 375 return
376 376 if err.errno != errno.ENOSPC:
377 377 raise
378 378 _explain_watch_limit(self.ui, self.dirstate, self.wprefix)
379 379
380 380 def setup(self):
381 381 self.ui.note(_('watching directories under %r\n') % self.wprefix)
382 382 self.add_watch(self.wprefix + '.hg', inotify.IN_DELETE)
383 383 self.check_dirstate()
384 384
385 385 def filestatus(self, fn, st):
386 386 try:
387 387 type_, mode, size, time = self.dirstate._map[fn][:4]
388 388 except KeyError:
389 389 type_ = '?'
390 390 if type_ == 'n':
391 391 st_mode, st_size, st_mtime = st
392 392 if size == -1:
393 393 return 'l'
394 394 if size and (size != st_size or (mode ^ st_mode) & 0100):
395 395 return 'm'
396 396 if time != int(st_mtime):
397 397 return 'l'
398 398 return 'n'
399 399 if type_ == '?' and self.dirstate._ignore(fn):
400 400 return 'i'
401 401 return type_
402 402
403 403 def updatefile(self, wfn, osstat):
404 404 '''
405 405 update the file entry of an existing file.
406 406
407 407 osstat: (mode, size, time) tuple, as returned by os.lstat(wfn)
408 408 '''
409 409
410 410 self._updatestatus(wfn, self.filestatus(wfn, osstat))
411 411
412 412 def deletefile(self, wfn, oldstatus):
413 413 '''
414 414 update the entry of a file which has been deleted.
415 415
416 416 oldstatus: char in statuskeys, status of the file before deletion
417 417 '''
418 418 if oldstatus == 'r':
419 419 newstatus = 'r'
420 420 elif oldstatus in 'almn':
421 421 newstatus = '!'
422 422 else:
423 423 newstatus = None
424 424
425 425 self.statcache.pop(wfn, None)
426 426 self._updatestatus(wfn, newstatus)
427 427
428 428 def _updatestatus(self, wfn, newstatus):
429 429 '''
430 430 Update the stored status of a file.
431 431
432 432 newstatus: - char in (statuskeys + 'ni'), new status to apply.
433 433 - or None, to stop tracking wfn
434 434 '''
435 435 root, fn = split(wfn)
436 436 d = self.tree.dir(root)
437 437
438 438 oldstatus = d.files.get(fn)
439 439 # oldstatus can be either:
440 440 # - None : fn is new
441 441 # - a char in statuskeys: fn is a (tracked) file
442 442
443 443 if self.ui.debugflag and oldstatus != newstatus:
444 444 self.ui.note(_('status: %r %s -> %s\n') %
445 445 (wfn, oldstatus, newstatus))
446 446
447 447 if oldstatus and oldstatus in self.statuskeys \
448 448 and oldstatus != newstatus:
449 449 del self.statustrees[oldstatus].dir(root).files[fn]
450 450
451 451 if newstatus in (None, 'i'):
452 452 d.files.pop(fn, None)
453 453 elif oldstatus != newstatus:
454 454 d.files[fn] = newstatus
455 455 if newstatus != 'n':
456 456 self.statustrees[newstatus].dir(root).files[fn] = newstatus
457 457
458 458
459 459 def check_deleted(self, key):
460 460 # Files that had been deleted but were present in the dirstate
461 461 # may have vanished from the dirstate; we must clean them up.
462 462 nuke = []
463 463 for wfn, ignore in self.statustrees[key].walk(key):
464 464 if wfn not in self.dirstate:
465 465 nuke.append(wfn)
466 466 for wfn in nuke:
467 467 root, fn = split(wfn)
468 468 del self.statustrees[key].dir(root).files[fn]
469 469 del self.tree.dir(root).files[fn]
470 470
471 471 def scan(self, topdir=''):
472 472 ds = self.dirstate._map.copy()
473 473 self.add_watch(join(self.wprefix, topdir), self.mask)
474 474 for root, dirs, files in walk(self.dirstate, self.wprefix, topdir):
475 475 for d in dirs:
476 476 self.add_watch(join(root, d), self.mask)
477 477 wroot = root[self.prefixlen:]
478 478 for fn in files:
479 479 wfn = join(wroot, fn)
480 480 self.updatefile(wfn, self.getstat(wfn))
481 481 ds.pop(wfn, None)
482 482 wtopdir = topdir
483 483 if wtopdir and wtopdir[-1] != '/':
484 484 wtopdir += '/'
485 485 for wfn, state in ds.iteritems():
486 486 if not wfn.startswith(wtopdir):
487 487 continue
488 488 try:
489 489 st = self.stat(wfn)
490 490 except OSError:
491 491 status = state[0]
492 492 self.deletefile(wfn, status)
493 493 else:
494 494 self.updatefile(wfn, st)
495 495 self.check_deleted('!')
496 496 self.check_deleted('r')
497 497
498 498 def check_dirstate(self):
499 499 ds_info = self.dirstate_info()
500 500 if ds_info == self.ds_info:
501 501 return
502 502 self.ds_info = ds_info
503 503 if not self.ui.debugflag:
504 504 self.last_event = None
505 505 self.ui.note(_('%s dirstate reload\n') % self.event_time())
506 506 self.dirstate.invalidate()
507 507 self.handle_timeout()
508 508 self.scan()
509 509 self.ui.note(_('%s end dirstate reload\n') % self.event_time())
510 510
511 511 def update_hgignore(self):
512 512 # An update of the ignore file can potentially change the
513 513 # states of all unknown and ignored files.
514 514
515 515 # XXX If the user has other ignore files outside the repo, or
516 516 # changes their list of ignore files at run time, we'll
517 517 # potentially never see changes to them. We could get the
518 518 # client to report to us what ignore data they're using.
519 519 # But it's easier to do nothing than to open that can of
520 520 # worms.
521 521
522 522 if '_ignore' in self.dirstate.__dict__:
523 523 delattr(self.dirstate, '_ignore')
524 524 self.ui.note(_('rescanning due to .hgignore change\n'))
525 525 self.handle_timeout()
526 526 self.scan()
527 527
528 528 def getstat(self, wpath):
529 529 try:
530 530 return self.statcache[wpath]
531 531 except KeyError:
532 532 try:
533 533 return self.stat(wpath)
534 534 except OSError, err:
535 535 if err.errno != errno.ENOENT:
536 536 raise
537 537
538 538 def stat(self, wpath):
539 539 try:
540 540 st = os.lstat(join(self.wprefix, wpath))
541 541 ret = st.st_mode, st.st_size, st.st_mtime
542 542 self.statcache[wpath] = ret
543 543 return ret
544 544 except OSError:
545 545 self.statcache.pop(wpath, None)
546 546 raise
547 547
548 548 @eventaction('c')
549 549 def created(self, wpath):
550 550 if wpath == '.hgignore':
551 551 self.update_hgignore()
552 552 try:
553 553 st = self.stat(wpath)
554 554 if stat.S_ISREG(st[0]):
555 555 self.updatefile(wpath, st)
556 556 except OSError:
557 557 pass
558 558
559 559 @eventaction('m')
560 560 def modified(self, wpath):
561 561 if wpath == '.hgignore':
562 562 self.update_hgignore()
563 563 try:
564 564 st = self.stat(wpath)
565 565 if stat.S_ISREG(st[0]):
566 566 if self.dirstate[wpath] in 'lmn':
567 567 self.updatefile(wpath, st)
568 568 except OSError:
569 569 pass
570 570
571 571 @eventaction('d')
572 572 def deleted(self, wpath):
573 573 if wpath == '.hgignore':
574 574 self.update_hgignore()
575 575 elif wpath.startswith('.hg/'):
576 576 if wpath == '.hg/wlock':
577 577 self.check_dirstate()
578 578 return
579 579
580 580 self.deletefile(wpath, self.dirstate[wpath])
581 581
582 582 def process_create(self, wpath, evt):
583 583 if self.ui.debugflag:
584 584 self.ui.note(_('%s event: created %s\n') %
585 585 (self.event_time(), wpath))
586 586
587 587 if evt.mask & inotify.IN_ISDIR:
588 588 self.scan(wpath)
589 589 else:
590 590 self.created(wpath)
591 591
592 592 def process_delete(self, wpath, evt):
593 593 if self.ui.debugflag:
594 594 self.ui.note(_('%s event: deleted %s\n') %
595 595 (self.event_time(), wpath))
596 596
597 597 if evt.mask & inotify.IN_ISDIR:
598 598 tree = self.tree.dir(wpath)
599 599 todelete = [wfn for wfn, ignore in tree.walk('?')]
600 600 for fn in todelete:
601 601 self.deletefile(fn, '?')
602 602 self.scan(wpath)
603 603 else:
604 604 self.deleted(wpath)
605 605
606 606 def process_modify(self, wpath, evt):
607 607 if self.ui.debugflag:
608 608 self.ui.note(_('%s event: modified %s\n') %
609 609 (self.event_time(), wpath))
610 610
611 611 if not (evt.mask & inotify.IN_ISDIR):
612 612 self.modified(wpath)
613 613
614 614 def process_unmount(self, evt):
615 615 self.ui.warn(_('filesystem containing %s was unmounted\n') %
616 616 evt.fullpath)
617 617 sys.exit(0)
618 618
619 619 def handle_pollevents(self, events):
620 620 if self.ui.debugflag:
621 621 self.ui.note(_('%s readable: %d bytes\n') %
622 622 (self.event_time(), self.threshold.readable()))
623 623 if not self.threshold():
624 624 if self.registered:
625 625 if self.ui.debugflag:
626 626 self.ui.note(_('%s below threshold - unhooking\n') %
627 627 (self.event_time()))
628 628 self.unregister()
629 629 self.timeout = 250
630 630 else:
631 631 self.read_events()
632 632
633 633 def read_events(self, bufsize=None):
634 634 events = self.watcher.read(bufsize)
635 635 if self.ui.debugflag:
636 636 self.ui.note(_('%s reading %d events\n') %
637 637 (self.event_time(), len(events)))
638 638 for evt in events:
639 639 assert evt.fullpath.startswith(self.wprefix)
640 640 wpath = evt.fullpath[self.prefixlen:]
641 641
642 642 # paths have been normalized, wpath never ends with a '/'
643 643
644 644 if wpath.startswith('.hg/') and evt.mask & inotify.IN_ISDIR:
645 645 # ignore subdirectories of .hg/ (merge, patches...)
646 646 continue
647 647
648 648 if evt.mask & inotify.IN_UNMOUNT:
649 649 self.process_unmount(wpath, evt)
650 650 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
651 651 self.process_modify(wpath, evt)
652 652 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
653 653 inotify.IN_MOVED_FROM):
654 654 self.process_delete(wpath, evt)
655 655 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
656 656 self.process_create(wpath, evt)
657 657
658 658 self.lastevent.clear()
659 659
660 660 def handle_timeout(self):
661 661 if not self.registered:
662 662 if self.ui.debugflag:
663 663 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
664 664 (self.event_time(), self.threshold.readable()))
665 665 self.read_events(0)
666 666 self.register(timeout=None)
667 667
668 668 self.timeout = None
669 669
670 670 def shutdown(self):
671 671 self.watcher.close()
672 672
673 673 def debug(self):
674 674 """
675 675 Returns a sorted list of relatives paths currently watched,
676 676 for debugging purposes.
677 677 """
678 678 return sorted(tuple[0][self.prefixlen:] for tuple in self.watcher)
679 679
680 680 class server(pollable):
681 681 """
682 682 Listens for client queries on unix socket inotify.sock
683 683 """
684 684 def __init__(self, ui, root, repowatcher, timeout):
685 685 self.ui = ui
686 686 self.repowatcher = repowatcher
687 687 self.sock = socket.socket(socket.AF_UNIX)
688 688 self.sockpath = join(root, '.hg/inotify.sock')
689 689 self.realsockpath = None
690 690 try:
691 691 self.sock.bind(self.sockpath)
692 692 except socket.error, err:
693 693 if err[0] == errno.EADDRINUSE:
694 694 raise AlreadyStartedException(_('could not start server: %s')
695 695 % err[1])
696 696 if err[0] == "AF_UNIX path too long":
697 697 tempdir = tempfile.mkdtemp(prefix="hg-inotify-")
698 698 self.realsockpath = os.path.join(tempdir, "inotify.sock")
699 699 try:
700 700 self.sock.bind(self.realsockpath)
701 701 os.symlink(self.realsockpath, self.sockpath)
702 702 except (OSError, socket.error), inst:
703 703 try:
704 704 os.unlink(self.realsockpath)
705 705 except:
706 706 pass
707 707 os.rmdir(tempdir)
708 708 if inst.errno == errno.EEXIST:
709 709 raise AlreadyStartedException(_('could not start server: %s')
710 710 % inst.strerror)
711 711 raise
712 712 else:
713 713 raise
714 714 self.sock.listen(5)
715 715 self.fileno = self.sock.fileno
716 716 self.register(timeout=timeout)
717 717
718 718 def handle_timeout(self):
719 719 pass
720 720
721 721 def answer_stat_query(self, cs):
722 722 names = cs.read().split('\0')
723 723
724 724 states = names.pop()
725 725
726 726 self.ui.note(_('answering query for %r\n') % states)
727 727
728 728 if self.repowatcher.timeout:
729 729 # We got a query while a rescan is pending. Make sure we
730 730 # rescan before responding, or we could give back a wrong
731 731 # answer.
732 732 self.repowatcher.handle_timeout()
733 733
734 734 visited = set()
735 735 if not names:
736 736 def genresult(states, tree):
737 737 for fn, state in tree.walk(states):
738 738 yield fn
739 739 else:
740 740 def genresult(states, tree):
741 741 for fn in names:
742 742 for f in tree.lookup(states, fn, visited):
743 743 yield f
744 744
745 745 return ['\0'.join(r) for r in [
746 746 genresult('l', self.repowatcher.statustrees['l']),
747 747 genresult('m', self.repowatcher.statustrees['m']),
748 748 genresult('a', self.repowatcher.statustrees['a']),
749 749 genresult('r', self.repowatcher.statustrees['r']),
750 750 genresult('!', self.repowatcher.statustrees['!']),
751 751 '?' in states
752 752 and genresult('?', self.repowatcher.statustrees['?'])
753 753 or [],
754 754 [],
755 755 'c' in states and genresult('n', self.repowatcher.tree) or [],
756 756 visited
757 757 ]]
758 758
759 759 def answer_dbug_query(self):
760 760 return ['\0'.join(self.repowatcher.debug())]
761 761
762 762 def handle_pollevents(self, events):
763 763 for e in events:
764 764 self.handle_pollevent()
765 765
766 766 def handle_pollevent(self):
767 767 sock, addr = self.sock.accept()
768 768
769 769 cs = common.recvcs(sock)
770 770 version = ord(cs.read(1))
771 771
772 772 if version != common.version:
773 773 self.ui.warn(_('received query from incompatible client '
774 774 'version %d\n') % version)
775 775 try:
776 776 # try to send back our version to the client
777 777 # this way, the client too is informed of the mismatch
778 778 sock.sendall(chr(common.version))
779 779 except:
780 780 pass
781 781 return
782 782
783 783 type = cs.read(4)
784 784
785 785 if type == 'STAT':
786 786 results = self.answer_stat_query(cs)
787 787 elif type == 'DBUG':
788 788 results = self.answer_dbug_query()
789 789 else:
790 790 self.ui.warn(_('unrecognized query type: %s\n') % type)
791 791 return
792 792
793 793 try:
794 794 try:
795 795 v = chr(common.version)
796 796
797 797 sock.sendall(v + type + struct.pack(common.resphdrfmts[type],
798 798 *map(len, results)))
799 799 sock.sendall(''.join(results))
800 800 finally:
801 801 sock.shutdown(socket.SHUT_WR)
802 802 except socket.error, err:
803 803 if err[0] != errno.EPIPE:
804 804 raise
805 805
806 806 def shutdown(self):
807 807 self.sock.close()
808 808 try:
809 809 os.unlink(self.sockpath)
810 810 if self.realsockpath:
811 811 os.unlink(self.realsockpath)
812 812 os.rmdir(os.path.dirname(self.realsockpath))
813 813 except OSError, err:
814 814 if err.errno != errno.ENOENT:
815 815 raise
816 816
817 817 class master(object):
818 818 def __init__(self, ui, dirstate, root, timeout=None):
819 819 self.ui = ui
820 820 self.repowatcher = repowatcher(ui, dirstate, root)
821 821 self.server = server(ui, root, self.repowatcher, timeout)
822 822
823 823 def shutdown(self):
824 824 for obj in pollable.instances.itervalues():
825 825 obj.shutdown()
826 826
827 827 def run(self):
828 828 self.repowatcher.setup()
829 829 self.ui.note(_('finished setup\n'))
830 830 if os.getenv('TIME_STARTUP'):
831 831 sys.exit(0)
832 832 pollable.run()
833 833
834 834 def start(ui, dirstate, root, opts):
835 835 timeout = opts.get('timeout')
836 836 if timeout:
837 837 timeout = float(timeout) * 1e3
838 838
839 839 class service(object):
840 840 def init(self):
841 841 try:
842 842 self.master = master(ui, dirstate, root, timeout)
843 843 except AlreadyStartedException, inst:
844 844 raise util.Abort(str(inst))
845 845
846 846 def run(self):
847 847 try:
848 848 self.master.run()
849 849 finally:
850 850 self.master.shutdown()
851 851
852 runargs = None
853 852 if 'inserve' not in sys.argv:
854 853 runargs = [sys.argv[0], 'inserve', '-R', root]
854 else:
855 runargs = sys.argv[:]
856
857 pidfile = ui.config('inotify', 'pidfile')
858 if opts['daemon'] and pidfile is not None and 'pid-file' not in runargs:
859 runargs.append("--pid-file=%s" % pidfile)
855 860
856 861 service = service()
857 862 logfile = ui.config('inotify', 'log')
858 863 cmdutil.service(opts, initfn=service.init, runfn=service.run,
859 864 logfile=logfile, runargs=runargs)
@@ -1,1282 +1,1282
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.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, incorporated herein by reference.
7 7
8 8 from node import hex, nullid, nullrev, short
9 9 from i18n import _
10 10 import os, sys, errno, re, glob
11 11 import mdiff, bdiff, util, templater, patch, error, encoding
12 12 import match as _match
13 13
14 14 revrangesep = ':'
15 15
16 16 def findpossible(cmd, table, strict=False):
17 17 """
18 18 Return cmd -> (aliases, command table entry)
19 19 for each matching command.
20 20 Return debug commands (or their aliases) only if no normal command matches.
21 21 """
22 22 choice = {}
23 23 debugchoice = {}
24 24 for e in table.keys():
25 25 aliases = e.lstrip("^").split("|")
26 26 found = None
27 27 if cmd in aliases:
28 28 found = cmd
29 29 elif not strict:
30 30 for a in aliases:
31 31 if a.startswith(cmd):
32 32 found = a
33 33 break
34 34 if found is not None:
35 35 if aliases[0].startswith("debug") or found.startswith("debug"):
36 36 debugchoice[found] = (aliases, table[e])
37 37 else:
38 38 choice[found] = (aliases, table[e])
39 39
40 40 if not choice and debugchoice:
41 41 choice = debugchoice
42 42
43 43 return choice
44 44
45 45 def findcmd(cmd, table, strict=True):
46 46 """Return (aliases, command table entry) for command string."""
47 47 choice = findpossible(cmd, table, strict)
48 48
49 49 if cmd in choice:
50 50 return choice[cmd]
51 51
52 52 if len(choice) > 1:
53 53 clist = choice.keys()
54 54 clist.sort()
55 55 raise error.AmbiguousCommand(cmd, clist)
56 56
57 57 if choice:
58 58 return choice.values()[0]
59 59
60 60 raise error.UnknownCommand(cmd)
61 61
62 62 def bail_if_changed(repo):
63 63 if repo.dirstate.parents()[1] != nullid:
64 64 raise util.Abort(_('outstanding uncommitted merge'))
65 65 modified, added, removed, deleted = repo.status()[:4]
66 66 if modified or added or removed or deleted:
67 67 raise util.Abort(_("outstanding uncommitted changes"))
68 68
69 69 def logmessage(opts):
70 70 """ get the log message according to -m and -l option """
71 71 message = opts.get('message')
72 72 logfile = opts.get('logfile')
73 73
74 74 if message and logfile:
75 75 raise util.Abort(_('options --message and --logfile are mutually '
76 76 'exclusive'))
77 77 if not message and logfile:
78 78 try:
79 79 if logfile == '-':
80 80 message = sys.stdin.read()
81 81 else:
82 82 message = open(logfile).read()
83 83 except IOError, inst:
84 84 raise util.Abort(_("can't read commit message '%s': %s") %
85 85 (logfile, inst.strerror))
86 86 return message
87 87
88 88 def loglimit(opts):
89 89 """get the log limit according to option -l/--limit"""
90 90 limit = opts.get('limit')
91 91 if limit:
92 92 try:
93 93 limit = int(limit)
94 94 except ValueError:
95 95 raise util.Abort(_('limit must be a positive integer'))
96 96 if limit <= 0: raise util.Abort(_('limit must be positive'))
97 97 else:
98 98 limit = sys.maxint
99 99 return limit
100 100
101 101 def remoteui(src, opts):
102 102 'build a remote ui from ui or repo and opts'
103 103 if hasattr(src, 'baseui'): # looks like a repository
104 104 dst = src.baseui.copy() # drop repo-specific config
105 105 src = src.ui # copy target options from repo
106 106 else: # assume it's a global ui object
107 107 dst = src.copy() # keep all global options
108 108
109 109 # copy ssh-specific options
110 110 for o in 'ssh', 'remotecmd':
111 111 v = opts.get(o) or src.config('ui', o)
112 112 if v:
113 113 dst.setconfig("ui", o, v)
114 114 # copy bundle-specific options
115 115 r = src.config('bundle', 'mainreporoot')
116 116 if r:
117 117 dst.setconfig('bundle', 'mainreporoot', r)
118 118
119 119 return dst
120 120
121 121 def revpair(repo, revs):
122 122 '''return pair of nodes, given list of revisions. second item can
123 123 be None, meaning use working dir.'''
124 124
125 125 def revfix(repo, val, defval):
126 126 if not val and val != 0 and defval is not None:
127 127 val = defval
128 128 return repo.lookup(val)
129 129
130 130 if not revs:
131 131 return repo.dirstate.parents()[0], None
132 132 end = None
133 133 if len(revs) == 1:
134 134 if revrangesep in revs[0]:
135 135 start, end = revs[0].split(revrangesep, 1)
136 136 start = revfix(repo, start, 0)
137 137 end = revfix(repo, end, len(repo) - 1)
138 138 else:
139 139 start = revfix(repo, revs[0], None)
140 140 elif len(revs) == 2:
141 141 if revrangesep in revs[0] or revrangesep in revs[1]:
142 142 raise util.Abort(_('too many revisions specified'))
143 143 start = revfix(repo, revs[0], None)
144 144 end = revfix(repo, revs[1], None)
145 145 else:
146 146 raise util.Abort(_('too many revisions specified'))
147 147 return start, end
148 148
149 149 def revrange(repo, revs):
150 150 """Yield revision as strings from a list of revision specifications."""
151 151
152 152 def revfix(repo, val, defval):
153 153 if not val and val != 0 and defval is not None:
154 154 return defval
155 155 return repo.changelog.rev(repo.lookup(val))
156 156
157 157 seen, l = set(), []
158 158 for spec in revs:
159 159 if revrangesep in spec:
160 160 start, end = spec.split(revrangesep, 1)
161 161 start = revfix(repo, start, 0)
162 162 end = revfix(repo, end, len(repo) - 1)
163 163 step = start > end and -1 or 1
164 164 for rev in xrange(start, end+step, step):
165 165 if rev in seen:
166 166 continue
167 167 seen.add(rev)
168 168 l.append(rev)
169 169 else:
170 170 rev = revfix(repo, spec, None)
171 171 if rev in seen:
172 172 continue
173 173 seen.add(rev)
174 174 l.append(rev)
175 175
176 176 return l
177 177
178 178 def make_filename(repo, pat, node,
179 179 total=None, seqno=None, revwidth=None, pathname=None):
180 180 node_expander = {
181 181 'H': lambda: hex(node),
182 182 'R': lambda: str(repo.changelog.rev(node)),
183 183 'h': lambda: short(node),
184 184 }
185 185 expander = {
186 186 '%': lambda: '%',
187 187 'b': lambda: os.path.basename(repo.root),
188 188 }
189 189
190 190 try:
191 191 if node:
192 192 expander.update(node_expander)
193 193 if node:
194 194 expander['r'] = (lambda:
195 195 str(repo.changelog.rev(node)).zfill(revwidth or 0))
196 196 if total is not None:
197 197 expander['N'] = lambda: str(total)
198 198 if seqno is not None:
199 199 expander['n'] = lambda: str(seqno)
200 200 if total is not None and seqno is not None:
201 201 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
202 202 if pathname is not None:
203 203 expander['s'] = lambda: os.path.basename(pathname)
204 204 expander['d'] = lambda: os.path.dirname(pathname) or '.'
205 205 expander['p'] = lambda: pathname
206 206
207 207 newname = []
208 208 patlen = len(pat)
209 209 i = 0
210 210 while i < patlen:
211 211 c = pat[i]
212 212 if c == '%':
213 213 i += 1
214 214 c = pat[i]
215 215 c = expander[c]()
216 216 newname.append(c)
217 217 i += 1
218 218 return ''.join(newname)
219 219 except KeyError, inst:
220 220 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
221 221 inst.args[0])
222 222
223 223 def make_file(repo, pat, node=None,
224 224 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
225 225
226 226 writable = 'w' in mode or 'a' in mode
227 227
228 228 if not pat or pat == '-':
229 229 return writable and sys.stdout or sys.stdin
230 230 if hasattr(pat, 'write') and writable:
231 231 return pat
232 232 if hasattr(pat, 'read') and 'r' in mode:
233 233 return pat
234 234 return open(make_filename(repo, pat, node, total, seqno, revwidth,
235 235 pathname),
236 236 mode)
237 237
238 238 def expandpats(pats):
239 239 if not util.expandglobs:
240 240 return list(pats)
241 241 ret = []
242 242 for p in pats:
243 243 kind, name = _match._patsplit(p, None)
244 244 if kind is None:
245 245 try:
246 246 globbed = glob.glob(name)
247 247 except re.error:
248 248 globbed = [name]
249 249 if globbed:
250 250 ret.extend(globbed)
251 251 continue
252 252 ret.append(p)
253 253 return ret
254 254
255 255 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
256 256 if not globbed and default == 'relpath':
257 257 pats = expandpats(pats or [])
258 258 m = _match.match(repo.root, repo.getcwd(), pats,
259 259 opts.get('include'), opts.get('exclude'), default)
260 260 def badfn(f, msg):
261 261 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
262 262 m.bad = badfn
263 263 return m
264 264
265 265 def matchall(repo):
266 266 return _match.always(repo.root, repo.getcwd())
267 267
268 268 def matchfiles(repo, files):
269 269 return _match.exact(repo.root, repo.getcwd(), files)
270 270
271 271 def findrenames(repo, added, removed, threshold):
272 272 '''find renamed files -- yields (before, after, score) tuples'''
273 273 ctx = repo['.']
274 274 for a in added:
275 275 aa = repo.wread(a)
276 276 bestname, bestscore = None, threshold
277 277 for r in removed:
278 278 if r not in ctx:
279 279 continue
280 280 rr = ctx.filectx(r).data()
281 281
282 282 # bdiff.blocks() returns blocks of matching lines
283 283 # count the number of bytes in each
284 284 equal = 0
285 285 alines = mdiff.splitnewlines(aa)
286 286 matches = bdiff.blocks(aa, rr)
287 287 for x1,x2,y1,y2 in matches:
288 288 for line in alines[x1:x2]:
289 289 equal += len(line)
290 290
291 291 lengths = len(aa) + len(rr)
292 292 if lengths:
293 293 myscore = equal*2.0 / lengths
294 294 if myscore >= bestscore:
295 295 bestname, bestscore = r, myscore
296 296 if bestname:
297 297 yield bestname, a, bestscore
298 298
299 299 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
300 300 if dry_run is None:
301 301 dry_run = opts.get('dry_run')
302 302 if similarity is None:
303 303 similarity = float(opts.get('similarity') or 0)
304 304 # we'd use status here, except handling of symlinks and ignore is tricky
305 305 added, unknown, deleted, removed = [], [], [], []
306 306 audit_path = util.path_auditor(repo.root)
307 307 m = match(repo, pats, opts)
308 308 for abs in repo.walk(m):
309 309 target = repo.wjoin(abs)
310 310 good = True
311 311 try:
312 312 audit_path(abs)
313 313 except:
314 314 good = False
315 315 rel = m.rel(abs)
316 316 exact = m.exact(abs)
317 317 if good and abs not in repo.dirstate:
318 318 unknown.append(abs)
319 319 if repo.ui.verbose or not exact:
320 320 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
321 321 elif repo.dirstate[abs] != 'r' and (not good or not util.lexists(target)
322 322 or (os.path.isdir(target) and not os.path.islink(target))):
323 323 deleted.append(abs)
324 324 if repo.ui.verbose or not exact:
325 325 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
326 326 # for finding renames
327 327 elif repo.dirstate[abs] == 'r':
328 328 removed.append(abs)
329 329 elif repo.dirstate[abs] == 'a':
330 330 added.append(abs)
331 331 if not dry_run:
332 332 repo.remove(deleted)
333 333 repo.add(unknown)
334 334 if similarity > 0:
335 335 for old, new, score in findrenames(repo, added + unknown,
336 336 removed + deleted, similarity):
337 337 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
338 338 repo.ui.status(_('recording removal of %s as rename to %s '
339 339 '(%d%% similar)\n') %
340 340 (m.rel(old), m.rel(new), score * 100))
341 341 if not dry_run:
342 342 repo.copy(old, new)
343 343
344 344 def copy(ui, repo, pats, opts, rename=False):
345 345 # called with the repo lock held
346 346 #
347 347 # hgsep => pathname that uses "/" to separate directories
348 348 # ossep => pathname that uses os.sep to separate directories
349 349 cwd = repo.getcwd()
350 350 targets = {}
351 351 after = opts.get("after")
352 352 dryrun = opts.get("dry_run")
353 353
354 354 def walkpat(pat):
355 355 srcs = []
356 356 m = match(repo, [pat], opts, globbed=True)
357 357 for abs in repo.walk(m):
358 358 state = repo.dirstate[abs]
359 359 rel = m.rel(abs)
360 360 exact = m.exact(abs)
361 361 if state in '?r':
362 362 if exact and state == '?':
363 363 ui.warn(_('%s: not copying - file is not managed\n') % rel)
364 364 if exact and state == 'r':
365 365 ui.warn(_('%s: not copying - file has been marked for'
366 366 ' remove\n') % rel)
367 367 continue
368 368 # abs: hgsep
369 369 # rel: ossep
370 370 srcs.append((abs, rel, exact))
371 371 return srcs
372 372
373 373 # abssrc: hgsep
374 374 # relsrc: ossep
375 375 # otarget: ossep
376 376 def copyfile(abssrc, relsrc, otarget, exact):
377 377 abstarget = util.canonpath(repo.root, cwd, otarget)
378 378 reltarget = repo.pathto(abstarget, cwd)
379 379 target = repo.wjoin(abstarget)
380 380 src = repo.wjoin(abssrc)
381 381 state = repo.dirstate[abstarget]
382 382
383 383 # check for collisions
384 384 prevsrc = targets.get(abstarget)
385 385 if prevsrc is not None:
386 386 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
387 387 (reltarget, repo.pathto(abssrc, cwd),
388 388 repo.pathto(prevsrc, cwd)))
389 389 return
390 390
391 391 # check for overwrites
392 392 exists = os.path.exists(target)
393 393 if not after and exists or after and state in 'mn':
394 394 if not opts['force']:
395 395 ui.warn(_('%s: not overwriting - file exists\n') %
396 396 reltarget)
397 397 return
398 398
399 399 if after:
400 400 if not exists:
401 401 return
402 402 elif not dryrun:
403 403 try:
404 404 if exists:
405 405 os.unlink(target)
406 406 targetdir = os.path.dirname(target) or '.'
407 407 if not os.path.isdir(targetdir):
408 408 os.makedirs(targetdir)
409 409 util.copyfile(src, target)
410 410 except IOError, inst:
411 411 if inst.errno == errno.ENOENT:
412 412 ui.warn(_('%s: deleted in working copy\n') % relsrc)
413 413 else:
414 414 ui.warn(_('%s: cannot copy - %s\n') %
415 415 (relsrc, inst.strerror))
416 416 return True # report a failure
417 417
418 418 if ui.verbose or not exact:
419 419 if rename:
420 420 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
421 421 else:
422 422 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
423 423
424 424 targets[abstarget] = abssrc
425 425
426 426 # fix up dirstate
427 427 origsrc = repo.dirstate.copied(abssrc) or abssrc
428 428 if abstarget == origsrc: # copying back a copy?
429 429 if state not in 'mn' and not dryrun:
430 430 repo.dirstate.normallookup(abstarget)
431 431 else:
432 432 if repo.dirstate[origsrc] == 'a' and origsrc == abssrc:
433 433 if not ui.quiet:
434 434 ui.warn(_("%s has not been committed yet, so no copy "
435 435 "data will be stored for %s.\n")
436 436 % (repo.pathto(origsrc, cwd), reltarget))
437 437 if repo.dirstate[abstarget] in '?r' and not dryrun:
438 438 repo.add([abstarget])
439 439 elif not dryrun:
440 440 repo.copy(origsrc, abstarget)
441 441
442 442 if rename and not dryrun:
443 443 repo.remove([abssrc], not after)
444 444
445 445 # pat: ossep
446 446 # dest ossep
447 447 # srcs: list of (hgsep, hgsep, ossep, bool)
448 448 # return: function that takes hgsep and returns ossep
449 449 def targetpathfn(pat, dest, srcs):
450 450 if os.path.isdir(pat):
451 451 abspfx = util.canonpath(repo.root, cwd, pat)
452 452 abspfx = util.localpath(abspfx)
453 453 if destdirexists:
454 454 striplen = len(os.path.split(abspfx)[0])
455 455 else:
456 456 striplen = len(abspfx)
457 457 if striplen:
458 458 striplen += len(os.sep)
459 459 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
460 460 elif destdirexists:
461 461 res = lambda p: os.path.join(dest,
462 462 os.path.basename(util.localpath(p)))
463 463 else:
464 464 res = lambda p: dest
465 465 return res
466 466
467 467 # pat: ossep
468 468 # dest ossep
469 469 # srcs: list of (hgsep, hgsep, ossep, bool)
470 470 # return: function that takes hgsep and returns ossep
471 471 def targetpathafterfn(pat, dest, srcs):
472 472 if _match.patkind(pat):
473 473 # a mercurial pattern
474 474 res = lambda p: os.path.join(dest,
475 475 os.path.basename(util.localpath(p)))
476 476 else:
477 477 abspfx = util.canonpath(repo.root, cwd, pat)
478 478 if len(abspfx) < len(srcs[0][0]):
479 479 # A directory. Either the target path contains the last
480 480 # component of the source path or it does not.
481 481 def evalpath(striplen):
482 482 score = 0
483 483 for s in srcs:
484 484 t = os.path.join(dest, util.localpath(s[0])[striplen:])
485 485 if os.path.exists(t):
486 486 score += 1
487 487 return score
488 488
489 489 abspfx = util.localpath(abspfx)
490 490 striplen = len(abspfx)
491 491 if striplen:
492 492 striplen += len(os.sep)
493 493 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
494 494 score = evalpath(striplen)
495 495 striplen1 = len(os.path.split(abspfx)[0])
496 496 if striplen1:
497 497 striplen1 += len(os.sep)
498 498 if evalpath(striplen1) > score:
499 499 striplen = striplen1
500 500 res = lambda p: os.path.join(dest,
501 501 util.localpath(p)[striplen:])
502 502 else:
503 503 # a file
504 504 if destdirexists:
505 505 res = lambda p: os.path.join(dest,
506 506 os.path.basename(util.localpath(p)))
507 507 else:
508 508 res = lambda p: dest
509 509 return res
510 510
511 511
512 512 pats = expandpats(pats)
513 513 if not pats:
514 514 raise util.Abort(_('no source or destination specified'))
515 515 if len(pats) == 1:
516 516 raise util.Abort(_('no destination specified'))
517 517 dest = pats.pop()
518 518 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
519 519 if not destdirexists:
520 520 if len(pats) > 1 or _match.patkind(pats[0]):
521 521 raise util.Abort(_('with multiple sources, destination must be an '
522 522 'existing directory'))
523 523 if util.endswithsep(dest):
524 524 raise util.Abort(_('destination %s is not a directory') % dest)
525 525
526 526 tfn = targetpathfn
527 527 if after:
528 528 tfn = targetpathafterfn
529 529 copylist = []
530 530 for pat in pats:
531 531 srcs = walkpat(pat)
532 532 if not srcs:
533 533 continue
534 534 copylist.append((tfn(pat, dest, srcs), srcs))
535 535 if not copylist:
536 536 raise util.Abort(_('no files to copy'))
537 537
538 538 errors = 0
539 539 for targetpath, srcs in copylist:
540 540 for abssrc, relsrc, exact in srcs:
541 541 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
542 542 errors += 1
543 543
544 544 if errors:
545 545 ui.warn(_('(consider using --after)\n'))
546 546
547 547 return errors
548 548
549 549 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
550 550 runargs=None):
551 551 '''Run a command as a service.'''
552 552
553 553 if opts['daemon'] and not opts['daemon_pipefds']:
554 554 rfd, wfd = os.pipe()
555 555 if not runargs:
556 556 runargs = sys.argv[:]
557 557 runargs.append('--daemon-pipefds=%d,%d' % (rfd, wfd))
558 558 # Don't pass --cwd to the child process, because we've already
559 559 # changed directory.
560 560 for i in xrange(1,len(runargs)):
561 561 if runargs[i].startswith('--cwd='):
562 562 del runargs[i]
563 563 break
564 564 elif runargs[i].startswith('--cwd'):
565 565 del runargs[i:i+2]
566 566 break
567 567 pid = os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
568 568 runargs[0], runargs)
569 569 os.close(wfd)
570 570 os.read(rfd, 1)
571 571 if parentfn:
572 572 return parentfn(pid)
573 573 else:
574 os._exit(0)
574 return
575 575
576 576 if initfn:
577 577 initfn()
578 578
579 579 if opts['pid_file']:
580 580 fp = open(opts['pid_file'], 'w')
581 581 fp.write(str(os.getpid()) + '\n')
582 582 fp.close()
583 583
584 584 if opts['daemon_pipefds']:
585 585 rfd, wfd = [int(x) for x in opts['daemon_pipefds'].split(',')]
586 586 os.close(rfd)
587 587 try:
588 588 os.setsid()
589 589 except AttributeError:
590 590 pass
591 591 os.write(wfd, 'y')
592 592 os.close(wfd)
593 593 sys.stdout.flush()
594 594 sys.stderr.flush()
595 595
596 596 nullfd = os.open(util.nulldev, os.O_RDWR)
597 597 logfilefd = nullfd
598 598 if logfile:
599 599 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
600 600 os.dup2(nullfd, 0)
601 601 os.dup2(logfilefd, 1)
602 602 os.dup2(logfilefd, 2)
603 603 if nullfd not in (0, 1, 2):
604 604 os.close(nullfd)
605 605 if logfile and logfilefd not in (0, 1, 2):
606 606 os.close(logfilefd)
607 607
608 608 if runfn:
609 609 return runfn()
610 610
611 611 class changeset_printer(object):
612 612 '''show changeset information when templating not requested.'''
613 613
614 614 def __init__(self, ui, repo, patch, diffopts, buffered):
615 615 self.ui = ui
616 616 self.repo = repo
617 617 self.buffered = buffered
618 618 self.patch = patch
619 619 self.diffopts = diffopts
620 620 self.header = {}
621 621 self.hunk = {}
622 622 self.lastheader = None
623 623
624 624 def flush(self, rev):
625 625 if rev in self.header:
626 626 h = self.header[rev]
627 627 if h != self.lastheader:
628 628 self.lastheader = h
629 629 self.ui.write(h)
630 630 del self.header[rev]
631 631 if rev in self.hunk:
632 632 self.ui.write(self.hunk[rev])
633 633 del self.hunk[rev]
634 634 return 1
635 635 return 0
636 636
637 637 def show(self, ctx, copies=(), **props):
638 638 if self.buffered:
639 639 self.ui.pushbuffer()
640 640 self._show(ctx, copies, props)
641 641 self.hunk[ctx.rev()] = self.ui.popbuffer()
642 642 else:
643 643 self._show(ctx, copies, props)
644 644
645 645 def _show(self, ctx, copies, props):
646 646 '''show a single changeset or file revision'''
647 647 changenode = ctx.node()
648 648 rev = ctx.rev()
649 649
650 650 if self.ui.quiet:
651 651 self.ui.write("%d:%s\n" % (rev, short(changenode)))
652 652 return
653 653
654 654 log = self.repo.changelog
655 655 date = util.datestr(ctx.date())
656 656
657 657 hexfunc = self.ui.debugflag and hex or short
658 658
659 659 parents = [(p, hexfunc(log.node(p)))
660 660 for p in self._meaningful_parentrevs(log, rev)]
661 661
662 662 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)))
663 663
664 664 branch = ctx.branch()
665 665 # don't show the default branch name
666 666 if branch != 'default':
667 667 branch = encoding.tolocal(branch)
668 668 self.ui.write(_("branch: %s\n") % branch)
669 669 for tag in self.repo.nodetags(changenode):
670 670 self.ui.write(_("tag: %s\n") % tag)
671 671 for parent in parents:
672 672 self.ui.write(_("parent: %d:%s\n") % parent)
673 673
674 674 if self.ui.debugflag:
675 675 mnode = ctx.manifestnode()
676 676 self.ui.write(_("manifest: %d:%s\n") %
677 677 (self.repo.manifest.rev(mnode), hex(mnode)))
678 678 self.ui.write(_("user: %s\n") % ctx.user())
679 679 self.ui.write(_("date: %s\n") % date)
680 680
681 681 if self.ui.debugflag:
682 682 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
683 683 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
684 684 files):
685 685 if value:
686 686 self.ui.write("%-12s %s\n" % (key, " ".join(value)))
687 687 elif ctx.files() and self.ui.verbose:
688 688 self.ui.write(_("files: %s\n") % " ".join(ctx.files()))
689 689 if copies and self.ui.verbose:
690 690 copies = ['%s (%s)' % c for c in copies]
691 691 self.ui.write(_("copies: %s\n") % ' '.join(copies))
692 692
693 693 extra = ctx.extra()
694 694 if extra and self.ui.debugflag:
695 695 for key, value in sorted(extra.items()):
696 696 self.ui.write(_("extra: %s=%s\n")
697 697 % (key, value.encode('string_escape')))
698 698
699 699 description = ctx.description().strip()
700 700 if description:
701 701 if self.ui.verbose:
702 702 self.ui.write(_("description:\n"))
703 703 self.ui.write(description)
704 704 self.ui.write("\n\n")
705 705 else:
706 706 self.ui.write(_("summary: %s\n") %
707 707 description.splitlines()[0])
708 708 self.ui.write("\n")
709 709
710 710 self.showpatch(changenode)
711 711
712 712 def showpatch(self, node):
713 713 if self.patch:
714 714 prev = self.repo.changelog.parents(node)[0]
715 715 chunks = patch.diff(self.repo, prev, node, match=self.patch,
716 716 opts=patch.diffopts(self.ui, self.diffopts))
717 717 for chunk in chunks:
718 718 self.ui.write(chunk)
719 719 self.ui.write("\n")
720 720
721 721 def _meaningful_parentrevs(self, log, rev):
722 722 """Return list of meaningful (or all if debug) parentrevs for rev.
723 723
724 724 For merges (two non-nullrev revisions) both parents are meaningful.
725 725 Otherwise the first parent revision is considered meaningful if it
726 726 is not the preceding revision.
727 727 """
728 728 parents = log.parentrevs(rev)
729 729 if not self.ui.debugflag and parents[1] == nullrev:
730 730 if parents[0] >= rev - 1:
731 731 parents = []
732 732 else:
733 733 parents = [parents[0]]
734 734 return parents
735 735
736 736
737 737 class changeset_templater(changeset_printer):
738 738 '''format changeset information.'''
739 739
740 740 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
741 741 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
742 742 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
743 743 self.t = templater.templater(mapfile, {'formatnode': formatnode},
744 744 cache={
745 745 'parent': '{rev}:{node|formatnode} ',
746 746 'manifest': '{rev}:{node|formatnode}',
747 747 'filecopy': '{name} ({source})'})
748 748 # Cache mapping from rev to a tuple with tag date, tag
749 749 # distance and tag name
750 750 self._latesttagcache = {-1: (0, 0, 'null')}
751 751
752 752 def use_template(self, t):
753 753 '''set template string to use'''
754 754 self.t.cache['changeset'] = t
755 755
756 756 def _meaningful_parentrevs(self, ctx):
757 757 """Return list of meaningful (or all if debug) parentrevs for rev.
758 758 """
759 759 parents = ctx.parents()
760 760 if len(parents) > 1:
761 761 return parents
762 762 if self.ui.debugflag:
763 763 return [parents[0], self.repo['null']]
764 764 if parents[0].rev() >= ctx.rev() - 1:
765 765 return []
766 766 return parents
767 767
768 768 def _latesttaginfo(self, rev):
769 769 '''return date, distance and name for the latest tag of rev'''
770 770 todo = [rev]
771 771 while todo:
772 772 rev = todo.pop()
773 773 if rev in self._latesttagcache:
774 774 continue
775 775 ctx = self.repo[rev]
776 776 tags = [t for t in ctx.tags() if self.repo.tagtype(t) == 'global']
777 777 if tags:
778 778 self._latesttagcache[rev] = ctx.date()[0], 0, ':'.join(sorted(tags))
779 779 continue
780 780 try:
781 781 # The tuples are laid out so the right one can be found by comparison.
782 782 pdate, pdist, ptag = max(
783 783 self._latesttagcache[p.rev()] for p in ctx.parents())
784 784 except KeyError:
785 785 # Cache miss - recurse
786 786 todo.append(rev)
787 787 todo.extend(p.rev() for p in ctx.parents())
788 788 continue
789 789 self._latesttagcache[rev] = pdate, pdist + 1, ptag
790 790 return self._latesttagcache[rev]
791 791
792 792 def _show(self, ctx, copies, props):
793 793 '''show a single changeset or file revision'''
794 794
795 795 def showlist(name, values, plural=None, **args):
796 796 '''expand set of values.
797 797 name is name of key in template map.
798 798 values is list of strings or dicts.
799 799 plural is plural of name, if not simply name + 's'.
800 800
801 801 expansion works like this, given name 'foo'.
802 802
803 803 if values is empty, expand 'no_foos'.
804 804
805 805 if 'foo' not in template map, return values as a string,
806 806 joined by space.
807 807
808 808 expand 'start_foos'.
809 809
810 810 for each value, expand 'foo'. if 'last_foo' in template
811 811 map, expand it instead of 'foo' for last key.
812 812
813 813 expand 'end_foos'.
814 814 '''
815 815 if plural: names = plural
816 816 else: names = name + 's'
817 817 if not values:
818 818 noname = 'no_' + names
819 819 if noname in self.t:
820 820 yield self.t(noname, **args)
821 821 return
822 822 if name not in self.t:
823 823 if isinstance(values[0], str):
824 824 yield ' '.join(values)
825 825 else:
826 826 for v in values:
827 827 yield dict(v, **args)
828 828 return
829 829 startname = 'start_' + names
830 830 if startname in self.t:
831 831 yield self.t(startname, **args)
832 832 vargs = args.copy()
833 833 def one(v, tag=name):
834 834 try:
835 835 vargs.update(v)
836 836 except (AttributeError, ValueError):
837 837 try:
838 838 for a, b in v:
839 839 vargs[a] = b
840 840 except ValueError:
841 841 vargs[name] = v
842 842 return self.t(tag, **vargs)
843 843 lastname = 'last_' + name
844 844 if lastname in self.t:
845 845 last = values.pop()
846 846 else:
847 847 last = None
848 848 for v in values:
849 849 yield one(v)
850 850 if last is not None:
851 851 yield one(last, tag=lastname)
852 852 endname = 'end_' + names
853 853 if endname in self.t:
854 854 yield self.t(endname, **args)
855 855
856 856 def showbranches(**args):
857 857 branch = ctx.branch()
858 858 if branch != 'default':
859 859 branch = encoding.tolocal(branch)
860 860 return showlist('branch', [branch], plural='branches', **args)
861 861
862 862 def showparents(**args):
863 863 parents = [[('rev', p.rev()), ('node', p.hex())]
864 864 for p in self._meaningful_parentrevs(ctx)]
865 865 return showlist('parent', parents, **args)
866 866
867 867 def showtags(**args):
868 868 return showlist('tag', ctx.tags(), **args)
869 869
870 870 def showextras(**args):
871 871 for key, value in sorted(ctx.extra().items()):
872 872 args = args.copy()
873 873 args.update(dict(key=key, value=value))
874 874 yield self.t('extra', **args)
875 875
876 876 def showcopies(**args):
877 877 c = [{'name': x[0], 'source': x[1]} for x in copies]
878 878 return showlist('file_copy', c, plural='file_copies', **args)
879 879
880 880 files = []
881 881 def getfiles():
882 882 if not files:
883 883 files[:] = self.repo.status(ctx.parents()[0].node(),
884 884 ctx.node())[:3]
885 885 return files
886 886 def showfiles(**args):
887 887 return showlist('file', ctx.files(), **args)
888 888 def showmods(**args):
889 889 return showlist('file_mod', getfiles()[0], **args)
890 890 def showadds(**args):
891 891 return showlist('file_add', getfiles()[1], **args)
892 892 def showdels(**args):
893 893 return showlist('file_del', getfiles()[2], **args)
894 894 def showmanifest(**args):
895 895 args = args.copy()
896 896 args.update(dict(rev=self.repo.manifest.rev(ctx.changeset()[0]),
897 897 node=hex(ctx.changeset()[0])))
898 898 return self.t('manifest', **args)
899 899
900 900 def showdiffstat(**args):
901 901 diff = patch.diff(self.repo, ctx.parents()[0].node(), ctx.node())
902 902 files, adds, removes = 0, 0, 0
903 903 for i in patch.diffstatdata(util.iterlines(diff)):
904 904 files += 1
905 905 adds += i[1]
906 906 removes += i[2]
907 907 return '%s: +%s/-%s' % (files, adds, removes)
908 908
909 909 def showlatesttag(**args):
910 910 return self._latesttaginfo(ctx.rev())[2]
911 911 def showlatesttagdistance(**args):
912 912 return self._latesttaginfo(ctx.rev())[1]
913 913
914 914 defprops = {
915 915 'author': ctx.user(),
916 916 'branches': showbranches,
917 917 'date': ctx.date(),
918 918 'desc': ctx.description().strip(),
919 919 'file_adds': showadds,
920 920 'file_dels': showdels,
921 921 'file_mods': showmods,
922 922 'files': showfiles,
923 923 'file_copies': showcopies,
924 924 'manifest': showmanifest,
925 925 'node': ctx.hex(),
926 926 'parents': showparents,
927 927 'rev': ctx.rev(),
928 928 'tags': showtags,
929 929 'extras': showextras,
930 930 'diffstat': showdiffstat,
931 931 'latesttag': showlatesttag,
932 932 'latesttagdistance': showlatesttagdistance,
933 933 }
934 934 props = props.copy()
935 935 props.update(defprops)
936 936
937 937 # find correct templates for current mode
938 938
939 939 tmplmodes = [
940 940 (True, None),
941 941 (self.ui.verbose, 'verbose'),
942 942 (self.ui.quiet, 'quiet'),
943 943 (self.ui.debugflag, 'debug'),
944 944 ]
945 945
946 946 types = {'header': '', 'changeset': 'changeset'}
947 947 for mode, postfix in tmplmodes:
948 948 for type in types:
949 949 cur = postfix and ('%s_%s' % (type, postfix)) or type
950 950 if mode and cur in self.t:
951 951 types[type] = cur
952 952
953 953 try:
954 954
955 955 # write header
956 956 if types['header']:
957 957 h = templater.stringify(self.t(types['header'], **props))
958 958 if self.buffered:
959 959 self.header[ctx.rev()] = h
960 960 else:
961 961 self.ui.write(h)
962 962
963 963 # write changeset metadata, then patch if requested
964 964 key = types['changeset']
965 965 self.ui.write(templater.stringify(self.t(key, **props)))
966 966 self.showpatch(ctx.node())
967 967
968 968 except KeyError, inst:
969 969 msg = _("%s: no key named '%s'")
970 970 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
971 971 except SyntaxError, inst:
972 972 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
973 973
974 974 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
975 975 """show one changeset using template or regular display.
976 976
977 977 Display format will be the first non-empty hit of:
978 978 1. option 'template'
979 979 2. option 'style'
980 980 3. [ui] setting 'logtemplate'
981 981 4. [ui] setting 'style'
982 982 If all of these values are either the unset or the empty string,
983 983 regular display via changeset_printer() is done.
984 984 """
985 985 # options
986 986 patch = False
987 987 if opts.get('patch'):
988 988 patch = matchfn or matchall(repo)
989 989
990 990 tmpl = opts.get('template')
991 991 style = None
992 992 if tmpl:
993 993 tmpl = templater.parsestring(tmpl, quoted=False)
994 994 else:
995 995 style = opts.get('style')
996 996
997 997 # ui settings
998 998 if not (tmpl or style):
999 999 tmpl = ui.config('ui', 'logtemplate')
1000 1000 if tmpl:
1001 1001 tmpl = templater.parsestring(tmpl)
1002 1002 else:
1003 1003 style = ui.config('ui', 'style')
1004 1004
1005 1005 if not (tmpl or style):
1006 1006 return changeset_printer(ui, repo, patch, opts, buffered)
1007 1007
1008 1008 mapfile = None
1009 1009 if style and not tmpl:
1010 1010 mapfile = style
1011 1011 if not os.path.split(mapfile)[0]:
1012 1012 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1013 1013 or templater.templatepath(mapfile))
1014 1014 if mapname: mapfile = mapname
1015 1015
1016 1016 try:
1017 1017 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
1018 1018 except SyntaxError, inst:
1019 1019 raise util.Abort(inst.args[0])
1020 1020 if tmpl: t.use_template(tmpl)
1021 1021 return t
1022 1022
1023 1023 def finddate(ui, repo, date):
1024 1024 """Find the tipmost changeset that matches the given date spec"""
1025 1025
1026 1026 df = util.matchdate(date)
1027 1027 m = matchall(repo)
1028 1028 results = {}
1029 1029
1030 1030 def prep(ctx, fns):
1031 1031 d = ctx.date()
1032 1032 if df(d[0]):
1033 1033 results[ctx.rev()] = d
1034 1034
1035 1035 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1036 1036 rev = ctx.rev()
1037 1037 if rev in results:
1038 1038 ui.status(_("Found revision %s from %s\n") %
1039 1039 (rev, util.datestr(results[rev])))
1040 1040 return str(rev)
1041 1041
1042 1042 raise util.Abort(_("revision matching date not found"))
1043 1043
1044 1044 def walkchangerevs(repo, match, opts, prepare):
1045 1045 '''Iterate over files and the revs in which they changed.
1046 1046
1047 1047 Callers most commonly need to iterate backwards over the history
1048 1048 in which they are interested. Doing so has awful (quadratic-looking)
1049 1049 performance, so we use iterators in a "windowed" way.
1050 1050
1051 1051 We walk a window of revisions in the desired order. Within the
1052 1052 window, we first walk forwards to gather data, then in the desired
1053 1053 order (usually backwards) to display it.
1054 1054
1055 1055 This function returns an iterator yielding contexts. Before
1056 1056 yielding each context, the iterator will first call the prepare
1057 1057 function on each context in the window in forward order.'''
1058 1058
1059 1059 def increasing_windows(start, end, windowsize=8, sizelimit=512):
1060 1060 if start < end:
1061 1061 while start < end:
1062 1062 yield start, min(windowsize, end-start)
1063 1063 start += windowsize
1064 1064 if windowsize < sizelimit:
1065 1065 windowsize *= 2
1066 1066 else:
1067 1067 while start > end:
1068 1068 yield start, min(windowsize, start-end-1)
1069 1069 start -= windowsize
1070 1070 if windowsize < sizelimit:
1071 1071 windowsize *= 2
1072 1072
1073 1073 follow = opts.get('follow') or opts.get('follow_first')
1074 1074
1075 1075 if not len(repo):
1076 1076 return []
1077 1077
1078 1078 if follow:
1079 1079 defrange = '%s:0' % repo['.'].rev()
1080 1080 else:
1081 1081 defrange = '-1:0'
1082 1082 revs = revrange(repo, opts['rev'] or [defrange])
1083 1083 wanted = set()
1084 1084 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1085 1085 fncache = {}
1086 1086 change = util.cachefunc(repo.changectx)
1087 1087
1088 1088 if not slowpath and not match.files():
1089 1089 # No files, no patterns. Display all revs.
1090 1090 wanted = set(revs)
1091 1091 copies = []
1092 1092
1093 1093 if not slowpath:
1094 1094 # Only files, no patterns. Check the history of each file.
1095 1095 def filerevgen(filelog, node):
1096 1096 cl_count = len(repo)
1097 1097 if node is None:
1098 1098 last = len(filelog) - 1
1099 1099 else:
1100 1100 last = filelog.rev(node)
1101 1101 for i, window in increasing_windows(last, nullrev):
1102 1102 revs = []
1103 1103 for j in xrange(i - window, i + 1):
1104 1104 n = filelog.node(j)
1105 1105 revs.append((filelog.linkrev(j),
1106 1106 follow and filelog.renamed(n)))
1107 1107 for rev in reversed(revs):
1108 1108 # only yield rev for which we have the changelog, it can
1109 1109 # happen while doing "hg log" during a pull or commit
1110 1110 if rev[0] < cl_count:
1111 1111 yield rev
1112 1112 def iterfiles():
1113 1113 for filename in match.files():
1114 1114 yield filename, None
1115 1115 for filename_node in copies:
1116 1116 yield filename_node
1117 1117 minrev, maxrev = min(revs), max(revs)
1118 1118 for file_, node in iterfiles():
1119 1119 filelog = repo.file(file_)
1120 1120 if not len(filelog):
1121 1121 if node is None:
1122 1122 # A zero count may be a directory or deleted file, so
1123 1123 # try to find matching entries on the slow path.
1124 1124 if follow:
1125 1125 raise util.Abort(_('cannot follow nonexistent file: "%s"') % file_)
1126 1126 slowpath = True
1127 1127 break
1128 1128 else:
1129 1129 continue
1130 1130 for rev, copied in filerevgen(filelog, node):
1131 1131 if rev <= maxrev:
1132 1132 if rev < minrev:
1133 1133 break
1134 1134 fncache.setdefault(rev, [])
1135 1135 fncache[rev].append(file_)
1136 1136 wanted.add(rev)
1137 1137 if follow and copied:
1138 1138 copies.append(copied)
1139 1139 if slowpath:
1140 1140 if follow:
1141 1141 raise util.Abort(_('can only follow copies/renames for explicit '
1142 1142 'filenames'))
1143 1143
1144 1144 # The slow path checks files modified in every changeset.
1145 1145 def changerevgen():
1146 1146 for i, window in increasing_windows(len(repo) - 1, nullrev):
1147 1147 for j in xrange(i - window, i + 1):
1148 1148 yield change(j)
1149 1149
1150 1150 for ctx in changerevgen():
1151 1151 matches = filter(match, ctx.files())
1152 1152 if matches:
1153 1153 fncache[ctx.rev()] = matches
1154 1154 wanted.add(ctx.rev())
1155 1155
1156 1156 class followfilter(object):
1157 1157 def __init__(self, onlyfirst=False):
1158 1158 self.startrev = nullrev
1159 1159 self.roots = []
1160 1160 self.onlyfirst = onlyfirst
1161 1161
1162 1162 def match(self, rev):
1163 1163 def realparents(rev):
1164 1164 if self.onlyfirst:
1165 1165 return repo.changelog.parentrevs(rev)[0:1]
1166 1166 else:
1167 1167 return filter(lambda x: x != nullrev,
1168 1168 repo.changelog.parentrevs(rev))
1169 1169
1170 1170 if self.startrev == nullrev:
1171 1171 self.startrev = rev
1172 1172 return True
1173 1173
1174 1174 if rev > self.startrev:
1175 1175 # forward: all descendants
1176 1176 if not self.roots:
1177 1177 self.roots.append(self.startrev)
1178 1178 for parent in realparents(rev):
1179 1179 if parent in self.roots:
1180 1180 self.roots.append(rev)
1181 1181 return True
1182 1182 else:
1183 1183 # backwards: all parents
1184 1184 if not self.roots:
1185 1185 self.roots.extend(realparents(self.startrev))
1186 1186 if rev in self.roots:
1187 1187 self.roots.remove(rev)
1188 1188 self.roots.extend(realparents(rev))
1189 1189 return True
1190 1190
1191 1191 return False
1192 1192
1193 1193 # it might be worthwhile to do this in the iterator if the rev range
1194 1194 # is descending and the prune args are all within that range
1195 1195 for rev in opts.get('prune', ()):
1196 1196 rev = repo.changelog.rev(repo.lookup(rev))
1197 1197 ff = followfilter()
1198 1198 stop = min(revs[0], revs[-1])
1199 1199 for x in xrange(rev, stop-1, -1):
1200 1200 if ff.match(x):
1201 1201 wanted.discard(x)
1202 1202
1203 1203 def iterate():
1204 1204 if follow and not match.files():
1205 1205 ff = followfilter(onlyfirst=opts.get('follow_first'))
1206 1206 def want(rev):
1207 1207 return ff.match(rev) and rev in wanted
1208 1208 else:
1209 1209 def want(rev):
1210 1210 return rev in wanted
1211 1211
1212 1212 for i, window in increasing_windows(0, len(revs)):
1213 1213 change = util.cachefunc(repo.changectx)
1214 1214 nrevs = [rev for rev in revs[i:i+window] if want(rev)]
1215 1215 for rev in sorted(nrevs):
1216 1216 fns = fncache.get(rev)
1217 1217 ctx = change(rev)
1218 1218 if not fns:
1219 1219 def fns_generator():
1220 1220 for f in ctx.files():
1221 1221 if match(f):
1222 1222 yield f
1223 1223 fns = fns_generator()
1224 1224 prepare(ctx, fns)
1225 1225 for rev in nrevs:
1226 1226 yield change(rev)
1227 1227 return iterate()
1228 1228
1229 1229 def commit(ui, repo, commitfunc, pats, opts):
1230 1230 '''commit the specified files or all outstanding changes'''
1231 1231 date = opts.get('date')
1232 1232 if date:
1233 1233 opts['date'] = util.parsedate(date)
1234 1234 message = logmessage(opts)
1235 1235
1236 1236 # extract addremove carefully -- this function can be called from a command
1237 1237 # that doesn't support addremove
1238 1238 if opts.get('addremove'):
1239 1239 addremove(repo, pats, opts)
1240 1240
1241 1241 return commitfunc(ui, repo, message, match(repo, pats, opts), opts)
1242 1242
1243 1243 def commiteditor(repo, ctx, subs):
1244 1244 if ctx.description():
1245 1245 return ctx.description()
1246 1246 return commitforceeditor(repo, ctx, subs)
1247 1247
1248 1248 def commitforceeditor(repo, ctx, subs):
1249 1249 edittext = []
1250 1250 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1251 1251 if ctx.description():
1252 1252 edittext.append(ctx.description())
1253 1253 edittext.append("")
1254 1254 edittext.append("") # Empty line between message and comments.
1255 1255 edittext.append(_("HG: Enter commit message."
1256 1256 " Lines beginning with 'HG:' are removed."))
1257 1257 edittext.append(_("HG: Leave message empty to abort commit."))
1258 1258 edittext.append("HG: --")
1259 1259 edittext.append(_("HG: user: %s") % ctx.user())
1260 1260 if ctx.p2():
1261 1261 edittext.append(_("HG: branch merge"))
1262 1262 if ctx.branch():
1263 1263 edittext.append(_("HG: branch '%s'")
1264 1264 % encoding.tolocal(ctx.branch()))
1265 1265 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1266 1266 edittext.extend([_("HG: added %s") % f for f in added])
1267 1267 edittext.extend([_("HG: changed %s") % f for f in modified])
1268 1268 edittext.extend([_("HG: removed %s") % f for f in removed])
1269 1269 if not added and not modified and not removed:
1270 1270 edittext.append(_("HG: no files changed"))
1271 1271 edittext.append("")
1272 1272 # run editor in the repository root
1273 1273 olddir = os.getcwd()
1274 1274 os.chdir(repo.root)
1275 1275 text = repo.ui.edit("\n".join(edittext), ctx.user())
1276 1276 text = re.sub("(?m)^HG:.*\n", "", text)
1277 1277 os.chdir(olddir)
1278 1278
1279 1279 if not text.strip():
1280 1280 raise util.Abort(_("empty commit message"))
1281 1281
1282 1282 return text
@@ -1,83 +1,96
1 1 #!/bin/sh
2 2
3 3 "$TESTDIR/hghave" inotify || exit 80
4 4
5 hg init
5 hg init repo1
6 cd repo1
6 7
7 8 touch a b c d e
8 9 mkdir dir
9 10 mkdir dir/bar
10 11 touch dir/x dir/y dir/bar/foo
11 12
12 13 hg ci -Am m
14 cd ..
15 hg clone repo1 repo2
13 16
14 17 echo "[extensions]" >> $HGRCPATH
15 18 echo "inotify=" >> $HGRCPATH
16 19
20 cd repo2
21 echo b >> a
22 # check that daemon started automatically works correctly
23 # and make sure that inotify.pidfile works
24 hg --config "inotify.pidfile=../hg2.pid" status
25
26 # make sure that pidfile worked. Output should be silent.
27 kill `cat ../hg2.pid`
28
29 cd ../repo1
17 30 echo % inserve
18 31 hg inserve -d --pid-file=hg.pid
19 32 cat hg.pid >> "$DAEMON_PIDS"
20 33
21 34 # let the daemon finish its stuff
22 35 sleep 1
23 36 # issue907
24 37 hg status
25 38 echo % clean
26 39 hg status -c
27 40 echo % all
28 41 hg status -A
29 42
30 43 echo '% path patterns'
31 44 echo x > dir/x
32 45 hg status .
33 46 hg status dir
34 47 cd dir
35 48 hg status .
36 49 cd ..
37 50
38 51 #issue 1375
39 52 #Testing that we can remove a folder and then add a file with the same name
40 53 echo % issue 1375
41 54
42 55 mkdir h
43 56 echo h > h/h
44 57 hg ci -Am t
45 58 hg rm h
46 59
47 60 echo h >h
48 61 hg add h
49 62
50 63 hg status
51 64 hg ci -m0
52 65
53 66 # Test for issue1735: inotify watches files in .hg/merge
54 67 hg st
55 68
56 69 echo a > a
57 70
58 71 hg ci -Am a
59 72 hg st
60 73
61 74 echo b >> a
62 75 hg ci -m ab
63 76 hg st
64 77
65 78 echo c >> a
66 79 hg st
67 80
68 81 hg up 0
69 82 hg st
70 83
71 84 HGMERGE=internal:local hg up
72 85 hg st
73 86
74 87 # Test for 1844: "hg ci folder" will not commit all changes beneath "folder"
75 88 mkdir 1844
76 89 echo a > 1844/foo
77 90 hg add 1844
78 91 hg ci -m 'working'
79 92
80 93 echo b >> 1844/foo
81 94 hg ci 1844 -m 'broken'
82 95
83 96 kill `cat hg.pid`
@@ -1,71 +1,71
1 1 #!/bin/sh
2 2
3 3 # issues when status queries are issued when dirstate is dirty
4 4
5 5 "$TESTDIR/hghave" inotify || exit 80
6 6
7 7 echo "[extensions]" >> $HGRCPATH
8 8 echo "inotify=" >> $HGRCPATH
9 9 echo "fetch=" >> $HGRCPATH
10 10
11 11 echo % issue1810: inotify and fetch
12 12 mkdir test; cd test
13 13 hg init
14 14 hg inserve -d --pid-file=../hg.pid
15 15 cat ../hg.pid >> "$DAEMON_PIDS"
16 16
17 17 echo foo > foo
18 18 hg add
19 19 hg ci -m foo
20 20
21 21 cd ..
22 22
23 hg --config "extensions.inotify=!" clone test test2
23 hg --config "inotify.pidfile=../hg2.pid" clone test test2
24 cat ../hg2.pid >> "$DAEMON_PIDS"
25
24 26 cd test2
25 hg inserve -d --pid-file=../hg2.pid
26 cat ../hg2.pid >> "$DAEMON_PIDS"
27 27 echo bar > bar
28 28 hg add
29 29 hg ci -m bar
30 30 cd ../test
31 31 echo spam > spam
32 32 hg add
33 33 hg ci -m spam
34 34 cd ../test2
35 35 hg st
36 36
37 37 # abort, outstanding changes
38 38 hg fetch -q
39 39 hg st
40 40 cd ..
41 41
42 42
43 43 echo % issue1719: inotify and mq
44 44
45 45 echo "mq=" >> $HGRCPATH
46 46
47 47 hg init test-1719
48 48 cd test-1719
49 49
50 50 echo % inserve
51 51 hg inserve -d --pid-file=../hg-test-1719.pid
52 52 cat ../hg-test-1719.pid >> "$DAEMON_PIDS"
53 53
54 54 echo content > file
55 55 hg add file
56 56
57 57 hg qnew -f test.patch
58 58
59 59 hg status
60 60 hg qpop
61 61
62 62 echo % st should not output anything
63 63 hg status
64 64
65 65 hg qpush
66 66
67 67 echo % st should not output anything
68 68 hg status
69 69
70 70 hg qrefresh
71 71 hg status
@@ -1,6 +1,7
1 1 % fail
2 2 abort: could not start server: File exists
3 could not talk to new inotify server: No such file or directory
3 4 abort: could not start server: File exists
4 5 % inserve
5 6 % status
6 7 ? hg.pid
@@ -1,47 +1,50
1 1 adding a
2 2 adding b
3 3 adding c
4 4 adding d
5 5 adding dir/bar/foo
6 6 adding dir/x
7 7 adding dir/y
8 8 adding e
9 updating to branch default
10 8 files updated, 0 files merged, 0 files removed, 0 files unresolved
11 M a
9 12 % inserve
10 13 ? hg.pid
11 14 % clean
12 15 C a
13 16 C b
14 17 C c
15 18 C d
16 19 C dir/bar/foo
17 20 C dir/x
18 21 C dir/y
19 22 C e
20 23 % all
21 24 ? hg.pid
22 25 C a
23 26 C b
24 27 C c
25 28 C d
26 29 C dir/bar/foo
27 30 C dir/x
28 31 C dir/y
29 32 C e
30 33 % path patterns
31 34 M dir/x
32 35 ? hg.pid
33 36 M dir/x
34 37 M x
35 38 % issue 1375
36 39 adding h/h
37 40 adding hg.pid
38 41 removing h/h
39 42 A h
40 43 R h/h
41 44 M a
42 45 merging a
43 46 1 files updated, 1 files merged, 2 files removed, 0 files unresolved
44 47 M a
45 48 3 files updated, 1 files merged, 0 files removed, 0 files unresolved
46 49 M a
47 50 adding 1844/foo
General Comments 0
You need to be logged in to leave comments. Login now