commands.py 125 KB
Newer Older
mpm@selenic.com's avatar
mpm@selenic.com committed
1 2 3 4 5 6 7
# commands.py - command processing for mercurial
#
# Copyright 2005 Matt Mackall <mpm@selenic.com>
#
# This software may be used and distributed according to the terms
# of the GNU General Public License, incorporated herein by reference.

8
from demandload import demandload
9
from node import *
10
from i18n import gettext as _
mpm@selenic.com's avatar
mpm@selenic.com committed
11
demandload(globals(), "os re sys signal shutil imp urllib pdb")
12 13 14
demandload(globals(), "fancyopts ui hg util lock revlog templater bundlerepo")
demandload(globals(), "fnmatch hgweb mdiff random signal tempfile time")
demandload(globals(), "traceback errno socket version struct atexit sets bz2")
15
demandload(globals(), "archival changegroup")
16

17 18
class UnknownCommand(Exception):
    """Exception raised if command is not in the command table."""
19 20
class AmbiguousCommand(Exception):
    """Exception raised if command shortcut matches more than one command."""
21

22
def filterfiles(filters, files):
23
    l = [x for x in files if x in filters]
24

25
    for t in filters:
26 27 28
        if t and t[-1] != "/":
            t += "/"
        l += [x for x in files if x.startswith(t)]
29 30
    return l

31
def relpath(repo, args):
32 33
    cwd = repo.getcwd()
    if cwd:
34
        return [util.normpath(os.path.join(cwd, x)) for x in args]
35
    return args
36

37 38 39 40 41 42
def matchpats(repo, pats=[], opts={}, head=''):
    cwd = repo.getcwd()
    if not pats and cwd:
        opts['include'] = [os.path.join(cwd, i) for i in opts['include']]
        opts['exclude'] = [os.path.join(cwd, x) for x in opts['exclude']]
        cwd = ''
43
    return util.cmdmatcher(repo.root, cwd, pats or ['.'], opts.get('include'),
44
                           opts.get('exclude'), head)
mpm@selenic.com's avatar
mpm@selenic.com committed
45

46
def makewalk(repo, pats, opts, node=None, head='', badmatch=None):
47
    files, matchfn, anypats = matchpats(repo, pats, opts, head)
48
    exact = dict(zip(files, files))
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
49
    def walk():
50
        for src, fn in repo.walk(node=node, files=files, match=matchfn,
51
                                 badmatch=badmatch):
52
            yield src, fn, util.pathto(repo.getcwd(), fn), fn in exact
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
53 54
    return files, matchfn, walk()

55 56
def walk(repo, pats, opts, node=None, head='', badmatch=None):
    files, matchfn, results = makewalk(repo, pats, opts, node, head, badmatch)
Thomas Arendsen Hein's avatar
Thomas Arendsen Hein committed
57 58
    for r in results:
        yield r
59

60
def walkchangerevs(ui, repo, pats, opts):
61 62 63 64 65 66 67 68 69 70
    '''Iterate over files and the revs they changed in.

    Callers most commonly need to iterate backwards over the history
    it is interested in.  Doing so has awful (quadratic-looking)
    performance, so we use iterators in a "windowed" way.

    We walk a window of revisions in the desired order.  Within the
    window, we first walk forwards to gather data, then in the desired
    order (usually backwards) to display it.

71
    This function returns an (iterator, getchange, matchfn) tuple.  The
72 73 74 75 76 77 78 79 80 81 82 83 84 85
    getchange function returns the changelog entry for a numeric
    revision.  The iterator yields 3-tuples.  They will be of one of
    the following forms:

    "window", incrementing, lastrev: stepping through a window,
    positive if walking forwards through revs, last rev in the
    sequence iterated over - use to reset state for the current window

    "add", rev, fns: out-of-order traversal of the given file names
    fns, which changed during revision rev - use to gather data for
    possible display

    "iter", rev, None: in-order traversal of the revs earlier iterated
    over with "add" - use to display data'''
86

87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
    def increasing_windows(start, end, windowsize=8, sizelimit=512):
        if start < end:
            while start < end:
                yield start, min(windowsize, end-start)
                start += windowsize
                if windowsize < sizelimit:
                    windowsize *= 2
        else:
            while start > end:
                yield start, min(windowsize, start-end-1)
                start -= windowsize
                if windowsize < sizelimit:
                    windowsize *= 2


102
    files, matchfn, anypats = matchpats(repo, pats, opts)
103

104
    if repo.changelog.count() == 0:
105
        return [], False, matchfn
106

Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
107 108 109 110
    revs = map(int, revrange(ui, repo, opts['rev'] or ['tip:0']))
    wanted = {}
    slowpath = anypats
    fncache = {}
111 112 113 114 115 116 117 118

    chcache = {}
    def getchange(rev):
        ch = chcache.get(rev)
        if ch is None:
            chcache[rev] = ch = repo.changelog.read(repo.lookup(str(rev)))
        return ch

Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
119 120 121 122 123 124
    if not slowpath and not files:
        # No files, no patterns.  Display all revs.
        wanted = dict(zip(revs, revs))
    if not slowpath:
        # Only files, no patterns.  Check the history of each file.
        def filerevgen(filelog):
125
            for i, window in increasing_windows(filelog.count()-1, -1):
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
126
                revs = []
127
                for j in xrange(i - window, i + 1):
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
128 129 130 131 132 133
                    revs.append(filelog.linkrev(filelog.node(j)))
                revs.reverse()
                for rev in revs:
                    yield rev

        minrev, maxrev = min(revs), max(revs)
134 135
        for file_ in files:
            filelog = repo.file(file_)
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
136 137 138 139 140 141 142
            # A zero count may be a directory or deleted file, so
            # try to find matching entries on the slow path.
            if filelog.count() == 0:
                slowpath = True
                break
            for rev in filerevgen(filelog):
                if rev <= maxrev:
Thomas Arendsen Hein's avatar
Thomas Arendsen Hein committed
143 144
                    if rev < minrev:
                        break
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
145
                    fncache.setdefault(rev, [])
146
                    fncache[rev].append(file_)
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
147 148 149 150
                    wanted[rev] = 1
    if slowpath:
        # The slow path checks files modified in every changeset.
        def changerevgen():
151
            for i, window in increasing_windows(repo.changelog.count()-1, -1):
152
                for j in xrange(i - window, i + 1):
153
                    yield j, getchange(j)[3]
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
154 155 156 157 158 159 160

        for rev, changefiles in changerevgen():
            matches = filter(matchfn, changefiles)
            if matches:
                fncache[rev] = matches
                wanted[rev] = 1

161
    def iterate():
162
        for i, window in increasing_windows(0, len(revs)):
163
            yield 'window', revs[0] < revs[-1], revs[-1]
164
            nrevs = [rev for rev in revs[i:i+window]
165 166 167 168 169 170 171 172
                     if rev in wanted]
            srevs = list(nrevs)
            srevs.sort()
            for rev in srevs:
                fns = fncache.get(rev) or filter(matchfn, getchange(rev)[3])
                yield 'add', rev, fns
            for rev in nrevs:
                yield 'iter', rev, None
173
    return iterate(), getchange, matchfn
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
174

mpm@selenic.com's avatar
mpm@selenic.com committed
175 176
revrangesep = ':'

177
def revrange(ui, repo, revs, revlog=None):
178
    """Yield revision as strings from a list of revision specifications."""
mpm@selenic.com's avatar
mpm@selenic.com committed
179 180 181 182
    if revlog is None:
        revlog = repo.changelog
    revcount = revlog.count()
    def fix(val, defval):
183 184
        if not val:
            return defval
mpm@selenic.com's avatar
mpm@selenic.com committed
185 186
        try:
            num = int(val)
187 188
            if str(num) != val:
                raise ValueError
189 190 191 192
            if num < 0:
                num += revcount
            if num < 0:
                num = 0
193
            elif num >= revcount:
mpm@selenic.com's avatar
mpm@selenic.com committed
194 195 196 197 198 199 200 201
                raise ValueError
        except ValueError:
            try:
                num = repo.changelog.rev(repo.lookup(val))
            except KeyError:
                try:
                    num = revlog.rev(revlog.lookup(val))
                except KeyError:
202
                    raise util.Abort(_('invalid revision identifier %s'), val)
mpm@selenic.com's avatar
mpm@selenic.com committed
203
        return num
204
    seen = {}
mpm@selenic.com's avatar
mpm@selenic.com committed
205 206 207 208 209
    for spec in revs:
        if spec.find(revrangesep) >= 0:
            start, end = spec.split(revrangesep, 1)
            start = fix(start, 0)
            end = fix(end, revcount - 1)
210 211
            step = start > end and -1 or 1
            for rev in xrange(start, end+step, step):
212 213
                if rev in seen:
                    continue
214
                seen[rev] = 1
mpm@selenic.com's avatar
mpm@selenic.com committed
215 216
                yield str(rev)
        else:
217
            rev = fix(spec, None)
218 219
            if rev in seen:
                continue
220 221
            seen[rev] = 1
            yield str(rev)
mpm@selenic.com's avatar
mpm@selenic.com committed
222

223
def make_filename(repo, r, pat, node=None,
224
                  total=None, seqno=None, revwidth=None, pathname=None):
225
    node_expander = {
226
        'H': lambda: hex(node),
227
        'R': lambda: str(r.rev(node)),
228
        'h': lambda: short(node),
229 230 231 232 233 234
        }
    expander = {
        '%': lambda: '%',
        'b': lambda: os.path.basename(repo.root),
        }

235 236 237 238 239 240 241 242 243 244 245
    try:
        if node:
            expander.update(node_expander)
        if node and revwidth is not None:
            expander['r'] = lambda: str(r.rev(node)).zfill(revwidth)
        if total is not None:
            expander['N'] = lambda: str(total)
        if seqno is not None:
            expander['n'] = lambda: str(seqno)
        if total is not None and seqno is not None:
            expander['n'] = lambda:str(seqno).zfill(len(str(total)))
246 247 248 249
        if pathname is not None:
            expander['s'] = lambda: os.path.basename(pathname)
            expander['d'] = lambda: os.path.dirname(pathname) or '.'
            expander['p'] = lambda: pathname
250 251 252 253 254

        newname = []
        patlen = len(pat)
        i = 0
        while i < patlen:
255
            c = pat[i]
256 257 258 259 260 261
            if c == '%':
                i += 1
                c = pat[i]
                c = expander[c]()
            newname.append(c)
            i += 1
262
        return ''.join(newname)
263
    except KeyError, inst:
264
        raise util.Abort(_("invalid format spec '%%%s' in output file name"),
265
                    inst.args[0])
266

267
def make_file(repo, r, pat, node=None,
268
              total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
269
    if not pat or pat == '-':
Thomas Arendsen Hein's avatar
Thomas Arendsen Hein committed
270
        return 'w' in mode and sys.stdout or sys.stdin
271 272 273 274
    if hasattr(pat, 'write') and 'w' in mode:
        return pat
    if hasattr(pat, 'read') and 'r' in mode:
        return pat
275 276
    return open(make_filename(repo, r, pat, node, total, seqno, revwidth,
                              pathname),
277 278
                mode)

279 280
def write_bundle(cg, filename=None, compress=True):
    """Write a bundle file and return its filename.
281

282 283 284 285
    Existing files will not be overwritten.
    If no filename is specified, a temporary file is created.
    bz2 compression can be turned off.
    The bundle file will be deleted in case of errors.
286
    """
287 288 289 290 291
    class nocompress(object):
        def compress(self, x):
            return x
        def flush(self):
            return ""
292 293 294

    fh = None
    cleanup = None
295
    try:
296 297 298 299 300 301 302 303 304
        if filename:
            if os.path.exists(filename):
                raise util.Abort(_("file '%s' already exists"), filename)
            fh = open(filename, "wb")
        else:
            fd, filename = tempfile.mkstemp(suffix=".hg", prefix="hg-bundle-")
            fh = os.fdopen(fd, "wb")
        cleanup = filename

305 306 307 308
        if compress:
            fh.write("HG10")
            z = bz2.BZ2Compressor(9)
        else:
309
            fh.write("HG10UN")
310
            z = nocompress()
311 312 313 314 315 316 317 318 319 320 321
        # parse the changegroup data, otherwise we will block
        # in case of sshrepo because we don't know the end of the stream

        # an empty chunkiter is the end of the changegroup
        empty = False
        while not empty:
            empty = True
            for chunk in changegroup.chunkiter(cg):
                empty = False
                fh.write(z.compress(changegroup.genchunk(chunk)))
            fh.write(z.compress(changegroup.closechunk()))
322
        fh.write(z.flush())
323 324 325 326 327 328 329
        cleanup = None
        return filename
    finally:
        if fh is not None:
            fh.close()
        if cleanup is not None:
            os.unlink(cleanup)
330

331
def dodiff(fp, ui, repo, node1, node2, files=None, match=util.always,
332
           changes=None, text=False, opts={}):
333 334 335 336 337 338 339
    if not node1:
        node1 = repo.dirstate.parents()[0]
    # reading the data for node1 early allows it to play nicely
    # with repo.changes and the revlog cache.
    change = repo.changelog.read(node1)
    mmap = repo.manifest.read(change[0])
    date1 = util.datestr(change[2])
340

341
    if not changes:
342
        changes = repo.changes(node1, node2, files, match=match)
343
    modified, added, removed, deleted, unknown = changes
344
    if files:
345
        modified, added, removed = map(lambda x: filterfiles(files, x),
346
                                       (modified, added, removed))
347

348
    if not modified and not added and not removed:
349 350
        return

351 352 353
    if node2:
        change = repo.changelog.read(node2)
        mmap2 = repo.manifest.read(change[0])
354
        date2 = util.datestr(change[2])
355 356
        def read(f):
            return repo.file(f).read(mmap2[f])
357
    else:
358
        date2 = util.datestr()
359
        def read(f):
360
            return repo.wread(f)
361

362 363 364
    if ui.quiet:
        r = None
    else:
365
        hexfunc = ui.verbose and hex or short
366 367
        r = [hexfunc(node) for node in [node1, node2] if node]

368
    diffopts = ui.diffopts()
369 370
    showfunc = opts.get('show_function') or diffopts['showfunc']
    ignorews = opts.get('ignore_all_space') or diffopts['ignorews']
371
    for f in modified:
mpm@selenic.com's avatar
mpm@selenic.com committed
372 373 374
        to = None
        if f in mmap:
            to = repo.file(f).read(mmap[f])
375
        tn = read(f)
376 377
        fp.write(mdiff.unidiff(to, date1, tn, date2, f, r, text=text,
                               showfunc=showfunc, ignorews=ignorews))
378
    for f in added:
379
        to = None
380
        tn = read(f)
381 382
        fp.write(mdiff.unidiff(to, date1, tn, date2, f, r, text=text,
                               showfunc=showfunc, ignorews=ignorews))
383
    for f in removed:
384
        to = repo.file(f).read(mmap[f])
385
        tn = None
386 387
        fp.write(mdiff.unidiff(to, date1, tn, date2, f, r, text=text,
                               showfunc=showfunc, ignorews=ignorews))
388

389
def trimuser(ui, name, rev, revcache):
390
    """trim the name of the user who committed a change"""
391 392 393 394
    user = revcache.get(rev)
    if user is None:
        user = revcache[rev] = ui.shortuser(name)
    return user
395

396
class changeset_templater(object):
397 398
    '''use templater module to format changeset information.'''

399
    def __init__(self, ui, repo, mapfile):
400 401 402
        self.t = templater.templater(mapfile, templater.common_filters,
                                     cache={'parent': '{rev}:{node|short} ',
                                            'manifest': '{rev}:{node|short}'})
403 404
        self.ui = ui
        self.repo = repo
405

406
    def use_template(self, t):
407
        '''set template string to use'''
408
        self.t.cache['changeset'] = t
409

410
    def write(self, thing, header=False):
411 412
        '''write expanded template.
        uses in-order recursive traverse of iterators.'''
413 414
        for t in thing:
            if hasattr(t, '__iter__'):
415 416 417
                self.write(t, header=header)
            elif header:
                self.ui.write_header(t)
418 419 420
            else:
                self.ui.write(t)

421 422 423
    def write_header(self, thing):
        self.write(thing, header=True)

424
    def show(self, rev=0, changenode=None, brinfo=None):
425
        '''show a single changeset or file revision'''
426 427 428 429 430 431 432 433 434
        log = self.repo.changelog
        if changenode is None:
            changenode = log.node(rev)
        elif not rev:
            rev = log.rev(changenode)

        changes = log.read(changenode)

        def showlist(name, values, plural=None, **args):
435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
            '''expand set of values.
            name is name of key in template map.
            values is list of strings or dicts.
            plural is plural of name, if not simply name + 's'.

            expansion works like this, given name 'foo'.

            if values is empty, expand 'no_foos'.

            if 'foo' not in template map, return values as a string,
            joined by space.

            expand 'start_foos'.

            for each value, expand 'foo'. if 'last_foo' in template
            map, expand it instead of 'foo' for last key.

            expand 'end_foos'.
            '''
454 455 456 457 458 459 460 461
            if plural: names = plural
            else: names = name + 's'
            if not values:
                noname = 'no_' + names
                if noname in self.t:
                    yield self.t(noname, **args)
                return
            if name not in self.t:
462 463 464 465 466
                if isinstance(values[0], str):
                    yield ' '.join(values)
                else:
                    for v in values:
                        yield dict(v, **args)
467 468 469 470
                return
            startname = 'start_' + names
            if startname in self.t:
                yield self.t(startname, **args)
471
            vargs = args.copy()
Vadim Gelfer's avatar
Vadim Gelfer committed
472
            def one(v, tag=name):
473 474
                try:
                    vargs.update(v)
Vadim Gelfer's avatar
Vadim Gelfer committed
475 476 477 478 479 480
                except (AttributeError, ValueError):
                    try:
                        for a, b in v:
                            vargs[a] = b
                    except ValueError:
                        vargs[name] = v
Vadim Gelfer's avatar
Vadim Gelfer committed
481
                return self.t(tag, **vargs)
482 483 484 485 486 487 488 489
            lastname = 'last_' + name
            if lastname in self.t:
                last = values.pop()
            else:
                last = None
            for v in values:
                yield one(v)
            if last is not None:
Vadim Gelfer's avatar
Vadim Gelfer committed
490
                yield one(last, tag=lastname)
491 492 493 494 495 496 497 498 499 500 501 502 503
            endname = 'end_' + names
            if endname in self.t:
                yield self.t(endname, **args)

        if brinfo:
            def showbranches(**args):
                if changenode in brinfo:
                    for x in showlist('branch', brinfo[changenode],
                                      plural='branches', **args):
                        yield x
        else:
            showbranches = ''

504 505 506
        if self.ui.debugflag:
            def showmanifest(**args):
                args = args.copy()
Vadim Gelfer's avatar
Vadim Gelfer committed
507 508
                args.update(dict(rev=self.repo.manifest.rev(changes[0]),
                                 node=hex(changes[0])))
509 510 511
                yield self.t('manifest', **args)
        else:
            showmanifest = ''
512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556

        def showparents(**args):
            parents = [[('rev', log.rev(p)), ('node', hex(p))]
                       for p in log.parents(changenode)
                       if self.ui.debugflag or p != nullid]
            if (not self.ui.debugflag and len(parents) == 1 and
                parents[0][0][1] == rev - 1):
                return
            for x in showlist('parent', parents, **args):
                yield x

        def showtags(**args):
            for x in showlist('tag', self.repo.nodetags(changenode), **args):
                yield x

        if self.ui.debugflag:
            files = self.repo.changes(log.parents(changenode)[0], changenode)
            def showfiles(**args):
                for x in showlist('file', files[0], **args): yield x
            def showadds(**args):
                for x in showlist('file_add', files[1], **args): yield x
            def showdels(**args):
                for x in showlist('file_del', files[2], **args): yield x
        else:
            def showfiles(**args):
                for x in showlist('file', changes[3], **args): yield x
            showadds = ''
            showdels = ''

        props = {
            'author': changes[1],
            'branches': showbranches,
            'date': changes[2],
            'desc': changes[4],
            'file_adds': showadds,
            'file_dels': showdels,
            'files': showfiles,
            'manifest': showmanifest,
            'node': hex(changenode),
            'parents': showparents,
            'rev': rev,
            'tags': showtags,
            }

        try:
557 558 559 560 561 562 563 564 565 566 567 568
            if self.ui.debugflag and 'header_debug' in self.t:
                key = 'header_debug'
            elif self.ui.quiet and 'header_quiet' in self.t:
                key = 'header_quiet'
            elif self.ui.verbose and 'header_verbose' in self.t:
                key = 'header_verbose'
            elif 'header' in self.t:
                key = 'header'
            else:
                key = ''
            if key:
                self.write_header(self.t(key, **props))
569 570 571 572 573
            if self.ui.debugflag and 'changeset_debug' in self.t:
                key = 'changeset_debug'
            elif self.ui.quiet and 'changeset_quiet' in self.t:
                key = 'changeset_quiet'
            elif self.ui.verbose and 'changeset_verbose' in self.t:
574
                key = 'changeset_verbose'
575
            else:
576
                key = 'changeset'
577
            self.write(self.t(key, **props))
578 579 580 581 582 583 584
        except KeyError, inst:
            raise util.Abort(_("%s: no key named '%s'") % (self.t.mapfile,
                                                           inst.args[0]))
        except SyntaxError, inst:
            raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))

class changeset_printer(object):
585 586
    '''show changeset information when templating not requested.'''

587 588 589 590 591
    def __init__(self, ui, repo):
        self.ui = ui
        self.repo = repo

    def show(self, rev=0, changenode=None, brinfo=None):
592
        '''show a single changeset or file revision'''
593 594 595 596 597 598 599 600 601
        log = self.repo.changelog
        if changenode is None:
            changenode = log.node(rev)
        elif not rev:
            rev = log.rev(changenode)

        if self.ui.quiet:
            self.ui.write("%d:%s\n" % (rev, short(changenode)))
            return
602

603 604
        changes = log.read(changenode)
        date = util.datestr(changes[2])
605

606 607 608 609 610 611
        parents = [(log.rev(p), self.ui.verbose and hex(p) or short(p))
                   for p in log.parents(changenode)
                   if self.ui.debugflag or p != nullid]
        if (not self.ui.debugflag and len(parents) == 1 and
            parents[0][0] == rev-1):
            parents = []
612

613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637
        if self.ui.verbose:
            self.ui.write(_("changeset:   %d:%s\n") % (rev, hex(changenode)))
        else:
            self.ui.write(_("changeset:   %d:%s\n") % (rev, short(changenode)))

        for tag in self.repo.nodetags(changenode):
            self.ui.status(_("tag:         %s\n") % tag)
        for parent in parents:
            self.ui.write(_("parent:      %d:%s\n") % parent)

        if brinfo and changenode in brinfo:
            br = brinfo[changenode]
            self.ui.write(_("branch:      %s\n") % " ".join(br))

        self.ui.debug(_("manifest:    %d:%s\n") %
                      (self.repo.manifest.rev(changes[0]), hex(changes[0])))
        self.ui.status(_("user:        %s\n") % changes[1])
        self.ui.status(_("date:        %s\n") % date)

        if self.ui.debugflag:
            files = self.repo.changes(log.parents(changenode)[0], changenode)
            for key, value in zip([_("files:"), _("files+:"), _("files-:")],
                                  files):
                if value:
                    self.ui.note("%-12s %s\n" % (key, " ".join(value)))
mpm@selenic.com's avatar
mpm@selenic.com committed
638
        else:
639 640 641 642 643 644 645 646 647 648 649 650 651 652
            self.ui.note(_("files:       %s\n") % " ".join(changes[3]))

        description = changes[4].strip()
        if description:
            if self.ui.verbose:
                self.ui.status(_("description:\n"))
                self.ui.status(description)
                self.ui.status("\n\n")
            else:
                self.ui.status(_("summary:     %s\n") %
                               description.splitlines()[0])
        self.ui.status("\n")

def show_changeset(ui, repo, opts):
653
    '''show one changeset.  uses template or regular display.  caller
654
    can pass in 'style' and 'template' options in opts.'''
655

656 657 658 659 660 661
    tmpl = opts.get('template')
    if tmpl:
        tmpl = templater.parsestring(tmpl, quoted=False)
    else:
        tmpl = ui.config('ui', 'logtemplate')
        if tmpl: tmpl = templater.parsestring(tmpl)
662
    mapfile = opts.get('style') or ui.config('ui', 'style')
663 664 665
    if tmpl or mapfile:
        if mapfile:
            if not os.path.isfile(mapfile):
666 667
                mapname = templater.templatepath('map-cmdline.' + mapfile)
                if not mapname: mapname = templater.templatepath(mapfile)
668 669 670 671 672 673 674 675
                if mapname: mapfile = mapname
        try:
            t = changeset_templater(ui, repo, mapfile)
        except SyntaxError, inst:
            raise util.Abort(inst.args[0])
        if tmpl: t.use_template(tmpl)
        return t
    return changeset_printer(ui, repo)
676

677
def show_version(ui):
678
    """output version and copyright information"""
679
    ui.write(_("Mercurial Distributed SCM (version %s)\n")
680
             % version.get_version())
681
    ui.status(_(
682 683 684 685
        "\nCopyright (C) 2005 Matt Mackall <mpm@selenic.com>\n"
        "This is free software; see the source for copying conditions. "
        "There is NO\nwarranty; "
        "not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"
686
    ))
687

688
def help_(ui, cmd=None, with_version=False):
689
    """show help for a given command or all commands"""
690
    option_lists = []
691
    if cmd and cmd != 'shortlist':
692 693 694
        if with_version:
            show_version(ui)
            ui.write('\n')
695
        aliases, i = find(cmd)
696
        # synopsis
697 698
        ui.write("%s\n\n" % i[2])

699 700
        # description
        doc = i[0].__doc__
701 702
        if not doc:
            doc = _("(No help text available)")
703 704 705 706 707
        if ui.quiet:
            doc = doc.splitlines(0)[0]
        ui.write("%s\n" % doc.rstrip())

        if not ui.quiet:
708
            # aliases
709 710
            if len(aliases) > 1:
                ui.write(_("\naliases: %s\n") % ', '.join(aliases[1:]))
711

712 713 714
            # options
            if i[1]:
                option_lists.append(("options", i[1]))
715

mpm@selenic.com's avatar
mpm@selenic.com committed
716
    else:
717
        # program name
718
        if ui.verbose or with_version:
719
            show_version(ui)
mpm@selenic.com's avatar
mpm@selenic.com committed
720
        else:
721
            ui.status(_("Mercurial Distributed SCM\n"))
722
        ui.status('\n')
723

724
        # list of commands
725
        if cmd == "shortlist":
726 727
            ui.status(_('basic commands (use "hg help" '
                        'for the full list or option "-v" for details):\n\n'))
728
        elif ui.verbose:
729
            ui.status(_('list of commands:\n\n'))
730
        else:
731 732
            ui.status(_('list of commands (use "hg help -v" '
                        'to show aliases and global options):\n\n'))
mpm@selenic.com's avatar
mpm@selenic.com committed
733 734

        h = {}
735
        cmds = {}
736 737
        for c, e in table.items():
            f = c.split("|")[0]
738
            if cmd == "shortlist" and not f.startswith("^"):
mpm@selenic.com's avatar
mpm@selenic.com committed
739
                continue
740
            f = f.lstrip("^")
mpm@selenic.com's avatar
mpm@selenic.com committed
741
            if not ui.debugflag and f.startswith("debug"):
742
                continue
743 744 745 746
            doc = e[0].__doc__
            if not doc:
                doc = _("(No help text available)")
            h[f] = doc.splitlines(0)[0].rstrip()
747
            cmds[f] = c.lstrip("^")
mpm@selenic.com's avatar
mpm@selenic.com committed
748 749 750 751 752

        fns = h.keys()
        fns.sort()
        m = max(map(len, fns))
        for f in fns:
753 754
            if ui.verbose:
                commands = cmds[f].replace("|",", ")
755
                ui.write(" %s:\n      %s\n"%(commands, h[f]))
756 757
            else:
                ui.write(' %-*s   %s\n' % (m, f, h[f]))
mpm@selenic.com's avatar
mpm@selenic.com committed
758

759 760
    # global options
    if ui.verbose:
761 762 763 764 765 766 767 768 769 770
        option_lists.append(("global options", globalopts))

    # list all option lists
    opt_output = []
    for title, options in option_lists:
        opt_output.append(("\n%s:\n" % title, None))
        for shortopt, longopt, default, desc in options:
            opt_output.append(("%2s%s" % (shortopt and "-%s" % shortopt,
                                          longopt and " --%s" % longopt),
                               "%s%s" % (desc,
771 772
                                         default
                                         and _(" (default: %s)") % default
773 774 775 776 777 778 779 780 781
                                         or "")))

    if opt_output:
        opts_len = max([len(line[0]) for line in opt_output if line[1]])
        for first, second in opt_output:
            if second:
                ui.write(" %-*s  %s\n" % (opts_len, first, second))
            else:
                ui.write("%s\n" % first)
mpm@selenic.com's avatar
mpm@selenic.com committed
782 783

# Commands start here, listed alphabetically
784

785
def add(ui, repo, *pats, **opts):
786 787 788 789 790 791
    """add the specified files on the next commit

    Schedule files to be version controlled and added to the repository.

    The files will be added to the repository at the next commit.

792
    If no names are given, add all files in the repository.
793 794
    """

795
    names = []
796 797
    for src, abs, rel, exact in walk(repo, pats, opts):
        if exact:
798 799
            if ui.verbose:
                ui.status(_('adding %s\n') % rel)
800 801
            names.append(abs)
        elif repo.dirstate.state(abs) == '?':
802
            ui.status(_('adding %s\n') % rel)
803 804
            names.append(abs)
    repo.add(names)
805

806
def addremove(ui, repo, *pats, **opts):
807 808 809 810 811 812 813
    """add all new files, delete all missing files

    Add all new files and remove all missing files from the repository.

    New files are ignored if they match any of the patterns in .hgignore. As
    with add, these changes take effect at the next commit.
    """
814 815 816
    return addremove_lock(ui, repo, pats, opts)

def addremove_lock(ui, repo, pats, opts, wlock=None):
817
    add, remove = [], []
818
    for src, abs, rel, exact in walk(repo, pats, opts):
819 820
        if src == 'f' and repo.dirstate.state(abs) == '?':
            add.append(abs)
821
            if ui.verbose or not exact:
822
                ui.status(_('adding %s\n') % ((pats and rel) or abs))
823
        if repo.dirstate.state(abs) != 'r' and not os.path.exists(rel):
824
            remove.append(abs)
825
            if ui.verbose or not exact:
826
                ui.status(_('removing %s\n') % ((pats and rel) or abs))
827 828
    repo.add(add, wlock=wlock)
    repo.remove(remove, wlock=wlock)
829

830
def annotate(ui, repo, *pats, **opts):
831 832 833 834 835 836 837 838 839 840 841
    """show changeset information per file line

    List changes in files, showing the revision id responsible for each line

    This command is useful to discover who did a change or when a change took
    place.

    Without the -a option, annotate will avoid processing files it
    detects as binary. With -a, annotate will generate an annotation
    anyway, probably with undesirable results.
    """
842
    def getnode(rev):
843
        return short(repo.changelog.node(rev))
844

845
    ucache = {}
846
    def getname(rev):
847
        cl = repo.changelog.read(repo.changelog.node(rev))
848
        return trimuser(ui, cl[1], rev, ucache)
849

850 851
    dcache = {}
    def getdate(rev):
852
        datestr = dcache.get(rev)
853 854 855
        if datestr is None:
            cl = repo.changelog.read(repo.changelog.node(rev))
            datestr = dcache[rev] = util.datestr(cl[2])
856
        return datestr
857

858
    if not pats:
859
        raise util.Abort(_('at least one file name or pattern required'))
860

Matt Mackall's avatar
Matt Mackall committed
861 862
    opmap = [['user', getname], ['number', str], ['changeset', getnode],
             ['date', getdate]]
863
    if not opts['user'] and not opts['changeset'] and not opts['date']:
864
        opts['number'] = 1
865

866 867
    if opts['rev']:
        node = repo.changelog.lookup(opts['rev'])