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