commands.py 52.7 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 lock")
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 atexit")
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 relpath(repo, args):
27 28
    cwd = repo.getcwd()
    if cwd:
29
        return [util.normpath(os.path.join(cwd, x)) for x in args]
30
    return args
31

32 33
def matchpats(repo, cwd, pats = [], opts = {}, head = ''):
    return util.matcher(repo, cwd, pats or ['.'], opts.get('include'),
mpm@selenic.com's avatar
mpm@selenic.com committed
34 35
                        opts.get('exclude'), head)

Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
36
def makewalk(repo, pats, opts, head = ''):
37
    cwd = repo.getcwd()
38
    files, matchfn = matchpats(repo, cwd, pats, opts, head)
39
    exact = dict(zip(files, files))
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
40 41
    def walk():
        for src, fn in repo.walk(files = files, match = matchfn):
42
            yield src, fn, util.pathto(cwd, fn), fn in exact
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
43 44 45 46 47
    return files, matchfn, walk()

def walk(repo, pats, opts, head = ''):
    files, matchfn, results = makewalk(repo, pats, opts, head)
    for r in results: yield r
48

mpm@selenic.com's avatar
mpm@selenic.com committed
49 50
revrangesep = ':'

51
def revrange(ui, repo, revs, revlog=None):
mpm@selenic.com's avatar
mpm@selenic.com committed
52 53 54 55
    if revlog is None:
        revlog = repo.changelog
    revcount = revlog.count()
    def fix(val, defval):
56 57
        if not val:
            return defval
mpm@selenic.com's avatar
mpm@selenic.com committed
58 59
        try:
            num = int(val)
60 61 62 63
            if str(num) != val:
                raise ValueError
            if num < 0:
                num += revcount
mpm@selenic.com's avatar
mpm@selenic.com committed
64 65 66 67 68 69 70 71 72
            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:
73
                    raise util.Abort('invalid revision identifier %s', val)
mpm@selenic.com's avatar
mpm@selenic.com committed
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
        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

91 92
def make_filename(repo, r, pat, node=None,
                  total=None, seqno=None, revwidth=None):
93 94 95 96 97 98 99 100 101 102
    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),
        }

103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
    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)))

        newname = []
        patlen = len(pat)
        i = 0
        while i < patlen:
119
            c = pat[i]
120 121 122 123 124 125
            if c == '%':
                i += 1
                c = pat[i]
                c = expander[c]()
            newname.append(c)
            i += 1
126
        return ''.join(newname)
127
    except KeyError, inst:
128
        raise util.Abort("invalid format spec '%%%s' in output file name",
129
                    inst.args[0])
130

131 132 133 134 135 136 137 138 139 140 141 142
def make_file(repo, r, pat, node=None,
              total=None, seqno=None, revwidth=None, mode='wb'):
    if not pat or pat == '-':
        if 'w' in mode: return sys.stdout
        else: return sys.stdin
    if hasattr(pat, 'write') and 'w' in mode:
        return pat
    if hasattr(pat, 'read') and 'r' in mode:
        return pat
    return open(make_filename(repo, r, pat, node, total, seqno, revwidth),
                mode)

143
def dodiff(fp, ui, repo, node1, node2, files=None, match=util.always,
144
           changes=None, text=False):
145 146 147
    def date(c):
        return time.asctime(time.gmtime(float(c[2].split(' ')[0])))

148 149 150 151
    if not changes:
        (c, a, d, u) = repo.changes(node1, node2, files, match = match)
    else:
        (c, a, d, u) = changes
152 153 154 155 156 157
    if files:
        c, a, d = map(lambda x: filterfiles(files, x), (c, a, d))

    if not c and not a and not d:
        return

158 159 160 161
    if node2:
        change = repo.changelog.read(node2)
        mmap2 = repo.manifest.read(change[0])
        date2 = date(change)
162 163
        def read(f):
            return repo.file(f).read(mmap2[f])
164 165 166 167
    else:
        date2 = time.asctime()
        if not node1:
            node1 = repo.dirstate.parents()[0]
168 169
        def read(f):
            return repo.wfile(f).read()
170

171 172 173 174 175 176
    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]

177 178 179 180 181
    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
182 183 184
        to = None
        if f in mmap:
            to = repo.file(f).read(mmap[f])
185
        tn = read(f)
186
        fp.write(mdiff.unidiff(to, date1, tn, date2, f, r, text=text))
187
    for f in a:
188
        to = None
189
        tn = read(f)
190
        fp.write(mdiff.unidiff(to, date1, tn, date2, f, r, text=text))
191 192
    for f in d:
        to = repo.file(f).read(mmap[f])
193
        tn = None
194
        fp.write(mdiff.unidiff(to, date1, tn, date2, f, r, text=text))
195

196
def show_changeset(ui, repo, rev=0, changenode=None, filelog=None, brinfo=None):
197 198 199 200 201 202 203 204 205 206
    """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
207
        changerev = rev
208 209 210 211 212 213 214
        if changenode is None:
            changenode = changelog.node(changerev)
        elif not changerev:
            rev = changerev = changelog.rev(changenode)
        node = changenode

    if ui.quiet:
215
        ui.write("%d:%s\n" % (rev, hg.short(node)))
216 217 218 219
        return

    changes = changelog.read(changenode)

220
    t, tz = changes[2].split(' ')
221 222 223 224 225
    # a conversion tool was sticking non-integer offsets into repos
    try:
        tz = int(tz)
    except ValueError:
        tz = 0
226 227
    date = time.asctime(time.localtime(float(t))) + " %+05d" % (int(tz)/-36)

228 229 230
    parents = [(log.rev(p), ui.verbose and hg.hex(p) or hg.short(p))
               for p in log.parents(node)
               if ui.debugflag or p != hg.nullid]
231 232 233
    if not ui.debugflag and len(parents) == 1 and parents[0][0] == rev-1:
        parents = []

234 235 236 237 238
    if ui.verbose:
        ui.write("changeset:   %d:%s\n" % (changerev, hg.hex(changenode)))
    else:
        ui.write("changeset:   %d:%s\n" % (changerev, hg.short(changenode)))

Matt Mackall's avatar
Matt Mackall committed
239 240 241 242
    for tag in repo.nodetags(changenode):
        ui.status("tag:         %s\n" % tag)
    for parent in parents:
        ui.write("parent:      %d:%s\n" % parent)
243
    if filelog:
Matt Mackall's avatar
Matt Mackall committed
244
        ui.debug("file rev:    %d:%s\n" % (filerev, hg.hex(filenode)))
245

246 247 248 249
    if brinfo and changenode in brinfo:
        br = brinfo[changenode]
        ui.write("branch:      %s\n" % " ".join(br))

250
    ui.debug("manifest:    %d:%s\n" % (repo.manifest.rev(changes[0]),
Matt Mackall's avatar
Matt Mackall committed
251
                                      hg.hex(changes[0])))
252
    ui.status("user:        %s\n" % changes[1])
253
    ui.status("date:        %s\n" % date)
254

255
    if ui.debugflag:
256
        files = repo.changes(changelog.parents(changenode)[0], changenode)
257 258 259
        for key, value in zip(["files:", "files+:", "files-:"], files):
            if value:
                ui.note("%-12s %s\n" % (key, " ".join(value)))
260 261
    else:
        ui.note("files:       %s\n" % " ".join(changes[3]))
262

263
    description = changes[4].strip()
264
    if description:
mpm@selenic.com's avatar
mpm@selenic.com committed
265 266
        if ui.verbose:
            ui.status("description:\n")
267
            ui.status(description)
268
            ui.status("\n\n")
mpm@selenic.com's avatar
mpm@selenic.com committed
269
        else:
270
            ui.status("summary:     %s\n" % description.splitlines()[0])
271 272
    ui.status("\n")

273
def show_version(ui):
274
    """output version and copyright information"""
275 276
    ui.write("Mercurial Distributed SCM (version %s)\n"
             % version.get_version())
277 278 279 280 281 282 283
    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"
    )

284 285
def help_(ui, cmd=None):
    """show help for a given command or all commands"""
286
    if cmd and cmd != 'shortlist':
287 288
        key, i = find(cmd)
        # synopsis
289 290
        ui.write("%s\n\n" % i[2])

291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
        # description
        doc = i[0].__doc__
        if ui.quiet:
            doc = doc.splitlines(0)[0]
        ui.write("%s\n" % doc.rstrip())

        # aliases
        if not ui.quiet:
            aliases = ', '.join(key.split('|')[1:])
            if aliases:
                ui.write("\naliases: %s\n" % aliases)

        # options
        if not ui.quiet and i[1]:
            ui.write("\noptions:\n\n")
306 307 308 309 310 311 312 313 314 315 316 317
            for s, l, d, c in i[1]:
                opt = ' '
                if s:
                    opt = opt + '-' + s + ' '
                if l:
                    opt = opt + '--' + l + ' '
                if d:
                    opt = opt + '(' + str(d) + ')'
                ui.write(opt, "\n")
                if c:
                    ui.write('   %s\n' % c)

mpm@selenic.com's avatar
mpm@selenic.com committed
318
    else:
319
        # program name
mpm@selenic.com's avatar
mpm@selenic.com committed
320
        if ui.verbose:
321
            show_version(ui)
mpm@selenic.com's avatar
mpm@selenic.com committed
322
        else:
323 324
            ui.status("Mercurial Distributed SCM\n")
        ui.status('\n')
325

326
        # list of commands
327
        if cmd == "shortlist":
328 329 330 331 332 333 334
            ui.status('basic commands (use "hg help" '
                      'for the full list or option "-v" for details):\n\n')
        elif ui.verbose:
            ui.status('list of commands:\n\n')
        else:
            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
335 336

        h = {}
337
        cmds = {}
338 339
        for c, e in table.items():
            f = c.split("|")[0]
340
            if cmd == "shortlist" and not f.startswith("^"):
mpm@selenic.com's avatar
mpm@selenic.com committed
341
                continue
342
            f = f.lstrip("^")
mpm@selenic.com's avatar
mpm@selenic.com committed
343
            if not ui.debugflag and f.startswith("debug"):
344
                continue
mpm@selenic.com's avatar
mpm@selenic.com committed
345
            d = ""
346 347 348
            if e[0].__doc__:
                d = e[0].__doc__.splitlines(0)[0].rstrip()
            h[f] = d
349
            cmds[f]=c.lstrip("^")
mpm@selenic.com's avatar
mpm@selenic.com committed
350 351 352 353 354

        fns = h.keys()
        fns.sort()
        m = max(map(len, fns))
        for f in fns:
355 356 357 358 359
            if ui.verbose:
                commands = cmds[f].replace("|",", ")
                ui.write(" %s:\n      %s\n"%(commands,h[f]))
            else:
                ui.write(' %-*s   %s\n' % (m, f, h[f]))
mpm@selenic.com's avatar
mpm@selenic.com committed
360

361 362 363 364 365 366 367 368 369 370 371 372 373 374
    # global options
    if ui.verbose:
        ui.write("\nglobal options:\n\n")
        for s, l, d, c in globalopts:
            opt = ' '
            if s:
                opt = opt + '-' + s + ' '
            if l:
                opt = opt + '--' + l + ' '
            if d:
                opt = opt + '(' + str(d) + ')'
            ui.write(opt, "\n")
            if c:
                ui.write('    %s\n' % c)
mpm@selenic.com's avatar
mpm@selenic.com committed
375 376

# Commands start here, listed alphabetically
377

378
def add(ui, repo, *pats, **opts):
379
    '''add the specified files on the next commit'''
380
    names = []
381 382
    for src, abs, rel, exact in walk(repo, pats, opts):
        if exact:
383 384 385 386 387
            names.append(abs)
        elif repo.dirstate.state(abs) == '?':
            ui.status('adding %s\n' % rel)
            names.append(abs)
    repo.add(names)
388

389
def addremove(ui, repo, *pats, **opts):
mpm@selenic.com's avatar
mpm@selenic.com committed
390
    """add all new files, delete all missing files"""
391
    add, remove = [], []
392
    for src, abs, rel, exact in walk(repo, pats, opts):
393 394
        if src == 'f' and repo.dirstate.state(abs) == '?':
            add.append(abs)
395
            if not exact: ui.status('adding ', rel, '\n')
396
        if repo.dirstate.state(abs) != 'r' and not os.path.exists(rel):
397
            remove.append(abs)
398
            if not exact: ui.status('removing ', rel, '\n')
399 400
    repo.add(add)
    repo.remove(remove)
401

402
def annotate(ui, repo, *pats, **opts):
mpm@selenic.com's avatar
mpm@selenic.com committed
403
    """show changeset information per file line"""
404 405 406 407 408 409 410 411 412 413 414 415
    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]
416 417 418
            f = name.find('<')
            if f >= 0:
                name = name[f+1:]
419 420
            bcache[rev] = name
            return name
421

422
    if not pats:
423
        raise util.Abort('at least one file name or pattern required')
424

425 426
    bcache = {}
    opmap = [['user', getname], ['number', str], ['changeset', getnode]]
427 428
    if not opts['user'] and not opts['changeset']:
        opts['number'] = 1
429

430 431
    if opts['rev']:
        node = repo.changelog.lookup(opts['rev'])
432 433
    else:
        node = repo.dirstate.parents()[0]
434 435
    change = repo.changelog.read(node)
    mmap = repo.manifest.read(change[0])
436
    for src, abs, rel, exact in walk(repo, pats, opts):
mpm@selenic.com's avatar
mpm@selenic.com committed
437 438 439 440
        if abs not in mmap:
            ui.warn("warning: %s is not in the repository!\n" % rel)
            continue

441
        lines = repo.file(abs).annotate(mmap[abs])
442 443 444
        pieces = []

        for o, f in opmap:
445
            if opts[o]:
446
                l = [f(n) for n, dummy in lines]
mpm@selenic.com's avatar
mpm@selenic.com committed
447 448 449
                if l:
                    m = max(map(len, l))
                    pieces.append(["%*s" % (m, x) for x in l])
450

mpm@selenic.com's avatar
mpm@selenic.com committed
451 452 453
        if pieces:
            for p, l in zip(zip(*pieces), lines):
                ui.write("%s: %s" % (" ".join(p), l[1]))
454

455
def cat(ui, repo, file1, rev=None, **opts):
mpm@selenic.com's avatar
mpm@selenic.com committed
456
    """output the latest or given revision of a file"""
457 458
    r = repo.file(relpath(repo, [file1])[0])
    if rev:
459 460 461 462 463 464 465 466
        try:
            # assume all revision numbers are for changesets
            n = repo.lookup(rev)
            change = repo.changelog.read(n)
            m = repo.manifest.read(change[0])
            n = m[relpath(repo, [file1])[0]]
        except hg.RepoError, KeyError:
            n = r.lookup(rev)
467 468
    else:
        n = r.tip()
469
    fp = make_file(repo, r, opts['output'], node=n)
470
    fp.write(r.read(n))
mpm@selenic.com's avatar
mpm@selenic.com committed
471

472
def clone(ui, source, dest=None, **opts):
mpm@selenic.com's avatar
mpm@selenic.com committed
473 474
    """make a copy of an existing repository"""
    if dest is None:
475
        dest = os.path.basename(os.path.normpath(source))
476 477 478 479

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

481 482
    dest = os.path.realpath(dest)

483 484
    class Dircleanup:
        def __init__(self, dir_):
Matt Mackall's avatar
Matt Mackall committed
485
            self.rmtree = shutil.rmtree
486 487
            self.dir_ = dir_
            os.mkdir(dir_)
488
        def close(self):
489
            self.dir_ = None
490
        def __del__(self):
491 492
            if self.dir_:
                self.rmtree(self.dir_, True)
493

494 495 496 497 498
    if opts['ssh']:
        ui.setconfig("ui", "ssh", opts['ssh'])
    if opts['remotecmd']:
        ui.setconfig("ui", "remotecmd", opts['remotecmd'])

499
    d = Dircleanup(dest)
500
    source = ui.expandpath(source)
501
    abspath = source
502 503
    other = hg.repository(ui, source)

Mikael Berthe's avatar
Mikael Berthe committed
504 505
    if other.dev() != -1:
        abspath = os.path.abspath(source)
506 507 508 509
        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")
510 511 512
        # we use a lock here because because we're not nicely ordered
        l = lock.lock(os.path.join(source, ".hg", "lock"))

513 514
        util.copytree(os.path.join(source, ".hg"), os.path.join(dest, ".hg"),
                      copyfile)
515
        try:
516
            os.unlink(os.path.join(dest, ".hg", "dirstate"))
517
        except OSError:
518
            pass
mpm@selenic.com's avatar
mpm@selenic.com committed
519

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

522 523
    else:
        repo = hg.repository(ui, dest, create=1)
Matt Mackall's avatar
Matt Mackall committed
524
        repo.pull(other)
525

526 527
    f = repo.opener("hgrc", "w")
    f.write("[paths]\n")
528
    f.write("default = %s\n" % abspath)
529

530 531 532 533
    if not opts['noupdate']:
        update(ui, repo)

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

535
def commit(ui, repo, *pats, **opts):
536
    """commit the specified files or all outstanding changes"""
537 538 539
    if opts['text']:
        ui.warn("Warning: -t and --text is deprecated,"
                " please use -m or --message instead.\n")
540
    message = opts['message'] or opts['text']
541
    logfile = opts['logfile']
542
    if not message and logfile:
543
        try:
544 545 546 547
            if logfile == '-':
                message = sys.stdin.read()
            else:
                message = open(logfile).read()
548
        except IOError, why:
549
            ui.warn("Can't read commit message %s: %s\n" % (logfile, why))
550

mpm@selenic.com's avatar
mpm@selenic.com committed
551
    if opts['addremove']:
552 553 554 555 556
        addremove(ui, repo, *pats, **opts)
    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']]
557
    fns, match = matchpats(repo, (pats and repo.getcwd()) or '', pats, opts)
558 559 560 561 562 563
    if pats:
        c, a, d, u = repo.changes(files = fns, match = match)
        files = c + a + [fn for fn in d if repo.dirstate.state(fn) == 'r']
    else:
        files = []
    repo.commit(files, message, opts['user'], opts['date'], match)
564

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

569 570
def debugcheckstate(ui, repo):
    """validate the correctness of the current dirstate"""
571
    parent1, parent2 = repo.dirstate.parents()
572 573
    repo.dirstate.read()
    dc = repo.dirstate.map
574 575 576 577 578 579 580 581 582 583
    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:
584
            ui.warn("%s in state %s, but not in manifest1\n" % (f, state))
585 586
            errors += 1
        if state in "a" and f in m1:
587
            ui.warn("%s in state %s, but also in manifest1\n" % (f, state))
588 589
            errors += 1
        if state in "m" and f not in m1 and f not in m2:
590 591
            ui.warn("%s in state %s, but not in either manifest\n" %
                    (f, state))
592 593 594 595
            errors += 1
    for f in m1:
        state = repo.dirstate.state(f)
        if state not in "nrm":
596
            ui.warn("%s in manifest1, but listed as state %s" % (f, state))
597 598
            errors += 1
    if errors:
599
        raise util.Abort(".hg/dirstate inconsistent with current parent's manifest")
600

601 602
def debugstate(ui, repo):
    """show the contents of the current dirstate"""
603 604
    repo.dirstate.read()
    dc = repo.dirstate.map
605 606
    keys = dc.keys()
    keys.sort()
607
    for file_ in keys:
608 609 610 611
        ui.write("%c %3o %10d %s %s\n"
                 % (dc[file_][0], dc[file_][1] & 0777, dc[file_][2],
                    time.strftime("%x %X",
                                  time.localtime(dc[file_][3])), file_))
612

613
def debugindex(ui, file_):
614
    """dump the contents of an index file"""
615
    r = hg.revlog(hg.opener(""), file_, "")
616
    ui.write("   rev    offset  length   base linkrev" +
mpm@selenic.com's avatar
mpm@selenic.com committed
617
             " nodeid       p1           p2\n")
mpm@selenic.com's avatar
mpm@selenic.com committed
618 619
    for i in range(r.count()):
        e = r.index[i]
mpm@selenic.com's avatar
mpm@selenic.com committed
620
        ui.write("% 6d % 9d % 7d % 6d % 7d %s %s %s\n" % (
621
                i, e[0], e[1], e[2], e[3],
mpm@selenic.com's avatar
mpm@selenic.com committed
622
            hg.short(e[6]), hg.short(e[4]), hg.short(e[5])))
mpm@selenic.com's avatar
mpm@selenic.com committed
623

624
def debugindexdot(ui, file_):
625
    """dump an index DAG as a .dot file"""
626
    r = hg.revlog(hg.opener(""), file_, "")
627
    ui.write("digraph G {\n")
mpm@selenic.com's avatar
mpm@selenic.com committed
628 629
    for i in range(r.count()):
        e = r.index[i]
630
        ui.write("\t%d -> %d\n" % (r.rev(e[4]), i))
mpm@selenic.com's avatar
mpm@selenic.com committed
631
        if e[5] != hg.nullid:
632 633
            ui.write("\t%d -> %d\n" % (r.rev(e[5]), i))
    ui.write("}\n")
mpm@selenic.com's avatar
mpm@selenic.com committed
634

635 636
def debugwalk(ui, repo, *pats, **opts):
    items = list(walk(repo, pats, opts))
637
    if not items: return
638 639 640 641 642 643
    fmt = '%%s  %%-%ds  %%-%ds  %%s' % (
        max([len(abs) for (src, abs, rel, exact) in items]),
        max([len(rel) for (src, abs, rel, exact) in items]))
    exactly = {True: 'exact', False: ''}
    for src, abs, rel, exact in items:
        print fmt % (src, abs, rel, exactly[exact])
644

645
def diff(ui, repo, *pats, **opts):
mpm@selenic.com's avatar
mpm@selenic.com committed
646
    """diff working directory (or selected files)"""
647
    node1, node2 = None, None
648 649
    revs = [repo.lookup(x) for x in opts['rev']]

650 651 652 653
    if len(revs) > 0:
        node1 = revs[0]
    if len(revs) > 1:
        node2 = revs[1]
654
    if len(revs) > 2:
655
        raise util.Abort("too many revisions to diff")
656

657
    files = []
658 659 660
    match = util.always
    if pats:
        roots, match, results = makewalk(repo, pats, opts)
661
        for src, abs, rel, exact in results:
662
            files.append(abs)
663

664 665
    dodiff(sys.stdout, ui, repo, node1, node2, files, match=match,
           text=opts['text'])
666

mpm@selenic.com's avatar
mpm@selenic.com committed
667
def doexport(ui, repo, changeset, seqno, total, revwidth, opts):
668 669 670
    node = repo.lookup(changeset)
    prev, other = repo.changelog.parents(node)
    change = repo.changelog.read(node)
671

672 673 674 675
    fp = make_file(repo, repo.changelog, opts['output'],
                   node=node, total=total, seqno=seqno,
                   revwidth=revwidth)
    if fp != sys.stdout:
676
        ui.note("%s\n" % fp.name)
mpm@selenic.com's avatar
mpm@selenic.com committed
677

678 679 680 681
    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
682
    if other != hg.nullid:
683 684 685
        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
686

687
    dodiff(fp, ui, repo, prev, node, text=opts['text'])
688
    if fp != sys.stdout: fp.close()
mpm@selenic.com's avatar
mpm@selenic.com committed
689 690 691

def export(ui, repo, *changesets, **opts):
    """dump the header and diffs for one or more changesets"""
692
    if not changesets:
693
        raise util.Abort("export requires at least one changeset")
mpm@selenic.com's avatar
mpm@selenic.com committed
694 695 696 697
    seqno = 0
    revs = list(revrange(ui, repo, changesets))
    total = len(revs)
    revwidth = max(len(revs[0]), len(revs[-1]))
698
    ui.note(len(revs) > 1 and "Exporting patches:\n" or "Exporting patch:\n")
mpm@selenic.com's avatar
mpm@selenic.com committed
699 700 701
    for cset in revs:
        seqno += 1
        doexport(ui, repo, cset, seqno, total, revwidth, opts)
702

703
def forget(ui, repo, *pats, **opts):
704
    """don't add the specified files on the next commit"""
705
    forget = []
706
    for src, abs, rel, exact in walk(repo, pats, opts):
707 708
        if repo.dirstate.state(abs) == 'a':
            forget.append(abs)
709
            if not exact: ui.status('forgetting ', rel, '\n')
710
    repo.forget(forget)
711

712
def heads(ui, repo, **opts):
713
    """show current repository heads"""
714 715 716 717
    heads = repo.changelog.heads()
    br = None
    if opts['branches']:
        br = repo.branchlookup(heads)
718
    for n in repo.changelog.heads():
719
        show_changeset(ui, repo, changenode=n, brinfo=br)
720

721 722
def identify(ui, repo):
    """print information about the working copy"""
mpm@selenic.com's avatar
mpm@selenic.com committed
723
    parents = [p for p in repo.dirstate.parents() if p != hg.nullid]
724
    if not parents:
mpm@selenic.com's avatar
mpm@selenic.com committed
725
        ui.write("unknown\n")
726 727
        return

728
    hexfunc = ui.verbose and hg.hex or hg.short
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
729
    (c, a, d, u) = repo.changes()
730 731 732
    output = ["%s%s" % ('+'.join([hexfunc(parent) for parent in parents]),
                        (c or a or d) and "+" or "")]

733
    if not ui.quiet:
734 735 736 737
        # 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 ' + '
738 739
        if parenttags:
            output.append(' + '.join(parenttags))
740

741
    ui.write("%s\n" % ' '.join(output))
742

mpm@selenic.com's avatar
mpm@selenic.com committed
743 744 745
def import_(ui, repo, patch1, *patches, **opts):
    """import an ordered set of patches"""
    patches = (patch1,) + patches
746

747 748 749 750 751 752
    if not opts['force']:
        (c, a, d, u) = repo.changes()
        if c or a or d:
            ui.warn("abort: outstanding uncommitted changes!\n")
            return 1

mpm@selenic.com's avatar
mpm@selenic.com committed
753 754 755 756 757 758 759
    d = opts["base"]
    strip = opts["strip"]

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

760
        message = []
761 762 763 764 765
        user = None
        hgpatch = False
        for line in file(pf):
            line = line.rstrip()
            if line.startswith("--- ") or line.startswith("diff -r"):
766
                break
767 768 769 770
            elif hgpatch:
                # parse values when importing the result of an hg export
                if line.startswith("# User "):
                    user = line[7:]
771
                    ui.debug('User: %s\n' % user)
772
                elif not line.startswith("# ") and line:
773
                    message.append(line)
774 775 776
                    hgpatch = False
            elif line == '# HG changeset patch':
                hgpatch = True
777
                message = []       # We may have collected garbage
778
            else:
779
                message.append(line)
780