##// END OF EJS Templates
py3: convert keys of kwargs back to bytes using pycompat.byteskwargs()
Pulkit Goyal -
r32897:799db2af default
parent child Browse files
Show More
@@ -1,243 +1,245
1 1 """strip changesets and their descendants from history
2 2
3 3 This extension allows you to strip changesets and all their descendants from the
4 4 repository. See the command help for details.
5 5 """
6 6 from __future__ import absolute_import
7 7
8 8 from mercurial.i18n import _
9 9 from mercurial import (
10 10 bookmarks as bookmarksmod,
11 11 cmdutil,
12 12 error,
13 13 hg,
14 14 lock as lockmod,
15 15 merge,
16 16 node as nodemod,
17 pycompat,
17 18 registrar,
18 19 repair,
19 20 scmutil,
20 21 util,
21 22 )
22 23 nullid = nodemod.nullid
23 24 release = lockmod.release
24 25
25 26 cmdtable = {}
26 27 command = registrar.command(cmdtable)
27 28 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
28 29 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
29 30 # be specifying the version(s) of Mercurial they are tested with, or
30 31 # leave the attribute unspecified.
31 32 testedwith = 'ships-with-hg-core'
32 33
33 34 def checksubstate(repo, baserev=None):
34 35 '''return list of subrepos at a different revision than substate.
35 36 Abort if any subrepos have uncommitted changes.'''
36 37 inclsubs = []
37 38 wctx = repo[None]
38 39 if baserev:
39 40 bctx = repo[baserev]
40 41 else:
41 42 bctx = wctx.parents()[0]
42 43 for s in sorted(wctx.substate):
43 44 wctx.sub(s).bailifchanged(True)
44 45 if s not in bctx.substate or bctx.sub(s).dirty():
45 46 inclsubs.append(s)
46 47 return inclsubs
47 48
48 49 def checklocalchanges(repo, force=False, excsuffix=''):
49 50 cmdutil.checkunfinished(repo)
50 51 s = repo.status()
51 52 if not force:
52 53 if s.modified or s.added or s.removed or s.deleted:
53 54 _("local changes found") # i18n tool detection
54 55 raise error.Abort(_("local changes found" + excsuffix))
55 56 if checksubstate(repo):
56 57 _("local changed subrepos found") # i18n tool detection
57 58 raise error.Abort(_("local changed subrepos found" + excsuffix))
58 59 return s
59 60
60 61 def strip(ui, repo, revs, update=True, backup=True, force=None, bookmarks=None):
61 62 wlock = lock = None
62 63 try:
63 64 wlock = repo.wlock()
64 65 lock = repo.lock()
65 66
66 67 if update:
67 68 checklocalchanges(repo, force=force)
68 69 urev, p2 = repo.changelog.parents(revs[0])
69 70 if (util.safehasattr(repo, 'mq') and
70 71 p2 != nullid
71 72 and p2 in [x.node for x in repo.mq.applied]):
72 73 urev = p2
73 74 hg.clean(repo, urev)
74 75 repo.dirstate.write(repo.currenttransaction())
75 76
76 77 repair.strip(ui, repo, revs, backup)
77 78
78 79 repomarks = repo._bookmarks
79 80 if bookmarks:
80 81 with repo.transaction('strip') as tr:
81 82 if repo._activebookmark in bookmarks:
82 83 bookmarksmod.deactivate(repo)
83 84 for bookmark in bookmarks:
84 85 del repomarks[bookmark]
85 86 repomarks.recordchange(tr)
86 87 for bookmark in sorted(bookmarks):
87 88 ui.write(_("bookmark '%s' deleted\n") % bookmark)
88 89 finally:
89 90 release(lock, wlock)
90 91
91 92
92 93 @command("strip",
93 94 [
94 95 ('r', 'rev', [], _('strip specified revision (optional, '
95 96 'can specify revisions without this '
96 97 'option)'), _('REV')),
97 98 ('f', 'force', None, _('force removal of changesets, discard '
98 99 'uncommitted changes (no backup)')),
99 100 ('', 'no-backup', None, _('no backups')),
100 101 ('', 'nobackup', None, _('no backups (DEPRECATED)')),
101 102 ('n', '', None, _('ignored (DEPRECATED)')),
102 103 ('k', 'keep', None, _("do not modify working directory during "
103 104 "strip")),
104 105 ('B', 'bookmark', [], _("remove revs only reachable from given"
105 106 " bookmark"))],
106 107 _('hg strip [-k] [-f] [-B bookmark] [-r] REV...'))
107 108 def stripcmd(ui, repo, *revs, **opts):
108 109 """strip changesets and all their descendants from the repository
109 110
110 111 The strip command removes the specified changesets and all their
111 112 descendants. If the working directory has uncommitted changes, the
112 113 operation is aborted unless the --force flag is supplied, in which
113 114 case changes will be discarded.
114 115
115 116 If a parent of the working directory is stripped, then the working
116 117 directory will automatically be updated to the most recent
117 118 available ancestor of the stripped parent after the operation
118 119 completes.
119 120
120 121 Any stripped changesets are stored in ``.hg/strip-backup`` as a
121 122 bundle (see :hg:`help bundle` and :hg:`help unbundle`). They can
122 123 be restored by running :hg:`unbundle .hg/strip-backup/BUNDLE`,
123 124 where BUNDLE is the bundle file created by the strip. Note that
124 125 the local revision numbers will in general be different after the
125 126 restore.
126 127
127 128 Use the --no-backup option to discard the backup bundle once the
128 129 operation completes.
129 130
130 131 Strip is not a history-rewriting operation and can be used on
131 132 changesets in the public phase. But if the stripped changesets have
132 133 been pushed to a remote repository you will likely pull them again.
133 134
134 135 Return 0 on success.
135 136 """
137 opts = pycompat.byteskwargs(opts)
136 138 backup = True
137 139 if opts.get('no_backup') or opts.get('nobackup'):
138 140 backup = False
139 141
140 142 cl = repo.changelog
141 143 revs = list(revs) + opts.get('rev')
142 144 revs = set(scmutil.revrange(repo, revs))
143 145
144 146 with repo.wlock():
145 147 bookmarks = set(opts.get('bookmark'))
146 148 if bookmarks:
147 149 repomarks = repo._bookmarks
148 150 if not bookmarks.issubset(repomarks):
149 151 raise error.Abort(_("bookmark '%s' not found") %
150 152 ','.join(sorted(bookmarks - set(repomarks.keys()))))
151 153
152 154 # If the requested bookmark is not the only one pointing to a
153 155 # a revision we have to only delete the bookmark and not strip
154 156 # anything. revsets cannot detect that case.
155 157 nodetobookmarks = {}
156 158 for mark, node in repomarks.iteritems():
157 159 nodetobookmarks.setdefault(node, []).append(mark)
158 160 for marks in nodetobookmarks.values():
159 161 if bookmarks.issuperset(marks):
160 162 rsrevs = repair.stripbmrevset(repo, marks[0])
161 163 revs.update(set(rsrevs))
162 164 if not revs:
163 165 lock = tr = None
164 166 try:
165 167 lock = repo.lock()
166 168 tr = repo.transaction('bookmark')
167 169 for bookmark in bookmarks:
168 170 del repomarks[bookmark]
169 171 repomarks.recordchange(tr)
170 172 tr.close()
171 173 for bookmark in sorted(bookmarks):
172 174 ui.write(_("bookmark '%s' deleted\n") % bookmark)
173 175 finally:
174 176 release(lock, tr)
175 177
176 178 if not revs:
177 179 raise error.Abort(_('empty revision set'))
178 180
179 181 descendants = set(cl.descendants(revs))
180 182 strippedrevs = revs.union(descendants)
181 183 roots = revs.difference(descendants)
182 184
183 185 update = False
184 186 # if one of the wdir parent is stripped we'll need
185 187 # to update away to an earlier revision
186 188 for p in repo.dirstate.parents():
187 189 if p != nullid and cl.rev(p) in strippedrevs:
188 190 update = True
189 191 break
190 192
191 193 rootnodes = set(cl.node(r) for r in roots)
192 194
193 195 q = getattr(repo, 'mq', None)
194 196 if q is not None and q.applied:
195 197 # refresh queue state if we're about to strip
196 198 # applied patches
197 199 if cl.rev(repo.lookup('qtip')) in strippedrevs:
198 200 q.applieddirty = True
199 201 start = 0
200 202 end = len(q.applied)
201 203 for i, statusentry in enumerate(q.applied):
202 204 if statusentry.node in rootnodes:
203 205 # if one of the stripped roots is an applied
204 206 # patch, only part of the queue is stripped
205 207 start = i
206 208 break
207 209 del q.applied[start:end]
208 210 q.savedirty()
209 211
210 212 revs = sorted(rootnodes)
211 213 if update and opts.get('keep'):
212 214 urev, p2 = repo.changelog.parents(revs[0])
213 215 if (util.safehasattr(repo, 'mq') and p2 != nullid
214 216 and p2 in [x.node for x in repo.mq.applied]):
215 217 urev = p2
216 218 uctx = repo[urev]
217 219
218 220 # only reset the dirstate for files that would actually change
219 221 # between the working context and uctx
220 222 descendantrevs = repo.revs("%s::." % uctx.rev())
221 223 changedfiles = []
222 224 for rev in descendantrevs:
223 225 # blindly reset the files, regardless of what actually changed
224 226 changedfiles.extend(repo[rev].files())
225 227
226 228 # reset files that only changed in the dirstate too
227 229 dirstate = repo.dirstate
228 230 dirchanges = [f for f in dirstate if dirstate[f] != 'n']
229 231 changedfiles.extend(dirchanges)
230 232
231 233 repo.dirstate.rebuild(urev, uctx.manifest(), changedfiles)
232 234 repo.dirstate.write(repo.currenttransaction())
233 235
234 236 # clear resolve state
235 237 merge.mergestate.clean(repo, repo['.'].node())
236 238
237 239 update = False
238 240
239 241
240 242 strip(ui, repo, revs, backup=backup, update=update,
241 243 force=opts.get('force'), bookmarks=bookmarks)
242 244
243 245 return 0
@@ -1,491 +1,492
1 1 # formatter.py - generic output formatting for mercurial
2 2 #
3 3 # Copyright 2012 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """Generic output formatting for Mercurial
9 9
10 10 The formatter provides API to show data in various ways. The following
11 11 functions should be used in place of ui.write():
12 12
13 13 - fm.write() for unconditional output
14 14 - fm.condwrite() to show some extra data conditionally in plain output
15 15 - fm.context() to provide changectx to template output
16 16 - fm.data() to provide extra data to JSON or template output
17 17 - fm.plain() to show raw text that isn't provided to JSON or template output
18 18
19 19 To show structured data (e.g. date tuples, dicts, lists), apply fm.format*()
20 20 beforehand so the data is converted to the appropriate data type. Use
21 21 fm.isplain() if you need to convert or format data conditionally which isn't
22 22 supported by the formatter API.
23 23
24 24 To build nested structure (i.e. a list of dicts), use fm.nested().
25 25
26 26 See also https://www.mercurial-scm.org/wiki/GenericTemplatingPlan
27 27
28 28 fm.condwrite() vs 'if cond:':
29 29
30 30 In most cases, use fm.condwrite() so users can selectively show the data
31 31 in template output. If it's costly to build data, use plain 'if cond:' with
32 32 fm.write().
33 33
34 34 fm.nested() vs fm.formatdict() (or fm.formatlist()):
35 35
36 36 fm.nested() should be used to form a tree structure (a list of dicts of
37 37 lists of dicts...) which can be accessed through template keywords, e.g.
38 38 "{foo % "{bar % {...}} {baz % {...}}"}". On the other hand, fm.formatdict()
39 39 exports a dict-type object to template, which can be accessed by e.g.
40 40 "{get(foo, key)}" function.
41 41
42 42 Doctest helper:
43 43
44 44 >>> def show(fn, verbose=False, **opts):
45 45 ... import sys
46 46 ... from . import ui as uimod
47 47 ... ui = uimod.ui()
48 48 ... ui.fout = sys.stdout # redirect to doctest
49 49 ... ui.verbose = verbose
50 50 ... return fn(ui, ui.formatter(fn.__name__, opts))
51 51
52 52 Basic example:
53 53
54 54 >>> def files(ui, fm):
55 55 ... files = [('foo', 123, (0, 0)), ('bar', 456, (1, 0))]
56 56 ... for f in files:
57 57 ... fm.startitem()
58 58 ... fm.write('path', '%s', f[0])
59 59 ... fm.condwrite(ui.verbose, 'date', ' %s',
60 60 ... fm.formatdate(f[2], '%Y-%m-%d %H:%M:%S'))
61 61 ... fm.data(size=f[1])
62 62 ... fm.plain('\\n')
63 63 ... fm.end()
64 64 >>> show(files)
65 65 foo
66 66 bar
67 67 >>> show(files, verbose=True)
68 68 foo 1970-01-01 00:00:00
69 69 bar 1970-01-01 00:00:01
70 70 >>> show(files, template='json')
71 71 [
72 72 {
73 73 "date": [0, 0],
74 74 "path": "foo",
75 75 "size": 123
76 76 },
77 77 {
78 78 "date": [1, 0],
79 79 "path": "bar",
80 80 "size": 456
81 81 }
82 82 ]
83 83 >>> show(files, template='path: {path}\\ndate: {date|rfc3339date}\\n')
84 84 path: foo
85 85 date: 1970-01-01T00:00:00+00:00
86 86 path: bar
87 87 date: 1970-01-01T00:00:01+00:00
88 88
89 89 Nested example:
90 90
91 91 >>> def subrepos(ui, fm):
92 92 ... fm.startitem()
93 93 ... fm.write('repo', '[%s]\\n', 'baz')
94 94 ... files(ui, fm.nested('files'))
95 95 ... fm.end()
96 96 >>> show(subrepos)
97 97 [baz]
98 98 foo
99 99 bar
100 100 >>> show(subrepos, template='{repo}: {join(files % "{path}", ", ")}\\n')
101 101 baz: foo, bar
102 102 """
103 103
104 104 from __future__ import absolute_import
105 105
106 106 import collections
107 107 import contextlib
108 108 import itertools
109 109 import os
110 110
111 111 from .i18n import _
112 112 from .node import (
113 113 hex,
114 114 short,
115 115 )
116 116
117 117 from . import (
118 118 error,
119 119 pycompat,
120 120 templatefilters,
121 121 templatekw,
122 122 templater,
123 123 util,
124 124 )
125 125
126 126 pickle = util.pickle
127 127
128 128 class _nullconverter(object):
129 129 '''convert non-primitive data types to be processed by formatter'''
130 130 @staticmethod
131 131 def formatdate(date, fmt):
132 132 '''convert date tuple to appropriate format'''
133 133 return date
134 134 @staticmethod
135 135 def formatdict(data, key, value, fmt, sep):
136 136 '''convert dict or key-value pairs to appropriate dict format'''
137 137 # use plain dict instead of util.sortdict so that data can be
138 138 # serialized as a builtin dict in pickle output
139 139 return dict(data)
140 140 @staticmethod
141 141 def formatlist(data, name, fmt, sep):
142 142 '''convert iterable to appropriate list format'''
143 143 return list(data)
144 144
145 145 class baseformatter(object):
146 146 def __init__(self, ui, topic, opts, converter):
147 147 self._ui = ui
148 148 self._topic = topic
149 149 self._style = opts.get("style")
150 150 self._template = opts.get("template")
151 151 self._converter = converter
152 152 self._item = None
153 153 # function to convert node to string suitable for this output
154 154 self.hexfunc = hex
155 155 def __enter__(self):
156 156 return self
157 157 def __exit__(self, exctype, excvalue, traceback):
158 158 if exctype is None:
159 159 self.end()
160 160 def _showitem(self):
161 161 '''show a formatted item once all data is collected'''
162 162 pass
163 163 def startitem(self):
164 164 '''begin an item in the format list'''
165 165 if self._item is not None:
166 166 self._showitem()
167 167 self._item = {}
168 168 def formatdate(self, date, fmt='%a %b %d %H:%M:%S %Y %1%2'):
169 169 '''convert date tuple to appropriate format'''
170 170 return self._converter.formatdate(date, fmt)
171 171 def formatdict(self, data, key='key', value='value', fmt='%s=%s', sep=' '):
172 172 '''convert dict or key-value pairs to appropriate dict format'''
173 173 return self._converter.formatdict(data, key, value, fmt, sep)
174 174 def formatlist(self, data, name, fmt='%s', sep=' '):
175 175 '''convert iterable to appropriate list format'''
176 176 # name is mandatory argument for now, but it could be optional if
177 177 # we have default template keyword, e.g. {item}
178 178 return self._converter.formatlist(data, name, fmt, sep)
179 179 def context(self, **ctxs):
180 180 '''insert context objects to be used to render template keywords'''
181 181 pass
182 182 def data(self, **data):
183 183 '''insert data into item that's not shown in default output'''
184 184 data = pycompat.byteskwargs(data)
185 185 self._item.update(data)
186 186 def write(self, fields, deftext, *fielddata, **opts):
187 187 '''do default text output while assigning data to item'''
188 188 fieldkeys = fields.split()
189 189 assert len(fieldkeys) == len(fielddata)
190 190 self._item.update(zip(fieldkeys, fielddata))
191 191 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
192 192 '''do conditional write (primarily for plain formatter)'''
193 193 fieldkeys = fields.split()
194 194 assert len(fieldkeys) == len(fielddata)
195 195 self._item.update(zip(fieldkeys, fielddata))
196 196 def plain(self, text, **opts):
197 197 '''show raw text for non-templated mode'''
198 198 pass
199 199 def isplain(self):
200 200 '''check for plain formatter usage'''
201 201 return False
202 202 def nested(self, field):
203 203 '''sub formatter to store nested data in the specified field'''
204 204 self._item[field] = data = []
205 205 return _nestedformatter(self._ui, self._converter, data)
206 206 def end(self):
207 207 '''end output for the formatter'''
208 208 if self._item is not None:
209 209 self._showitem()
210 210
211 211 def nullformatter(ui, topic):
212 212 '''formatter that prints nothing'''
213 213 return baseformatter(ui, topic, opts={}, converter=_nullconverter)
214 214
215 215 class _nestedformatter(baseformatter):
216 216 '''build sub items and store them in the parent formatter'''
217 217 def __init__(self, ui, converter, data):
218 218 baseformatter.__init__(self, ui, topic='', opts={}, converter=converter)
219 219 self._data = data
220 220 def _showitem(self):
221 221 self._data.append(self._item)
222 222
223 223 def _iteritems(data):
224 224 '''iterate key-value pairs in stable order'''
225 225 if isinstance(data, dict):
226 226 return sorted(data.iteritems())
227 227 return data
228 228
229 229 class _plainconverter(object):
230 230 '''convert non-primitive data types to text'''
231 231 @staticmethod
232 232 def formatdate(date, fmt):
233 233 '''stringify date tuple in the given format'''
234 234 return util.datestr(date, fmt)
235 235 @staticmethod
236 236 def formatdict(data, key, value, fmt, sep):
237 237 '''stringify key-value pairs separated by sep'''
238 238 return sep.join(fmt % (k, v) for k, v in _iteritems(data))
239 239 @staticmethod
240 240 def formatlist(data, name, fmt, sep):
241 241 '''stringify iterable separated by sep'''
242 242 return sep.join(fmt % e for e in data)
243 243
244 244 class plainformatter(baseformatter):
245 245 '''the default text output scheme'''
246 246 def __init__(self, ui, out, topic, opts):
247 247 baseformatter.__init__(self, ui, topic, opts, _plainconverter)
248 248 if ui.debugflag:
249 249 self.hexfunc = hex
250 250 else:
251 251 self.hexfunc = short
252 252 if ui is out:
253 253 self._write = ui.write
254 254 else:
255 255 self._write = lambda s, **opts: out.write(s)
256 256 def startitem(self):
257 257 pass
258 258 def data(self, **data):
259 259 pass
260 260 def write(self, fields, deftext, *fielddata, **opts):
261 261 self._write(deftext % fielddata, **opts)
262 262 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
263 263 '''do conditional write'''
264 264 if cond:
265 265 self._write(deftext % fielddata, **opts)
266 266 def plain(self, text, **opts):
267 267 self._write(text, **opts)
268 268 def isplain(self):
269 269 return True
270 270 def nested(self, field):
271 271 # nested data will be directly written to ui
272 272 return self
273 273 def end(self):
274 274 pass
275 275
276 276 class debugformatter(baseformatter):
277 277 def __init__(self, ui, out, topic, opts):
278 278 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
279 279 self._out = out
280 280 self._out.write("%s = [\n" % self._topic)
281 281 def _showitem(self):
282 282 self._out.write(" " + repr(self._item) + ",\n")
283 283 def end(self):
284 284 baseformatter.end(self)
285 285 self._out.write("]\n")
286 286
287 287 class pickleformatter(baseformatter):
288 288 def __init__(self, ui, out, topic, opts):
289 289 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
290 290 self._out = out
291 291 self._data = []
292 292 def _showitem(self):
293 293 self._data.append(self._item)
294 294 def end(self):
295 295 baseformatter.end(self)
296 296 self._out.write(pickle.dumps(self._data))
297 297
298 298 class jsonformatter(baseformatter):
299 299 def __init__(self, ui, out, topic, opts):
300 300 baseformatter.__init__(self, ui, topic, opts, _nullconverter)
301 301 self._out = out
302 302 self._out.write("[")
303 303 self._first = True
304 304 def _showitem(self):
305 305 if self._first:
306 306 self._first = False
307 307 else:
308 308 self._out.write(",")
309 309
310 310 self._out.write("\n {\n")
311 311 first = True
312 312 for k, v in sorted(self._item.items()):
313 313 if first:
314 314 first = False
315 315 else:
316 316 self._out.write(",\n")
317 317 u = templatefilters.json(v, paranoid=False)
318 318 self._out.write(' "%s": %s' % (k, u))
319 319 self._out.write("\n }")
320 320 def end(self):
321 321 baseformatter.end(self)
322 322 self._out.write("\n]\n")
323 323
324 324 class _templateconverter(object):
325 325 '''convert non-primitive data types to be processed by templater'''
326 326 @staticmethod
327 327 def formatdate(date, fmt):
328 328 '''return date tuple'''
329 329 return date
330 330 @staticmethod
331 331 def formatdict(data, key, value, fmt, sep):
332 332 '''build object that can be evaluated as either plain string or dict'''
333 333 data = util.sortdict(_iteritems(data))
334 334 def f():
335 335 yield _plainconverter.formatdict(data, key, value, fmt, sep)
336 336 return templatekw.hybriddict(data, key=key, value=value, fmt=fmt,
337 337 gen=f())
338 338 @staticmethod
339 339 def formatlist(data, name, fmt, sep):
340 340 '''build object that can be evaluated as either plain string or list'''
341 341 data = list(data)
342 342 def f():
343 343 yield _plainconverter.formatlist(data, name, fmt, sep)
344 344 return templatekw.hybridlist(data, name=name, fmt=fmt, gen=f())
345 345
346 346 class templateformatter(baseformatter):
347 347 def __init__(self, ui, out, topic, opts):
348 348 baseformatter.__init__(self, ui, topic, opts, _templateconverter)
349 349 self._out = out
350 350 spec = lookuptemplate(ui, topic, opts.get('template', ''))
351 351 self._tref = spec.ref
352 352 self._t = loadtemplater(ui, spec, cache=templatekw.defaulttempl)
353 353 self._counter = itertools.count()
354 354 self._cache = {} # for templatekw/funcs to store reusable data
355 355 def context(self, **ctxs):
356 356 '''insert context objects to be used to render template keywords'''
357 ctxs = pycompat.byteskwargs(ctxs)
357 358 assert all(k == 'ctx' for k in ctxs)
358 359 self._item.update(ctxs)
359 360 def _showitem(self):
360 361 # TODO: add support for filectx. probably each template keyword or
361 362 # function will have to declare dependent resources. e.g.
362 363 # @templatekeyword(..., requires=('ctx',))
363 364 props = {}
364 365 if 'ctx' in self._item:
365 366 props.update(templatekw.keywords)
366 367 props['index'] = next(self._counter)
367 368 # explicitly-defined fields precede templatekw
368 369 props.update(self._item)
369 370 if 'ctx' in self._item:
370 371 # but template resources must be always available
371 372 props['templ'] = self._t
372 373 props['repo'] = props['ctx'].repo()
373 374 props['revcache'] = {}
374 375 props = pycompat.strkwargs(props)
375 376 g = self._t(self._tref, ui=self._ui, cache=self._cache, **props)
376 377 self._out.write(templater.stringify(g))
377 378
378 379 templatespec = collections.namedtuple(r'templatespec',
379 380 r'ref tmpl mapfile')
380 381
381 382 def lookuptemplate(ui, topic, tmpl):
382 383 """Find the template matching the given -T/--template spec 'tmpl'
383 384
384 385 'tmpl' can be any of the following:
385 386
386 387 - a literal template (e.g. '{rev}')
387 388 - a map-file name or path (e.g. 'changelog')
388 389 - a reference to [templates] in config file
389 390 - a path to raw template file
390 391
391 392 A map file defines a stand-alone template environment. If a map file
392 393 selected, all templates defined in the file will be loaded, and the
393 394 template matching the given topic will be rendered. No aliases will be
394 395 loaded from user config.
395 396
396 397 If no map file selected, all templates in [templates] section will be
397 398 available as well as aliases in [templatealias].
398 399 """
399 400
400 401 # looks like a literal template?
401 402 if '{' in tmpl:
402 403 return templatespec('', tmpl, None)
403 404
404 405 # perhaps a stock style?
405 406 if not os.path.split(tmpl)[0]:
406 407 mapname = (templater.templatepath('map-cmdline.' + tmpl)
407 408 or templater.templatepath(tmpl))
408 409 if mapname and os.path.isfile(mapname):
409 410 return templatespec(topic, None, mapname)
410 411
411 412 # perhaps it's a reference to [templates]
412 413 if ui.config('templates', tmpl):
413 414 return templatespec(tmpl, None, None)
414 415
415 416 if tmpl == 'list':
416 417 ui.write(_("available styles: %s\n") % templater.stylelist())
417 418 raise error.Abort(_("specify a template"))
418 419
419 420 # perhaps it's a path to a map or a template
420 421 if ('/' in tmpl or '\\' in tmpl) and os.path.isfile(tmpl):
421 422 # is it a mapfile for a style?
422 423 if os.path.basename(tmpl).startswith("map-"):
423 424 return templatespec(topic, None, os.path.realpath(tmpl))
424 425 with util.posixfile(tmpl, 'rb') as f:
425 426 tmpl = f.read()
426 427 return templatespec('', tmpl, None)
427 428
428 429 # constant string?
429 430 return templatespec('', tmpl, None)
430 431
431 432 def loadtemplater(ui, spec, cache=None):
432 433 """Create a templater from either a literal template or loading from
433 434 a map file"""
434 435 assert not (spec.tmpl and spec.mapfile)
435 436 if spec.mapfile:
436 437 return templater.templater.frommapfile(spec.mapfile, cache=cache)
437 438 return maketemplater(ui, spec.tmpl, cache=cache)
438 439
439 440 def maketemplater(ui, tmpl, cache=None):
440 441 """Create a templater from a string template 'tmpl'"""
441 442 aliases = ui.configitems('templatealias')
442 443 t = templater.templater(cache=cache, aliases=aliases)
443 444 t.cache.update((k, templater.unquotestring(v))
444 445 for k, v in ui.configitems('templates'))
445 446 if tmpl:
446 447 t.cache[''] = tmpl
447 448 return t
448 449
449 450 def formatter(ui, out, topic, opts):
450 451 template = opts.get("template", "")
451 452 if template == "json":
452 453 return jsonformatter(ui, out, topic, opts)
453 454 elif template == "pickle":
454 455 return pickleformatter(ui, out, topic, opts)
455 456 elif template == "debug":
456 457 return debugformatter(ui, out, topic, opts)
457 458 elif template != "":
458 459 return templateformatter(ui, out, topic, opts)
459 460 # developer config: ui.formatdebug
460 461 elif ui.configbool('ui', 'formatdebug'):
461 462 return debugformatter(ui, out, topic, opts)
462 463 # deprecated config: ui.formatjson
463 464 elif ui.configbool('ui', 'formatjson'):
464 465 return jsonformatter(ui, out, topic, opts)
465 466 return plainformatter(ui, out, topic, opts)
466 467
467 468 @contextlib.contextmanager
468 469 def openformatter(ui, filename, topic, opts):
469 470 """Create a formatter that writes outputs to the specified file
470 471
471 472 Must be invoked using the 'with' statement.
472 473 """
473 474 with util.posixfile(filename, 'wb') as out:
474 475 with formatter(ui, out, topic, opts) as fm:
475 476 yield fm
476 477
477 478 @contextlib.contextmanager
478 479 def _neverending(fm):
479 480 yield fm
480 481
481 482 def maybereopen(fm, filename, opts):
482 483 """Create a formatter backed by file if filename specified, else return
483 484 the given formatter
484 485
485 486 Must be invoked using the 'with' statement. This will never call fm.end()
486 487 of the given formatter.
487 488 """
488 489 if filename:
489 490 return openformatter(fm._ui, filename, fm._topic, opts)
490 491 else:
491 492 return _neverending(fm)
@@ -1,269 +1,270
1 1 # hook.py - hook support for mercurial
2 2 #
3 3 # Copyright 2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import os
11 11 import sys
12 12
13 13 from .i18n import _
14 14 from . import (
15 15 demandimport,
16 16 encoding,
17 17 error,
18 18 extensions,
19 19 pycompat,
20 20 util,
21 21 )
22 22
23 23 def _pythonhook(ui, repo, htype, hname, funcname, args, throw):
24 24 '''call python hook. hook is callable object, looked up as
25 25 name in python module. if callable returns "true", hook
26 26 fails, else passes. if hook raises exception, treated as
27 27 hook failure. exception propagates if throw is "true".
28 28
29 29 reason for "true" meaning "hook failed" is so that
30 30 unmodified commands (e.g. mercurial.commands.update) can
31 31 be run as hooks without wrappers to convert return values.'''
32 32
33 33 if callable(funcname):
34 34 obj = funcname
35 35 funcname = pycompat.sysbytes(obj.__module__ + r"." + obj.__name__)
36 36 else:
37 37 d = funcname.rfind('.')
38 38 if d == -1:
39 39 raise error.HookLoadError(
40 40 _('%s hook is invalid: "%s" not in a module')
41 41 % (hname, funcname))
42 42 modname = funcname[:d]
43 43 oldpaths = sys.path
44 44 if util.mainfrozen():
45 45 # binary installs require sys.path manipulation
46 46 modpath, modfile = os.path.split(modname)
47 47 if modpath and modfile:
48 48 sys.path = sys.path[:] + [modpath]
49 49 modname = modfile
50 50 with demandimport.deactivated():
51 51 try:
52 52 obj = __import__(modname)
53 53 except (ImportError, SyntaxError):
54 54 e1 = sys.exc_info()
55 55 try:
56 56 # extensions are loaded with hgext_ prefix
57 57 obj = __import__("hgext_%s" % modname)
58 58 except (ImportError, SyntaxError):
59 59 e2 = sys.exc_info()
60 60 if ui.tracebackflag:
61 61 ui.warn(_('exception from first failed import '
62 62 'attempt:\n'))
63 63 ui.traceback(e1)
64 64 if ui.tracebackflag:
65 65 ui.warn(_('exception from second failed import '
66 66 'attempt:\n'))
67 67 ui.traceback(e2)
68 68
69 69 if not ui.tracebackflag:
70 70 tracebackhint = _(
71 71 'run with --traceback for stack trace')
72 72 else:
73 73 tracebackhint = None
74 74 raise error.HookLoadError(
75 75 _('%s hook is invalid: import of "%s" failed') %
76 76 (hname, modname), hint=tracebackhint)
77 77 sys.path = oldpaths
78 78 try:
79 79 for p in funcname.split('.')[1:]:
80 80 obj = getattr(obj, p)
81 81 except AttributeError:
82 82 raise error.HookLoadError(
83 83 _('%s hook is invalid: "%s" is not defined')
84 84 % (hname, funcname))
85 85 if not callable(obj):
86 86 raise error.HookLoadError(
87 87 _('%s hook is invalid: "%s" is not callable')
88 88 % (hname, funcname))
89 89
90 90 ui.note(_("calling hook %s: %s\n") % (hname, funcname))
91 91 starttime = util.timer()
92 92
93 93 try:
94 94 r = obj(ui=ui, repo=repo, hooktype=htype, **args)
95 95 except Exception as exc:
96 96 if isinstance(exc, error.Abort):
97 97 ui.warn(_('error: %s hook failed: %s\n') %
98 98 (hname, exc.args[0]))
99 99 else:
100 100 ui.warn(_('error: %s hook raised an exception: '
101 101 '%s\n') % (hname, encoding.strtolocal(str(exc))))
102 102 if throw:
103 103 raise
104 104 if not ui.tracebackflag:
105 105 ui.warn(_('(run with --traceback for stack trace)\n'))
106 106 ui.traceback()
107 107 return True, True
108 108 finally:
109 109 duration = util.timer() - starttime
110 110 ui.log('pythonhook', 'pythonhook-%s: %s finished in %0.2f seconds\n',
111 111 htype, funcname, duration)
112 112 if r:
113 113 if throw:
114 114 raise error.HookAbort(_('%s hook failed') % hname)
115 115 ui.warn(_('warning: %s hook failed\n') % hname)
116 116 return r, False
117 117
118 118 def _exthook(ui, repo, htype, name, cmd, args, throw):
119 119 ui.note(_("running hook %s: %s\n") % (name, cmd))
120 120
121 121 starttime = util.timer()
122 122 env = {}
123 123
124 124 # make in-memory changes visible to external process
125 125 if repo is not None:
126 126 tr = repo.currenttransaction()
127 127 repo.dirstate.write(tr)
128 128 if tr and tr.writepending():
129 129 env['HG_PENDING'] = repo.root
130 130 env['HG_HOOKTYPE'] = htype
131 131 env['HG_HOOKNAME'] = name
132 132
133 133 for k, v in args.iteritems():
134 134 if callable(v):
135 135 v = v()
136 136 if isinstance(v, dict):
137 137 # make the dictionary element order stable across Python
138 138 # implementations
139 139 v = ('{' +
140 140 ', '.join('%r: %r' % i for i in sorted(v.iteritems())) +
141 141 '}')
142 142 env['HG_' + k.upper()] = v
143 143
144 144 if repo:
145 145 cwd = repo.root
146 146 else:
147 147 cwd = pycompat.getcwd()
148 148 r = ui.system(cmd, environ=env, cwd=cwd, blockedtag='exthook-%s' % (name,))
149 149
150 150 duration = util.timer() - starttime
151 151 ui.log('exthook', 'exthook-%s: %s finished in %0.2f seconds\n',
152 152 name, cmd, duration)
153 153 if r:
154 154 desc, r = util.explainexit(r)
155 155 if throw:
156 156 raise error.HookAbort(_('%s hook %s') % (name, desc))
157 157 ui.warn(_('warning: %s hook %s\n') % (name, desc))
158 158 return r
159 159
160 160 # represent an untrusted hook command
161 161 _fromuntrusted = object()
162 162
163 163 def _allhooks(ui):
164 164 """return a list of (hook-id, cmd) pairs sorted by priority"""
165 165 hooks = _hookitems(ui)
166 166 # Be careful in this section, propagating the real commands from untrusted
167 167 # sources would create a security vulnerability, make sure anything altered
168 168 # in that section uses "_fromuntrusted" as its command.
169 169 untrustedhooks = _hookitems(ui, _untrusted=True)
170 170 for name, value in untrustedhooks.items():
171 171 trustedvalue = hooks.get(name, (None, None, name, _fromuntrusted))
172 172 if value != trustedvalue:
173 173 (lp, lo, lk, lv) = trustedvalue
174 174 hooks[name] = (lp, lo, lk, _fromuntrusted)
175 175 # (end of the security sensitive section)
176 176 return [(k, v) for p, o, k, v in sorted(hooks.values())]
177 177
178 178 def _hookitems(ui, _untrusted=False):
179 179 """return all hooks items ready to be sorted"""
180 180 hooks = {}
181 181 for name, cmd in ui.configitems('hooks', untrusted=_untrusted):
182 182 if not name.startswith('priority'):
183 183 priority = ui.configint('hooks', 'priority.%s' % name, 0)
184 184 hooks[name] = (-priority, len(hooks), name, cmd)
185 185 return hooks
186 186
187 187 _redirect = False
188 188 def redirect(state):
189 189 global _redirect
190 190 _redirect = state
191 191
192 192 def hook(ui, repo, htype, throw=False, **args):
193 193 if not ui.callhooks:
194 194 return False
195 195
196 196 hooks = []
197 197 for hname, cmd in _allhooks(ui):
198 198 if hname.split('.')[0] == htype and cmd:
199 199 hooks.append((hname, cmd))
200 200
201 201 res = runhooks(ui, repo, htype, hooks, throw=throw, **args)
202 202 r = False
203 203 for hname, cmd in hooks:
204 204 r = res[hname][0] or r
205 205 return r
206 206
207 207 def runhooks(ui, repo, htype, hooks, throw=False, **args):
208 args = pycompat.byteskwargs(args)
208 209 res = {}
209 210 oldstdout = -1
210 211
211 212 try:
212 213 for hname, cmd in hooks:
213 214 if oldstdout == -1 and _redirect:
214 215 try:
215 216 stdoutno = util.stdout.fileno()
216 217 stderrno = util.stderr.fileno()
217 218 # temporarily redirect stdout to stderr, if possible
218 219 if stdoutno >= 0 and stderrno >= 0:
219 220 util.stdout.flush()
220 221 oldstdout = os.dup(stdoutno)
221 222 os.dup2(stderrno, stdoutno)
222 223 except (OSError, AttributeError):
223 224 # files seem to be bogus, give up on redirecting (WSGI, etc)
224 225 pass
225 226
226 227 if cmd is _fromuntrusted:
227 228 if throw:
228 229 raise error.HookAbort(
229 230 _('untrusted hook %s not executed') % hname,
230 231 hint = _("see 'hg help config.trusted'"))
231 232 ui.warn(_('warning: untrusted hook %s not executed\n') % hname)
232 233 r = 1
233 234 raised = False
234 235 elif callable(cmd):
235 236 r, raised = _pythonhook(ui, repo, htype, hname, cmd, args,
236 237 throw)
237 238 elif cmd.startswith('python:'):
238 239 if cmd.count(':') >= 2:
239 240 path, cmd = cmd[7:].rsplit(':', 1)
240 241 path = util.expandpath(path)
241 242 if repo:
242 243 path = os.path.join(repo.root, path)
243 244 try:
244 245 mod = extensions.loadpath(path, 'hghook.%s' % hname)
245 246 except Exception:
246 247 ui.write(_("loading %s hook failed:\n") % hname)
247 248 raise
248 249 hookfn = getattr(mod, cmd)
249 250 else:
250 251 hookfn = cmd[7:].strip()
251 252 r, raised = _pythonhook(ui, repo, htype, hname, hookfn, args,
252 253 throw)
253 254 else:
254 255 r = _exthook(ui, repo, htype, hname, cmd, args, throw)
255 256 raised = False
256 257
257 258 res[hname] = r, raised
258 259
259 260 # The stderr is fully buffered on Windows when connected to a pipe.
260 261 # A forcible flush is required to make small stderr data in the
261 262 # remote side available to the client immediately.
262 263 util.stderr.flush()
263 264 finally:
264 265 if _redirect and oldstdout >= 0:
265 266 util.stdout.flush() # write hook output to stderr fd
266 267 os.dup2(oldstdout, stdoutno)
267 268 os.close(oldstdout)
268 269
269 270 return res
General Comments 0
You need to be logged in to leave comments. Login now