commands.py 43.3 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 18 19
class Abort(Exception):
    """Raised if a command needs to print an error and exit."""

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

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

29
def relfilter(repo, files):
30 31 32
    cwd = repo.getcwd()
    if cwd:
        return filterfiles([util.pconvert(cwd)], files)
33
    return files
34

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

42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
def matchpats(ui, cwd, pats = [], opts = {}):
    head = ''
    if opts.get('rootless'): head = '(?:.*/|)'
    def reify(name, tail):
        if name.startswith('re:'):
            return name[3:]
        elif name.startswith('glob:'):
            return head + util.globre(name[5:], '', tail)
        elif name.startswith('path:'):
            return '^' + re.escape(name[5:]) + '$'
        return head + util.globre(name, '', tail)
    cwdsep = cwd + os.sep
    def under(fn):
        if not cwd or fn.startswith(cwdsep): return True
    def matchfn(pats, tail, ifempty = util.always):
        if not pats: return ifempty
        pat = '(?:%s)' % '|'.join([reify(p, tail) for p in pats])
        if cwd: pat = re.escape(cwd + os.sep) + pat
        ui.debug('regexp: %s\n' % pat)
        return re.compile(pat).match
    patmatch = matchfn(pats, '$')
    incmatch = matchfn(opts.get('include'), '(?:/|$)', under)
    excmatch = matchfn(opts.get('exclude'), '(?:/|$)', util.never)
    return lambda fn: (incmatch(fn) and not excmatch(fn) and
                       (fn.endswith('/') or patmatch(fn)))

def walk(repo, pats, opts):
    cwd = repo.getcwd()
    if cwd: c = len(cwd) + 1
71 72 73
    for src, fn in repo.walk(match = matchpats(repo.ui, cwd, pats, opts)):
        if cwd: yield src, fn, fn[c:]
        else: yield src, fn, fn
74

mpm@selenic.com's avatar
mpm@selenic.com committed
75 76
revrangesep = ':'

77
def revrange(ui, repo, revs, revlog=None):
mpm@selenic.com's avatar
mpm@selenic.com committed
78 79 80 81
    if revlog is None:
        revlog = repo.changelog
    revcount = revlog.count()
    def fix(val, defval):
82 83
        if not val:
            return defval
mpm@selenic.com's avatar
mpm@selenic.com committed
84 85
        try:
            num = int(val)
86 87 88 89
            if str(num) != val:
                raise ValueError
            if num < 0:
                num += revcount
mpm@selenic.com's avatar
mpm@selenic.com committed
90 91 92 93 94 95 96 97 98
            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:
99
                    raise Abort('invalid revision identifier %s', val)
mpm@selenic.com's avatar
mpm@selenic.com committed
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
        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

117 118 119 120 121
def make_file(repo, r, pat, node=None,
              total=None, seqno=None, revwidth=None, mode='wb'):
    if pat == '-':
        if 'w' in mode: return sys.stdout
        else: return sys.stdin
122 123 124 125 126 127 128 129 130 131
    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),
        }

132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
    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:
148
            c = pat[i]
149 150 151 152 153 154
            if c == '%':
                i += 1
                c = pat[i]
                c = expander[c]()
            newname.append(c)
            i += 1
155
        return open(''.join(newname), mode)
156 157 158
    except KeyError, inst:
        raise Abort("invalid format spec '%%%s' in output file name",
                    inst.args[0])
159

160
def dodiff(fp, ui, repo, files=None, node1=None, node2=None):
161 162 163
    def date(c):
        return time.asctime(time.gmtime(float(c[2].split(' ')[0])))

mpm@selenic.com's avatar
mpm@selenic.com committed
164
    (c, a, d, u) = repo.changes(node1, node2, files)
165 166 167 168 169 170
    if files:
        c, a, d = map(lambda x: filterfiles(files, x), (c, a, d))

    if not c and not a and not d:
        return

171 172 173 174
    if node2:
        change = repo.changelog.read(node2)
        mmap2 = repo.manifest.read(change[0])
        date2 = date(change)
175 176
        def read(f):
            return repo.file(f).read(mmap2[f])
177 178 179 180
    else:
        date2 = time.asctime()
        if not node1:
            node1 = repo.dirstate.parents()[0]
181 182
        def read(f):
            return repo.wfile(f).read()
183

184 185 186 187 188 189
    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]

190 191 192 193 194
    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
195 196 197
        to = None
        if f in mmap:
            to = repo.file(f).read(mmap[f])
198
        tn = read(f)
mpm@selenic.com's avatar
mpm@selenic.com committed
199
        fp.write(mdiff.unidiff(to, date1, tn, date2, f, r))
200
    for f in a:
201
        to = None
202
        tn = read(f)
mpm@selenic.com's avatar
mpm@selenic.com committed
203
        fp.write(mdiff.unidiff(to, date1, tn, date2, f, r))
204 205
    for f in d:
        to = repo.file(f).read(mmap[f])
206
        tn = None
mpm@selenic.com's avatar
mpm@selenic.com committed
207
        fp.write(mdiff.unidiff(to, date1, tn, date2, f, r))
208 209 210 211 212 213 214 215 216 217 218 219

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
220
        changerev = rev
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
        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
239 240 241 242 243
    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)
244
    if filelog:
Matt Mackall's avatar
Matt Mackall committed
245 246 247
        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])))
248 249 250
    ui.status("user:        %s\n" % changes[1])
    ui.status("date:        %s\n" % time.asctime(
        time.localtime(float(changes[2].split(' ')[0]))))
251
    if ui.debugflag:
252
        files = repo.changes(changelog.parents(changenode)[0], changenode)
253 254 255
        for key, value in zip(["files:", "files+:", "files-:"], files):
            if value:
                ui.note("%-12s %s\n" % (key, " ".join(value)))
256 257
    else:
        ui.note("files:       %s\n" % " ".join(changes[3]))
258
    description = changes[4].strip()
259
    if description:
mpm@selenic.com's avatar
mpm@selenic.com committed
260 261
        if ui.verbose:
            ui.status("description:\n")
262
            ui.status(description)
263
            ui.status("\n\n")
mpm@selenic.com's avatar
mpm@selenic.com committed
264
        else:
265
            ui.status("summary:     %s\n" % description.splitlines()[0])
266 267
    ui.status("\n")

268
def show_version(ui):
269 270 271 272 273 274 275 276 277
    """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"
    )

278 279
def help_(ui, cmd=None):
    """show help for a given command or all commands"""
280 281 282 283
    if cmd:
        try:
            i = find(cmd)
            ui.write("%s\n\n" % i[2])
mpm@selenic.com's avatar
mpm@selenic.com committed
284 285 286

            if i[1]:
                for s, l, d, c in i[1]:
287 288 289 290 291 292 293
                    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
294
                    ui.write(opt, "\n")
295 296
                    if c:
                        ui.write('   %s\n' % c)
mpm@selenic.com's avatar
mpm@selenic.com committed
297 298
                ui.write("\n")

299 300
            ui.write(i[0].__doc__, "\n")
        except UnknownCommand:
mpm@selenic.com's avatar
mpm@selenic.com committed
301
            ui.warn("hg: unknown command %s\n" % cmd)
302
        sys.exit(0)
mpm@selenic.com's avatar
mpm@selenic.com committed
303
    else:
mpm@selenic.com's avatar
mpm@selenic.com committed
304
        if ui.verbose:
305
            show_version(ui)
306
            ui.write('\n')
mpm@selenic.com's avatar
mpm@selenic.com committed
307 308 309
        if ui.verbose:
            ui.write('hg commands:\n\n')
        else:
310
            ui.write('basic hg commands (use "hg help -v" for more):\n\n')
mpm@selenic.com's avatar
mpm@selenic.com committed
311 312

        h = {}
313 314
        for c, e in table.items():
            f = c.split("|")[0]
mpm@selenic.com's avatar
mpm@selenic.com committed
315 316 317
            if not ui.verbose and not f.startswith("^"):
                continue
            if not ui.debugflag and f.startswith("debug"):
318
                continue
mpm@selenic.com's avatar
mpm@selenic.com committed
319
            f = f.lstrip("^")
mpm@selenic.com's avatar
mpm@selenic.com committed
320
            d = ""
321 322 323
            if e[0].__doc__:
                d = e[0].__doc__.splitlines(0)[0].rstrip()
            h[f] = d
mpm@selenic.com's avatar
mpm@selenic.com committed
324 325 326 327 328

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

# Commands start here, listed alphabetically
332

333
def add(ui, repo, *pats, **opts):
334
    '''add the specified files on the next commit'''
335 336
    names = []
    q = dict(zip(pats, pats))
337
    for src, abs, rel in walk(repo, pats, opts):
338 339 340 341 342 343
        if rel in q or abs in q:
            names.append(abs)
        elif repo.dirstate.state(abs) == '?':
            ui.status('adding %s\n' % rel)
            names.append(abs)
    repo.add(names)
344

345
def addremove(ui, repo, *files):
mpm@selenic.com's avatar
mpm@selenic.com committed
346
    """add all new files, delete all missing files"""
347 348 349 350 351 352 353 354 355 356 357 358 359
    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
360
        (c, a, d, u) = repo.changes()
mpm@selenic.com's avatar
mpm@selenic.com committed
361
    repo.add(u)
362
    repo.remove(d)
363

364
def annotate(ui, repo, file1, *files, **opts):
mpm@selenic.com's avatar
mpm@selenic.com committed
365
    """show changeset information per file line"""
366 367 368 369 370 371 372 373 374 375 376 377
    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]
378 379 380
            f = name.find('<')
            if f >= 0:
                name = name[f+1:]
381 382
            bcache[rev] = name
            return name
383

384 385
    bcache = {}
    opmap = [['user', getname], ['number', str], ['changeset', getnode]]
386 387
    if not opts['user'] and not opts['changeset']:
        opts['number'] = 1
388

389 390
    if opts['rev']:
        node = repo.changelog.lookup(opts['rev'])
391 392
    else:
        node = repo.dirstate.parents()[0]
393 394
    change = repo.changelog.read(node)
    mmap = repo.manifest.read(change[0])
395
    for f in relpath(repo, (file1,) + files):
396 397 398 399
        lines = repo.file(f).annotate(mmap[f])
        pieces = []

        for o, f in opmap:
400
            if opts[o]:
401
                l = [f(n) for n, dummy in lines]
402
                m = max(map(len, l))
403
                pieces.append(["%*s" % (m, x) for x in l])
404

405
        for p, l in zip(zip(*pieces), lines):
406
            ui.write("%s: %s" % (" ".join(p), l[1]))
407

408
def cat(ui, repo, file1, rev=None, **opts):
mpm@selenic.com's avatar
mpm@selenic.com committed
409
    """output the latest or given revision of a file"""
410 411 412 413 414
    r = repo.file(relpath(repo, [file1])[0])
    if rev:
        n = r.lookup(rev)
    else:
        n = r.tip()
415
    fp = make_file(repo, r, opts['output'], node=n)
416
    fp.write(r.read(n))
mpm@selenic.com's avatar
mpm@selenic.com committed
417

418
def clone(ui, source, dest=None, **opts):
mpm@selenic.com's avatar
mpm@selenic.com committed
419 420
    """make a copy of an existing repository"""
    if dest is None:
421
        dest = os.path.basename(os.path.normpath(source))
422 423 424 425

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

427 428
    class Dircleanup:
        def __init__(self, dir_):
Matt Mackall's avatar
Matt Mackall committed
429
            self.rmtree = shutil.rmtree
430 431
            self.dir_ = dir_
            os.mkdir(dir_)
432
        def close(self):
433
            self.dir_ = None
434
        def __del__(self):
435 436
            if self.dir_:
                self.rmtree(self.dir_, True)
437

438
    d = Dircleanup(dest)
439
    abspath = source
440 441 442
    source = ui.expandpath(source)
    other = hg.repository(ui, source)

Mikael Berthe's avatar
Mikael Berthe committed
443 444
    if other.dev() != -1:
        abspath = os.path.abspath(source)
445 446 447 448 449 450
        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)
451
        try:
452 453 454
            os.unlink(os.path.join(dest, ".hg", "dirstate"))
        except IOError:
            pass
mpm@selenic.com's avatar
mpm@selenic.com committed
455

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

458 459
    else:
        repo = hg.repository(ui, dest, create=1)
Matt Mackall's avatar
Matt Mackall committed
460
        repo.pull(other)
461

462 463
    f = repo.opener("hgrc", "w")
    f.write("[paths]\n")
464
    f.write("default = %s\n" % abspath)
465

466 467 468 469
    if not opts['noupdate']:
        update(ui, repo)

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

471
def commit(ui, repo, *files, **opts):
472
    """commit the specified files or all outstanding changes"""
473
    text = opts['text']
474 475 476 477 478 479
    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))
480

mpm@selenic.com's avatar
mpm@selenic.com committed
481 482
    if opts['addremove']:
        addremove(ui, repo, *files)
483
    repo.commit(relpath(repo, files), text, opts['user'], opts['date'])
484

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

489 490
def debugcheckstate(ui, repo):
    """validate the correctness of the current dirstate"""
491
    parent1, parent2 = repo.dirstate.parents()
492 493
    repo.dirstate.read()
    dc = repo.dirstate.map
494 495 496 497 498 499 500 501 502 503
    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:
504
            ui.warn("%s in state %s, but not in manifest1\n" % (f, state))
505 506
            errors += 1
        if state in "a" and f in m1:
507
            ui.warn("%s in state %s, but also in manifest1\n" % (f, state))
508 509
            errors += 1
        if state in "m" and f not in m1 and f not in m2:
510 511
            ui.warn("%s in state %s, but not in either manifest\n" %
                    (f, state))
512 513 514 515
            errors += 1
    for f in m1:
        state = repo.dirstate.state(f)
        if state not in "nrm":
516
            ui.warn("%s in manifest1, but listed as state %s" % (f, state))
517 518
            errors += 1
    if errors:
519
        raise Abort(".hg/dirstate inconsistent with current parent's manifest")
520

521 522
def debugstate(ui, repo):
    """show the contents of the current dirstate"""
523 524
    repo.dirstate.read()
    dc = repo.dirstate.map
525 526
    keys = dc.keys()
    keys.sort()
527 528
    for file_ in keys:
        ui.write("%c %s\n" % (dc[file_][0], file_))
529

530
def debugindex(ui, file_):
531
    """dump the contents of an index file"""
532
    r = hg.revlog(hg.opener(""), file_, "")
533 534
    ui.write("   rev    offset  length   base linkrev" +
             " p1           p2           nodeid\n")
mpm@selenic.com's avatar
mpm@selenic.com committed
535 536
    for i in range(r.count()):
        e = r.index[i]
537 538 539
        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
540

541
def debugindexdot(ui, file_):
542
    """dump an index DAG as a .dot file"""
543
    r = hg.revlog(hg.opener(""), file_, "")
544
    ui.write("digraph G {\n")
mpm@selenic.com's avatar
mpm@selenic.com committed
545 546
    for i in range(r.count()):
        e = r.index[i]
547
        ui.write("\t%d -> %d\n" % (r.rev(e[4]), i))
mpm@selenic.com's avatar
mpm@selenic.com committed
548
        if e[5] != hg.nullid:
549 550
            ui.write("\t%d -> %d\n" % (r.rev(e[5]), i))
    ui.write("}\n")
mpm@selenic.com's avatar
mpm@selenic.com committed
551

552
def diff(ui, repo, *pats, **opts):
mpm@selenic.com's avatar
mpm@selenic.com committed
553
    """diff working directory (or selected files)"""
554 555 556
    revs = []
    if opts['rev']:
        revs = map(lambda x: repo.lookup(x), opts['rev'])
557

558
    if len(revs) > 2:
559
        raise Abort("too many revisions to diff")
560

561 562 563
    files = []
    for src, abs, rel in walk(repo, pats, opts):
        files.append(abs)
mpm@selenic.com's avatar
mpm@selenic.com committed
564
    dodiff(sys.stdout, ui, repo, files, *revs)
565

mpm@selenic.com's avatar
mpm@selenic.com committed
566
def doexport(ui, repo, changeset, seqno, total, revwidth, opts):
567 568 569
    node = repo.lookup(changeset)
    prev, other = repo.changelog.parents(node)
    change = repo.changelog.read(node)
570

571 572 573 574 575
    fp = make_file(repo, repo.changelog, opts['output'],
                   node=node, total=total, seqno=seqno,
                   revwidth=revwidth)
    if fp != sys.stdout:
        ui.note("Exporting patch to '%s'.\n" % fp.name)
mpm@selenic.com's avatar
mpm@selenic.com committed
576

577 578 579 580
    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
581
    if other != hg.nullid:
582 583 584
        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
585 586 587 588 589

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

def export(ui, repo, *changesets, **opts):
    """dump the header and diffs for one or more changesets"""
590
    if not changesets:
591
        raise Abort("export requires at least one changeset")
mpm@selenic.com's avatar
mpm@selenic.com committed
592 593 594 595 596 597 598
    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)
599

600
def forget(ui, repo, file1, *files):
601
    """don't add the specified files on the next commit"""
602
    repo.forget(relpath(repo, (file1,) + files))
603

604
def heads(ui, repo):
605
    """show current repository heads"""
606
    for n in repo.changelog.heads():
607
        show_changeset(ui, repo, changenode=n)
608

609 610
def identify(ui, repo):
    """print information about the working copy"""
mpm@selenic.com's avatar
mpm@selenic.com committed
611
    parents = [p for p in repo.dirstate.parents() if p != hg.nullid]
612
    if not parents:
mpm@selenic.com's avatar
mpm@selenic.com committed
613
        ui.write("unknown\n")
614 615
        return

616
    hexfunc = ui.verbose and hg.hex or hg.short
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
617
    (c, a, d, u) = repo.changes()
618 619 620
    output = ["%s%s" % ('+'.join([hexfunc(parent) for parent in parents]),
                        (c or a or d) and "+" or "")]

621
    if not ui.quiet:
622 623 624 625 626
        # 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))
627

628
    ui.write("%s\n" % ' '.join(output))
629

mpm@selenic.com's avatar
mpm@selenic.com committed
630 631 632 633 634
def import_(ui, repo, patch1, *patches, **opts):
    """import an ordered set of patches"""
    try:
        import psyco
        psyco.full()
635
    except ImportError:
mpm@selenic.com's avatar
mpm@selenic.com committed
636 637 638
        pass

    patches = (patch1,) + patches
639

mpm@selenic.com's avatar
mpm@selenic.com committed
640 641 642 643 644 645 646
    d = opts["base"]
    strip = opts["strip"]

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

647 648 649 650 651 652
        text = []
        user = None
        hgpatch = False
        for line in file(pf):
            line = line.rstrip()
            if line.startswith("--- ") or line.startswith("diff -r"):
653
                break
654 655 656 657
            elif hgpatch:
                # parse values when importing the result of an hg export
                if line.startswith("# User "):
                    user = line[7:]
658
                    ui.debug('User: %s\n' % user)
659 660 661 662 663 664 665
                elif not line.startswith("# ") and line:
                    text.append(line)
                    hgpatch = False
            elif line == '# HG changeset patch':
                hgpatch = True
            else:
                text.append(line)
666

mpm@selenic.com's avatar
mpm@selenic.com committed
667
        # make sure text isn't empty
668 669
        if not text:
            text = "imported patch %s\n" % patch
670 671 672
        else:
            text = "%s\n" % '\n'.join(text)
        ui.debug('text:\n%s\n' % text)
mpm@selenic.com's avatar
mpm@selenic.com committed
673 674 675 676 677

        f = os.popen("patch -p%d < %s" % (strip, pf))
        files = []
        for l in f.read().splitlines():
            l.rstrip('\r\n');
678
            ui.status("%s\n" % l)
679
            if l.startswith('patching file '):
mpm@selenic.com's avatar
mpm@selenic.com committed
680 681 682 683 684
                pf = l[14:]
                if pf not in files:
                    files.append(pf)
        patcherr = f.close()
        if patcherr:
685
            raise Abort("patch failed")
mpm@selenic.com's avatar
mpm@selenic.com committed
686 687 688

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

mpm@selenic.com's avatar
mpm@selenic.com committed
691 692
def init(ui, source=None):
    """create a new repository in the current directory"""
693 694

    if source:
695
        raise Abort("no longer supported: use \"hg clone\" instead")
696
    hg.repository(ui, ".", create=1)
697

Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
698 699
def locate(ui, repo, *pats, **opts):
    """locate files matching specific patterns"""
700 701 702
    if opts['print0']: end = '\0'
    else: end = '\n'
    opts['rootless'] = True
703
    for src, abs, rel in walk(repo, pats, opts):
704
        if repo.dirstate.state(abs) == '?': continue
705
        if opts['fullpath']:
706 707 708
            ui.write(os.path.join(repo.root, abs), end)
        else:
            ui.write(rel, end)
Bryan O'Sullivan's avatar
Bryan O'Sullivan committed
709

710
def log(ui, repo, f=None, **opts):
711 712
    """show the revision history of the repository or a single file"""
    if f:
713 714
        files = relpath(repo, [f])
        filelog = repo.file(files[0])
715 716
        log = filelog
        lookup = filelog.lookup
717
    else:
718
        files = None
719 720 721 722 723 724 725 726 727 728 729 730 731
        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))
732

733 734
    for i in revlist or range(log.count() - 1, -1, -1):
        show_changeset(ui, repo, filelog=filelog, rev=i)
735 736 737 738 739 740 741
        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)
742
            ui.write("\n\n")
743

744 745 746 747 748
def ls(ui, repo, *pats, **opts):
    """list files"""
    for src, abs, rel in walk(repo, pats, opts):
        ui.write(rel, '\n')

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

    for f in files:
767
        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
768

769 770 771 772
def parents(ui, repo, rev=None):
    """show the parents of the working dir or revision"""
    if rev:
        p = repo.changelog.parents(repo.lookup(rev))
773 774 775 776 777
    else:
        p = repo.dirstate.parents()

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

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

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

    return r
794

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

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

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

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

mpm@selenic.com's avatar
mpm@selenic.com committed
817
    files = relpath(repo, list(flist))