1# Minimal support for git commands on an hg repository
2#
3# Copyright 2005, 2006 Chris Mason <mason@suse.com>
4#
5# This software may be used and distributed according to the terms of the
6# GNU General Public License version 2 or any later version.
7
8'''browse the repository in a graphical way
9
10The hgk extension allows browsing the history of a repository in a
11graphical way. It requires Tcl/Tk version 8.4 or later. (Tcl/Tk is not
12distributed with Mercurial.)
13
14hgk consists of two parts: a Tcl script that does the displaying and
15querying of information, and an extension to Mercurial named hgk.py,
16which provides hooks for hgk to get information. hgk can be found in
17the contrib directory, and the extension is shipped in the hgext
18repository, and needs to be enabled.
19
20The :hg:`view` command will launch the hgk Tcl script. For this command
21to work, hgk must be in your search path. Alternately, you can specify
22the path to hgk in your configuration file::
23
24  [hgk]
25  path = /location/of/hgk
26
27hgk can make use of the extdiff extension to visualize revisions.
28Assuming you had already configured extdiff vdiff command, just add::
29
30  [hgk]
31  vdiff=vdiff
32
33Revisions context menu will now display additional entries to fire
34vdiff on hovered and selected revisions.
35'''
36
37from __future__ import absolute_import
38
39import os
40
41from mercurial.i18n import _
42from mercurial.node import (
43    nullrev,
44    short,
45)
46from mercurial import (
47    commands,
48    obsolete,
49    patch,
50    pycompat,
51    registrar,
52    scmutil,
53)
54
55cmdtable = {}
56command = registrar.command(cmdtable)
57# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
58# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
59# be specifying the version(s) of Mercurial they are tested with, or
60# leave the attribute unspecified.
61testedwith = b'ships-with-hg-core'
62
63configtable = {}
64configitem = registrar.configitem(configtable)
65
66configitem(
67    b'hgk',
68    b'path',
69    default=b'hgk',
70)
71
72
73@command(
74    b'debug-diff-tree',
75    [
76        (b'p', b'patch', None, _(b'generate patch')),
77        (b'r', b'recursive', None, _(b'recursive')),
78        (b'P', b'pretty', None, _(b'pretty')),
79        (b's', b'stdin', None, _(b'stdin')),
80        (b'C', b'copy', None, _(b'detect copies')),
81        (b'S', b'search', b"", _(b'search')),
82    ],
83    b'[OPTION]... NODE1 NODE2 [FILE]...',
84    inferrepo=True,
85)
86def difftree(ui, repo, node1=None, node2=None, *files, **opts):
87    """diff trees from two commits"""
88
89    def __difftree(repo, node1, node2, files=None):
90        assert node2 is not None
91        if files is None:
92            files = []
93        mmap = repo[node1].manifest()
94        mmap2 = repo[node2].manifest()
95        m = scmutil.match(repo[node1], files)
96        st = repo.status(node1, node2, m)
97        empty = short(repo.nullid)
98
99        for f in st.modified:
100            # TODO get file permissions
101            ui.writenoi18n(
102                b":100664 100664 %s %s M\t%s\t%s\n"
103                % (short(mmap[f]), short(mmap2[f]), f, f)
104            )
105        for f in st.added:
106            ui.writenoi18n(
107                b":000000 100664 %s %s N\t%s\t%s\n"
108                % (empty, short(mmap2[f]), f, f)
109            )
110        for f in st.removed:
111            ui.writenoi18n(
112                b":100664 000000 %s %s D\t%s\t%s\n"
113                % (short(mmap[f]), empty, f, f)
114            )
115
116    ##
117
118    while True:
119        if opts['stdin']:
120            line = ui.fin.readline()
121            if not line:
122                break
123            line = line.rstrip(pycompat.oslinesep).split(b' ')
124            node1 = line[0]
125            if len(line) > 1:
126                node2 = line[1]
127            else:
128                node2 = None
129        node1 = repo.lookup(node1)
130        if node2:
131            node2 = repo.lookup(node2)
132        else:
133            node2 = node1
134            node1 = repo.changelog.parents(node1)[0]
135        if opts['patch']:
136            if opts['pretty']:
137                catcommit(ui, repo, node2, b"")
138            m = scmutil.match(repo[node1], files)
139            diffopts = patch.difffeatureopts(ui)
140            diffopts.git = True
141            chunks = patch.diff(repo, node1, node2, match=m, opts=diffopts)
142            for chunk in chunks:
143                ui.write(chunk)
144        else:
145            __difftree(repo, node1, node2, files=files)
146        if not opts['stdin']:
147            break
148
149
150def catcommit(ui, repo, n, prefix, ctx=None):
151    nlprefix = b'\n' + prefix
152    if ctx is None:
153        ctx = repo[n]
154    # use ctx.node() instead ??
155    ui.write((b"tree %s\n" % short(ctx.changeset()[0])))
156    for p in ctx.parents():
157        ui.write((b"parent %s\n" % p))
158
159    date = ctx.date()
160    description = ctx.description().replace(b"\0", b"")
161    ui.write((b"author %s %d %d\n" % (ctx.user(), int(date[0]), date[1])))
162
163    if b'committer' in ctx.extra():
164        ui.write((b"committer %s\n" % ctx.extra()[b'committer']))
165
166    ui.write((b"revision %d\n" % ctx.rev()))
167    ui.write((b"branch %s\n" % ctx.branch()))
168    if obsolete.isenabled(repo, obsolete.createmarkersopt):
169        if ctx.obsolete():
170            ui.writenoi18n(b"obsolete\n")
171    ui.write((b"phase %s\n\n" % ctx.phasestr()))
172
173    if prefix != b"":
174        ui.write(
175            b"%s%s\n" % (prefix, description.replace(b'\n', nlprefix).strip())
176        )
177    else:
178        ui.write(description + b"\n")
179    if prefix:
180        ui.write(b'\0')
181
182
183@command(b'debug-merge-base', [], _(b'REV REV'))
184def base(ui, repo, node1, node2):
185    """output common ancestor information"""
186    node1 = repo.lookup(node1)
187    node2 = repo.lookup(node2)
188    n = repo.changelog.ancestor(node1, node2)
189    ui.write(short(n) + b"\n")
190
191
192@command(
193    b'debug-cat-file',
194    [(b's', b'stdin', None, _(b'stdin'))],
195    _(b'[OPTION]... TYPE FILE'),
196    inferrepo=True,
197)
198def catfile(ui, repo, type=None, r=None, **opts):
199    """cat a specific revision"""
200    # in stdin mode, every line except the commit is prefixed with two
201    # spaces.  This way the our caller can find the commit without magic
202    # strings
203    #
204    prefix = b""
205    if opts['stdin']:
206        line = ui.fin.readline()
207        if not line:
208            return
209        (type, r) = line.rstrip(pycompat.oslinesep).split(b' ')
210        prefix = b"    "
211    else:
212        if not type or not r:
213            ui.warn(_(b"cat-file: type or revision not supplied\n"))
214            commands.help_(ui, b'cat-file')
215
216    while r:
217        if type != b"commit":
218            ui.warn(_(b"aborting hg cat-file only understands commits\n"))
219            return 1
220        n = repo.lookup(r)
221        catcommit(ui, repo, n, prefix)
222        if opts['stdin']:
223            line = ui.fin.readline()
224            if not line:
225                break
226            (type, r) = line.rstrip(pycompat.oslinesep).split(b' ')
227        else:
228            break
229
230
231# git rev-tree is a confusing thing.  You can supply a number of
232# commit sha1s on the command line, and it walks the commit history
233# telling you which commits are reachable from the supplied ones via
234# a bitmask based on arg position.
235# you can specify a commit to stop at by starting the sha1 with ^
236def revtree(ui, args, repo, full=b"tree", maxnr=0, parents=False):
237    def chlogwalk():
238        count = len(repo)
239        i = count
240        l = [0] * 100
241        chunk = 100
242        while True:
243            if chunk > i:
244                chunk = i
245                i = 0
246            else:
247                i -= chunk
248
249            for x in pycompat.xrange(chunk):
250                if i + x >= count:
251                    l[chunk - x :] = [0] * (chunk - x)
252                    break
253                if full is not None:
254                    if (i + x) in repo:
255                        l[x] = repo[i + x]
256                        l[x].changeset()  # force reading
257                else:
258                    if (i + x) in repo:
259                        l[x] = 1
260            for x in pycompat.xrange(chunk - 1, -1, -1):
261                if l[x] != 0:
262                    yield (i + x, full is not None and l[x] or None)
263            if i == 0:
264                break
265
266    # calculate and return the reachability bitmask for sha
267    def is_reachable(ar, reachable, sha):
268        if len(ar) == 0:
269            return 1
270        mask = 0
271        for i in pycompat.xrange(len(ar)):
272            if sha in reachable[i]:
273                mask |= 1 << i
274
275        return mask
276
277    reachable = []
278    stop_sha1 = []
279    want_sha1 = []
280    count = 0
281
282    # figure out which commits they are asking for and which ones they
283    # want us to stop on
284    for i, arg in enumerate(args):
285        if arg.startswith(b'^'):
286            s = repo.lookup(arg[1:])
287            stop_sha1.append(s)
288            want_sha1.append(s)
289        elif arg != b'HEAD':
290            want_sha1.append(repo.lookup(arg))
291
292    # calculate the graph for the supplied commits
293    for i, n in enumerate(want_sha1):
294        reachable.append(set())
295        visit = [n]
296        reachable[i].add(n)
297        while visit:
298            n = visit.pop(0)
299            if n in stop_sha1:
300                continue
301            for p in repo.changelog.parents(n):
302                if p not in reachable[i]:
303                    reachable[i].add(p)
304                    visit.append(p)
305                if p in stop_sha1:
306                    continue
307
308    # walk the repository looking for commits that are in our
309    # reachability graph
310    for i, ctx in chlogwalk():
311        if i not in repo:
312            continue
313        n = repo.changelog.node(i)
314        mask = is_reachable(want_sha1, reachable, n)
315        if mask:
316            parentstr = b""
317            if parents:
318                pp = repo.changelog.parents(n)
319                if pp[0] != repo.nullid:
320                    parentstr += b" " + short(pp[0])
321                if pp[1] != repo.nullid:
322                    parentstr += b" " + short(pp[1])
323            if not full:
324                ui.write(b"%s%s\n" % (short(n), parentstr))
325            elif full == b"commit":
326                ui.write(b"%s%s\n" % (short(n), parentstr))
327                catcommit(ui, repo, n, b'    ', ctx)
328            else:
329                (p1, p2) = repo.changelog.parents(n)
330                (h, h1, h2) = map(short, (n, p1, p2))
331                (i1, i2) = map(repo.changelog.rev, (p1, p2))
332
333                date = ctx.date()[0]
334                ui.write(b"%s %s:%s" % (date, h, mask))
335                mask = is_reachable(want_sha1, reachable, p1)
336                if i1 != nullrev and mask > 0:
337                    ui.write(b"%s:%s " % (h1, mask)),
338                mask = is_reachable(want_sha1, reachable, p2)
339                if i2 != nullrev and mask > 0:
340                    ui.write(b"%s:%s " % (h2, mask))
341                ui.write(b"\n")
342            if maxnr and count >= maxnr:
343                break
344            count += 1
345
346
347# git rev-list tries to order things by date, and has the ability to stop
348# at a given commit without walking the whole repo.  TODO add the stop
349# parameter
350@command(
351    b'debug-rev-list',
352    [
353        (b'H', b'header', None, _(b'header')),
354        (b't', b'topo-order', None, _(b'topo-order')),
355        (b'p', b'parents', None, _(b'parents')),
356        (b'n', b'max-count', 0, _(b'max-count')),
357    ],
358    b'[OPTION]... REV...',
359)
360def revlist(ui, repo, *revs, **opts):
361    """print revisions"""
362    if opts['header']:
363        full = b"commit"
364    else:
365        full = None
366    copy = [x for x in revs]
367    revtree(ui, copy, repo, full, opts['max_count'], opts[r'parents'])
368
369
370@command(
371    b'view',
372    [(b'l', b'limit', b'', _(b'limit number of changes displayed'), _(b'NUM'))],
373    _(b'[-l LIMIT] [REVRANGE]'),
374    helpcategory=command.CATEGORY_CHANGE_NAVIGATION,
375)
376def view(ui, repo, *etc, **opts):
377    """start interactive history viewer"""
378    opts = pycompat.byteskwargs(opts)
379    os.chdir(repo.root)
380    optstr = b' '.join(
381        [b'--%s %s' % (k, v) for k, v in pycompat.iteritems(opts) if v]
382    )
383    if repo.filtername is None:
384        optstr += b'--hidden'
385
386    cmd = ui.config(b"hgk", b"path") + b" %s %s" % (optstr, b" ".join(etc))
387    ui.debug(b"running %s\n" % cmd)
388    ui.system(cmd, blockedtag=b'hgk_view')
389