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