commands.py 42.2 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
demandload(globals(), "os re sys signal shutil")
10
demandload(globals(), "fancyopts ui hg util")
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
11
demandload(globals(), "fnmatch hgweb mdiff random signal time traceback")
12
demandload(globals(), "errno socket version struct")
13

14 15
class UnknownCommand(Exception):
    """Exception raised if command is not in the command table."""
16

17
def filterfiles(filters, files):
18
    l = [x for x in files if x in filters]
19

20
    for t in filters:
21 22 23
        if t and t[-1] != "/":
            t += "/"
        l += [x for x in files if x.startswith(t)]
24 25
    return l

26
def relfilter(repo, files):
27 28 29
    cwd = repo.getcwd()
    if cwd:
        return filterfiles([util.pconvert(cwd)], files)
30
    return files
31

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

mpm@selenic.com's avatar
mpm@selenic.com committed
39 40
revrangesep = ':'

41
def revrange(ui, repo, revs, revlog=None):
mpm@selenic.com's avatar
mpm@selenic.com committed
42 43 44 45
    if revlog is None:
        revlog = repo.changelog
    revcount = revlog.count()
    def fix(val, defval):
46 47
        if not val:
            return defval
mpm@selenic.com's avatar
mpm@selenic.com committed
48 49
        try:
            num = int(val)
50 51 52 53
            if str(num) != val:
                raise ValueError
            if num < 0:
                num += revcount
mpm@selenic.com's avatar
mpm@selenic.com committed
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
            if not (0 <= num < revcount):
                raise ValueError
        except ValueError:
            try:
                num = repo.changelog.rev(repo.lookup(val))
            except KeyError:
                try:
                    num = revlog.rev(revlog.lookup(val))
                except KeyError:
                    ui.warn('abort: invalid revision identifier %s\n' % val)
                    sys.exit(1)
        return num
    for spec in revs:
        if spec.find(revrangesep) >= 0:
            start, end = spec.split(revrangesep, 1)
            start = fix(start, 0)
            end = fix(end, revcount - 1)
            if end > start:
                end += 1
                step = 1
            else:
                end -= 1
                step = -1
            for rev in xrange(start, end, step):
                yield str(rev)
        else:
            yield spec

82 83 84 85 86 87 88 89 90 91 92 93
def make_filename(repo, r, pat, node=None,
                  total=None, seqno=None, revwidth=None):
    node_expander = {
        'H': lambda: hg.hex(node),
        'R': lambda: str(r.rev(node)),
        'h': lambda: hg.short(node),
        }
    expander = {
        '%': lambda: '%',
        'b': lambda: os.path.basename(repo.root),
        }

94 95
    if node:
        expander.update(node_expander)
96 97
    if node and revwidth is not None:
        expander['r'] = lambda: str(r.rev(node)).zfill(revwidth)
98 99 100 101
    if total is not None:
        expander['N'] = lambda: str(total)
    if seqno is not None:
        expander['n'] = lambda: str(seqno)
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
    if total is not None and seqno is not None:
        expander['n'] = lambda:str(seqno).zfill(len(str(total)))

    newname = []
    patlen = len(pat)
    i = 0
    while i < patlen:
        c = pat[i]
        if c == '%':
            i += 1
            c = pat[i]
            c = expander[c]()
        newname.append(c)
        i += 1
    return ''.join(newname)

118
def dodiff(fp, ui, repo, files=None, node1=None, node2=None):
119 120 121
    def date(c):
        return time.asctime(time.gmtime(float(c[2].split(' ')[0])))

mpm@selenic.com's avatar
mpm@selenic.com committed
122
    (c, a, d, u) = repo.changes(node1, node2, files)
123 124 125 126 127 128
    if files:
        c, a, d = map(lambda x: filterfiles(files, x), (c, a, d))

    if not c and not a and not d:
        return

129 130 131 132
    if node2:
        change = repo.changelog.read(node2)
        mmap2 = repo.manifest.read(change[0])
        date2 = date(change)
133 134
        def read(f):
            return repo.file(f).read(mmap2[f])
135 136 137 138
    else:
        date2 = time.asctime()
        if not node1:
            node1 = repo.dirstate.parents()[0]
139 140
        def read(f):
            return repo.wfile(f).read()
141

142 143 144 145 146 147
    if ui.quiet:
        r = None
    else:
        hexfunc = ui.verbose and hg.hex or hg.short
        r = [hexfunc(node) for node in [node1, node2] if node]

148 149 150 151 152
    change = repo.changelog.read(node1)
    mmap = repo.manifest.read(change[0])
    date1 = date(change)

    for f in c:
mpm@selenic.com's avatar
mpm@selenic.com committed
153 154 155
        to = None
        if f in mmap:
            to = repo.file(f).read(mmap[f])
156
        tn = read(f)
mpm@selenic.com's avatar
mpm@selenic.com committed
157
        fp.write(mdiff.unidiff(to, date1, tn, date2, f, r))
158
    for f in a:
159
        to = None
160
        tn = read(f)
mpm@selenic.com's avatar
mpm@selenic.com committed
161
        fp.write(mdiff.unidiff(to, date1, tn, date2, f, r))
162 163
    for f in d:
        to = repo.file(f).read(mmap[f])
164
        tn = None
mpm@selenic.com's avatar
mpm@selenic.com committed
165
        fp.write(mdiff.unidiff(to, date1, tn, date2, f, r))
166 167 168 169 170 171 172 173 174 175 176 177

def show_changeset(ui, repo, rev=0, changenode=None, filelog=None):
    """show a single changeset or file revision"""
    changelog = repo.changelog
    if filelog:
        log = filelog
        filerev = rev
        node = filenode = filelog.node(filerev)
        changerev = filelog.linkrev(filenode)
        changenode = changenode or changelog.node(changerev)
    else:
        log = changelog
178
        changerev = rev
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
        if changenode is None:
            changenode = changelog.node(changerev)
        elif not changerev:
            rev = changerev = changelog.rev(changenode)
        node = changenode

    if ui.quiet:
        ui.write("%d:%s\n" % (rev, hg.hex(node)))
        return

    changes = changelog.read(changenode)

    parents = [(log.rev(parent), hg.hex(parent))
               for parent in log.parents(node)
               if ui.debugflag or parent != hg.nullid]
    if not ui.debugflag and len(parents) == 1 and parents[0][0] == rev-1:
        parents = []

Matt Mackall's avatar
Matt Mackall committed
197 198 199 200 201
    ui.write("changeset:   %d:%s\n" % (changerev, hg.hex(changenode)))
    for tag in repo.nodetags(changenode):
        ui.status("tag:         %s\n" % tag)
    for parent in parents:
        ui.write("parent:      %d:%s\n" % parent)
202
    if filelog:
Matt Mackall's avatar
Matt Mackall committed
203 204 205
        ui.debug("file rev:    %d:%s\n" % (filerev, hg.hex(filenode)))
    ui.note("manifest:    %d:%s\n" % (repo.manifest.rev(changes[0]),
                                      hg.hex(changes[0])))
206 207 208
    ui.status("user:        %s\n" % changes[1])
    ui.status("date:        %s\n" % time.asctime(
        time.localtime(float(changes[2].split(' ')[0]))))
209
    if ui.debugflag:
210
        files = repo.changes(changelog.parents(changenode)[0], changenode)
211 212 213
        for key, value in zip(["files:", "files+:", "files-:"], files):
            if value:
                ui.note("%-12s %s\n" % (key, " ".join(value)))
214 215
    else:
        ui.note("files:       %s\n" % " ".join(changes[3]))
216
    description = changes[4].strip()
217
    if description:
mpm@selenic.com's avatar
mpm@selenic.com committed
218 219
        if ui.verbose:
            ui.status("description:\n")
220
            ui.status(description)
221
            ui.status("\n\n")
mpm@selenic.com's avatar
mpm@selenic.com committed
222
        else:
223
            ui.status("summary:     %s\n" % description.splitlines()[0])
224 225
    ui.status("\n")

226
def show_version(ui):
227 228 229 230 231 232 233 234 235
    """output version and copyright information"""
    ui.write("Mercurial version %s\n" % version.get_version())
    ui.status(
        "\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"
    )

236 237
def help_(ui, cmd=None):
    """show help for a given command or all commands"""
238 239 240 241
    if cmd:
        try:
            i = find(cmd)
            ui.write("%s\n\n" % i[2])
mpm@selenic.com's avatar
mpm@selenic.com committed
242 243 244

            if i[1]:
                for s, l, d, c in i[1]:
245 246 247 248 249 250 251
                    opt = ' '
                    if s:
                        opt = opt + '-' + s + ' '
                    if l:
                        opt = opt + '--' + l + ' '
                    if d:
                        opt = opt + '(' + str(d) + ')'
mpm@selenic.com's avatar
mpm@selenic.com committed
252
                    ui.write(opt, "\n")
253 254
                    if c:
                        ui.write('   %s\n' % c)
mpm@selenic.com's avatar
mpm@selenic.com committed
255 256
                ui.write("\n")

257 258
            ui.write(i[0].__doc__, "\n")
        except UnknownCommand:
mpm@selenic.com's avatar
mpm@selenic.com committed
259
            ui.warn("hg: unknown command %s\n" % cmd)
260
        sys.exit(0)
mpm@selenic.com's avatar
mpm@selenic.com committed
261
    else:
mpm@selenic.com's avatar
mpm@selenic.com committed
262
        if ui.verbose:
263
            show_version(ui)
264
            ui.write('\n')
mpm@selenic.com's avatar
mpm@selenic.com committed
265 266 267
        if ui.verbose:
            ui.write('hg commands:\n\n')
        else:
268
            ui.write('basic hg commands (use "hg help -v" for more):\n\n')
mpm@selenic.com's avatar
mpm@selenic.com committed
269 270

        h = {}
271 272
        for c, e in table.items():
            f = c.split("|")[0]
mpm@selenic.com's avatar
mpm@selenic.com committed
273 274 275
            if not ui.verbose and not f.startswith("^"):
                continue
            if not ui.debugflag and f.startswith("debug"):
276
                continue
mpm@selenic.com's avatar
mpm@selenic.com committed
277
            f = f.lstrip("^")
mpm@selenic.com's avatar
mpm@selenic.com committed
278
            d = ""
279 280 281
            if e[0].__doc__:
                d = e[0].__doc__.splitlines(0)[0].rstrip()
            h[f] = d
mpm@selenic.com's avatar
mpm@selenic.com committed
282 283 284 285 286

        fns = h.keys()
        fns.sort()
        m = max(map(len, fns))
        for f in fns:
287
            ui.write(' %-*s   %s\n' % (m, f, h[f]))
mpm@selenic.com's avatar
mpm@selenic.com committed
288 289

# Commands start here, listed alphabetically
290

291
def add(ui, repo, file1, *files):
292
    '''add the specified files on the next commit'''
293
    repo.add(relpath(repo, (file1,) + files))
294

295
def addremove(ui, repo, *files):
mpm@selenic.com's avatar
mpm@selenic.com committed
296
    """add all new files, delete all missing files"""
297 298 299 300 301 302 303 304 305 306 307 308 309
    if files:
        files = relpath(repo, files)
        d = []
        u = []
        for f in files:
            p = repo.wjoin(f)
            s = repo.dirstate.state(f)
            isfile = os.path.isfile(p)
            if s != 'r' and not isfile:
                d.append(f)
            elif s not in 'nmai' and isfile:
                u.append(f)
    else:
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
310
        (c, a, d, u) = repo.changes()
mpm@selenic.com's avatar
mpm@selenic.com committed
311
    repo.add(u)
312
    repo.remove(d)
313

314
def annotate(u, repo, file1, *files, **ops):
mpm@selenic.com's avatar
mpm@selenic.com committed
315
    """show changeset information per file line"""
316 317 318 319 320 321 322 323 324 325 326 327
    def getnode(rev):
        return hg.short(repo.changelog.node(rev))

    def getname(rev):
        try:
            return bcache[rev]
        except KeyError:
            cl = repo.changelog.read(repo.changelog.node(rev))
            name = cl[1]
            f = name.find('@')
            if f >= 0:
                name = name[:f]
328 329 330
            f = name.find('<')
            if f >= 0:
                name = name[f+1:]
331 332
            bcache[rev] = name
            return name
333

334 335 336 337 338
    bcache = {}
    opmap = [['user', getname], ['number', str], ['changeset', getnode]]
    if not ops['user'] and not ops['changeset']:
        ops['number'] = 1

339
    node = repo.dirstate.parents()[0]
340 341 342 343
    if ops['revision']:
        node = repo.changelog.lookup(ops['revision'])
    change = repo.changelog.read(node)
    mmap = repo.manifest.read(change[0])
344
    for f in relpath(repo, (file1,) + files):
345 346 347 348 349
        lines = repo.file(f).annotate(mmap[f])
        pieces = []

        for o, f in opmap:
            if ops[o]:
350
                l = [f(n) for n, dummy in lines]
351
                m = max(map(len, l))
352
                pieces.append(["%*s" % (m, x) for x in l])
353

354
        for p, l in zip(zip(*pieces), lines):
355 356
            u.write(" ".join(p) + ": " + l[1])

357
def cat(ui, repo, file1, rev=None, **opts):
mpm@selenic.com's avatar
mpm@selenic.com committed
358
    """output the latest or given revision of a file"""
359 360 361 362 363
    r = repo.file(relpath(repo, [file1])[0])
    if rev:
        n = r.lookup(rev)
    else:
        n = r.tip()
364 365 366 367 368 369 370 371 372 373 374
    if opts['output'] and opts['output'] != '-':
        try:
            outname = make_filename(repo, r, opts['output'], node=n)
            fp = open(outname, 'wb')
        except KeyError, inst:
            ui.warn("error: invlaid format spec '%%%s' in output file name\n" %
                    inst.args[0])
            sys.exit(1);
    else:
        fp = sys.stdout
    fp.write(r.read(n))
mpm@selenic.com's avatar
mpm@selenic.com committed
375

376
def clone(ui, source, dest=None, **opts):
mpm@selenic.com's avatar
mpm@selenic.com committed
377 378
    """make a copy of an existing repository"""
    if dest is None:
379
        dest = os.path.basename(os.path.normpath(source))
380 381 382 383

    if os.path.exists(dest):
        ui.warn("abort: destination '%s' already exists\n" % dest)
        return 1
384

385 386
    class Dircleanup:
        def __init__(self, dir_):
Matt Mackall's avatar
Matt Mackall committed
387
            self.rmtree = shutil.rmtree
388 389
            self.dir_ = dir_
            os.mkdir(dir_)
390
        def close(self):
391
            self.dir_ = None
392
        def __del__(self):
393 394
            if self.dir_:
                self.rmtree(self.dir_, True)
395

396
    d = Dircleanup(dest)
397
    abspath = source
398 399 400
    source = ui.expandpath(source)
    other = hg.repository(ui, source)

Mikael Berthe's avatar
Mikael Berthe committed
401 402
    if other.dev() != -1:
        abspath = os.path.abspath(source)
403 404 405 406 407 408
        copyfile = (os.stat(dest).st_dev == other.dev()
                    and getattr(os, 'link', None) or shutil.copy2)
        if copyfile is not shutil.copy2:
            ui.note("cloning by hardlink\n")
        util.copytree(os.path.join(source, ".hg"), os.path.join(dest, ".hg"),
                      copyfile)
409
        try:
410 411 412
            os.unlink(os.path.join(dest, ".hg", "dirstate"))
        except IOError:
            pass
mpm@selenic.com's avatar
mpm@selenic.com committed
413

414
        repo = hg.repository(ui, dest)
mpm@selenic.com's avatar
mpm@selenic.com committed
415

416 417
    else:
        repo = hg.repository(ui, dest, create=1)
Matt Mackall's avatar
Matt Mackall committed
418
        repo.pull(other)
419

420 421
    f = repo.opener("hgrc", "w")
    f.write("[paths]\n")
422
    f.write("default = %s\n" % abspath)
423

424 425 426 427
    if not opts['noupdate']:
        update(ui, repo)

    d.close()
mpm@selenic.com's avatar
mpm@selenic.com committed
428

429
def commit(ui, repo, *files, **opts):
430
    """commit the specified files or all outstanding changes"""
431
    text = opts['text']
432 433 434 435 436 437
    logfile = opts['logfile']
    if not text and logfile:
        try:
            text = open(logfile).read()
        except IOError, why:
            ui.warn("Can't read commit text %s: %s\n" % (logfile, why))
438

mpm@selenic.com's avatar
mpm@selenic.com committed
439 440
    if opts['addremove']:
        addremove(ui, repo, *files)
441
    repo.commit(relpath(repo, files), text, opts['user'], opts['date'])
442

mpm@selenic.com's avatar
mpm@selenic.com committed
443 444 445 446
def copy(ui, repo, source, dest):
    """mark a file as copied or renamed for the next commit"""
    return repo.copy(*relpath(repo, (source, dest)))

447 448
def debugcheckstate(ui, repo):
    """validate the correctness of the current dirstate"""
449
    parent1, parent2 = repo.dirstate.parents()
450 451
    repo.dirstate.read()
    dc = repo.dirstate.map
452 453 454 455 456 457 458 459 460 461
    keys = dc.keys()
    keys.sort()
    m1n = repo.changelog.read(parent1)[0]
    m2n = repo.changelog.read(parent2)[0]
    m1 = repo.manifest.read(m1n)
    m2 = repo.manifest.read(m2n)
    errors = 0
    for f in dc:
        state = repo.dirstate.state(f)
        if state in "nr" and f not in m1:
462
            ui.warn("%s in state %s, but not in manifest1\n" % (f, state))
463 464
            errors += 1
        if state in "a" and f in m1:
465
            ui.warn("%s in state %s, but also in manifest1\n" % (f, state))
466 467
            errors += 1
        if state in "m" and f not in m1 and f not in m2:
468 469
            ui.warn("%s in state %s, but not in either manifest\n" %
                    (f, state))
470 471 472 473
            errors += 1
    for f in m1:
        state = repo.dirstate.state(f)
        if state not in "nrm":
474
            ui.warn("%s in manifest1, but listed as state %s" % (f, state))
475 476
            errors += 1
    if errors:
477
        ui.warn(".hg/dirstate inconsistent with current parent's manifest\n")
478 479
        sys.exit(1)

480 481
def debugstate(ui, repo):
    """show the contents of the current dirstate"""
482 483
    repo.dirstate.read()
    dc = repo.dirstate.map
484 485
    keys = dc.keys()
    keys.sort()
486 487
    for file_ in keys:
        ui.write("%c %s\n" % (dc[file_][0], file_))
488

489
def debugindex(ui, file_):
490
    """dump the contents of an index file"""
491
    r = hg.revlog(hg.opener(""), file_, "")
492 493
    ui.write("   rev    offset  length   base linkrev" +
             " p1           p2           nodeid\n")
mpm@selenic.com's avatar
mpm@selenic.com committed
494 495
    for i in range(r.count()):
        e = r.index[i]
496 497 498
        ui.write("% 6d % 9d % 7d % 6d % 7d %s.. %s.. %s..\n" % (
                i, e[0], e[1], e[2], e[3],
            hg.hex(e[4][:5]), hg.hex(e[5][:5]), hg.hex(e[6][:5])))
mpm@selenic.com's avatar
mpm@selenic.com committed
499

500
def debugindexdot(ui, file_):
501
    """dump an index DAG as a .dot file"""
502
    r = hg.revlog(hg.opener(""), file_, "")
503
    ui.write("digraph G {\n")
mpm@selenic.com's avatar
mpm@selenic.com committed
504 505
    for i in range(r.count()):
        e = r.index[i]
506
        ui.write("\t%d -> %d\n" % (r.rev(e[4]), i))
mpm@selenic.com's avatar
mpm@selenic.com committed
507
        if e[5] != hg.nullid:
508 509
            ui.write("\t%d -> %d\n" % (r.rev(e[5]), i))
    ui.write("}\n")
mpm@selenic.com's avatar
mpm@selenic.com committed
510

511
def diff(ui, repo, *files, **opts):
mpm@selenic.com's avatar
mpm@selenic.com committed
512
    """diff working directory (or selected files)"""
513 514 515
    revs = []
    if opts['rev']:
        revs = map(lambda x: repo.lookup(x), opts['rev'])
516

517
    if len(revs) > 2:
Thomas Arendsen Hein's avatar
Thomas Arendsen Hein committed
518
        ui.warn("too many revisions to diff\n")
519 520 521 522 523 524 525
        sys.exit(1)

    if files:
        files = relpath(repo, files)
    else:
        files = relpath(repo, [""])

mpm@selenic.com's avatar
mpm@selenic.com committed
526
    dodiff(sys.stdout, ui, repo, files, *revs)
527

mpm@selenic.com's avatar
mpm@selenic.com committed
528
def doexport(ui, repo, changeset, seqno, total, revwidth, opts):
529 530 531
    node = repo.lookup(changeset)
    prev, other = repo.changelog.parents(node)
    change = repo.changelog.read(node)
532

mpm@selenic.com's avatar
mpm@selenic.com committed
533 534
    if opts['output'] and opts['output'] != '-':
        try:
535 536 537 538
            outname = make_filename(repo, repo.changelog, opts['output'],
                                    node=node, total=total, seqno=seqno,
                                    revwidth=revwidth)
            fp = open(outname, 'wb')
mpm@selenic.com's avatar
mpm@selenic.com committed
539 540 541 542 543 544 545
        except KeyError, inst:
            ui.warn("error: invalid format spec '%%%s' in output file name\n" %
                    inst.args[0])
            sys.exit(1)
    else:
        fp = sys.stdout

546 547 548 549
    fp.write("# HG changeset patch\n")
    fp.write("# User %s\n" % change[1])
    fp.write("# Node ID %s\n" % hg.hex(node))
    fp.write("# Parent  %s\n" % hg.hex(prev))
mpm@selenic.com's avatar
mpm@selenic.com committed
550
    if other != hg.nullid:
551 552 553
        fp.write("# Parent  %s\n" % hg.hex(other))
    fp.write(change[4].rstrip())
    fp.write("\n\n")
mpm@selenic.com's avatar
mpm@selenic.com committed
554 555 556 557 558

    dodiff(fp, ui, repo, None, prev, node)

def export(ui, repo, *changesets, **opts):
    """dump the header and diffs for one or more changesets"""
559 560 561
    if not changesets:
        ui.warn("error: export requires at least one changeset\n")
        sys.exit(1)
mpm@selenic.com's avatar
mpm@selenic.com committed
562 563 564 565 566 567 568
    seqno = 0
    revs = list(revrange(ui, repo, changesets))
    total = len(revs)
    revwidth = max(len(revs[0]), len(revs[-1]))
    for cset in revs:
        seqno += 1
        doexport(ui, repo, cset, seqno, total, revwidth, opts)
569

570
def forget(ui, repo, file1, *files):
571
    """don't add the specified files on the next commit"""
572
    repo.forget(relpath(repo, (file1,) + files))
573

574
def heads(ui, repo):
575
    """show current repository heads"""
576
    for n in repo.changelog.heads():
577
        show_changeset(ui, repo, changenode=n)
578

579 580
def identify(ui, repo):
    """print information about the working copy"""
mpm@selenic.com's avatar
mpm@selenic.com committed
581
    parents = [p for p in repo.dirstate.parents() if p != hg.nullid]
582
    if not parents:
mpm@selenic.com's avatar
mpm@selenic.com committed
583
        ui.write("unknown\n")
584 585
        return

586
    hexfunc = ui.verbose and hg.hex or hg.short
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
587
    (c, a, d, u) = repo.changes()
588 589 590
    output = ["%s%s" % ('+'.join([hexfunc(parent) for parent in parents]),
                        (c or a or d) and "+" or "")]

591
    if not ui.quiet:
592 593 594 595 596
        # multiple tags for a single parent separated by '/'
        parenttags = ['/'.join(tags)
                      for tags in map(repo.nodetags, parents) if tags]
        # tags for multiple parents separated by ' + '
        output.append(' + '.join(parenttags))
597

598
    ui.write("%s\n" % ' '.join(output))
599

mpm@selenic.com's avatar
mpm@selenic.com committed
600 601 602 603 604
def import_(ui, repo, patch1, *patches, **opts):
    """import an ordered set of patches"""
    try:
        import psyco
        psyco.full()
605
    except ImportError:
mpm@selenic.com's avatar
mpm@selenic.com committed
606 607 608
        pass

    patches = (patch1,) + patches
609

mpm@selenic.com's avatar
mpm@selenic.com committed
610 611 612 613 614 615 616
    d = opts["base"]
    strip = opts["strip"]

    for patch in patches:
        ui.status("applying %s\n" % patch)
        pf = os.path.join(d, patch)

617 618 619 620 621 622
        text = []
        user = None
        hgpatch = False
        for line in file(pf):
            line = line.rstrip()
            if line.startswith("--- ") or line.startswith("diff -r"):
623
                break
624 625 626 627
            elif hgpatch:
                # parse values when importing the result of an hg export
                if line.startswith("# User "):
                    user = line[7:]
628
                    ui.debug('User: %s\n' % user)
629 630 631 632 633 634 635
                elif not line.startswith("# ") and line:
                    text.append(line)
                    hgpatch = False
            elif line == '# HG changeset patch':
                hgpatch = True
            else:
                text.append(line)
636

mpm@selenic.com's avatar
mpm@selenic.com committed
637
        # make sure text isn't empty
638 639
        if not text:
            text = "imported patch %s\n" % patch
640 641 642
        else:
            text = "%s\n" % '\n'.join(text)
        ui.debug('text:\n%s\n' % text)
mpm@selenic.com's avatar
mpm@selenic.com committed
643 644 645 646 647

        f = os.popen("patch -p%d < %s" % (strip, pf))
        files = []
        for l in f.read().splitlines():
            l.rstrip('\r\n');
648
            ui.status("%s\n" % l)
649
            if l.startswith('patching file '):
mpm@selenic.com's avatar
mpm@selenic.com committed
650 651 652 653 654 655 656
                pf = l[14:]
                if pf not in files:
                    files.append(pf)
        patcherr = f.close()
        if patcherr:
            sys.stderr.write("patch failed")
            sys.exit(1)
mpm@selenic.com's avatar
mpm@selenic.com committed
657 658 659

        if len(files) > 0:
            addremove(ui, repo, *files)
660
        repo.commit(files, text, user)
mpm@selenic.com's avatar
mpm@selenic.com committed
661

mpm@selenic.com's avatar
mpm@selenic.com committed
662 663
def init(ui, source=None):
    """create a new repository in the current directory"""
664 665

    if source:
mpm@selenic.com's avatar
mpm@selenic.com committed
666 667
        ui.warn("no longer supported: use \"hg clone\" instead\n")
        sys.exit(1)
668
    hg.repository(ui, ".", create=1)
669

Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
670 671 672 673 674 675
def locate(ui, repo, *pats, **opts):
    """locate files matching specific patterns"""
    if [p for p in pats if os.sep in p]:
        ui.warn("error: patterns may not contain '%s'\n" % os.sep)
        ui.warn("use '-i <dir>' instead\n")
        sys.exit(1)
676
    def compile(pats, head='^', tail=os.sep, on_empty=True):
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
677 678
        if not pats:
            class c:
679 680
                def match(self, x):
                    return on_empty
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
681
            return c()
682 683 684
        fnpats = [fnmatch.translate(os.path.normpath(os.path.normcase(p)))[:-1]
                  for p in pats]
        regexp = r'%s(?:%s)%s' % (head, '|'.join(fnpats), tail)
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
685
        return re.compile(regexp)
686
    exclude = compile(opts['exclude'], on_empty=False)
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
687
    include = compile(opts['include'])
688
    pat = compile(pats, head='', tail='$')
689 690 691 692 693
    end = opts['print0'] and '\0' or '\n'
    if opts['rev']:
        node = repo.manifest.lookup(opts['rev'])
    else:
        node = repo.manifest.tip()
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
694 695 696 697 698 699 700 701
    manifest = repo.manifest.read(node)
    cwd = repo.getcwd()
    cwd_plus = cwd and (cwd + os.sep)
    found = []
    for f in manifest:
        f = os.path.normcase(f)
        if exclude.match(f) or not(include.match(f) and
                                   f.startswith(cwd_plus) and
702 703 704 705 706 707
                                   pat.match(os.path.basename(f))):
            continue
        if opts['fullpath']:
            f = os.path.join(repo.root, f)
        elif cwd:
            f = f[len(cwd_plus):]
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
708 709
        found.append(f)
    found.sort()
710 711
    for f in found:
        ui.write(f, end)
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
712

713
def log(ui, repo, f=None, **opts):
714 715
    """show the revision history of the repository or a single file"""
    if f:
716 717
        files = relpath(repo, [f])
        filelog = repo.file(files[0])
718 719
        log = filelog
        lookup = filelog.lookup
720
    else:
721
        files = None
722 723 724 725 726 727 728 729 730 731 732 733 734
        filelog = None
        log = repo.changelog
        lookup = repo.lookup
    revlist = []
    revs = [log.rev(lookup(rev)) for rev in opts['rev']]
    while revs:
        if len(revs) == 1:
            revlist.append(revs.pop(0))
        else:
            a = revs.pop(0)
            b = revs.pop(0)
            off = a > b and -1 or 1
            revlist.extend(range(a, b + off, off))
735

736 737
    for i in revlist or range(log.count() - 1, -1, -1):
        show_changeset(ui, repo, filelog=filelog, rev=i)
738 739 740 741 742 743 744
        if opts['patch']:
            if filelog:
                filenode = filelog.node(i)
                i = filelog.linkrev(filenode)
            changenode = repo.changelog.node(i)
            prev, other = repo.changelog.parents(changenode)
            dodiff(sys.stdout, ui, repo, files, prev, changenode)
745
            ui.write("\n\n")
746

747
def manifest(ui, repo, rev=None):
mpm@selenic.com's avatar
mpm@selenic.com committed
748
    """output the latest or given revision of the project manifest"""
mpm@selenic.com's avatar
mpm@selenic.com committed
749
    if rev:
750 751 752 753 754
        try:
            # assume all revision numbers are for changesets
            n = repo.lookup(rev)
            change = repo.changelog.read(n)
            n = change[0]
755
        except hg.RepoError:
756
            n = repo.manifest.lookup(rev)
757 758
    else:
        n = repo.manifest.tip()
mpm@selenic.com's avatar
mpm@selenic.com committed
759
    m = repo.manifest.read(n)
760
    mf = repo.manifest.readflags(n)
mpm@selenic.com's avatar
mpm@selenic.com committed
761 762 763 764
    files = m.keys()
    files.sort()

    for f in files:
765
        ui.write("%40s %3s %s\n" % (hg.hex(m[f]), mf[f] and "755" or "644", f))
mpm@selenic.com's avatar
mpm@selenic.com committed
766

767
def parents(ui, repo, node=None):
768 769 770 771 772 773 774 775
    '''show the parents of the current working dir'''
    if node:
        p = repo.changelog.parents(repo.lookup(hg.bin(node)))
    else:
        p = repo.dirstate.parents()

    for n in p:
        if n != hg.nullid:
776
            show_changeset(ui, repo, changenode=n)
777

mpm@selenic.com's avatar
mpm@selenic.com committed
778
def pull(ui, repo, source="default", **opts):
779
    """pull changes from the specified source"""
780
    source = ui.expandpath(source)
mpm@selenic.com's avatar
mpm@selenic.com committed
781
    ui.status('pulling from %s\n' % (source))
782

783
    other = hg.repository(ui, source)
Matt Mackall's avatar
Matt Mackall committed
784 785
    r = repo.pull(other)
    if not r:
mpm@selenic.com's avatar
mpm@selenic.com committed
786 787
        if opts['update']:
            return update(ui, repo)
mpm@selenic.com's avatar
mpm@selenic.com committed
788
        else:
mpm@selenic.com's avatar
mpm@selenic.com committed
789 790 791
            ui.status("(run 'hg update' to get a working copy)\n")

    return r
792

793
def push(ui, repo, dest="default-push"):
mpm@selenic.com's avatar
mpm@selenic.com committed
794
    """push changes to the specified destination"""
795
    dest = ui.expandpath(dest)
796
    ui.status('pushing to %s\n' % (dest))
797

798 799 800
    other = hg.repository(ui, dest)
    r = repo.push(other)
    return r
mpm@selenic.com's avatar
mpm@selenic.com committed
801

mpm@selenic.com's avatar
mpm@selenic.com committed
802
def rawcommit(ui, repo, *flist, **rc):
803 804 805 806
    "raw commit interface"

    text = rc['text']
    if not text and rc['logfile']:
807 808 809 810
        try:
            text = open(rc['logfile']).read()
        except IOError:
            pass
811
    if not text and not rc['logfile']:
812
        ui.warn("abort: missing commit text\n")
813 814
        return 1

mpm@selenic.com's avatar
mpm@selenic.com committed
815
    files = relpath(repo, list(flist))
816 817
    if rc['files']:
        files += open(rc['files']).read().splitlines()
818 819

    rc['parent'] = map(repo.lookup, rc['parent'])
820

821
    repo.rawcommit(files, text, rc['user'], rc['date'], *rc['parent'])
822

823
def recover(ui, repo):
mpm@selenic.com's avatar
mpm@selenic.com committed
824
    """roll back an interrupted transaction"""
825 826
    repo.recover()

827
def remove(ui, repo, file1, *files):
828
    """remove the specified files on the next commit"""