##// END OF EJS Templates
merge with stable
Augie Fackler -
r35415:a5154168 merge default
parent child Browse files
Show More
@@ -1,1647 +1,1648
1 # histedit.py - interactive history editing for mercurial
1 # histedit.py - interactive history editing for mercurial
2 #
2 #
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
3 # Copyright 2009 Augie Fackler <raf@durin42.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 """interactive history editing
7 """interactive history editing
8
8
9 With this extension installed, Mercurial gains one new command: histedit. Usage
9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 is as follows, assuming the following history::
10 is as follows, assuming the following history::
11
11
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 | Add delta
13 | Add delta
14 |
14 |
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 | Add gamma
16 | Add gamma
17 |
17 |
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 | Add beta
19 | Add beta
20 |
20 |
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 Add alpha
22 Add alpha
23
23
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 file open in your editor::
25 file open in your editor::
26
26
27 pick c561b4e977df Add beta
27 pick c561b4e977df Add beta
28 pick 030b686bedc4 Add gamma
28 pick 030b686bedc4 Add gamma
29 pick 7c2fd3b9020c Add delta
29 pick 7c2fd3b9020c Add delta
30
30
31 # Edit history between c561b4e977df and 7c2fd3b9020c
31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 #
32 #
33 # Commits are listed from least to most recent
33 # Commits are listed from least to most recent
34 #
34 #
35 # Commands:
35 # Commands:
36 # p, pick = use commit
36 # p, pick = use commit
37 # e, edit = use commit, but stop for amending
37 # e, edit = use commit, but stop for amending
38 # f, fold = use commit, but combine it with the one above
38 # f, fold = use commit, but combine it with the one above
39 # r, roll = like fold, but discard this commit's description and date
39 # r, roll = like fold, but discard this commit's description and date
40 # d, drop = remove commit from history
40 # d, drop = remove commit from history
41 # m, mess = edit commit message without changing commit content
41 # m, mess = edit commit message without changing commit content
42 # b, base = checkout changeset and apply further changesets from there
42 # b, base = checkout changeset and apply further changesets from there
43 #
43 #
44
44
45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
46 for each revision in your history. For example, if you had meant to add gamma
46 for each revision in your history. For example, if you had meant to add gamma
47 before beta, and then wanted to add delta in the same revision as beta, you
47 before beta, and then wanted to add delta in the same revision as beta, you
48 would reorganize the file to look like this::
48 would reorganize the file to look like this::
49
49
50 pick 030b686bedc4 Add gamma
50 pick 030b686bedc4 Add gamma
51 pick c561b4e977df Add beta
51 pick c561b4e977df Add beta
52 fold 7c2fd3b9020c Add delta
52 fold 7c2fd3b9020c Add delta
53
53
54 # Edit history between c561b4e977df and 7c2fd3b9020c
54 # Edit history between c561b4e977df and 7c2fd3b9020c
55 #
55 #
56 # Commits are listed from least to most recent
56 # Commits are listed from least to most recent
57 #
57 #
58 # Commands:
58 # Commands:
59 # p, pick = use commit
59 # p, pick = use commit
60 # e, edit = use commit, but stop for amending
60 # e, edit = use commit, but stop for amending
61 # f, fold = use commit, but combine it with the one above
61 # f, fold = use commit, but combine it with the one above
62 # r, roll = like fold, but discard this commit's description and date
62 # r, roll = like fold, but discard this commit's description and date
63 # d, drop = remove commit from history
63 # d, drop = remove commit from history
64 # m, mess = edit commit message without changing commit content
64 # m, mess = edit commit message without changing commit content
65 # b, base = checkout changeset and apply further changesets from there
65 # b, base = checkout changeset and apply further changesets from there
66 #
66 #
67
67
68 At which point you close the editor and ``histedit`` starts working. When you
68 At which point you close the editor and ``histedit`` starts working. When you
69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
70 those revisions together, offering you a chance to clean up the commit message::
70 those revisions together, offering you a chance to clean up the commit message::
71
71
72 Add beta
72 Add beta
73 ***
73 ***
74 Add delta
74 Add delta
75
75
76 Edit the commit message to your liking, then close the editor. The date used
76 Edit the commit message to your liking, then close the editor. The date used
77 for the commit will be the later of the two commits' dates. For this example,
77 for the commit will be the later of the two commits' dates. For this example,
78 let's assume that the commit message was changed to ``Add beta and delta.``
78 let's assume that the commit message was changed to ``Add beta and delta.``
79 After histedit has run and had a chance to remove any old or temporary
79 After histedit has run and had a chance to remove any old or temporary
80 revisions it needed, the history looks like this::
80 revisions it needed, the history looks like this::
81
81
82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
83 | Add beta and delta.
83 | Add beta and delta.
84 |
84 |
85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
86 | Add gamma
86 | Add gamma
87 |
87 |
88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
89 Add alpha
89 Add alpha
90
90
91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
92 ones) until after it has completed all the editing operations, so it will
92 ones) until after it has completed all the editing operations, so it will
93 probably perform several strip operations when it's done. For the above example,
93 probably perform several strip operations when it's done. For the above example,
94 it had to run strip twice. Strip can be slow depending on a variety of factors,
94 it had to run strip twice. Strip can be slow depending on a variety of factors,
95 so you might need to be a little patient. You can choose to keep the original
95 so you might need to be a little patient. You can choose to keep the original
96 revisions by passing the ``--keep`` flag.
96 revisions by passing the ``--keep`` flag.
97
97
98 The ``edit`` operation will drop you back to a command prompt,
98 The ``edit`` operation will drop you back to a command prompt,
99 allowing you to edit files freely, or even use ``hg record`` to commit
99 allowing you to edit files freely, or even use ``hg record`` to commit
100 some changes as a separate commit. When you're done, any remaining
100 some changes as a separate commit. When you're done, any remaining
101 uncommitted changes will be committed as well. When done, run ``hg
101 uncommitted changes will be committed as well. When done, run ``hg
102 histedit --continue`` to finish this step. If there are uncommitted
102 histedit --continue`` to finish this step. If there are uncommitted
103 changes, you'll be prompted for a new commit message, but the default
103 changes, you'll be prompted for a new commit message, but the default
104 commit message will be the original message for the ``edit`` ed
104 commit message will be the original message for the ``edit`` ed
105 revision, and the date of the original commit will be preserved.
105 revision, and the date of the original commit will be preserved.
106
106
107 The ``message`` operation will give you a chance to revise a commit
107 The ``message`` operation will give you a chance to revise a commit
108 message without changing the contents. It's a shortcut for doing
108 message without changing the contents. It's a shortcut for doing
109 ``edit`` immediately followed by `hg histedit --continue``.
109 ``edit`` immediately followed by `hg histedit --continue``.
110
110
111 If ``histedit`` encounters a conflict when moving a revision (while
111 If ``histedit`` encounters a conflict when moving a revision (while
112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
113 ``edit`` with the difference that it won't prompt you for a commit
113 ``edit`` with the difference that it won't prompt you for a commit
114 message when done. If you decide at this point that you don't like how
114 message when done. If you decide at this point that you don't like how
115 much work it will be to rearrange history, or that you made a mistake,
115 much work it will be to rearrange history, or that you made a mistake,
116 you can use ``hg histedit --abort`` to abandon the new changes you
116 you can use ``hg histedit --abort`` to abandon the new changes you
117 have made and return to the state before you attempted to edit your
117 have made and return to the state before you attempted to edit your
118 history.
118 history.
119
119
120 If we clone the histedit-ed example repository above and add four more
120 If we clone the histedit-ed example repository above and add four more
121 changes, such that we have the following history::
121 changes, such that we have the following history::
122
122
123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
124 | Add theta
124 | Add theta
125 |
125 |
126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
127 | Add eta
127 | Add eta
128 |
128 |
129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
130 | Add zeta
130 | Add zeta
131 |
131 |
132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
133 | Add epsilon
133 | Add epsilon
134 |
134 |
135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
136 | Add beta and delta.
136 | Add beta and delta.
137 |
137 |
138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
139 | Add gamma
139 | Add gamma
140 |
140 |
141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
142 Add alpha
142 Add alpha
143
143
144 If you run ``hg histedit --outgoing`` on the clone then it is the same
144 If you run ``hg histedit --outgoing`` on the clone then it is the same
145 as running ``hg histedit 836302820282``. If you need plan to push to a
145 as running ``hg histedit 836302820282``. If you need plan to push to a
146 repository that Mercurial does not detect to be related to the source
146 repository that Mercurial does not detect to be related to the source
147 repo, you can add a ``--force`` option.
147 repo, you can add a ``--force`` option.
148
148
149 Config
149 Config
150 ------
150 ------
151
151
152 Histedit rule lines are truncated to 80 characters by default. You
152 Histedit rule lines are truncated to 80 characters by default. You
153 can customize this behavior by setting a different length in your
153 can customize this behavior by setting a different length in your
154 configuration file::
154 configuration file::
155
155
156 [histedit]
156 [histedit]
157 linelen = 120 # truncate rule lines at 120 characters
157 linelen = 120 # truncate rule lines at 120 characters
158
158
159 ``hg histedit`` attempts to automatically choose an appropriate base
159 ``hg histedit`` attempts to automatically choose an appropriate base
160 revision to use. To change which base revision is used, define a
160 revision to use. To change which base revision is used, define a
161 revset in your configuration file::
161 revset in your configuration file::
162
162
163 [histedit]
163 [histedit]
164 defaultrev = only(.) & draft()
164 defaultrev = only(.) & draft()
165
165
166 By default each edited revision needs to be present in histedit commands.
166 By default each edited revision needs to be present in histedit commands.
167 To remove revision you need to use ``drop`` operation. You can configure
167 To remove revision you need to use ``drop`` operation. You can configure
168 the drop to be implicit for missing commits by adding::
168 the drop to be implicit for missing commits by adding::
169
169
170 [histedit]
170 [histedit]
171 dropmissing = True
171 dropmissing = True
172
172
173 By default, histedit will close the transaction after each action. For
173 By default, histedit will close the transaction after each action. For
174 performance purposes, you can configure histedit to use a single transaction
174 performance purposes, you can configure histedit to use a single transaction
175 across the entire histedit. WARNING: This setting introduces a significant risk
175 across the entire histedit. WARNING: This setting introduces a significant risk
176 of losing the work you've done in a histedit if the histedit aborts
176 of losing the work you've done in a histedit if the histedit aborts
177 unexpectedly::
177 unexpectedly::
178
178
179 [histedit]
179 [histedit]
180 singletransaction = True
180 singletransaction = True
181
181
182 """
182 """
183
183
184 from __future__ import absolute_import
184 from __future__ import absolute_import
185
185
186 import errno
186 import errno
187 import os
187 import os
188
188
189 from mercurial.i18n import _
189 from mercurial.i18n import _
190 from mercurial import (
190 from mercurial import (
191 bundle2,
191 bundle2,
192 cmdutil,
192 cmdutil,
193 context,
193 context,
194 copies,
194 copies,
195 destutil,
195 destutil,
196 discovery,
196 discovery,
197 error,
197 error,
198 exchange,
198 exchange,
199 extensions,
199 extensions,
200 hg,
200 hg,
201 lock,
201 lock,
202 merge as mergemod,
202 merge as mergemod,
203 mergeutil,
203 mergeutil,
204 node,
204 node,
205 obsolete,
205 obsolete,
206 pycompat,
206 pycompat,
207 registrar,
207 registrar,
208 repair,
208 repair,
209 scmutil,
209 scmutil,
210 util,
210 util,
211 )
211 )
212
212
213 pickle = util.pickle
213 pickle = util.pickle
214 release = lock.release
214 release = lock.release
215 cmdtable = {}
215 cmdtable = {}
216 command = registrar.command(cmdtable)
216 command = registrar.command(cmdtable)
217
217
218 configtable = {}
218 configtable = {}
219 configitem = registrar.configitem(configtable)
219 configitem = registrar.configitem(configtable)
220 configitem('experimental', 'histedit.autoverb',
220 configitem('experimental', 'histedit.autoverb',
221 default=False,
221 default=False,
222 )
222 )
223 configitem('histedit', 'defaultrev',
223 configitem('histedit', 'defaultrev',
224 default=configitem.dynamicdefault,
224 default=configitem.dynamicdefault,
225 )
225 )
226 configitem('histedit', 'dropmissing',
226 configitem('histedit', 'dropmissing',
227 default=False,
227 default=False,
228 )
228 )
229 configitem('histedit', 'linelen',
229 configitem('histedit', 'linelen',
230 default=80,
230 default=80,
231 )
231 )
232 configitem('histedit', 'singletransaction',
232 configitem('histedit', 'singletransaction',
233 default=False,
233 default=False,
234 )
234 )
235
235
236 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
236 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
237 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
237 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
238 # be specifying the version(s) of Mercurial they are tested with, or
238 # be specifying the version(s) of Mercurial they are tested with, or
239 # leave the attribute unspecified.
239 # leave the attribute unspecified.
240 testedwith = 'ships-with-hg-core'
240 testedwith = 'ships-with-hg-core'
241
241
242 actiontable = {}
242 actiontable = {}
243 primaryactions = set()
243 primaryactions = set()
244 secondaryactions = set()
244 secondaryactions = set()
245 tertiaryactions = set()
245 tertiaryactions = set()
246 internalactions = set()
246 internalactions = set()
247
247
248 def geteditcomment(ui, first, last):
248 def geteditcomment(ui, first, last):
249 """ construct the editor comment
249 """ construct the editor comment
250 The comment includes::
250 The comment includes::
251 - an intro
251 - an intro
252 - sorted primary commands
252 - sorted primary commands
253 - sorted short commands
253 - sorted short commands
254 - sorted long commands
254 - sorted long commands
255 - additional hints
255 - additional hints
256
256
257 Commands are only included once.
257 Commands are only included once.
258 """
258 """
259 intro = _("""Edit history between %s and %s
259 intro = _("""Edit history between %s and %s
260
260
261 Commits are listed from least to most recent
261 Commits are listed from least to most recent
262
262
263 You can reorder changesets by reordering the lines
263 You can reorder changesets by reordering the lines
264
264
265 Commands:
265 Commands:
266 """)
266 """)
267 actions = []
267 actions = []
268 def addverb(v):
268 def addverb(v):
269 a = actiontable[v]
269 a = actiontable[v]
270 lines = a.message.split("\n")
270 lines = a.message.split("\n")
271 if len(a.verbs):
271 if len(a.verbs):
272 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
272 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
273 actions.append(" %s = %s" % (v, lines[0]))
273 actions.append(" %s = %s" % (v, lines[0]))
274 actions.extend([' %s' for l in lines[1:]])
274 actions.extend([' %s' for l in lines[1:]])
275
275
276 for v in (
276 for v in (
277 sorted(primaryactions) +
277 sorted(primaryactions) +
278 sorted(secondaryactions) +
278 sorted(secondaryactions) +
279 sorted(tertiaryactions)
279 sorted(tertiaryactions)
280 ):
280 ):
281 addverb(v)
281 addverb(v)
282 actions.append('')
282 actions.append('')
283
283
284 hints = []
284 hints = []
285 if ui.configbool('histedit', 'dropmissing'):
285 if ui.configbool('histedit', 'dropmissing'):
286 hints.append("Deleting a changeset from the list "
286 hints.append("Deleting a changeset from the list "
287 "will DISCARD it from the edited history!")
287 "will DISCARD it from the edited history!")
288
288
289 lines = (intro % (first, last)).split('\n') + actions + hints
289 lines = (intro % (first, last)).split('\n') + actions + hints
290
290
291 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
291 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
292
292
293 class histeditstate(object):
293 class histeditstate(object):
294 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
294 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
295 topmost=None, replacements=None, lock=None, wlock=None):
295 topmost=None, replacements=None, lock=None, wlock=None):
296 self.repo = repo
296 self.repo = repo
297 self.actions = actions
297 self.actions = actions
298 self.keep = keep
298 self.keep = keep
299 self.topmost = topmost
299 self.topmost = topmost
300 self.parentctxnode = parentctxnode
300 self.parentctxnode = parentctxnode
301 self.lock = lock
301 self.lock = lock
302 self.wlock = wlock
302 self.wlock = wlock
303 self.backupfile = None
303 self.backupfile = None
304 if replacements is None:
304 if replacements is None:
305 self.replacements = []
305 self.replacements = []
306 else:
306 else:
307 self.replacements = replacements
307 self.replacements = replacements
308
308
309 def read(self):
309 def read(self):
310 """Load histedit state from disk and set fields appropriately."""
310 """Load histedit state from disk and set fields appropriately."""
311 try:
311 try:
312 state = self.repo.vfs.read('histedit-state')
312 state = self.repo.vfs.read('histedit-state')
313 except IOError as err:
313 except IOError as err:
314 if err.errno != errno.ENOENT:
314 if err.errno != errno.ENOENT:
315 raise
315 raise
316 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
316 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
317
317
318 if state.startswith('v1\n'):
318 if state.startswith('v1\n'):
319 data = self._load()
319 data = self._load()
320 parentctxnode, rules, keep, topmost, replacements, backupfile = data
320 parentctxnode, rules, keep, topmost, replacements, backupfile = data
321 else:
321 else:
322 data = pickle.loads(state)
322 data = pickle.loads(state)
323 parentctxnode, rules, keep, topmost, replacements = data
323 parentctxnode, rules, keep, topmost, replacements = data
324 backupfile = None
324 backupfile = None
325
325
326 self.parentctxnode = parentctxnode
326 self.parentctxnode = parentctxnode
327 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
327 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
328 actions = parserules(rules, self)
328 actions = parserules(rules, self)
329 self.actions = actions
329 self.actions = actions
330 self.keep = keep
330 self.keep = keep
331 self.topmost = topmost
331 self.topmost = topmost
332 self.replacements = replacements
332 self.replacements = replacements
333 self.backupfile = backupfile
333 self.backupfile = backupfile
334
334
335 def write(self, tr=None):
335 def write(self, tr=None):
336 if tr:
336 if tr:
337 tr.addfilegenerator('histedit-state', ('histedit-state',),
337 tr.addfilegenerator('histedit-state', ('histedit-state',),
338 self._write, location='plain')
338 self._write, location='plain')
339 else:
339 else:
340 with self.repo.vfs("histedit-state", "w") as f:
340 with self.repo.vfs("histedit-state", "w") as f:
341 self._write(f)
341 self._write(f)
342
342
343 def _write(self, fp):
343 def _write(self, fp):
344 fp.write('v1\n')
344 fp.write('v1\n')
345 fp.write('%s\n' % node.hex(self.parentctxnode))
345 fp.write('%s\n' % node.hex(self.parentctxnode))
346 fp.write('%s\n' % node.hex(self.topmost))
346 fp.write('%s\n' % node.hex(self.topmost))
347 fp.write('%s\n' % self.keep)
347 fp.write('%s\n' % self.keep)
348 fp.write('%d\n' % len(self.actions))
348 fp.write('%d\n' % len(self.actions))
349 for action in self.actions:
349 for action in self.actions:
350 fp.write('%s\n' % action.tostate())
350 fp.write('%s\n' % action.tostate())
351 fp.write('%d\n' % len(self.replacements))
351 fp.write('%d\n' % len(self.replacements))
352 for replacement in self.replacements:
352 for replacement in self.replacements:
353 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
353 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
354 for r in replacement[1])))
354 for r in replacement[1])))
355 backupfile = self.backupfile
355 backupfile = self.backupfile
356 if not backupfile:
356 if not backupfile:
357 backupfile = ''
357 backupfile = ''
358 fp.write('%s\n' % backupfile)
358 fp.write('%s\n' % backupfile)
359
359
360 def _load(self):
360 def _load(self):
361 fp = self.repo.vfs('histedit-state', 'r')
361 fp = self.repo.vfs('histedit-state', 'r')
362 lines = [l[:-1] for l in fp.readlines()]
362 lines = [l[:-1] for l in fp.readlines()]
363
363
364 index = 0
364 index = 0
365 lines[index] # version number
365 lines[index] # version number
366 index += 1
366 index += 1
367
367
368 parentctxnode = node.bin(lines[index])
368 parentctxnode = node.bin(lines[index])
369 index += 1
369 index += 1
370
370
371 topmost = node.bin(lines[index])
371 topmost = node.bin(lines[index])
372 index += 1
372 index += 1
373
373
374 keep = lines[index] == 'True'
374 keep = lines[index] == 'True'
375 index += 1
375 index += 1
376
376
377 # Rules
377 # Rules
378 rules = []
378 rules = []
379 rulelen = int(lines[index])
379 rulelen = int(lines[index])
380 index += 1
380 index += 1
381 for i in xrange(rulelen):
381 for i in xrange(rulelen):
382 ruleaction = lines[index]
382 ruleaction = lines[index]
383 index += 1
383 index += 1
384 rule = lines[index]
384 rule = lines[index]
385 index += 1
385 index += 1
386 rules.append((ruleaction, rule))
386 rules.append((ruleaction, rule))
387
387
388 # Replacements
388 # Replacements
389 replacements = []
389 replacements = []
390 replacementlen = int(lines[index])
390 replacementlen = int(lines[index])
391 index += 1
391 index += 1
392 for i in xrange(replacementlen):
392 for i in xrange(replacementlen):
393 replacement = lines[index]
393 replacement = lines[index]
394 original = node.bin(replacement[:40])
394 original = node.bin(replacement[:40])
395 succ = [node.bin(replacement[i:i + 40]) for i in
395 succ = [node.bin(replacement[i:i + 40]) for i in
396 range(40, len(replacement), 40)]
396 range(40, len(replacement), 40)]
397 replacements.append((original, succ))
397 replacements.append((original, succ))
398 index += 1
398 index += 1
399
399
400 backupfile = lines[index]
400 backupfile = lines[index]
401 index += 1
401 index += 1
402
402
403 fp.close()
403 fp.close()
404
404
405 return parentctxnode, rules, keep, topmost, replacements, backupfile
405 return parentctxnode, rules, keep, topmost, replacements, backupfile
406
406
407 def clear(self):
407 def clear(self):
408 if self.inprogress():
408 if self.inprogress():
409 self.repo.vfs.unlink('histedit-state')
409 self.repo.vfs.unlink('histedit-state')
410
410
411 def inprogress(self):
411 def inprogress(self):
412 return self.repo.vfs.exists('histedit-state')
412 return self.repo.vfs.exists('histedit-state')
413
413
414
414
415 class histeditaction(object):
415 class histeditaction(object):
416 def __init__(self, state, node):
416 def __init__(self, state, node):
417 self.state = state
417 self.state = state
418 self.repo = state.repo
418 self.repo = state.repo
419 self.node = node
419 self.node = node
420
420
421 @classmethod
421 @classmethod
422 def fromrule(cls, state, rule):
422 def fromrule(cls, state, rule):
423 """Parses the given rule, returning an instance of the histeditaction.
423 """Parses the given rule, returning an instance of the histeditaction.
424 """
424 """
425 rulehash = rule.strip().split(' ', 1)[0]
425 rulehash = rule.strip().split(' ', 1)[0]
426 try:
426 try:
427 rev = node.bin(rulehash)
427 rev = node.bin(rulehash)
428 except TypeError:
428 except TypeError:
429 raise error.ParseError("invalid changeset %s" % rulehash)
429 raise error.ParseError("invalid changeset %s" % rulehash)
430 return cls(state, rev)
430 return cls(state, rev)
431
431
432 def verify(self, prev, expected, seen):
432 def verify(self, prev, expected, seen):
433 """ Verifies semantic correctness of the rule"""
433 """ Verifies semantic correctness of the rule"""
434 repo = self.repo
434 repo = self.repo
435 ha = node.hex(self.node)
435 ha = node.hex(self.node)
436 try:
436 try:
437 self.node = repo[ha].node()
437 self.node = repo[ha].node()
438 except error.RepoError:
438 except error.RepoError:
439 raise error.ParseError(_('unknown changeset %s listed')
439 raise error.ParseError(_('unknown changeset %s listed')
440 % ha[:12])
440 % ha[:12])
441 if self.node is not None:
441 if self.node is not None:
442 self._verifynodeconstraints(prev, expected, seen)
442 self._verifynodeconstraints(prev, expected, seen)
443
443
444 def _verifynodeconstraints(self, prev, expected, seen):
444 def _verifynodeconstraints(self, prev, expected, seen):
445 # by default command need a node in the edited list
445 # by default command need a node in the edited list
446 if self.node not in expected:
446 if self.node not in expected:
447 raise error.ParseError(_('%s "%s" changeset was not a candidate')
447 raise error.ParseError(_('%s "%s" changeset was not a candidate')
448 % (self.verb, node.short(self.node)),
448 % (self.verb, node.short(self.node)),
449 hint=_('only use listed changesets'))
449 hint=_('only use listed changesets'))
450 # and only one command per node
450 # and only one command per node
451 if self.node in seen:
451 if self.node in seen:
452 raise error.ParseError(_('duplicated command for changeset %s') %
452 raise error.ParseError(_('duplicated command for changeset %s') %
453 node.short(self.node))
453 node.short(self.node))
454
454
455 def torule(self):
455 def torule(self):
456 """build a histedit rule line for an action
456 """build a histedit rule line for an action
457
457
458 by default lines are in the form:
458 by default lines are in the form:
459 <hash> <rev> <summary>
459 <hash> <rev> <summary>
460 """
460 """
461 ctx = self.repo[self.node]
461 ctx = self.repo[self.node]
462 summary = _getsummary(ctx)
462 summary = _getsummary(ctx)
463 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
463 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
464 # trim to 75 columns by default so it's not stupidly wide in my editor
464 # trim to 75 columns by default so it's not stupidly wide in my editor
465 # (the 5 more are left for verb)
465 # (the 5 more are left for verb)
466 maxlen = self.repo.ui.configint('histedit', 'linelen')
466 maxlen = self.repo.ui.configint('histedit', 'linelen')
467 maxlen = max(maxlen, 22) # avoid truncating hash
467 maxlen = max(maxlen, 22) # avoid truncating hash
468 return util.ellipsis(line, maxlen)
468 return util.ellipsis(line, maxlen)
469
469
470 def tostate(self):
470 def tostate(self):
471 """Print an action in format used by histedit state files
471 """Print an action in format used by histedit state files
472 (the first line is a verb, the remainder is the second)
472 (the first line is a verb, the remainder is the second)
473 """
473 """
474 return "%s\n%s" % (self.verb, node.hex(self.node))
474 return "%s\n%s" % (self.verb, node.hex(self.node))
475
475
476 def run(self):
476 def run(self):
477 """Runs the action. The default behavior is simply apply the action's
477 """Runs the action. The default behavior is simply apply the action's
478 rulectx onto the current parentctx."""
478 rulectx onto the current parentctx."""
479 self.applychange()
479 self.applychange()
480 self.continuedirty()
480 self.continuedirty()
481 return self.continueclean()
481 return self.continueclean()
482
482
483 def applychange(self):
483 def applychange(self):
484 """Applies the changes from this action's rulectx onto the current
484 """Applies the changes from this action's rulectx onto the current
485 parentctx, but does not commit them."""
485 parentctx, but does not commit them."""
486 repo = self.repo
486 repo = self.repo
487 rulectx = repo[self.node]
487 rulectx = repo[self.node]
488 repo.ui.pushbuffer(error=True, labeled=True)
488 repo.ui.pushbuffer(error=True, labeled=True)
489 hg.update(repo, self.state.parentctxnode, quietempty=True)
489 hg.update(repo, self.state.parentctxnode, quietempty=True)
490 stats = applychanges(repo.ui, repo, rulectx, {})
490 stats = applychanges(repo.ui, repo, rulectx, {})
491 repo.dirstate.setbranch(rulectx.branch())
491 if stats and stats[3] > 0:
492 if stats and stats[3] > 0:
492 buf = repo.ui.popbuffer()
493 buf = repo.ui.popbuffer()
493 repo.ui.write(*buf)
494 repo.ui.write(*buf)
494 raise error.InterventionRequired(
495 raise error.InterventionRequired(
495 _('Fix up the change (%s %s)') %
496 _('Fix up the change (%s %s)') %
496 (self.verb, node.short(self.node)),
497 (self.verb, node.short(self.node)),
497 hint=_('hg histedit --continue to resume'))
498 hint=_('hg histedit --continue to resume'))
498 else:
499 else:
499 repo.ui.popbuffer()
500 repo.ui.popbuffer()
500
501
501 def continuedirty(self):
502 def continuedirty(self):
502 """Continues the action when changes have been applied to the working
503 """Continues the action when changes have been applied to the working
503 copy. The default behavior is to commit the dirty changes."""
504 copy. The default behavior is to commit the dirty changes."""
504 repo = self.repo
505 repo = self.repo
505 rulectx = repo[self.node]
506 rulectx = repo[self.node]
506
507
507 editor = self.commiteditor()
508 editor = self.commiteditor()
508 commit = commitfuncfor(repo, rulectx)
509 commit = commitfuncfor(repo, rulectx)
509
510
510 commit(text=rulectx.description(), user=rulectx.user(),
511 commit(text=rulectx.description(), user=rulectx.user(),
511 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
512 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
512
513
513 def commiteditor(self):
514 def commiteditor(self):
514 """The editor to be used to edit the commit message."""
515 """The editor to be used to edit the commit message."""
515 return False
516 return False
516
517
517 def continueclean(self):
518 def continueclean(self):
518 """Continues the action when the working copy is clean. The default
519 """Continues the action when the working copy is clean. The default
519 behavior is to accept the current commit as the new version of the
520 behavior is to accept the current commit as the new version of the
520 rulectx."""
521 rulectx."""
521 ctx = self.repo['.']
522 ctx = self.repo['.']
522 if ctx.node() == self.state.parentctxnode:
523 if ctx.node() == self.state.parentctxnode:
523 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
524 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
524 node.short(self.node))
525 node.short(self.node))
525 return ctx, [(self.node, tuple())]
526 return ctx, [(self.node, tuple())]
526 if ctx.node() == self.node:
527 if ctx.node() == self.node:
527 # Nothing changed
528 # Nothing changed
528 return ctx, []
529 return ctx, []
529 return ctx, [(self.node, (ctx.node(),))]
530 return ctx, [(self.node, (ctx.node(),))]
530
531
531 def commitfuncfor(repo, src):
532 def commitfuncfor(repo, src):
532 """Build a commit function for the replacement of <src>
533 """Build a commit function for the replacement of <src>
533
534
534 This function ensure we apply the same treatment to all changesets.
535 This function ensure we apply the same treatment to all changesets.
535
536
536 - Add a 'histedit_source' entry in extra.
537 - Add a 'histedit_source' entry in extra.
537
538
538 Note that fold has its own separated logic because its handling is a bit
539 Note that fold has its own separated logic because its handling is a bit
539 different and not easily factored out of the fold method.
540 different and not easily factored out of the fold method.
540 """
541 """
541 phasemin = src.phase()
542 phasemin = src.phase()
542 def commitfunc(**kwargs):
543 def commitfunc(**kwargs):
543 overrides = {('phases', 'new-commit'): phasemin}
544 overrides = {('phases', 'new-commit'): phasemin}
544 with repo.ui.configoverride(overrides, 'histedit'):
545 with repo.ui.configoverride(overrides, 'histedit'):
545 extra = kwargs.get(r'extra', {}).copy()
546 extra = kwargs.get(r'extra', {}).copy()
546 extra['histedit_source'] = src.hex()
547 extra['histedit_source'] = src.hex()
547 kwargs[r'extra'] = extra
548 kwargs[r'extra'] = extra
548 return repo.commit(**kwargs)
549 return repo.commit(**kwargs)
549 return commitfunc
550 return commitfunc
550
551
551 def applychanges(ui, repo, ctx, opts):
552 def applychanges(ui, repo, ctx, opts):
552 """Merge changeset from ctx (only) in the current working directory"""
553 """Merge changeset from ctx (only) in the current working directory"""
553 wcpar = repo.dirstate.parents()[0]
554 wcpar = repo.dirstate.parents()[0]
554 if ctx.p1().node() == wcpar:
555 if ctx.p1().node() == wcpar:
555 # edits are "in place" we do not need to make any merge,
556 # edits are "in place" we do not need to make any merge,
556 # just applies changes on parent for editing
557 # just applies changes on parent for editing
557 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
558 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
558 stats = None
559 stats = None
559 else:
560 else:
560 try:
561 try:
561 # ui.forcemerge is an internal variable, do not document
562 # ui.forcemerge is an internal variable, do not document
562 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
563 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
563 'histedit')
564 'histedit')
564 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
565 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
565 finally:
566 finally:
566 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
567 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
567 return stats
568 return stats
568
569
569 def collapse(repo, first, last, commitopts, skipprompt=False):
570 def collapse(repo, first, last, commitopts, skipprompt=False):
570 """collapse the set of revisions from first to last as new one.
571 """collapse the set of revisions from first to last as new one.
571
572
572 Expected commit options are:
573 Expected commit options are:
573 - message
574 - message
574 - date
575 - date
575 - username
576 - username
576 Commit message is edited in all cases.
577 Commit message is edited in all cases.
577
578
578 This function works in memory."""
579 This function works in memory."""
579 ctxs = list(repo.set('%d::%d', first, last))
580 ctxs = list(repo.set('%d::%d', first, last))
580 if not ctxs:
581 if not ctxs:
581 return None
582 return None
582 for c in ctxs:
583 for c in ctxs:
583 if not c.mutable():
584 if not c.mutable():
584 raise error.ParseError(
585 raise error.ParseError(
585 _("cannot fold into public change %s") % node.short(c.node()))
586 _("cannot fold into public change %s") % node.short(c.node()))
586 base = first.parents()[0]
587 base = first.parents()[0]
587
588
588 # commit a new version of the old changeset, including the update
589 # commit a new version of the old changeset, including the update
589 # collect all files which might be affected
590 # collect all files which might be affected
590 files = set()
591 files = set()
591 for ctx in ctxs:
592 for ctx in ctxs:
592 files.update(ctx.files())
593 files.update(ctx.files())
593
594
594 # Recompute copies (avoid recording a -> b -> a)
595 # Recompute copies (avoid recording a -> b -> a)
595 copied = copies.pathcopies(base, last)
596 copied = copies.pathcopies(base, last)
596
597
597 # prune files which were reverted by the updates
598 # prune files which were reverted by the updates
598 files = [f for f in files if not cmdutil.samefile(f, last, base)]
599 files = [f for f in files if not cmdutil.samefile(f, last, base)]
599 # commit version of these files as defined by head
600 # commit version of these files as defined by head
600 headmf = last.manifest()
601 headmf = last.manifest()
601 def filectxfn(repo, ctx, path):
602 def filectxfn(repo, ctx, path):
602 if path in headmf:
603 if path in headmf:
603 fctx = last[path]
604 fctx = last[path]
604 flags = fctx.flags()
605 flags = fctx.flags()
605 mctx = context.memfilectx(repo, ctx,
606 mctx = context.memfilectx(repo, ctx,
606 fctx.path(), fctx.data(),
607 fctx.path(), fctx.data(),
607 islink='l' in flags,
608 islink='l' in flags,
608 isexec='x' in flags,
609 isexec='x' in flags,
609 copied=copied.get(path))
610 copied=copied.get(path))
610 return mctx
611 return mctx
611 return None
612 return None
612
613
613 if commitopts.get('message'):
614 if commitopts.get('message'):
614 message = commitopts['message']
615 message = commitopts['message']
615 else:
616 else:
616 message = first.description()
617 message = first.description()
617 user = commitopts.get('user')
618 user = commitopts.get('user')
618 date = commitopts.get('date')
619 date = commitopts.get('date')
619 extra = commitopts.get('extra')
620 extra = commitopts.get('extra')
620
621
621 parents = (first.p1().node(), first.p2().node())
622 parents = (first.p1().node(), first.p2().node())
622 editor = None
623 editor = None
623 if not skipprompt:
624 if not skipprompt:
624 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
625 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
625 new = context.memctx(repo,
626 new = context.memctx(repo,
626 parents=parents,
627 parents=parents,
627 text=message,
628 text=message,
628 files=files,
629 files=files,
629 filectxfn=filectxfn,
630 filectxfn=filectxfn,
630 user=user,
631 user=user,
631 date=date,
632 date=date,
632 extra=extra,
633 extra=extra,
633 editor=editor)
634 editor=editor)
634 return repo.commitctx(new)
635 return repo.commitctx(new)
635
636
636 def _isdirtywc(repo):
637 def _isdirtywc(repo):
637 return repo[None].dirty(missing=True)
638 return repo[None].dirty(missing=True)
638
639
639 def abortdirty():
640 def abortdirty():
640 raise error.Abort(_('working copy has pending changes'),
641 raise error.Abort(_('working copy has pending changes'),
641 hint=_('amend, commit, or revert them and run histedit '
642 hint=_('amend, commit, or revert them and run histedit '
642 '--continue, or abort with histedit --abort'))
643 '--continue, or abort with histedit --abort'))
643
644
644 def action(verbs, message, priority=False, internal=False):
645 def action(verbs, message, priority=False, internal=False):
645 def wrap(cls):
646 def wrap(cls):
646 assert not priority or not internal
647 assert not priority or not internal
647 verb = verbs[0]
648 verb = verbs[0]
648 if priority:
649 if priority:
649 primaryactions.add(verb)
650 primaryactions.add(verb)
650 elif internal:
651 elif internal:
651 internalactions.add(verb)
652 internalactions.add(verb)
652 elif len(verbs) > 1:
653 elif len(verbs) > 1:
653 secondaryactions.add(verb)
654 secondaryactions.add(verb)
654 else:
655 else:
655 tertiaryactions.add(verb)
656 tertiaryactions.add(verb)
656
657
657 cls.verb = verb
658 cls.verb = verb
658 cls.verbs = verbs
659 cls.verbs = verbs
659 cls.message = message
660 cls.message = message
660 for verb in verbs:
661 for verb in verbs:
661 actiontable[verb] = cls
662 actiontable[verb] = cls
662 return cls
663 return cls
663 return wrap
664 return wrap
664
665
665 @action(['pick', 'p'],
666 @action(['pick', 'p'],
666 _('use commit'),
667 _('use commit'),
667 priority=True)
668 priority=True)
668 class pick(histeditaction):
669 class pick(histeditaction):
669 def run(self):
670 def run(self):
670 rulectx = self.repo[self.node]
671 rulectx = self.repo[self.node]
671 if rulectx.parents()[0].node() == self.state.parentctxnode:
672 if rulectx.parents()[0].node() == self.state.parentctxnode:
672 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
673 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
673 return rulectx, []
674 return rulectx, []
674
675
675 return super(pick, self).run()
676 return super(pick, self).run()
676
677
677 @action(['edit', 'e'],
678 @action(['edit', 'e'],
678 _('use commit, but stop for amending'),
679 _('use commit, but stop for amending'),
679 priority=True)
680 priority=True)
680 class edit(histeditaction):
681 class edit(histeditaction):
681 def run(self):
682 def run(self):
682 repo = self.repo
683 repo = self.repo
683 rulectx = repo[self.node]
684 rulectx = repo[self.node]
684 hg.update(repo, self.state.parentctxnode, quietempty=True)
685 hg.update(repo, self.state.parentctxnode, quietempty=True)
685 applychanges(repo.ui, repo, rulectx, {})
686 applychanges(repo.ui, repo, rulectx, {})
686 raise error.InterventionRequired(
687 raise error.InterventionRequired(
687 _('Editing (%s), you may commit or record as needed now.')
688 _('Editing (%s), you may commit or record as needed now.')
688 % node.short(self.node),
689 % node.short(self.node),
689 hint=_('hg histedit --continue to resume'))
690 hint=_('hg histedit --continue to resume'))
690
691
691 def commiteditor(self):
692 def commiteditor(self):
692 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
693 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
693
694
694 @action(['fold', 'f'],
695 @action(['fold', 'f'],
695 _('use commit, but combine it with the one above'))
696 _('use commit, but combine it with the one above'))
696 class fold(histeditaction):
697 class fold(histeditaction):
697 def verify(self, prev, expected, seen):
698 def verify(self, prev, expected, seen):
698 """ Verifies semantic correctness of the fold rule"""
699 """ Verifies semantic correctness of the fold rule"""
699 super(fold, self).verify(prev, expected, seen)
700 super(fold, self).verify(prev, expected, seen)
700 repo = self.repo
701 repo = self.repo
701 if not prev:
702 if not prev:
702 c = repo[self.node].parents()[0]
703 c = repo[self.node].parents()[0]
703 elif not prev.verb in ('pick', 'base'):
704 elif not prev.verb in ('pick', 'base'):
704 return
705 return
705 else:
706 else:
706 c = repo[prev.node]
707 c = repo[prev.node]
707 if not c.mutable():
708 if not c.mutable():
708 raise error.ParseError(
709 raise error.ParseError(
709 _("cannot fold into public change %s") % node.short(c.node()))
710 _("cannot fold into public change %s") % node.short(c.node()))
710
711
711
712
712 def continuedirty(self):
713 def continuedirty(self):
713 repo = self.repo
714 repo = self.repo
714 rulectx = repo[self.node]
715 rulectx = repo[self.node]
715
716
716 commit = commitfuncfor(repo, rulectx)
717 commit = commitfuncfor(repo, rulectx)
717 commit(text='fold-temp-revision %s' % node.short(self.node),
718 commit(text='fold-temp-revision %s' % node.short(self.node),
718 user=rulectx.user(), date=rulectx.date(),
719 user=rulectx.user(), date=rulectx.date(),
719 extra=rulectx.extra())
720 extra=rulectx.extra())
720
721
721 def continueclean(self):
722 def continueclean(self):
722 repo = self.repo
723 repo = self.repo
723 ctx = repo['.']
724 ctx = repo['.']
724 rulectx = repo[self.node]
725 rulectx = repo[self.node]
725 parentctxnode = self.state.parentctxnode
726 parentctxnode = self.state.parentctxnode
726 if ctx.node() == parentctxnode:
727 if ctx.node() == parentctxnode:
727 repo.ui.warn(_('%s: empty changeset\n') %
728 repo.ui.warn(_('%s: empty changeset\n') %
728 node.short(self.node))
729 node.short(self.node))
729 return ctx, [(self.node, (parentctxnode,))]
730 return ctx, [(self.node, (parentctxnode,))]
730
731
731 parentctx = repo[parentctxnode]
732 parentctx = repo[parentctxnode]
732 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
733 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
733 parentctx))
734 parentctx))
734 if not newcommits:
735 if not newcommits:
735 repo.ui.warn(_('%s: cannot fold - working copy is not a '
736 repo.ui.warn(_('%s: cannot fold - working copy is not a '
736 'descendant of previous commit %s\n') %
737 'descendant of previous commit %s\n') %
737 (node.short(self.node), node.short(parentctxnode)))
738 (node.short(self.node), node.short(parentctxnode)))
738 return ctx, [(self.node, (ctx.node(),))]
739 return ctx, [(self.node, (ctx.node(),))]
739
740
740 middlecommits = newcommits.copy()
741 middlecommits = newcommits.copy()
741 middlecommits.discard(ctx.node())
742 middlecommits.discard(ctx.node())
742
743
743 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
744 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
744 middlecommits)
745 middlecommits)
745
746
746 def skipprompt(self):
747 def skipprompt(self):
747 """Returns true if the rule should skip the message editor.
748 """Returns true if the rule should skip the message editor.
748
749
749 For example, 'fold' wants to show an editor, but 'rollup'
750 For example, 'fold' wants to show an editor, but 'rollup'
750 doesn't want to.
751 doesn't want to.
751 """
752 """
752 return False
753 return False
753
754
754 def mergedescs(self):
755 def mergedescs(self):
755 """Returns true if the rule should merge messages of multiple changes.
756 """Returns true if the rule should merge messages of multiple changes.
756
757
757 This exists mainly so that 'rollup' rules can be a subclass of
758 This exists mainly so that 'rollup' rules can be a subclass of
758 'fold'.
759 'fold'.
759 """
760 """
760 return True
761 return True
761
762
762 def firstdate(self):
763 def firstdate(self):
763 """Returns true if the rule should preserve the date of the first
764 """Returns true if the rule should preserve the date of the first
764 change.
765 change.
765
766
766 This exists mainly so that 'rollup' rules can be a subclass of
767 This exists mainly so that 'rollup' rules can be a subclass of
767 'fold'.
768 'fold'.
768 """
769 """
769 return False
770 return False
770
771
771 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
772 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
772 parent = ctx.parents()[0].node()
773 parent = ctx.parents()[0].node()
773 repo.ui.pushbuffer()
774 repo.ui.pushbuffer()
774 hg.update(repo, parent)
775 hg.update(repo, parent)
775 repo.ui.popbuffer()
776 repo.ui.popbuffer()
776 ### prepare new commit data
777 ### prepare new commit data
777 commitopts = {}
778 commitopts = {}
778 commitopts['user'] = ctx.user()
779 commitopts['user'] = ctx.user()
779 # commit message
780 # commit message
780 if not self.mergedescs():
781 if not self.mergedescs():
781 newmessage = ctx.description()
782 newmessage = ctx.description()
782 else:
783 else:
783 newmessage = '\n***\n'.join(
784 newmessage = '\n***\n'.join(
784 [ctx.description()] +
785 [ctx.description()] +
785 [repo[r].description() for r in internalchanges] +
786 [repo[r].description() for r in internalchanges] +
786 [oldctx.description()]) + '\n'
787 [oldctx.description()]) + '\n'
787 commitopts['message'] = newmessage
788 commitopts['message'] = newmessage
788 # date
789 # date
789 if self.firstdate():
790 if self.firstdate():
790 commitopts['date'] = ctx.date()
791 commitopts['date'] = ctx.date()
791 else:
792 else:
792 commitopts['date'] = max(ctx.date(), oldctx.date())
793 commitopts['date'] = max(ctx.date(), oldctx.date())
793 extra = ctx.extra().copy()
794 extra = ctx.extra().copy()
794 # histedit_source
795 # histedit_source
795 # note: ctx is likely a temporary commit but that the best we can do
796 # note: ctx is likely a temporary commit but that the best we can do
796 # here. This is sufficient to solve issue3681 anyway.
797 # here. This is sufficient to solve issue3681 anyway.
797 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
798 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
798 commitopts['extra'] = extra
799 commitopts['extra'] = extra
799 phasemin = max(ctx.phase(), oldctx.phase())
800 phasemin = max(ctx.phase(), oldctx.phase())
800 overrides = {('phases', 'new-commit'): phasemin}
801 overrides = {('phases', 'new-commit'): phasemin}
801 with repo.ui.configoverride(overrides, 'histedit'):
802 with repo.ui.configoverride(overrides, 'histedit'):
802 n = collapse(repo, ctx, repo[newnode], commitopts,
803 n = collapse(repo, ctx, repo[newnode], commitopts,
803 skipprompt=self.skipprompt())
804 skipprompt=self.skipprompt())
804 if n is None:
805 if n is None:
805 return ctx, []
806 return ctx, []
806 repo.ui.pushbuffer()
807 repo.ui.pushbuffer()
807 hg.update(repo, n)
808 hg.update(repo, n)
808 repo.ui.popbuffer()
809 repo.ui.popbuffer()
809 replacements = [(oldctx.node(), (newnode,)),
810 replacements = [(oldctx.node(), (newnode,)),
810 (ctx.node(), (n,)),
811 (ctx.node(), (n,)),
811 (newnode, (n,)),
812 (newnode, (n,)),
812 ]
813 ]
813 for ich in internalchanges:
814 for ich in internalchanges:
814 replacements.append((ich, (n,)))
815 replacements.append((ich, (n,)))
815 return repo[n], replacements
816 return repo[n], replacements
816
817
817 @action(['base', 'b'],
818 @action(['base', 'b'],
818 _('checkout changeset and apply further changesets from there'))
819 _('checkout changeset and apply further changesets from there'))
819 class base(histeditaction):
820 class base(histeditaction):
820
821
821 def run(self):
822 def run(self):
822 if self.repo['.'].node() != self.node:
823 if self.repo['.'].node() != self.node:
823 mergemod.update(self.repo, self.node, False, True)
824 mergemod.update(self.repo, self.node, False, True)
824 # branchmerge, force)
825 # branchmerge, force)
825 return self.continueclean()
826 return self.continueclean()
826
827
827 def continuedirty(self):
828 def continuedirty(self):
828 abortdirty()
829 abortdirty()
829
830
830 def continueclean(self):
831 def continueclean(self):
831 basectx = self.repo['.']
832 basectx = self.repo['.']
832 return basectx, []
833 return basectx, []
833
834
834 def _verifynodeconstraints(self, prev, expected, seen):
835 def _verifynodeconstraints(self, prev, expected, seen):
835 # base can only be use with a node not in the edited set
836 # base can only be use with a node not in the edited set
836 if self.node in expected:
837 if self.node in expected:
837 msg = _('%s "%s" changeset was an edited list candidate')
838 msg = _('%s "%s" changeset was an edited list candidate')
838 raise error.ParseError(
839 raise error.ParseError(
839 msg % (self.verb, node.short(self.node)),
840 msg % (self.verb, node.short(self.node)),
840 hint=_('base must only use unlisted changesets'))
841 hint=_('base must only use unlisted changesets'))
841
842
842 @action(['_multifold'],
843 @action(['_multifold'],
843 _(
844 _(
844 """fold subclass used for when multiple folds happen in a row
845 """fold subclass used for when multiple folds happen in a row
845
846
846 We only want to fire the editor for the folded message once when
847 We only want to fire the editor for the folded message once when
847 (say) four changes are folded down into a single change. This is
848 (say) four changes are folded down into a single change. This is
848 similar to rollup, but we should preserve both messages so that
849 similar to rollup, but we should preserve both messages so that
849 when the last fold operation runs we can show the user all the
850 when the last fold operation runs we can show the user all the
850 commit messages in their editor.
851 commit messages in their editor.
851 """),
852 """),
852 internal=True)
853 internal=True)
853 class _multifold(fold):
854 class _multifold(fold):
854 def skipprompt(self):
855 def skipprompt(self):
855 return True
856 return True
856
857
857 @action(["roll", "r"],
858 @action(["roll", "r"],
858 _("like fold, but discard this commit's description and date"))
859 _("like fold, but discard this commit's description and date"))
859 class rollup(fold):
860 class rollup(fold):
860 def mergedescs(self):
861 def mergedescs(self):
861 return False
862 return False
862
863
863 def skipprompt(self):
864 def skipprompt(self):
864 return True
865 return True
865
866
866 def firstdate(self):
867 def firstdate(self):
867 return True
868 return True
868
869
869 @action(["drop", "d"],
870 @action(["drop", "d"],
870 _('remove commit from history'))
871 _('remove commit from history'))
871 class drop(histeditaction):
872 class drop(histeditaction):
872 def run(self):
873 def run(self):
873 parentctx = self.repo[self.state.parentctxnode]
874 parentctx = self.repo[self.state.parentctxnode]
874 return parentctx, [(self.node, tuple())]
875 return parentctx, [(self.node, tuple())]
875
876
876 @action(["mess", "m"],
877 @action(["mess", "m"],
877 _('edit commit message without changing commit content'),
878 _('edit commit message without changing commit content'),
878 priority=True)
879 priority=True)
879 class message(histeditaction):
880 class message(histeditaction):
880 def commiteditor(self):
881 def commiteditor(self):
881 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
882 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
882
883
883 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
884 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
884 """utility function to find the first outgoing changeset
885 """utility function to find the first outgoing changeset
885
886
886 Used by initialization code"""
887 Used by initialization code"""
887 if opts is None:
888 if opts is None:
888 opts = {}
889 opts = {}
889 dest = ui.expandpath(remote or 'default-push', remote or 'default')
890 dest = ui.expandpath(remote or 'default-push', remote or 'default')
890 dest, revs = hg.parseurl(dest, None)[:2]
891 dest, revs = hg.parseurl(dest, None)[:2]
891 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
892 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
892
893
893 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
894 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
894 other = hg.peer(repo, opts, dest)
895 other = hg.peer(repo, opts, dest)
895
896
896 if revs:
897 if revs:
897 revs = [repo.lookup(rev) for rev in revs]
898 revs = [repo.lookup(rev) for rev in revs]
898
899
899 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
900 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
900 if not outgoing.missing:
901 if not outgoing.missing:
901 raise error.Abort(_('no outgoing ancestors'))
902 raise error.Abort(_('no outgoing ancestors'))
902 roots = list(repo.revs("roots(%ln)", outgoing.missing))
903 roots = list(repo.revs("roots(%ln)", outgoing.missing))
903 if 1 < len(roots):
904 if 1 < len(roots):
904 msg = _('there are ambiguous outgoing revisions')
905 msg = _('there are ambiguous outgoing revisions')
905 hint = _("see 'hg help histedit' for more detail")
906 hint = _("see 'hg help histedit' for more detail")
906 raise error.Abort(msg, hint=hint)
907 raise error.Abort(msg, hint=hint)
907 return repo.lookup(roots[0])
908 return repo.lookup(roots[0])
908
909
909 @command('histedit',
910 @command('histedit',
910 [('', 'commands', '',
911 [('', 'commands', '',
911 _('read history edits from the specified file'), _('FILE')),
912 _('read history edits from the specified file'), _('FILE')),
912 ('c', 'continue', False, _('continue an edit already in progress')),
913 ('c', 'continue', False, _('continue an edit already in progress')),
913 ('', 'edit-plan', False, _('edit remaining actions list')),
914 ('', 'edit-plan', False, _('edit remaining actions list')),
914 ('k', 'keep', False,
915 ('k', 'keep', False,
915 _("don't strip old nodes after edit is complete")),
916 _("don't strip old nodes after edit is complete")),
916 ('', 'abort', False, _('abort an edit in progress')),
917 ('', 'abort', False, _('abort an edit in progress')),
917 ('o', 'outgoing', False, _('changesets not found in destination')),
918 ('o', 'outgoing', False, _('changesets not found in destination')),
918 ('f', 'force', False,
919 ('f', 'force', False,
919 _('force outgoing even for unrelated repositories')),
920 _('force outgoing even for unrelated repositories')),
920 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
921 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
921 cmdutil.formatteropts,
922 cmdutil.formatteropts,
922 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
923 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
923 def histedit(ui, repo, *freeargs, **opts):
924 def histedit(ui, repo, *freeargs, **opts):
924 """interactively edit changeset history
925 """interactively edit changeset history
925
926
926 This command lets you edit a linear series of changesets (up to
927 This command lets you edit a linear series of changesets (up to
927 and including the working directory, which should be clean).
928 and including the working directory, which should be clean).
928 You can:
929 You can:
929
930
930 - `pick` to [re]order a changeset
931 - `pick` to [re]order a changeset
931
932
932 - `drop` to omit changeset
933 - `drop` to omit changeset
933
934
934 - `mess` to reword the changeset commit message
935 - `mess` to reword the changeset commit message
935
936
936 - `fold` to combine it with the preceding changeset (using the later date)
937 - `fold` to combine it with the preceding changeset (using the later date)
937
938
938 - `roll` like fold, but discarding this commit's description and date
939 - `roll` like fold, but discarding this commit's description and date
939
940
940 - `edit` to edit this changeset (preserving date)
941 - `edit` to edit this changeset (preserving date)
941
942
942 - `base` to checkout changeset and apply further changesets from there
943 - `base` to checkout changeset and apply further changesets from there
943
944
944 There are a number of ways to select the root changeset:
945 There are a number of ways to select the root changeset:
945
946
946 - Specify ANCESTOR directly
947 - Specify ANCESTOR directly
947
948
948 - Use --outgoing -- it will be the first linear changeset not
949 - Use --outgoing -- it will be the first linear changeset not
949 included in destination. (See :hg:`help config.paths.default-push`)
950 included in destination. (See :hg:`help config.paths.default-push`)
950
951
951 - Otherwise, the value from the "histedit.defaultrev" config option
952 - Otherwise, the value from the "histedit.defaultrev" config option
952 is used as a revset to select the base revision when ANCESTOR is not
953 is used as a revset to select the base revision when ANCESTOR is not
953 specified. The first revision returned by the revset is used. By
954 specified. The first revision returned by the revset is used. By
954 default, this selects the editable history that is unique to the
955 default, this selects the editable history that is unique to the
955 ancestry of the working directory.
956 ancestry of the working directory.
956
957
957 .. container:: verbose
958 .. container:: verbose
958
959
959 If you use --outgoing, this command will abort if there are ambiguous
960 If you use --outgoing, this command will abort if there are ambiguous
960 outgoing revisions. For example, if there are multiple branches
961 outgoing revisions. For example, if there are multiple branches
961 containing outgoing revisions.
962 containing outgoing revisions.
962
963
963 Use "min(outgoing() and ::.)" or similar revset specification
964 Use "min(outgoing() and ::.)" or similar revset specification
964 instead of --outgoing to specify edit target revision exactly in
965 instead of --outgoing to specify edit target revision exactly in
965 such ambiguous situation. See :hg:`help revsets` for detail about
966 such ambiguous situation. See :hg:`help revsets` for detail about
966 selecting revisions.
967 selecting revisions.
967
968
968 .. container:: verbose
969 .. container:: verbose
969
970
970 Examples:
971 Examples:
971
972
972 - A number of changes have been made.
973 - A number of changes have been made.
973 Revision 3 is no longer needed.
974 Revision 3 is no longer needed.
974
975
975 Start history editing from revision 3::
976 Start history editing from revision 3::
976
977
977 hg histedit -r 3
978 hg histedit -r 3
978
979
979 An editor opens, containing the list of revisions,
980 An editor opens, containing the list of revisions,
980 with specific actions specified::
981 with specific actions specified::
981
982
982 pick 5339bf82f0ca 3 Zworgle the foobar
983 pick 5339bf82f0ca 3 Zworgle the foobar
983 pick 8ef592ce7cc4 4 Bedazzle the zerlog
984 pick 8ef592ce7cc4 4 Bedazzle the zerlog
984 pick 0a9639fcda9d 5 Morgify the cromulancy
985 pick 0a9639fcda9d 5 Morgify the cromulancy
985
986
986 Additional information about the possible actions
987 Additional information about the possible actions
987 to take appears below the list of revisions.
988 to take appears below the list of revisions.
988
989
989 To remove revision 3 from the history,
990 To remove revision 3 from the history,
990 its action (at the beginning of the relevant line)
991 its action (at the beginning of the relevant line)
991 is changed to 'drop'::
992 is changed to 'drop'::
992
993
993 drop 5339bf82f0ca 3 Zworgle the foobar
994 drop 5339bf82f0ca 3 Zworgle the foobar
994 pick 8ef592ce7cc4 4 Bedazzle the zerlog
995 pick 8ef592ce7cc4 4 Bedazzle the zerlog
995 pick 0a9639fcda9d 5 Morgify the cromulancy
996 pick 0a9639fcda9d 5 Morgify the cromulancy
996
997
997 - A number of changes have been made.
998 - A number of changes have been made.
998 Revision 2 and 4 need to be swapped.
999 Revision 2 and 4 need to be swapped.
999
1000
1000 Start history editing from revision 2::
1001 Start history editing from revision 2::
1001
1002
1002 hg histedit -r 2
1003 hg histedit -r 2
1003
1004
1004 An editor opens, containing the list of revisions,
1005 An editor opens, containing the list of revisions,
1005 with specific actions specified::
1006 with specific actions specified::
1006
1007
1007 pick 252a1af424ad 2 Blorb a morgwazzle
1008 pick 252a1af424ad 2 Blorb a morgwazzle
1008 pick 5339bf82f0ca 3 Zworgle the foobar
1009 pick 5339bf82f0ca 3 Zworgle the foobar
1009 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1010 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1010
1011
1011 To swap revision 2 and 4, its lines are swapped
1012 To swap revision 2 and 4, its lines are swapped
1012 in the editor::
1013 in the editor::
1013
1014
1014 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1015 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1015 pick 5339bf82f0ca 3 Zworgle the foobar
1016 pick 5339bf82f0ca 3 Zworgle the foobar
1016 pick 252a1af424ad 2 Blorb a morgwazzle
1017 pick 252a1af424ad 2 Blorb a morgwazzle
1017
1018
1018 Returns 0 on success, 1 if user intervention is required (not only
1019 Returns 0 on success, 1 if user intervention is required (not only
1019 for intentional "edit" command, but also for resolving unexpected
1020 for intentional "edit" command, but also for resolving unexpected
1020 conflicts).
1021 conflicts).
1021 """
1022 """
1022 state = histeditstate(repo)
1023 state = histeditstate(repo)
1023 try:
1024 try:
1024 state.wlock = repo.wlock()
1025 state.wlock = repo.wlock()
1025 state.lock = repo.lock()
1026 state.lock = repo.lock()
1026 _histedit(ui, repo, state, *freeargs, **opts)
1027 _histedit(ui, repo, state, *freeargs, **opts)
1027 finally:
1028 finally:
1028 release(state.lock, state.wlock)
1029 release(state.lock, state.wlock)
1029
1030
1030 goalcontinue = 'continue'
1031 goalcontinue = 'continue'
1031 goalabort = 'abort'
1032 goalabort = 'abort'
1032 goaleditplan = 'edit-plan'
1033 goaleditplan = 'edit-plan'
1033 goalnew = 'new'
1034 goalnew = 'new'
1034
1035
1035 def _getgoal(opts):
1036 def _getgoal(opts):
1036 if opts.get('continue'):
1037 if opts.get('continue'):
1037 return goalcontinue
1038 return goalcontinue
1038 if opts.get('abort'):
1039 if opts.get('abort'):
1039 return goalabort
1040 return goalabort
1040 if opts.get('edit_plan'):
1041 if opts.get('edit_plan'):
1041 return goaleditplan
1042 return goaleditplan
1042 return goalnew
1043 return goalnew
1043
1044
1044 def _readfile(ui, path):
1045 def _readfile(ui, path):
1045 if path == '-':
1046 if path == '-':
1046 with ui.timeblockedsection('histedit'):
1047 with ui.timeblockedsection('histedit'):
1047 return ui.fin.read()
1048 return ui.fin.read()
1048 else:
1049 else:
1049 with open(path, 'rb') as f:
1050 with open(path, 'rb') as f:
1050 return f.read()
1051 return f.read()
1051
1052
1052 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1053 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1053 # TODO only abort if we try to histedit mq patches, not just
1054 # TODO only abort if we try to histedit mq patches, not just
1054 # blanket if mq patches are applied somewhere
1055 # blanket if mq patches are applied somewhere
1055 mq = getattr(repo, 'mq', None)
1056 mq = getattr(repo, 'mq', None)
1056 if mq and mq.applied:
1057 if mq and mq.applied:
1057 raise error.Abort(_('source has mq patches applied'))
1058 raise error.Abort(_('source has mq patches applied'))
1058
1059
1059 # basic argument incompatibility processing
1060 # basic argument incompatibility processing
1060 outg = opts.get('outgoing')
1061 outg = opts.get('outgoing')
1061 editplan = opts.get('edit_plan')
1062 editplan = opts.get('edit_plan')
1062 abort = opts.get('abort')
1063 abort = opts.get('abort')
1063 force = opts.get('force')
1064 force = opts.get('force')
1064 if force and not outg:
1065 if force and not outg:
1065 raise error.Abort(_('--force only allowed with --outgoing'))
1066 raise error.Abort(_('--force only allowed with --outgoing'))
1066 if goal == 'continue':
1067 if goal == 'continue':
1067 if any((outg, abort, revs, freeargs, rules, editplan)):
1068 if any((outg, abort, revs, freeargs, rules, editplan)):
1068 raise error.Abort(_('no arguments allowed with --continue'))
1069 raise error.Abort(_('no arguments allowed with --continue'))
1069 elif goal == 'abort':
1070 elif goal == 'abort':
1070 if any((outg, revs, freeargs, rules, editplan)):
1071 if any((outg, revs, freeargs, rules, editplan)):
1071 raise error.Abort(_('no arguments allowed with --abort'))
1072 raise error.Abort(_('no arguments allowed with --abort'))
1072 elif goal == 'edit-plan':
1073 elif goal == 'edit-plan':
1073 if any((outg, revs, freeargs)):
1074 if any((outg, revs, freeargs)):
1074 raise error.Abort(_('only --commands argument allowed with '
1075 raise error.Abort(_('only --commands argument allowed with '
1075 '--edit-plan'))
1076 '--edit-plan'))
1076 else:
1077 else:
1077 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1078 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1078 raise error.Abort(_('history edit already in progress, try '
1079 raise error.Abort(_('history edit already in progress, try '
1079 '--continue or --abort'))
1080 '--continue or --abort'))
1080 if outg:
1081 if outg:
1081 if revs:
1082 if revs:
1082 raise error.Abort(_('no revisions allowed with --outgoing'))
1083 raise error.Abort(_('no revisions allowed with --outgoing'))
1083 if len(freeargs) > 1:
1084 if len(freeargs) > 1:
1084 raise error.Abort(
1085 raise error.Abort(
1085 _('only one repo argument allowed with --outgoing'))
1086 _('only one repo argument allowed with --outgoing'))
1086 else:
1087 else:
1087 revs.extend(freeargs)
1088 revs.extend(freeargs)
1088 if len(revs) == 0:
1089 if len(revs) == 0:
1089 defaultrev = destutil.desthistedit(ui, repo)
1090 defaultrev = destutil.desthistedit(ui, repo)
1090 if defaultrev is not None:
1091 if defaultrev is not None:
1091 revs.append(defaultrev)
1092 revs.append(defaultrev)
1092
1093
1093 if len(revs) != 1:
1094 if len(revs) != 1:
1094 raise error.Abort(
1095 raise error.Abort(
1095 _('histedit requires exactly one ancestor revision'))
1096 _('histedit requires exactly one ancestor revision'))
1096
1097
1097 def _histedit(ui, repo, state, *freeargs, **opts):
1098 def _histedit(ui, repo, state, *freeargs, **opts):
1098 opts = pycompat.byteskwargs(opts)
1099 opts = pycompat.byteskwargs(opts)
1099 fm = ui.formatter('histedit', opts)
1100 fm = ui.formatter('histedit', opts)
1100 fm.startitem()
1101 fm.startitem()
1101 goal = _getgoal(opts)
1102 goal = _getgoal(opts)
1102 revs = opts.get('rev', [])
1103 revs = opts.get('rev', [])
1103 rules = opts.get('commands', '')
1104 rules = opts.get('commands', '')
1104 state.keep = opts.get('keep', False)
1105 state.keep = opts.get('keep', False)
1105
1106
1106 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1107 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1107
1108
1108 # rebuild state
1109 # rebuild state
1109 if goal == goalcontinue:
1110 if goal == goalcontinue:
1110 state.read()
1111 state.read()
1111 state = bootstrapcontinue(ui, state, opts)
1112 state = bootstrapcontinue(ui, state, opts)
1112 elif goal == goaleditplan:
1113 elif goal == goaleditplan:
1113 _edithisteditplan(ui, repo, state, rules)
1114 _edithisteditplan(ui, repo, state, rules)
1114 return
1115 return
1115 elif goal == goalabort:
1116 elif goal == goalabort:
1116 _aborthistedit(ui, repo, state)
1117 _aborthistedit(ui, repo, state)
1117 return
1118 return
1118 else:
1119 else:
1119 # goal == goalnew
1120 # goal == goalnew
1120 _newhistedit(ui, repo, state, revs, freeargs, opts)
1121 _newhistedit(ui, repo, state, revs, freeargs, opts)
1121
1122
1122 _continuehistedit(ui, repo, state)
1123 _continuehistedit(ui, repo, state)
1123 _finishhistedit(ui, repo, state, fm)
1124 _finishhistedit(ui, repo, state, fm)
1124 fm.end()
1125 fm.end()
1125
1126
1126 def _continuehistedit(ui, repo, state):
1127 def _continuehistedit(ui, repo, state):
1127 """This function runs after either:
1128 """This function runs after either:
1128 - bootstrapcontinue (if the goal is 'continue')
1129 - bootstrapcontinue (if the goal is 'continue')
1129 - _newhistedit (if the goal is 'new')
1130 - _newhistedit (if the goal is 'new')
1130 """
1131 """
1131 # preprocess rules so that we can hide inner folds from the user
1132 # preprocess rules so that we can hide inner folds from the user
1132 # and only show one editor
1133 # and only show one editor
1133 actions = state.actions[:]
1134 actions = state.actions[:]
1134 for idx, (action, nextact) in enumerate(
1135 for idx, (action, nextact) in enumerate(
1135 zip(actions, actions[1:] + [None])):
1136 zip(actions, actions[1:] + [None])):
1136 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1137 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1137 state.actions[idx].__class__ = _multifold
1138 state.actions[idx].__class__ = _multifold
1138
1139
1139 # Force an initial state file write, so the user can run --abort/continue
1140 # Force an initial state file write, so the user can run --abort/continue
1140 # even if there's an exception before the first transaction serialize.
1141 # even if there's an exception before the first transaction serialize.
1141 state.write()
1142 state.write()
1142
1143
1143 total = len(state.actions)
1144 total = len(state.actions)
1144 pos = 0
1145 pos = 0
1145 tr = None
1146 tr = None
1146 # Don't use singletransaction by default since it rolls the entire
1147 # Don't use singletransaction by default since it rolls the entire
1147 # transaction back if an unexpected exception happens (like a
1148 # transaction back if an unexpected exception happens (like a
1148 # pretxncommit hook throws, or the user aborts the commit msg editor).
1149 # pretxncommit hook throws, or the user aborts the commit msg editor).
1149 if ui.configbool("histedit", "singletransaction"):
1150 if ui.configbool("histedit", "singletransaction"):
1150 # Don't use a 'with' for the transaction, since actions may close
1151 # Don't use a 'with' for the transaction, since actions may close
1151 # and reopen a transaction. For example, if the action executes an
1152 # and reopen a transaction. For example, if the action executes an
1152 # external process it may choose to commit the transaction first.
1153 # external process it may choose to commit the transaction first.
1153 tr = repo.transaction('histedit')
1154 tr = repo.transaction('histedit')
1154 with util.acceptintervention(tr):
1155 with util.acceptintervention(tr):
1155 while state.actions:
1156 while state.actions:
1156 state.write(tr=tr)
1157 state.write(tr=tr)
1157 actobj = state.actions[0]
1158 actobj = state.actions[0]
1158 pos += 1
1159 pos += 1
1159 ui.progress(_("editing"), pos, actobj.torule(),
1160 ui.progress(_("editing"), pos, actobj.torule(),
1160 _('changes'), total)
1161 _('changes'), total)
1161 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1162 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1162 actobj.torule()))
1163 actobj.torule()))
1163 parentctx, replacement_ = actobj.run()
1164 parentctx, replacement_ = actobj.run()
1164 state.parentctxnode = parentctx.node()
1165 state.parentctxnode = parentctx.node()
1165 state.replacements.extend(replacement_)
1166 state.replacements.extend(replacement_)
1166 state.actions.pop(0)
1167 state.actions.pop(0)
1167
1168
1168 state.write()
1169 state.write()
1169 ui.progress(_("editing"), None)
1170 ui.progress(_("editing"), None)
1170
1171
1171 def _finishhistedit(ui, repo, state, fm):
1172 def _finishhistedit(ui, repo, state, fm):
1172 """This action runs when histedit is finishing its session"""
1173 """This action runs when histedit is finishing its session"""
1173 repo.ui.pushbuffer()
1174 repo.ui.pushbuffer()
1174 hg.update(repo, state.parentctxnode, quietempty=True)
1175 hg.update(repo, state.parentctxnode, quietempty=True)
1175 repo.ui.popbuffer()
1176 repo.ui.popbuffer()
1176
1177
1177 mapping, tmpnodes, created, ntm = processreplacement(state)
1178 mapping, tmpnodes, created, ntm = processreplacement(state)
1178 if mapping:
1179 if mapping:
1179 for prec, succs in mapping.iteritems():
1180 for prec, succs in mapping.iteritems():
1180 if not succs:
1181 if not succs:
1181 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1182 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1182 else:
1183 else:
1183 ui.debug('histedit: %s is replaced by %s\n' % (
1184 ui.debug('histedit: %s is replaced by %s\n' % (
1184 node.short(prec), node.short(succs[0])))
1185 node.short(prec), node.short(succs[0])))
1185 if len(succs) > 1:
1186 if len(succs) > 1:
1186 m = 'histedit: %s'
1187 m = 'histedit: %s'
1187 for n in succs[1:]:
1188 for n in succs[1:]:
1188 ui.debug(m % node.short(n))
1189 ui.debug(m % node.short(n))
1189
1190
1190 if not state.keep:
1191 if not state.keep:
1191 if mapping:
1192 if mapping:
1192 movetopmostbookmarks(repo, state.topmost, ntm)
1193 movetopmostbookmarks(repo, state.topmost, ntm)
1193 # TODO update mq state
1194 # TODO update mq state
1194 else:
1195 else:
1195 mapping = {}
1196 mapping = {}
1196
1197
1197 for n in tmpnodes:
1198 for n in tmpnodes:
1198 mapping[n] = ()
1199 mapping[n] = ()
1199
1200
1200 # remove entries about unknown nodes
1201 # remove entries about unknown nodes
1201 nodemap = repo.unfiltered().changelog.nodemap
1202 nodemap = repo.unfiltered().changelog.nodemap
1202 mapping = {k: v for k, v in mapping.items()
1203 mapping = {k: v for k, v in mapping.items()
1203 if k in nodemap and all(n in nodemap for n in v)}
1204 if k in nodemap and all(n in nodemap for n in v)}
1204 scmutil.cleanupnodes(repo, mapping, 'histedit')
1205 scmutil.cleanupnodes(repo, mapping, 'histedit')
1205 hf = fm.hexfunc
1206 hf = fm.hexfunc
1206 fl = fm.formatlist
1207 fl = fm.formatlist
1207 fd = fm.formatdict
1208 fd = fm.formatdict
1208 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1209 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1209 for oldn, newn in mapping.iteritems()},
1210 for oldn, newn in mapping.iteritems()},
1210 key="oldnode", value="newnodes")
1211 key="oldnode", value="newnodes")
1211 fm.data(nodechanges=nodechanges)
1212 fm.data(nodechanges=nodechanges)
1212
1213
1213 state.clear()
1214 state.clear()
1214 if os.path.exists(repo.sjoin('undo')):
1215 if os.path.exists(repo.sjoin('undo')):
1215 os.unlink(repo.sjoin('undo'))
1216 os.unlink(repo.sjoin('undo'))
1216 if repo.vfs.exists('histedit-last-edit.txt'):
1217 if repo.vfs.exists('histedit-last-edit.txt'):
1217 repo.vfs.unlink('histedit-last-edit.txt')
1218 repo.vfs.unlink('histedit-last-edit.txt')
1218
1219
1219 def _aborthistedit(ui, repo, state):
1220 def _aborthistedit(ui, repo, state):
1220 try:
1221 try:
1221 state.read()
1222 state.read()
1222 __, leafs, tmpnodes, __ = processreplacement(state)
1223 __, leafs, tmpnodes, __ = processreplacement(state)
1223 ui.debug('restore wc to old parent %s\n'
1224 ui.debug('restore wc to old parent %s\n'
1224 % node.short(state.topmost))
1225 % node.short(state.topmost))
1225
1226
1226 # Recover our old commits if necessary
1227 # Recover our old commits if necessary
1227 if not state.topmost in repo and state.backupfile:
1228 if not state.topmost in repo and state.backupfile:
1228 backupfile = repo.vfs.join(state.backupfile)
1229 backupfile = repo.vfs.join(state.backupfile)
1229 f = hg.openpath(ui, backupfile)
1230 f = hg.openpath(ui, backupfile)
1230 gen = exchange.readbundle(ui, f, backupfile)
1231 gen = exchange.readbundle(ui, f, backupfile)
1231 with repo.transaction('histedit.abort') as tr:
1232 with repo.transaction('histedit.abort') as tr:
1232 bundle2.applybundle(repo, gen, tr, source='histedit',
1233 bundle2.applybundle(repo, gen, tr, source='histedit',
1233 url='bundle:' + backupfile)
1234 url='bundle:' + backupfile)
1234
1235
1235 os.remove(backupfile)
1236 os.remove(backupfile)
1236
1237
1237 # check whether we should update away
1238 # check whether we should update away
1238 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1239 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1239 state.parentctxnode, leafs | tmpnodes):
1240 state.parentctxnode, leafs | tmpnodes):
1240 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1241 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1241 cleanupnode(ui, repo, tmpnodes)
1242 cleanupnode(ui, repo, tmpnodes)
1242 cleanupnode(ui, repo, leafs)
1243 cleanupnode(ui, repo, leafs)
1243 except Exception:
1244 except Exception:
1244 if state.inprogress():
1245 if state.inprogress():
1245 ui.warn(_('warning: encountered an exception during histedit '
1246 ui.warn(_('warning: encountered an exception during histedit '
1246 '--abort; the repository may not have been completely '
1247 '--abort; the repository may not have been completely '
1247 'cleaned up\n'))
1248 'cleaned up\n'))
1248 raise
1249 raise
1249 finally:
1250 finally:
1250 state.clear()
1251 state.clear()
1251
1252
1252 def _edithisteditplan(ui, repo, state, rules):
1253 def _edithisteditplan(ui, repo, state, rules):
1253 state.read()
1254 state.read()
1254 if not rules:
1255 if not rules:
1255 comment = geteditcomment(ui,
1256 comment = geteditcomment(ui,
1256 node.short(state.parentctxnode),
1257 node.short(state.parentctxnode),
1257 node.short(state.topmost))
1258 node.short(state.topmost))
1258 rules = ruleeditor(repo, ui, state.actions, comment)
1259 rules = ruleeditor(repo, ui, state.actions, comment)
1259 else:
1260 else:
1260 rules = _readfile(ui, rules)
1261 rules = _readfile(ui, rules)
1261 actions = parserules(rules, state)
1262 actions = parserules(rules, state)
1262 ctxs = [repo[act.node] \
1263 ctxs = [repo[act.node] \
1263 for act in state.actions if act.node]
1264 for act in state.actions if act.node]
1264 warnverifyactions(ui, repo, actions, state, ctxs)
1265 warnverifyactions(ui, repo, actions, state, ctxs)
1265 state.actions = actions
1266 state.actions = actions
1266 state.write()
1267 state.write()
1267
1268
1268 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1269 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1269 outg = opts.get('outgoing')
1270 outg = opts.get('outgoing')
1270 rules = opts.get('commands', '')
1271 rules = opts.get('commands', '')
1271 force = opts.get('force')
1272 force = opts.get('force')
1272
1273
1273 cmdutil.checkunfinished(repo)
1274 cmdutil.checkunfinished(repo)
1274 cmdutil.bailifchanged(repo)
1275 cmdutil.bailifchanged(repo)
1275
1276
1276 topmost, empty = repo.dirstate.parents()
1277 topmost, empty = repo.dirstate.parents()
1277 if outg:
1278 if outg:
1278 if freeargs:
1279 if freeargs:
1279 remote = freeargs[0]
1280 remote = freeargs[0]
1280 else:
1281 else:
1281 remote = None
1282 remote = None
1282 root = findoutgoing(ui, repo, remote, force, opts)
1283 root = findoutgoing(ui, repo, remote, force, opts)
1283 else:
1284 else:
1284 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1285 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1285 if len(rr) != 1:
1286 if len(rr) != 1:
1286 raise error.Abort(_('The specified revisions must have '
1287 raise error.Abort(_('The specified revisions must have '
1287 'exactly one common root'))
1288 'exactly one common root'))
1288 root = rr[0].node()
1289 root = rr[0].node()
1289
1290
1290 revs = between(repo, root, topmost, state.keep)
1291 revs = between(repo, root, topmost, state.keep)
1291 if not revs:
1292 if not revs:
1292 raise error.Abort(_('%s is not an ancestor of working directory') %
1293 raise error.Abort(_('%s is not an ancestor of working directory') %
1293 node.short(root))
1294 node.short(root))
1294
1295
1295 ctxs = [repo[r] for r in revs]
1296 ctxs = [repo[r] for r in revs]
1296 if not rules:
1297 if not rules:
1297 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1298 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1298 actions = [pick(state, r) for r in revs]
1299 actions = [pick(state, r) for r in revs]
1299 rules = ruleeditor(repo, ui, actions, comment)
1300 rules = ruleeditor(repo, ui, actions, comment)
1300 else:
1301 else:
1301 rules = _readfile(ui, rules)
1302 rules = _readfile(ui, rules)
1302 actions = parserules(rules, state)
1303 actions = parserules(rules, state)
1303 warnverifyactions(ui, repo, actions, state, ctxs)
1304 warnverifyactions(ui, repo, actions, state, ctxs)
1304
1305
1305 parentctxnode = repo[root].parents()[0].node()
1306 parentctxnode = repo[root].parents()[0].node()
1306
1307
1307 state.parentctxnode = parentctxnode
1308 state.parentctxnode = parentctxnode
1308 state.actions = actions
1309 state.actions = actions
1309 state.topmost = topmost
1310 state.topmost = topmost
1310 state.replacements = []
1311 state.replacements = []
1311
1312
1312 # Create a backup so we can always abort completely.
1313 # Create a backup so we can always abort completely.
1313 backupfile = None
1314 backupfile = None
1314 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1315 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1315 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1316 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1316 'histedit')
1317 'histedit')
1317 state.backupfile = backupfile
1318 state.backupfile = backupfile
1318
1319
1319 def _getsummary(ctx):
1320 def _getsummary(ctx):
1320 # a common pattern is to extract the summary but default to the empty
1321 # a common pattern is to extract the summary but default to the empty
1321 # string
1322 # string
1322 summary = ctx.description() or ''
1323 summary = ctx.description() or ''
1323 if summary:
1324 if summary:
1324 summary = summary.splitlines()[0]
1325 summary = summary.splitlines()[0]
1325 return summary
1326 return summary
1326
1327
1327 def bootstrapcontinue(ui, state, opts):
1328 def bootstrapcontinue(ui, state, opts):
1328 repo = state.repo
1329 repo = state.repo
1329
1330
1330 ms = mergemod.mergestate.read(repo)
1331 ms = mergemod.mergestate.read(repo)
1331 mergeutil.checkunresolved(ms)
1332 mergeutil.checkunresolved(ms)
1332
1333
1333 if state.actions:
1334 if state.actions:
1334 actobj = state.actions.pop(0)
1335 actobj = state.actions.pop(0)
1335
1336
1336 if _isdirtywc(repo):
1337 if _isdirtywc(repo):
1337 actobj.continuedirty()
1338 actobj.continuedirty()
1338 if _isdirtywc(repo):
1339 if _isdirtywc(repo):
1339 abortdirty()
1340 abortdirty()
1340
1341
1341 parentctx, replacements = actobj.continueclean()
1342 parentctx, replacements = actobj.continueclean()
1342
1343
1343 state.parentctxnode = parentctx.node()
1344 state.parentctxnode = parentctx.node()
1344 state.replacements.extend(replacements)
1345 state.replacements.extend(replacements)
1345
1346
1346 return state
1347 return state
1347
1348
1348 def between(repo, old, new, keep):
1349 def between(repo, old, new, keep):
1349 """select and validate the set of revision to edit
1350 """select and validate the set of revision to edit
1350
1351
1351 When keep is false, the specified set can't have children."""
1352 When keep is false, the specified set can't have children."""
1352 ctxs = list(repo.set('%n::%n', old, new))
1353 ctxs = list(repo.set('%n::%n', old, new))
1353 if ctxs and not keep:
1354 if ctxs and not keep:
1354 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1355 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1355 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1356 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1356 raise error.Abort(_('can only histedit a changeset together '
1357 raise error.Abort(_('can only histedit a changeset together '
1357 'with all its descendants'))
1358 'with all its descendants'))
1358 if repo.revs('(%ld) and merge()', ctxs):
1359 if repo.revs('(%ld) and merge()', ctxs):
1359 raise error.Abort(_('cannot edit history that contains merges'))
1360 raise error.Abort(_('cannot edit history that contains merges'))
1360 root = ctxs[0] # list is already sorted by repo.set
1361 root = ctxs[0] # list is already sorted by repo.set
1361 if not root.mutable():
1362 if not root.mutable():
1362 raise error.Abort(_('cannot edit public changeset: %s') % root,
1363 raise error.Abort(_('cannot edit public changeset: %s') % root,
1363 hint=_("see 'hg help phases' for details"))
1364 hint=_("see 'hg help phases' for details"))
1364 return [c.node() for c in ctxs]
1365 return [c.node() for c in ctxs]
1365
1366
1366 def ruleeditor(repo, ui, actions, editcomment=""):
1367 def ruleeditor(repo, ui, actions, editcomment=""):
1367 """open an editor to edit rules
1368 """open an editor to edit rules
1368
1369
1369 rules are in the format [ [act, ctx], ...] like in state.rules
1370 rules are in the format [ [act, ctx], ...] like in state.rules
1370 """
1371 """
1371 if repo.ui.configbool("experimental", "histedit.autoverb"):
1372 if repo.ui.configbool("experimental", "histedit.autoverb"):
1372 newact = util.sortdict()
1373 newact = util.sortdict()
1373 for act in actions:
1374 for act in actions:
1374 ctx = repo[act.node]
1375 ctx = repo[act.node]
1375 summary = _getsummary(ctx)
1376 summary = _getsummary(ctx)
1376 fword = summary.split(' ', 1)[0].lower()
1377 fword = summary.split(' ', 1)[0].lower()
1377 added = False
1378 added = False
1378
1379
1379 # if it doesn't end with the special character '!' just skip this
1380 # if it doesn't end with the special character '!' just skip this
1380 if fword.endswith('!'):
1381 if fword.endswith('!'):
1381 fword = fword[:-1]
1382 fword = fword[:-1]
1382 if fword in primaryactions | secondaryactions | tertiaryactions:
1383 if fword in primaryactions | secondaryactions | tertiaryactions:
1383 act.verb = fword
1384 act.verb = fword
1384 # get the target summary
1385 # get the target summary
1385 tsum = summary[len(fword) + 1:].lstrip()
1386 tsum = summary[len(fword) + 1:].lstrip()
1386 # safe but slow: reverse iterate over the actions so we
1387 # safe but slow: reverse iterate over the actions so we
1387 # don't clash on two commits having the same summary
1388 # don't clash on two commits having the same summary
1388 for na, l in reversed(list(newact.iteritems())):
1389 for na, l in reversed(list(newact.iteritems())):
1389 actx = repo[na.node]
1390 actx = repo[na.node]
1390 asum = _getsummary(actx)
1391 asum = _getsummary(actx)
1391 if asum == tsum:
1392 if asum == tsum:
1392 added = True
1393 added = True
1393 l.append(act)
1394 l.append(act)
1394 break
1395 break
1395
1396
1396 if not added:
1397 if not added:
1397 newact[act] = []
1398 newact[act] = []
1398
1399
1399 # copy over and flatten the new list
1400 # copy over and flatten the new list
1400 actions = []
1401 actions = []
1401 for na, l in newact.iteritems():
1402 for na, l in newact.iteritems():
1402 actions.append(na)
1403 actions.append(na)
1403 actions += l
1404 actions += l
1404
1405
1405 rules = '\n'.join([act.torule() for act in actions])
1406 rules = '\n'.join([act.torule() for act in actions])
1406 rules += '\n\n'
1407 rules += '\n\n'
1407 rules += editcomment
1408 rules += editcomment
1408 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1409 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1409 repopath=repo.path, action='histedit')
1410 repopath=repo.path, action='histedit')
1410
1411
1411 # Save edit rules in .hg/histedit-last-edit.txt in case
1412 # Save edit rules in .hg/histedit-last-edit.txt in case
1412 # the user needs to ask for help after something
1413 # the user needs to ask for help after something
1413 # surprising happens.
1414 # surprising happens.
1414 f = open(repo.vfs.join('histedit-last-edit.txt'), 'w')
1415 f = open(repo.vfs.join('histedit-last-edit.txt'), 'w')
1415 f.write(rules)
1416 f.write(rules)
1416 f.close()
1417 f.close()
1417
1418
1418 return rules
1419 return rules
1419
1420
1420 def parserules(rules, state):
1421 def parserules(rules, state):
1421 """Read the histedit rules string and return list of action objects """
1422 """Read the histedit rules string and return list of action objects """
1422 rules = [l for l in (r.strip() for r in rules.splitlines())
1423 rules = [l for l in (r.strip() for r in rules.splitlines())
1423 if l and not l.startswith('#')]
1424 if l and not l.startswith('#')]
1424 actions = []
1425 actions = []
1425 for r in rules:
1426 for r in rules:
1426 if ' ' not in r:
1427 if ' ' not in r:
1427 raise error.ParseError(_('malformed line "%s"') % r)
1428 raise error.ParseError(_('malformed line "%s"') % r)
1428 verb, rest = r.split(' ', 1)
1429 verb, rest = r.split(' ', 1)
1429
1430
1430 if verb not in actiontable:
1431 if verb not in actiontable:
1431 raise error.ParseError(_('unknown action "%s"') % verb)
1432 raise error.ParseError(_('unknown action "%s"') % verb)
1432
1433
1433 action = actiontable[verb].fromrule(state, rest)
1434 action = actiontable[verb].fromrule(state, rest)
1434 actions.append(action)
1435 actions.append(action)
1435 return actions
1436 return actions
1436
1437
1437 def warnverifyactions(ui, repo, actions, state, ctxs):
1438 def warnverifyactions(ui, repo, actions, state, ctxs):
1438 try:
1439 try:
1439 verifyactions(actions, state, ctxs)
1440 verifyactions(actions, state, ctxs)
1440 except error.ParseError:
1441 except error.ParseError:
1441 if repo.vfs.exists('histedit-last-edit.txt'):
1442 if repo.vfs.exists('histedit-last-edit.txt'):
1442 ui.warn(_('warning: histedit rules saved '
1443 ui.warn(_('warning: histedit rules saved '
1443 'to: .hg/histedit-last-edit.txt\n'))
1444 'to: .hg/histedit-last-edit.txt\n'))
1444 raise
1445 raise
1445
1446
1446 def verifyactions(actions, state, ctxs):
1447 def verifyactions(actions, state, ctxs):
1447 """Verify that there exists exactly one action per given changeset and
1448 """Verify that there exists exactly one action per given changeset and
1448 other constraints.
1449 other constraints.
1449
1450
1450 Will abort if there are to many or too few rules, a malformed rule,
1451 Will abort if there are to many or too few rules, a malformed rule,
1451 or a rule on a changeset outside of the user-given range.
1452 or a rule on a changeset outside of the user-given range.
1452 """
1453 """
1453 expected = set(c.node() for c in ctxs)
1454 expected = set(c.node() for c in ctxs)
1454 seen = set()
1455 seen = set()
1455 prev = None
1456 prev = None
1456
1457
1457 if actions and actions[0].verb in ['roll', 'fold']:
1458 if actions and actions[0].verb in ['roll', 'fold']:
1458 raise error.ParseError(_('first changeset cannot use verb "%s"') %
1459 raise error.ParseError(_('first changeset cannot use verb "%s"') %
1459 actions[0].verb)
1460 actions[0].verb)
1460
1461
1461 for action in actions:
1462 for action in actions:
1462 action.verify(prev, expected, seen)
1463 action.verify(prev, expected, seen)
1463 prev = action
1464 prev = action
1464 if action.node is not None:
1465 if action.node is not None:
1465 seen.add(action.node)
1466 seen.add(action.node)
1466 missing = sorted(expected - seen) # sort to stabilize output
1467 missing = sorted(expected - seen) # sort to stabilize output
1467
1468
1468 if state.repo.ui.configbool('histedit', 'dropmissing'):
1469 if state.repo.ui.configbool('histedit', 'dropmissing'):
1469 if len(actions) == 0:
1470 if len(actions) == 0:
1470 raise error.ParseError(_('no rules provided'),
1471 raise error.ParseError(_('no rules provided'),
1471 hint=_('use strip extension to remove commits'))
1472 hint=_('use strip extension to remove commits'))
1472
1473
1473 drops = [drop(state, n) for n in missing]
1474 drops = [drop(state, n) for n in missing]
1474 # put the in the beginning so they execute immediately and
1475 # put the in the beginning so they execute immediately and
1475 # don't show in the edit-plan in the future
1476 # don't show in the edit-plan in the future
1476 actions[:0] = drops
1477 actions[:0] = drops
1477 elif missing:
1478 elif missing:
1478 raise error.ParseError(_('missing rules for changeset %s') %
1479 raise error.ParseError(_('missing rules for changeset %s') %
1479 node.short(missing[0]),
1480 node.short(missing[0]),
1480 hint=_('use "drop %s" to discard, see also: '
1481 hint=_('use "drop %s" to discard, see also: '
1481 "'hg help -e histedit.config'")
1482 "'hg help -e histedit.config'")
1482 % node.short(missing[0]))
1483 % node.short(missing[0]))
1483
1484
1484 def adjustreplacementsfrommarkers(repo, oldreplacements):
1485 def adjustreplacementsfrommarkers(repo, oldreplacements):
1485 """Adjust replacements from obsolescence markers
1486 """Adjust replacements from obsolescence markers
1486
1487
1487 Replacements structure is originally generated based on
1488 Replacements structure is originally generated based on
1488 histedit's state and does not account for changes that are
1489 histedit's state and does not account for changes that are
1489 not recorded there. This function fixes that by adding
1490 not recorded there. This function fixes that by adding
1490 data read from obsolescence markers"""
1491 data read from obsolescence markers"""
1491 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1492 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1492 return oldreplacements
1493 return oldreplacements
1493
1494
1494 unfi = repo.unfiltered()
1495 unfi = repo.unfiltered()
1495 nm = unfi.changelog.nodemap
1496 nm = unfi.changelog.nodemap
1496 obsstore = repo.obsstore
1497 obsstore = repo.obsstore
1497 newreplacements = list(oldreplacements)
1498 newreplacements = list(oldreplacements)
1498 oldsuccs = [r[1] for r in oldreplacements]
1499 oldsuccs = [r[1] for r in oldreplacements]
1499 # successors that have already been added to succstocheck once
1500 # successors that have already been added to succstocheck once
1500 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1501 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1501 succstocheck = list(seensuccs)
1502 succstocheck = list(seensuccs)
1502 while succstocheck:
1503 while succstocheck:
1503 n = succstocheck.pop()
1504 n = succstocheck.pop()
1504 missing = nm.get(n) is None
1505 missing = nm.get(n) is None
1505 markers = obsstore.successors.get(n, ())
1506 markers = obsstore.successors.get(n, ())
1506 if missing and not markers:
1507 if missing and not markers:
1507 # dead end, mark it as such
1508 # dead end, mark it as such
1508 newreplacements.append((n, ()))
1509 newreplacements.append((n, ()))
1509 for marker in markers:
1510 for marker in markers:
1510 nsuccs = marker[1]
1511 nsuccs = marker[1]
1511 newreplacements.append((n, nsuccs))
1512 newreplacements.append((n, nsuccs))
1512 for nsucc in nsuccs:
1513 for nsucc in nsuccs:
1513 if nsucc not in seensuccs:
1514 if nsucc not in seensuccs:
1514 seensuccs.add(nsucc)
1515 seensuccs.add(nsucc)
1515 succstocheck.append(nsucc)
1516 succstocheck.append(nsucc)
1516
1517
1517 return newreplacements
1518 return newreplacements
1518
1519
1519 def processreplacement(state):
1520 def processreplacement(state):
1520 """process the list of replacements to return
1521 """process the list of replacements to return
1521
1522
1522 1) the final mapping between original and created nodes
1523 1) the final mapping between original and created nodes
1523 2) the list of temporary node created by histedit
1524 2) the list of temporary node created by histedit
1524 3) the list of new commit created by histedit"""
1525 3) the list of new commit created by histedit"""
1525 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1526 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1526 allsuccs = set()
1527 allsuccs = set()
1527 replaced = set()
1528 replaced = set()
1528 fullmapping = {}
1529 fullmapping = {}
1529 # initialize basic set
1530 # initialize basic set
1530 # fullmapping records all operations recorded in replacement
1531 # fullmapping records all operations recorded in replacement
1531 for rep in replacements:
1532 for rep in replacements:
1532 allsuccs.update(rep[1])
1533 allsuccs.update(rep[1])
1533 replaced.add(rep[0])
1534 replaced.add(rep[0])
1534 fullmapping.setdefault(rep[0], set()).update(rep[1])
1535 fullmapping.setdefault(rep[0], set()).update(rep[1])
1535 new = allsuccs - replaced
1536 new = allsuccs - replaced
1536 tmpnodes = allsuccs & replaced
1537 tmpnodes = allsuccs & replaced
1537 # Reduce content fullmapping into direct relation between original nodes
1538 # Reduce content fullmapping into direct relation between original nodes
1538 # and final node created during history edition
1539 # and final node created during history edition
1539 # Dropped changeset are replaced by an empty list
1540 # Dropped changeset are replaced by an empty list
1540 toproceed = set(fullmapping)
1541 toproceed = set(fullmapping)
1541 final = {}
1542 final = {}
1542 while toproceed:
1543 while toproceed:
1543 for x in list(toproceed):
1544 for x in list(toproceed):
1544 succs = fullmapping[x]
1545 succs = fullmapping[x]
1545 for s in list(succs):
1546 for s in list(succs):
1546 if s in toproceed:
1547 if s in toproceed:
1547 # non final node with unknown closure
1548 # non final node with unknown closure
1548 # We can't process this now
1549 # We can't process this now
1549 break
1550 break
1550 elif s in final:
1551 elif s in final:
1551 # non final node, replace with closure
1552 # non final node, replace with closure
1552 succs.remove(s)
1553 succs.remove(s)
1553 succs.update(final[s])
1554 succs.update(final[s])
1554 else:
1555 else:
1555 final[x] = succs
1556 final[x] = succs
1556 toproceed.remove(x)
1557 toproceed.remove(x)
1557 # remove tmpnodes from final mapping
1558 # remove tmpnodes from final mapping
1558 for n in tmpnodes:
1559 for n in tmpnodes:
1559 del final[n]
1560 del final[n]
1560 # we expect all changes involved in final to exist in the repo
1561 # we expect all changes involved in final to exist in the repo
1561 # turn `final` into list (topologically sorted)
1562 # turn `final` into list (topologically sorted)
1562 nm = state.repo.changelog.nodemap
1563 nm = state.repo.changelog.nodemap
1563 for prec, succs in final.items():
1564 for prec, succs in final.items():
1564 final[prec] = sorted(succs, key=nm.get)
1565 final[prec] = sorted(succs, key=nm.get)
1565
1566
1566 # computed topmost element (necessary for bookmark)
1567 # computed topmost element (necessary for bookmark)
1567 if new:
1568 if new:
1568 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1569 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1569 elif not final:
1570 elif not final:
1570 # Nothing rewritten at all. we won't need `newtopmost`
1571 # Nothing rewritten at all. we won't need `newtopmost`
1571 # It is the same as `oldtopmost` and `processreplacement` know it
1572 # It is the same as `oldtopmost` and `processreplacement` know it
1572 newtopmost = None
1573 newtopmost = None
1573 else:
1574 else:
1574 # every body died. The newtopmost is the parent of the root.
1575 # every body died. The newtopmost is the parent of the root.
1575 r = state.repo.changelog.rev
1576 r = state.repo.changelog.rev
1576 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1577 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1577
1578
1578 return final, tmpnodes, new, newtopmost
1579 return final, tmpnodes, new, newtopmost
1579
1580
1580 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
1581 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
1581 """Move bookmark from oldtopmost to newly created topmost
1582 """Move bookmark from oldtopmost to newly created topmost
1582
1583
1583 This is arguably a feature and we may only want that for the active
1584 This is arguably a feature and we may only want that for the active
1584 bookmark. But the behavior is kept compatible with the old version for now.
1585 bookmark. But the behavior is kept compatible with the old version for now.
1585 """
1586 """
1586 if not oldtopmost or not newtopmost:
1587 if not oldtopmost or not newtopmost:
1587 return
1588 return
1588 oldbmarks = repo.nodebookmarks(oldtopmost)
1589 oldbmarks = repo.nodebookmarks(oldtopmost)
1589 if oldbmarks:
1590 if oldbmarks:
1590 with repo.lock(), repo.transaction('histedit') as tr:
1591 with repo.lock(), repo.transaction('histedit') as tr:
1591 marks = repo._bookmarks
1592 marks = repo._bookmarks
1592 changes = []
1593 changes = []
1593 for name in oldbmarks:
1594 for name in oldbmarks:
1594 changes.append((name, newtopmost))
1595 changes.append((name, newtopmost))
1595 marks.applychanges(repo, tr, changes)
1596 marks.applychanges(repo, tr, changes)
1596
1597
1597 def cleanupnode(ui, repo, nodes):
1598 def cleanupnode(ui, repo, nodes):
1598 """strip a group of nodes from the repository
1599 """strip a group of nodes from the repository
1599
1600
1600 The set of node to strip may contains unknown nodes."""
1601 The set of node to strip may contains unknown nodes."""
1601 with repo.lock():
1602 with repo.lock():
1602 # do not let filtering get in the way of the cleanse
1603 # do not let filtering get in the way of the cleanse
1603 # we should probably get rid of obsolescence marker created during the
1604 # we should probably get rid of obsolescence marker created during the
1604 # histedit, but we currently do not have such information.
1605 # histedit, but we currently do not have such information.
1605 repo = repo.unfiltered()
1606 repo = repo.unfiltered()
1606 # Find all nodes that need to be stripped
1607 # Find all nodes that need to be stripped
1607 # (we use %lr instead of %ln to silently ignore unknown items)
1608 # (we use %lr instead of %ln to silently ignore unknown items)
1608 nm = repo.changelog.nodemap
1609 nm = repo.changelog.nodemap
1609 nodes = sorted(n for n in nodes if n in nm)
1610 nodes = sorted(n for n in nodes if n in nm)
1610 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1611 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1611 if roots:
1612 if roots:
1612 repair.strip(ui, repo, roots)
1613 repair.strip(ui, repo, roots)
1613
1614
1614 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1615 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1615 if isinstance(nodelist, str):
1616 if isinstance(nodelist, str):
1616 nodelist = [nodelist]
1617 nodelist = [nodelist]
1617 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1618 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1618 state = histeditstate(repo)
1619 state = histeditstate(repo)
1619 state.read()
1620 state.read()
1620 histedit_nodes = {action.node for action
1621 histedit_nodes = {action.node for action
1621 in state.actions if action.node}
1622 in state.actions if action.node}
1622 common_nodes = histedit_nodes & set(nodelist)
1623 common_nodes = histedit_nodes & set(nodelist)
1623 if common_nodes:
1624 if common_nodes:
1624 raise error.Abort(_("histedit in progress, can't strip %s")
1625 raise error.Abort(_("histedit in progress, can't strip %s")
1625 % ', '.join(node.short(x) for x in common_nodes))
1626 % ', '.join(node.short(x) for x in common_nodes))
1626 return orig(ui, repo, nodelist, *args, **kwargs)
1627 return orig(ui, repo, nodelist, *args, **kwargs)
1627
1628
1628 extensions.wrapfunction(repair, 'strip', stripwrapper)
1629 extensions.wrapfunction(repair, 'strip', stripwrapper)
1629
1630
1630 def summaryhook(ui, repo):
1631 def summaryhook(ui, repo):
1631 if not os.path.exists(repo.vfs.join('histedit-state')):
1632 if not os.path.exists(repo.vfs.join('histedit-state')):
1632 return
1633 return
1633 state = histeditstate(repo)
1634 state = histeditstate(repo)
1634 state.read()
1635 state.read()
1635 if state.actions:
1636 if state.actions:
1636 # i18n: column positioning for "hg summary"
1637 # i18n: column positioning for "hg summary"
1637 ui.write(_('hist: %s (histedit --continue)\n') %
1638 ui.write(_('hist: %s (histedit --continue)\n') %
1638 (ui.label(_('%d remaining'), 'histedit.remaining') %
1639 (ui.label(_('%d remaining'), 'histedit.remaining') %
1639 len(state.actions)))
1640 len(state.actions)))
1640
1641
1641 def extsetup(ui):
1642 def extsetup(ui):
1642 cmdutil.summaryhooks.add('histedit', summaryhook)
1643 cmdutil.summaryhooks.add('histedit', summaryhook)
1643 cmdutil.unfinishedstates.append(
1644 cmdutil.unfinishedstates.append(
1644 ['histedit-state', False, True, _('histedit in progress'),
1645 ['histedit-state', False, True, _('histedit in progress'),
1645 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1646 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1646 cmdutil.afterresolvedstates.append(
1647 cmdutil.afterresolvedstates.append(
1647 ['histedit-state', _('hg histedit --continue')])
1648 ['histedit-state', _('hg histedit --continue')])
@@ -1,1527 +1,1527
1 # templater.py - template expansion for output
1 # templater.py - template expansion for output
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.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 from __future__ import absolute_import, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import os
10 import os
11 import re
11 import re
12 import types
12 import types
13
13
14 from .i18n import _
14 from .i18n import _
15 from . import (
15 from . import (
16 color,
16 color,
17 config,
17 config,
18 encoding,
18 encoding,
19 error,
19 error,
20 minirst,
20 minirst,
21 obsutil,
21 obsutil,
22 parser,
22 parser,
23 pycompat,
23 pycompat,
24 registrar,
24 registrar,
25 revset as revsetmod,
25 revset as revsetmod,
26 revsetlang,
26 revsetlang,
27 scmutil,
27 scmutil,
28 templatefilters,
28 templatefilters,
29 templatekw,
29 templatekw,
30 util,
30 util,
31 )
31 )
32
32
33 # template parsing
33 # template parsing
34
34
35 elements = {
35 elements = {
36 # token-type: binding-strength, primary, prefix, infix, suffix
36 # token-type: binding-strength, primary, prefix, infix, suffix
37 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
37 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
38 ".": (18, None, None, (".", 18), None),
38 ".": (18, None, None, (".", 18), None),
39 "%": (15, None, None, ("%", 15), None),
39 "%": (15, None, None, ("%", 15), None),
40 "|": (15, None, None, ("|", 15), None),
40 "|": (15, None, None, ("|", 15), None),
41 "*": (5, None, None, ("*", 5), None),
41 "*": (5, None, None, ("*", 5), None),
42 "/": (5, None, None, ("/", 5), None),
42 "/": (5, None, None, ("/", 5), None),
43 "+": (4, None, None, ("+", 4), None),
43 "+": (4, None, None, ("+", 4), None),
44 "-": (4, None, ("negate", 19), ("-", 4), None),
44 "-": (4, None, ("negate", 19), ("-", 4), None),
45 "=": (3, None, None, ("keyvalue", 3), None),
45 "=": (3, None, None, ("keyvalue", 3), None),
46 ",": (2, None, None, ("list", 2), None),
46 ",": (2, None, None, ("list", 2), None),
47 ")": (0, None, None, None, None),
47 ")": (0, None, None, None, None),
48 "integer": (0, "integer", None, None, None),
48 "integer": (0, "integer", None, None, None),
49 "symbol": (0, "symbol", None, None, None),
49 "symbol": (0, "symbol", None, None, None),
50 "string": (0, "string", None, None, None),
50 "string": (0, "string", None, None, None),
51 "template": (0, "template", None, None, None),
51 "template": (0, "template", None, None, None),
52 "end": (0, None, None, None, None),
52 "end": (0, None, None, None, None),
53 }
53 }
54
54
55 def tokenize(program, start, end, term=None):
55 def tokenize(program, start, end, term=None):
56 """Parse a template expression into a stream of tokens, which must end
56 """Parse a template expression into a stream of tokens, which must end
57 with term if specified"""
57 with term if specified"""
58 pos = start
58 pos = start
59 program = pycompat.bytestr(program)
59 program = pycompat.bytestr(program)
60 while pos < end:
60 while pos < end:
61 c = program[pos]
61 c = program[pos]
62 if c.isspace(): # skip inter-token whitespace
62 if c.isspace(): # skip inter-token whitespace
63 pass
63 pass
64 elif c in "(=,).%|+-*/": # handle simple operators
64 elif c in "(=,).%|+-*/": # handle simple operators
65 yield (c, None, pos)
65 yield (c, None, pos)
66 elif c in '"\'': # handle quoted templates
66 elif c in '"\'': # handle quoted templates
67 s = pos + 1
67 s = pos + 1
68 data, pos = _parsetemplate(program, s, end, c)
68 data, pos = _parsetemplate(program, s, end, c)
69 yield ('template', data, s)
69 yield ('template', data, s)
70 pos -= 1
70 pos -= 1
71 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
71 elif c == 'r' and program[pos:pos + 2] in ("r'", 'r"'):
72 # handle quoted strings
72 # handle quoted strings
73 c = program[pos + 1]
73 c = program[pos + 1]
74 s = pos = pos + 2
74 s = pos = pos + 2
75 while pos < end: # find closing quote
75 while pos < end: # find closing quote
76 d = program[pos]
76 d = program[pos]
77 if d == '\\': # skip over escaped characters
77 if d == '\\': # skip over escaped characters
78 pos += 2
78 pos += 2
79 continue
79 continue
80 if d == c:
80 if d == c:
81 yield ('string', program[s:pos], s)
81 yield ('string', program[s:pos], s)
82 break
82 break
83 pos += 1
83 pos += 1
84 else:
84 else:
85 raise error.ParseError(_("unterminated string"), s)
85 raise error.ParseError(_("unterminated string"), s)
86 elif c.isdigit():
86 elif c.isdigit():
87 s = pos
87 s = pos
88 while pos < end:
88 while pos < end:
89 d = program[pos]
89 d = program[pos]
90 if not d.isdigit():
90 if not d.isdigit():
91 break
91 break
92 pos += 1
92 pos += 1
93 yield ('integer', program[s:pos], s)
93 yield ('integer', program[s:pos], s)
94 pos -= 1
94 pos -= 1
95 elif (c == '\\' and program[pos:pos + 2] in (r"\'", r'\"')
95 elif (c == '\\' and program[pos:pos + 2] in (r"\'", r'\"')
96 or c == 'r' and program[pos:pos + 3] in (r"r\'", r'r\"')):
96 or c == 'r' and program[pos:pos + 3] in (r"r\'", r'r\"')):
97 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
97 # handle escaped quoted strings for compatibility with 2.9.2-3.4,
98 # where some of nested templates were preprocessed as strings and
98 # where some of nested templates were preprocessed as strings and
99 # then compiled. therefore, \"...\" was allowed. (issue4733)
99 # then compiled. therefore, \"...\" was allowed. (issue4733)
100 #
100 #
101 # processing flow of _evalifliteral() at 5ab28a2e9962:
101 # processing flow of _evalifliteral() at 5ab28a2e9962:
102 # outer template string -> stringify() -> compiletemplate()
102 # outer template string -> stringify() -> compiletemplate()
103 # ------------------------ ------------ ------------------
103 # ------------------------ ------------ ------------------
104 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
104 # {f("\\\\ {g(\"\\\"\")}"} \\ {g("\"")} [r'\\', {g("\"")}]
105 # ~~~~~~~~
105 # ~~~~~~~~
106 # escaped quoted string
106 # escaped quoted string
107 if c == 'r':
107 if c == 'r':
108 pos += 1
108 pos += 1
109 token = 'string'
109 token = 'string'
110 else:
110 else:
111 token = 'template'
111 token = 'template'
112 quote = program[pos:pos + 2]
112 quote = program[pos:pos + 2]
113 s = pos = pos + 2
113 s = pos = pos + 2
114 while pos < end: # find closing escaped quote
114 while pos < end: # find closing escaped quote
115 if program.startswith('\\\\\\', pos, end):
115 if program.startswith('\\\\\\', pos, end):
116 pos += 4 # skip over double escaped characters
116 pos += 4 # skip over double escaped characters
117 continue
117 continue
118 if program.startswith(quote, pos, end):
118 if program.startswith(quote, pos, end):
119 # interpret as if it were a part of an outer string
119 # interpret as if it were a part of an outer string
120 data = parser.unescapestr(program[s:pos])
120 data = parser.unescapestr(program[s:pos])
121 if token == 'template':
121 if token == 'template':
122 data = _parsetemplate(data, 0, len(data))[0]
122 data = _parsetemplate(data, 0, len(data))[0]
123 yield (token, data, s)
123 yield (token, data, s)
124 pos += 1
124 pos += 1
125 break
125 break
126 pos += 1
126 pos += 1
127 else:
127 else:
128 raise error.ParseError(_("unterminated string"), s)
128 raise error.ParseError(_("unterminated string"), s)
129 elif c.isalnum() or c in '_':
129 elif c.isalnum() or c in '_':
130 s = pos
130 s = pos
131 pos += 1
131 pos += 1
132 while pos < end: # find end of symbol
132 while pos < end: # find end of symbol
133 d = program[pos]
133 d = program[pos]
134 if not (d.isalnum() or d == "_"):
134 if not (d.isalnum() or d == "_"):
135 break
135 break
136 pos += 1
136 pos += 1
137 sym = program[s:pos]
137 sym = program[s:pos]
138 yield ('symbol', sym, s)
138 yield ('symbol', sym, s)
139 pos -= 1
139 pos -= 1
140 elif c == term:
140 elif c == term:
141 yield ('end', None, pos + 1)
141 yield ('end', None, pos + 1)
142 return
142 return
143 else:
143 else:
144 raise error.ParseError(_("syntax error"), pos)
144 raise error.ParseError(_("syntax error"), pos)
145 pos += 1
145 pos += 1
146 if term:
146 if term:
147 raise error.ParseError(_("unterminated template expansion"), start)
147 raise error.ParseError(_("unterminated template expansion"), start)
148 yield ('end', None, pos)
148 yield ('end', None, pos)
149
149
150 def _parsetemplate(tmpl, start, stop, quote=''):
150 def _parsetemplate(tmpl, start, stop, quote=''):
151 r"""
151 r"""
152 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
152 >>> _parsetemplate(b'foo{bar}"baz', 0, 12)
153 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
153 ([('string', 'foo'), ('symbol', 'bar'), ('string', '"baz')], 12)
154 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
154 >>> _parsetemplate(b'foo{bar}"baz', 0, 12, quote=b'"')
155 ([('string', 'foo'), ('symbol', 'bar')], 9)
155 ([('string', 'foo'), ('symbol', 'bar')], 9)
156 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
156 >>> _parsetemplate(b'foo"{bar}', 0, 9, quote=b'"')
157 ([('string', 'foo')], 4)
157 ([('string', 'foo')], 4)
158 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
158 >>> _parsetemplate(br'foo\"bar"baz', 0, 12, quote=b'"')
159 ([('string', 'foo"'), ('string', 'bar')], 9)
159 ([('string', 'foo"'), ('string', 'bar')], 9)
160 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
160 >>> _parsetemplate(br'foo\\"bar', 0, 10, quote=b'"')
161 ([('string', 'foo\\')], 6)
161 ([('string', 'foo\\')], 6)
162 """
162 """
163 parsed = []
163 parsed = []
164 sepchars = '{' + quote
164 sepchars = '{' + quote
165 pos = start
165 pos = start
166 p = parser.parser(elements)
166 p = parser.parser(elements)
167 while pos < stop:
167 while pos < stop:
168 n = min((tmpl.find(c, pos, stop) for c in sepchars),
168 n = min((tmpl.find(c, pos, stop) for c in sepchars),
169 key=lambda n: (n < 0, n))
169 key=lambda n: (n < 0, n))
170 if n < 0:
170 if n < 0:
171 parsed.append(('string', parser.unescapestr(tmpl[pos:stop])))
171 parsed.append(('string', parser.unescapestr(tmpl[pos:stop])))
172 pos = stop
172 pos = stop
173 break
173 break
174 c = tmpl[n:n + 1]
174 c = tmpl[n:n + 1]
175 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
175 bs = (n - pos) - len(tmpl[pos:n].rstrip('\\'))
176 if bs % 2 == 1:
176 if bs % 2 == 1:
177 # escaped (e.g. '\{', '\\\{', but not '\\{')
177 # escaped (e.g. '\{', '\\\{', but not '\\{')
178 parsed.append(('string', parser.unescapestr(tmpl[pos:n - 1]) + c))
178 parsed.append(('string', parser.unescapestr(tmpl[pos:n - 1]) + c))
179 pos = n + 1
179 pos = n + 1
180 continue
180 continue
181 if n > pos:
181 if n > pos:
182 parsed.append(('string', parser.unescapestr(tmpl[pos:n])))
182 parsed.append(('string', parser.unescapestr(tmpl[pos:n])))
183 if c == quote:
183 if c == quote:
184 return parsed, n + 1
184 return parsed, n + 1
185
185
186 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
186 parseres, pos = p.parse(tokenize(tmpl, n + 1, stop, '}'))
187 parsed.append(parseres)
187 parsed.append(parseres)
188
188
189 if quote:
189 if quote:
190 raise error.ParseError(_("unterminated string"), start)
190 raise error.ParseError(_("unterminated string"), start)
191 return parsed, pos
191 return parsed, pos
192
192
193 def _unnesttemplatelist(tree):
193 def _unnesttemplatelist(tree):
194 """Expand list of templates to node tuple
194 """Expand list of templates to node tuple
195
195
196 >>> def f(tree):
196 >>> def f(tree):
197 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
197 ... print(pycompat.sysstr(prettyformat(_unnesttemplatelist(tree))))
198 >>> f((b'template', []))
198 >>> f((b'template', []))
199 (string '')
199 (string '')
200 >>> f((b'template', [(b'string', b'foo')]))
200 >>> f((b'template', [(b'string', b'foo')]))
201 (string 'foo')
201 (string 'foo')
202 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
202 >>> f((b'template', [(b'string', b'foo'), (b'symbol', b'rev')]))
203 (template
203 (template
204 (string 'foo')
204 (string 'foo')
205 (symbol 'rev'))
205 (symbol 'rev'))
206 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
206 >>> f((b'template', [(b'symbol', b'rev')])) # template(rev) -> str
207 (template
207 (template
208 (symbol 'rev'))
208 (symbol 'rev'))
209 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
209 >>> f((b'template', [(b'template', [(b'string', b'foo')])]))
210 (string 'foo')
210 (string 'foo')
211 """
211 """
212 if not isinstance(tree, tuple):
212 if not isinstance(tree, tuple):
213 return tree
213 return tree
214 op = tree[0]
214 op = tree[0]
215 if op != 'template':
215 if op != 'template':
216 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
216 return (op,) + tuple(_unnesttemplatelist(x) for x in tree[1:])
217
217
218 assert len(tree) == 2
218 assert len(tree) == 2
219 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
219 xs = tuple(_unnesttemplatelist(x) for x in tree[1])
220 if not xs:
220 if not xs:
221 return ('string', '') # empty template ""
221 return ('string', '') # empty template ""
222 elif len(xs) == 1 and xs[0][0] == 'string':
222 elif len(xs) == 1 and xs[0][0] == 'string':
223 return xs[0] # fast path for string with no template fragment "x"
223 return xs[0] # fast path for string with no template fragment "x"
224 else:
224 else:
225 return (op,) + xs
225 return (op,) + xs
226
226
227 def parse(tmpl):
227 def parse(tmpl):
228 """Parse template string into tree"""
228 """Parse template string into tree"""
229 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
229 parsed, pos = _parsetemplate(tmpl, 0, len(tmpl))
230 assert pos == len(tmpl), 'unquoted template should be consumed'
230 assert pos == len(tmpl), 'unquoted template should be consumed'
231 return _unnesttemplatelist(('template', parsed))
231 return _unnesttemplatelist(('template', parsed))
232
232
233 def _parseexpr(expr):
233 def _parseexpr(expr):
234 """Parse a template expression into tree
234 """Parse a template expression into tree
235
235
236 >>> _parseexpr(b'"foo"')
236 >>> _parseexpr(b'"foo"')
237 ('string', 'foo')
237 ('string', 'foo')
238 >>> _parseexpr(b'foo(bar)')
238 >>> _parseexpr(b'foo(bar)')
239 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
239 ('func', ('symbol', 'foo'), ('symbol', 'bar'))
240 >>> _parseexpr(b'foo(')
240 >>> _parseexpr(b'foo(')
241 Traceback (most recent call last):
241 Traceback (most recent call last):
242 ...
242 ...
243 ParseError: ('not a prefix: end', 4)
243 ParseError: ('not a prefix: end', 4)
244 >>> _parseexpr(b'"foo" "bar"')
244 >>> _parseexpr(b'"foo" "bar"')
245 Traceback (most recent call last):
245 Traceback (most recent call last):
246 ...
246 ...
247 ParseError: ('invalid token', 7)
247 ParseError: ('invalid token', 7)
248 """
248 """
249 p = parser.parser(elements)
249 p = parser.parser(elements)
250 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
250 tree, pos = p.parse(tokenize(expr, 0, len(expr)))
251 if pos != len(expr):
251 if pos != len(expr):
252 raise error.ParseError(_('invalid token'), pos)
252 raise error.ParseError(_('invalid token'), pos)
253 return _unnesttemplatelist(tree)
253 return _unnesttemplatelist(tree)
254
254
255 def prettyformat(tree):
255 def prettyformat(tree):
256 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
256 return parser.prettyformat(tree, ('integer', 'string', 'symbol'))
257
257
258 def compileexp(exp, context, curmethods):
258 def compileexp(exp, context, curmethods):
259 """Compile parsed template tree to (func, data) pair"""
259 """Compile parsed template tree to (func, data) pair"""
260 t = exp[0]
260 t = exp[0]
261 if t in curmethods:
261 if t in curmethods:
262 return curmethods[t](exp, context)
262 return curmethods[t](exp, context)
263 raise error.ParseError(_("unknown method '%s'") % t)
263 raise error.ParseError(_("unknown method '%s'") % t)
264
264
265 # template evaluation
265 # template evaluation
266
266
267 def getsymbol(exp):
267 def getsymbol(exp):
268 if exp[0] == 'symbol':
268 if exp[0] == 'symbol':
269 return exp[1]
269 return exp[1]
270 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
270 raise error.ParseError(_("expected a symbol, got '%s'") % exp[0])
271
271
272 def getlist(x):
272 def getlist(x):
273 if not x:
273 if not x:
274 return []
274 return []
275 if x[0] == 'list':
275 if x[0] == 'list':
276 return getlist(x[1]) + [x[2]]
276 return getlist(x[1]) + [x[2]]
277 return [x]
277 return [x]
278
278
279 def gettemplate(exp, context):
279 def gettemplate(exp, context):
280 """Compile given template tree or load named template from map file;
280 """Compile given template tree or load named template from map file;
281 returns (func, data) pair"""
281 returns (func, data) pair"""
282 if exp[0] in ('template', 'string'):
282 if exp[0] in ('template', 'string'):
283 return compileexp(exp, context, methods)
283 return compileexp(exp, context, methods)
284 if exp[0] == 'symbol':
284 if exp[0] == 'symbol':
285 # unlike runsymbol(), here 'symbol' is always taken as template name
285 # unlike runsymbol(), here 'symbol' is always taken as template name
286 # even if it exists in mapping. this allows us to override mapping
286 # even if it exists in mapping. this allows us to override mapping
287 # by web templates, e.g. 'changelogtag' is redefined in map file.
287 # by web templates, e.g. 'changelogtag' is redefined in map file.
288 return context._load(exp[1])
288 return context._load(exp[1])
289 raise error.ParseError(_("expected template specifier"))
289 raise error.ParseError(_("expected template specifier"))
290
290
291 def findsymbolicname(arg):
291 def findsymbolicname(arg):
292 """Find symbolic name for the given compiled expression; returns None
292 """Find symbolic name for the given compiled expression; returns None
293 if nothing found reliably"""
293 if nothing found reliably"""
294 while True:
294 while True:
295 func, data = arg
295 func, data = arg
296 if func is runsymbol:
296 if func is runsymbol:
297 return data
297 return data
298 elif func is runfilter:
298 elif func is runfilter:
299 arg = data[0]
299 arg = data[0]
300 else:
300 else:
301 return None
301 return None
302
302
303 def evalrawexp(context, mapping, arg):
303 def evalrawexp(context, mapping, arg):
304 """Evaluate given argument as a bare template object which may require
304 """Evaluate given argument as a bare template object which may require
305 further processing (such as folding generator of strings)"""
305 further processing (such as folding generator of strings)"""
306 func, data = arg
306 func, data = arg
307 return func(context, mapping, data)
307 return func(context, mapping, data)
308
308
309 def evalfuncarg(context, mapping, arg):
309 def evalfuncarg(context, mapping, arg):
310 """Evaluate given argument as value type"""
310 """Evaluate given argument as value type"""
311 thing = evalrawexp(context, mapping, arg)
311 thing = evalrawexp(context, mapping, arg)
312 thing = templatekw.unwrapvalue(thing)
312 thing = templatekw.unwrapvalue(thing)
313 # evalrawexp() may return string, generator of strings or arbitrary object
313 # evalrawexp() may return string, generator of strings or arbitrary object
314 # such as date tuple, but filter does not want generator.
314 # such as date tuple, but filter does not want generator.
315 if isinstance(thing, types.GeneratorType):
315 if isinstance(thing, types.GeneratorType):
316 thing = stringify(thing)
316 thing = stringify(thing)
317 return thing
317 return thing
318
318
319 def evalboolean(context, mapping, arg):
319 def evalboolean(context, mapping, arg):
320 """Evaluate given argument as boolean, but also takes boolean literals"""
320 """Evaluate given argument as boolean, but also takes boolean literals"""
321 func, data = arg
321 func, data = arg
322 if func is runsymbol:
322 if func is runsymbol:
323 thing = func(context, mapping, data, default=None)
323 thing = func(context, mapping, data, default=None)
324 if thing is None:
324 if thing is None:
325 # not a template keyword, takes as a boolean literal
325 # not a template keyword, takes as a boolean literal
326 thing = util.parsebool(data)
326 thing = util.parsebool(data)
327 else:
327 else:
328 thing = func(context, mapping, data)
328 thing = func(context, mapping, data)
329 thing = templatekw.unwrapvalue(thing)
329 thing = templatekw.unwrapvalue(thing)
330 if isinstance(thing, bool):
330 if isinstance(thing, bool):
331 return thing
331 return thing
332 # other objects are evaluated as strings, which means 0 is True, but
332 # other objects are evaluated as strings, which means 0 is True, but
333 # empty dict/list should be False as they are expected to be ''
333 # empty dict/list should be False as they are expected to be ''
334 return bool(stringify(thing))
334 return bool(stringify(thing))
335
335
336 def evalinteger(context, mapping, arg, err=None):
336 def evalinteger(context, mapping, arg, err=None):
337 v = evalfuncarg(context, mapping, arg)
337 v = evalfuncarg(context, mapping, arg)
338 try:
338 try:
339 return int(v)
339 return int(v)
340 except (TypeError, ValueError):
340 except (TypeError, ValueError):
341 raise error.ParseError(err or _('not an integer'))
341 raise error.ParseError(err or _('not an integer'))
342
342
343 def evalstring(context, mapping, arg):
343 def evalstring(context, mapping, arg):
344 return stringify(evalrawexp(context, mapping, arg))
344 return stringify(evalrawexp(context, mapping, arg))
345
345
346 def evalstringliteral(context, mapping, arg):
346 def evalstringliteral(context, mapping, arg):
347 """Evaluate given argument as string template, but returns symbol name
347 """Evaluate given argument as string template, but returns symbol name
348 if it is unknown"""
348 if it is unknown"""
349 func, data = arg
349 func, data = arg
350 if func is runsymbol:
350 if func is runsymbol:
351 thing = func(context, mapping, data, default=data)
351 thing = func(context, mapping, data, default=data)
352 else:
352 else:
353 thing = func(context, mapping, data)
353 thing = func(context, mapping, data)
354 return stringify(thing)
354 return stringify(thing)
355
355
356 _evalfuncbytype = {
356 _evalfuncbytype = {
357 bool: evalboolean,
357 bool: evalboolean,
358 bytes: evalstring,
358 bytes: evalstring,
359 int: evalinteger,
359 int: evalinteger,
360 }
360 }
361
361
362 def evalastype(context, mapping, arg, typ):
362 def evalastype(context, mapping, arg, typ):
363 """Evaluate given argument and coerce its type"""
363 """Evaluate given argument and coerce its type"""
364 try:
364 try:
365 f = _evalfuncbytype[typ]
365 f = _evalfuncbytype[typ]
366 except KeyError:
366 except KeyError:
367 raise error.ProgrammingError('invalid type specified: %r' % typ)
367 raise error.ProgrammingError('invalid type specified: %r' % typ)
368 return f(context, mapping, arg)
368 return f(context, mapping, arg)
369
369
370 def runinteger(context, mapping, data):
370 def runinteger(context, mapping, data):
371 return int(data)
371 return int(data)
372
372
373 def runstring(context, mapping, data):
373 def runstring(context, mapping, data):
374 return data
374 return data
375
375
376 def _recursivesymbolblocker(key):
376 def _recursivesymbolblocker(key):
377 def showrecursion(**args):
377 def showrecursion(**args):
378 raise error.Abort(_("recursive reference '%s' in template") % key)
378 raise error.Abort(_("recursive reference '%s' in template") % key)
379 return showrecursion
379 return showrecursion
380
380
381 def _runrecursivesymbol(context, mapping, key):
381 def _runrecursivesymbol(context, mapping, key):
382 raise error.Abort(_("recursive reference '%s' in template") % key)
382 raise error.Abort(_("recursive reference '%s' in template") % key)
383
383
384 def runsymbol(context, mapping, key, default=''):
384 def runsymbol(context, mapping, key, default=''):
385 v = mapping.get(key)
385 v = mapping.get(key)
386 if v is None:
386 if v is None:
387 v = context._defaults.get(key)
387 v = context._defaults.get(key)
388 if v is None:
388 if v is None:
389 # put poison to cut recursion. we can't move this to parsing phase
389 # put poison to cut recursion. we can't move this to parsing phase
390 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
390 # because "x = {x}" is allowed if "x" is a keyword. (issue4758)
391 safemapping = mapping.copy()
391 safemapping = mapping.copy()
392 safemapping[key] = _recursivesymbolblocker(key)
392 safemapping[key] = _recursivesymbolblocker(key)
393 try:
393 try:
394 v = context.process(key, safemapping)
394 v = context.process(key, safemapping)
395 except TemplateNotFound:
395 except TemplateNotFound:
396 v = default
396 v = default
397 if callable(v):
397 if callable(v):
398 return v(**pycompat.strkwargs(mapping))
398 return v(**pycompat.strkwargs(mapping))
399 return v
399 return v
400
400
401 def buildtemplate(exp, context):
401 def buildtemplate(exp, context):
402 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
402 ctmpl = [compileexp(e, context, methods) for e in exp[1:]]
403 return (runtemplate, ctmpl)
403 return (runtemplate, ctmpl)
404
404
405 def runtemplate(context, mapping, template):
405 def runtemplate(context, mapping, template):
406 for arg in template:
406 for arg in template:
407 yield evalrawexp(context, mapping, arg)
407 yield evalrawexp(context, mapping, arg)
408
408
409 def buildfilter(exp, context):
409 def buildfilter(exp, context):
410 n = getsymbol(exp[2])
410 n = getsymbol(exp[2])
411 if n in context._filters:
411 if n in context._filters:
412 filt = context._filters[n]
412 filt = context._filters[n]
413 arg = compileexp(exp[1], context, methods)
413 arg = compileexp(exp[1], context, methods)
414 return (runfilter, (arg, filt))
414 return (runfilter, (arg, filt))
415 if n in funcs:
415 if n in funcs:
416 f = funcs[n]
416 f = funcs[n]
417 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
417 args = _buildfuncargs(exp[1], context, methods, n, f._argspec)
418 return (f, args)
418 return (f, args)
419 raise error.ParseError(_("unknown function '%s'") % n)
419 raise error.ParseError(_("unknown function '%s'") % n)
420
420
421 def runfilter(context, mapping, data):
421 def runfilter(context, mapping, data):
422 arg, filt = data
422 arg, filt = data
423 thing = evalfuncarg(context, mapping, arg)
423 thing = evalfuncarg(context, mapping, arg)
424 try:
424 try:
425 return filt(thing)
425 return filt(thing)
426 except (ValueError, AttributeError, TypeError):
426 except (ValueError, AttributeError, TypeError):
427 sym = findsymbolicname(arg)
427 sym = findsymbolicname(arg)
428 if sym:
428 if sym:
429 msg = (_("template filter '%s' is not compatible with keyword '%s'")
429 msg = (_("template filter '%s' is not compatible with keyword '%s'")
430 % (pycompat.sysbytes(filt.__name__), sym))
430 % (pycompat.sysbytes(filt.__name__), sym))
431 else:
431 else:
432 msg = (_("incompatible use of template filter '%s'")
432 msg = (_("incompatible use of template filter '%s'")
433 % pycompat.sysbytes(filt.__name__))
433 % pycompat.sysbytes(filt.__name__))
434 raise error.Abort(msg)
434 raise error.Abort(msg)
435
435
436 def buildmap(exp, context):
436 def buildmap(exp, context):
437 darg = compileexp(exp[1], context, methods)
437 darg = compileexp(exp[1], context, methods)
438 targ = gettemplate(exp[2], context)
438 targ = gettemplate(exp[2], context)
439 return (runmap, (darg, targ))
439 return (runmap, (darg, targ))
440
440
441 def runmap(context, mapping, data):
441 def runmap(context, mapping, data):
442 darg, targ = data
442 darg, targ = data
443 d = evalrawexp(context, mapping, darg)
443 d = evalrawexp(context, mapping, darg)
444 if util.safehasattr(d, 'itermaps'):
444 if util.safehasattr(d, 'itermaps'):
445 diter = d.itermaps()
445 diter = d.itermaps()
446 else:
446 else:
447 try:
447 try:
448 diter = iter(d)
448 diter = iter(d)
449 except TypeError:
449 except TypeError:
450 sym = findsymbolicname(darg)
450 sym = findsymbolicname(darg)
451 if sym:
451 if sym:
452 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
452 raise error.ParseError(_("keyword '%s' is not iterable") % sym)
453 else:
453 else:
454 raise error.ParseError(_("%r is not iterable") % d)
454 raise error.ParseError(_("%r is not iterable") % d)
455
455
456 for i, v in enumerate(diter):
456 for i, v in enumerate(diter):
457 lm = mapping.copy()
457 lm = mapping.copy()
458 lm['index'] = i
458 lm['index'] = i
459 if isinstance(v, dict):
459 if isinstance(v, dict):
460 lm.update(v)
460 lm.update(v)
461 lm['originalnode'] = mapping.get('node')
461 lm['originalnode'] = mapping.get('node')
462 yield evalrawexp(context, lm, targ)
462 yield evalrawexp(context, lm, targ)
463 else:
463 else:
464 # v is not an iterable of dicts, this happen when 'key'
464 # v is not an iterable of dicts, this happen when 'key'
465 # has been fully expanded already and format is useless.
465 # has been fully expanded already and format is useless.
466 # If so, return the expanded value.
466 # If so, return the expanded value.
467 yield v
467 yield v
468
468
469 def buildmember(exp, context):
469 def buildmember(exp, context):
470 darg = compileexp(exp[1], context, methods)
470 darg = compileexp(exp[1], context, methods)
471 memb = getsymbol(exp[2])
471 memb = getsymbol(exp[2])
472 return (runmember, (darg, memb))
472 return (runmember, (darg, memb))
473
473
474 def runmember(context, mapping, data):
474 def runmember(context, mapping, data):
475 darg, memb = data
475 darg, memb = data
476 d = evalrawexp(context, mapping, darg)
476 d = evalrawexp(context, mapping, darg)
477 if util.safehasattr(d, 'tomap'):
477 if util.safehasattr(d, 'tomap'):
478 lm = mapping.copy()
478 lm = mapping.copy()
479 lm.update(d.tomap())
479 lm.update(d.tomap())
480 return runsymbol(context, lm, memb)
480 return runsymbol(context, lm, memb)
481 if util.safehasattr(d, 'get'):
481 if util.safehasattr(d, 'get'):
482 return _getdictitem(d, memb)
482 return _getdictitem(d, memb)
483
483
484 sym = findsymbolicname(darg)
484 sym = findsymbolicname(darg)
485 if sym:
485 if sym:
486 raise error.ParseError(_("keyword '%s' has no member") % sym)
486 raise error.ParseError(_("keyword '%s' has no member") % sym)
487 else:
487 else:
488 raise error.ParseError(_("%r has no member") % d)
488 raise error.ParseError(_("%r has no member") % d)
489
489
490 def buildnegate(exp, context):
490 def buildnegate(exp, context):
491 arg = compileexp(exp[1], context, exprmethods)
491 arg = compileexp(exp[1], context, exprmethods)
492 return (runnegate, arg)
492 return (runnegate, arg)
493
493
494 def runnegate(context, mapping, data):
494 def runnegate(context, mapping, data):
495 data = evalinteger(context, mapping, data,
495 data = evalinteger(context, mapping, data,
496 _('negation needs an integer argument'))
496 _('negation needs an integer argument'))
497 return -data
497 return -data
498
498
499 def buildarithmetic(exp, context, func):
499 def buildarithmetic(exp, context, func):
500 left = compileexp(exp[1], context, exprmethods)
500 left = compileexp(exp[1], context, exprmethods)
501 right = compileexp(exp[2], context, exprmethods)
501 right = compileexp(exp[2], context, exprmethods)
502 return (runarithmetic, (func, left, right))
502 return (runarithmetic, (func, left, right))
503
503
504 def runarithmetic(context, mapping, data):
504 def runarithmetic(context, mapping, data):
505 func, left, right = data
505 func, left, right = data
506 left = evalinteger(context, mapping, left,
506 left = evalinteger(context, mapping, left,
507 _('arithmetic only defined on integers'))
507 _('arithmetic only defined on integers'))
508 right = evalinteger(context, mapping, right,
508 right = evalinteger(context, mapping, right,
509 _('arithmetic only defined on integers'))
509 _('arithmetic only defined on integers'))
510 try:
510 try:
511 return func(left, right)
511 return func(left, right)
512 except ZeroDivisionError:
512 except ZeroDivisionError:
513 raise error.Abort(_('division by zero is not defined'))
513 raise error.Abort(_('division by zero is not defined'))
514
514
515 def buildfunc(exp, context):
515 def buildfunc(exp, context):
516 n = getsymbol(exp[1])
516 n = getsymbol(exp[1])
517 if n in funcs:
517 if n in funcs:
518 f = funcs[n]
518 f = funcs[n]
519 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
519 args = _buildfuncargs(exp[2], context, exprmethods, n, f._argspec)
520 return (f, args)
520 return (f, args)
521 if n in context._filters:
521 if n in context._filters:
522 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
522 args = _buildfuncargs(exp[2], context, exprmethods, n, argspec=None)
523 if len(args) != 1:
523 if len(args) != 1:
524 raise error.ParseError(_("filter %s expects one argument") % n)
524 raise error.ParseError(_("filter %s expects one argument") % n)
525 f = context._filters[n]
525 f = context._filters[n]
526 return (runfilter, (args[0], f))
526 return (runfilter, (args[0], f))
527 raise error.ParseError(_("unknown function '%s'") % n)
527 raise error.ParseError(_("unknown function '%s'") % n)
528
528
529 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
529 def _buildfuncargs(exp, context, curmethods, funcname, argspec):
530 """Compile parsed tree of function arguments into list or dict of
530 """Compile parsed tree of function arguments into list or dict of
531 (func, data) pairs
531 (func, data) pairs
532
532
533 >>> context = engine(lambda t: (runsymbol, t))
533 >>> context = engine(lambda t: (runsymbol, t))
534 >>> def fargs(expr, argspec):
534 >>> def fargs(expr, argspec):
535 ... x = _parseexpr(expr)
535 ... x = _parseexpr(expr)
536 ... n = getsymbol(x[1])
536 ... n = getsymbol(x[1])
537 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
537 ... return _buildfuncargs(x[2], context, exprmethods, n, argspec)
538 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
538 >>> list(fargs(b'a(l=1, k=2)', b'k l m').keys())
539 ['l', 'k']
539 ['l', 'k']
540 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
540 >>> args = fargs(b'a(opts=1, k=2)', b'**opts')
541 >>> list(args.keys()), list(args[b'opts'].keys())
541 >>> list(args.keys()), list(args[b'opts'].keys())
542 (['opts'], ['opts', 'k'])
542 (['opts'], ['opts', 'k'])
543 """
543 """
544 def compiledict(xs):
544 def compiledict(xs):
545 return util.sortdict((k, compileexp(x, context, curmethods))
545 return util.sortdict((k, compileexp(x, context, curmethods))
546 for k, x in xs.iteritems())
546 for k, x in xs.iteritems())
547 def compilelist(xs):
547 def compilelist(xs):
548 return [compileexp(x, context, curmethods) for x in xs]
548 return [compileexp(x, context, curmethods) for x in xs]
549
549
550 if not argspec:
550 if not argspec:
551 # filter or function with no argspec: return list of positional args
551 # filter or function with no argspec: return list of positional args
552 return compilelist(getlist(exp))
552 return compilelist(getlist(exp))
553
553
554 # function with argspec: return dict of named args
554 # function with argspec: return dict of named args
555 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
555 _poskeys, varkey, _keys, optkey = argspec = parser.splitargspec(argspec)
556 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
556 treeargs = parser.buildargsdict(getlist(exp), funcname, argspec,
557 keyvaluenode='keyvalue', keynode='symbol')
557 keyvaluenode='keyvalue', keynode='symbol')
558 compargs = util.sortdict()
558 compargs = util.sortdict()
559 if varkey:
559 if varkey:
560 compargs[varkey] = compilelist(treeargs.pop(varkey))
560 compargs[varkey] = compilelist(treeargs.pop(varkey))
561 if optkey:
561 if optkey:
562 compargs[optkey] = compiledict(treeargs.pop(optkey))
562 compargs[optkey] = compiledict(treeargs.pop(optkey))
563 compargs.update(compiledict(treeargs))
563 compargs.update(compiledict(treeargs))
564 return compargs
564 return compargs
565
565
566 def buildkeyvaluepair(exp, content):
566 def buildkeyvaluepair(exp, content):
567 raise error.ParseError(_("can't use a key-value pair in this context"))
567 raise error.ParseError(_("can't use a key-value pair in this context"))
568
568
569 # dict of template built-in functions
569 # dict of template built-in functions
570 funcs = {}
570 funcs = {}
571
571
572 templatefunc = registrar.templatefunc(funcs)
572 templatefunc = registrar.templatefunc(funcs)
573
573
574 @templatefunc('date(date[, fmt])')
574 @templatefunc('date(date[, fmt])')
575 def date(context, mapping, args):
575 def date(context, mapping, args):
576 """Format a date. See :hg:`help dates` for formatting
576 """Format a date. See :hg:`help dates` for formatting
577 strings. The default is a Unix date format, including the timezone:
577 strings. The default is a Unix date format, including the timezone:
578 "Mon Sep 04 15:13:13 2006 0700"."""
578 "Mon Sep 04 15:13:13 2006 0700"."""
579 if not (1 <= len(args) <= 2):
579 if not (1 <= len(args) <= 2):
580 # i18n: "date" is a keyword
580 # i18n: "date" is a keyword
581 raise error.ParseError(_("date expects one or two arguments"))
581 raise error.ParseError(_("date expects one or two arguments"))
582
582
583 date = evalfuncarg(context, mapping, args[0])
583 date = evalfuncarg(context, mapping, args[0])
584 fmt = None
584 fmt = None
585 if len(args) == 2:
585 if len(args) == 2:
586 fmt = evalstring(context, mapping, args[1])
586 fmt = evalstring(context, mapping, args[1])
587 try:
587 try:
588 if fmt is None:
588 if fmt is None:
589 return util.datestr(date)
589 return util.datestr(date)
590 else:
590 else:
591 return util.datestr(date, fmt)
591 return util.datestr(date, fmt)
592 except (TypeError, ValueError):
592 except (TypeError, ValueError):
593 # i18n: "date" is a keyword
593 # i18n: "date" is a keyword
594 raise error.ParseError(_("date expects a date information"))
594 raise error.ParseError(_("date expects a date information"))
595
595
596 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
596 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
597 def dict_(context, mapping, args):
597 def dict_(context, mapping, args):
598 """Construct a dict from key-value pairs. A key may be omitted if
598 """Construct a dict from key-value pairs. A key may be omitted if
599 a value expression can provide an unambiguous name."""
599 a value expression can provide an unambiguous name."""
600 data = util.sortdict()
600 data = util.sortdict()
601
601
602 for v in args['args']:
602 for v in args['args']:
603 k = findsymbolicname(v)
603 k = findsymbolicname(v)
604 if not k:
604 if not k:
605 raise error.ParseError(_('dict key cannot be inferred'))
605 raise error.ParseError(_('dict key cannot be inferred'))
606 if k in data or k in args['kwargs']:
606 if k in data or k in args['kwargs']:
607 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
607 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
608 data[k] = evalfuncarg(context, mapping, v)
608 data[k] = evalfuncarg(context, mapping, v)
609
609
610 data.update((k, evalfuncarg(context, mapping, v))
610 data.update((k, evalfuncarg(context, mapping, v))
611 for k, v in args['kwargs'].iteritems())
611 for k, v in args['kwargs'].iteritems())
612 return templatekw.hybriddict(data)
612 return templatekw.hybriddict(data)
613
613
614 @templatefunc('diff([includepattern [, excludepattern]])')
614 @templatefunc('diff([includepattern [, excludepattern]])')
615 def diff(context, mapping, args):
615 def diff(context, mapping, args):
616 """Show a diff, optionally
616 """Show a diff, optionally
617 specifying files to include or exclude."""
617 specifying files to include or exclude."""
618 if len(args) > 2:
618 if len(args) > 2:
619 # i18n: "diff" is a keyword
619 # i18n: "diff" is a keyword
620 raise error.ParseError(_("diff expects zero, one, or two arguments"))
620 raise error.ParseError(_("diff expects zero, one, or two arguments"))
621
621
622 def getpatterns(i):
622 def getpatterns(i):
623 if i < len(args):
623 if i < len(args):
624 s = evalstring(context, mapping, args[i]).strip()
624 s = evalstring(context, mapping, args[i]).strip()
625 if s:
625 if s:
626 return [s]
626 return [s]
627 return []
627 return []
628
628
629 ctx = mapping['ctx']
629 ctx = mapping['ctx']
630 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
630 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
631
631
632 return ''.join(chunks)
632 return ''.join(chunks)
633
633
634 @templatefunc('extdata(source)', argspec='source')
634 @templatefunc('extdata(source)', argspec='source')
635 def extdata(context, mapping, args):
635 def extdata(context, mapping, args):
636 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
636 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
637 if 'source' not in args:
637 if 'source' not in args:
638 # i18n: "extdata" is a keyword
638 # i18n: "extdata" is a keyword
639 raise error.ParseError(_('extdata expects one argument'))
639 raise error.ParseError(_('extdata expects one argument'))
640
640
641 source = evalstring(context, mapping, args['source'])
641 source = evalstring(context, mapping, args['source'])
642 cache = mapping['cache'].setdefault('extdata', {})
642 cache = mapping['cache'].setdefault('extdata', {})
643 ctx = mapping['ctx']
643 ctx = mapping['ctx']
644 if source in cache:
644 if source in cache:
645 data = cache[source]
645 data = cache[source]
646 else:
646 else:
647 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
647 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
648 return data.get(ctx.rev(), '')
648 return data.get(ctx.rev(), '')
649
649
650 @templatefunc('files(pattern)')
650 @templatefunc('files(pattern)')
651 def files(context, mapping, args):
651 def files(context, mapping, args):
652 """All files of the current changeset matching the pattern. See
652 """All files of the current changeset matching the pattern. See
653 :hg:`help patterns`."""
653 :hg:`help patterns`."""
654 if not len(args) == 1:
654 if not len(args) == 1:
655 # i18n: "files" is a keyword
655 # i18n: "files" is a keyword
656 raise error.ParseError(_("files expects one argument"))
656 raise error.ParseError(_("files expects one argument"))
657
657
658 raw = evalstring(context, mapping, args[0])
658 raw = evalstring(context, mapping, args[0])
659 ctx = mapping['ctx']
659 ctx = mapping['ctx']
660 m = ctx.match([raw])
660 m = ctx.match([raw])
661 files = list(ctx.matches(m))
661 files = list(ctx.matches(m))
662 return templatekw.showlist("file", files, mapping)
662 return templatekw.showlist("file", files, mapping)
663
663
664 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
664 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
665 def fill(context, mapping, args):
665 def fill(context, mapping, args):
666 """Fill many
666 """Fill many
667 paragraphs with optional indentation. See the "fill" filter."""
667 paragraphs with optional indentation. See the "fill" filter."""
668 if not (1 <= len(args) <= 4):
668 if not (1 <= len(args) <= 4):
669 # i18n: "fill" is a keyword
669 # i18n: "fill" is a keyword
670 raise error.ParseError(_("fill expects one to four arguments"))
670 raise error.ParseError(_("fill expects one to four arguments"))
671
671
672 text = evalstring(context, mapping, args[0])
672 text = evalstring(context, mapping, args[0])
673 width = 76
673 width = 76
674 initindent = ''
674 initindent = ''
675 hangindent = ''
675 hangindent = ''
676 if 2 <= len(args) <= 4:
676 if 2 <= len(args) <= 4:
677 width = evalinteger(context, mapping, args[1],
677 width = evalinteger(context, mapping, args[1],
678 # i18n: "fill" is a keyword
678 # i18n: "fill" is a keyword
679 _("fill expects an integer width"))
679 _("fill expects an integer width"))
680 try:
680 try:
681 initindent = evalstring(context, mapping, args[2])
681 initindent = evalstring(context, mapping, args[2])
682 hangindent = evalstring(context, mapping, args[3])
682 hangindent = evalstring(context, mapping, args[3])
683 except IndexError:
683 except IndexError:
684 pass
684 pass
685
685
686 return templatefilters.fill(text, width, initindent, hangindent)
686 return templatefilters.fill(text, width, initindent, hangindent)
687
687
688 @templatefunc('formatnode(node)')
688 @templatefunc('formatnode(node)')
689 def formatnode(context, mapping, args):
689 def formatnode(context, mapping, args):
690 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
690 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
691 if len(args) != 1:
691 if len(args) != 1:
692 # i18n: "formatnode" is a keyword
692 # i18n: "formatnode" is a keyword
693 raise error.ParseError(_("formatnode expects one argument"))
693 raise error.ParseError(_("formatnode expects one argument"))
694
694
695 ui = mapping['ui']
695 ui = mapping['ui']
696 node = evalstring(context, mapping, args[0])
696 node = evalstring(context, mapping, args[0])
697 if ui.debugflag:
697 if ui.debugflag:
698 return node
698 return node
699 return templatefilters.short(node)
699 return templatefilters.short(node)
700
700
701 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
701 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
702 argspec='text width fillchar left')
702 argspec='text width fillchar left')
703 def pad(context, mapping, args):
703 def pad(context, mapping, args):
704 """Pad text with a
704 """Pad text with a
705 fill character."""
705 fill character."""
706 if 'text' not in args or 'width' not in args:
706 if 'text' not in args or 'width' not in args:
707 # i18n: "pad" is a keyword
707 # i18n: "pad" is a keyword
708 raise error.ParseError(_("pad() expects two to four arguments"))
708 raise error.ParseError(_("pad() expects two to four arguments"))
709
709
710 width = evalinteger(context, mapping, args['width'],
710 width = evalinteger(context, mapping, args['width'],
711 # i18n: "pad" is a keyword
711 # i18n: "pad" is a keyword
712 _("pad() expects an integer width"))
712 _("pad() expects an integer width"))
713
713
714 text = evalstring(context, mapping, args['text'])
714 text = evalstring(context, mapping, args['text'])
715
715
716 left = False
716 left = False
717 fillchar = ' '
717 fillchar = ' '
718 if 'fillchar' in args:
718 if 'fillchar' in args:
719 fillchar = evalstring(context, mapping, args['fillchar'])
719 fillchar = evalstring(context, mapping, args['fillchar'])
720 if len(color.stripeffects(fillchar)) != 1:
720 if len(color.stripeffects(fillchar)) != 1:
721 # i18n: "pad" is a keyword
721 # i18n: "pad" is a keyword
722 raise error.ParseError(_("pad() expects a single fill character"))
722 raise error.ParseError(_("pad() expects a single fill character"))
723 if 'left' in args:
723 if 'left' in args:
724 left = evalboolean(context, mapping, args['left'])
724 left = evalboolean(context, mapping, args['left'])
725
725
726 fillwidth = width - encoding.colwidth(color.stripeffects(text))
726 fillwidth = width - encoding.colwidth(color.stripeffects(text))
727 if fillwidth <= 0:
727 if fillwidth <= 0:
728 return text
728 return text
729 if left:
729 if left:
730 return fillchar * fillwidth + text
730 return fillchar * fillwidth + text
731 else:
731 else:
732 return text + fillchar * fillwidth
732 return text + fillchar * fillwidth
733
733
734 @templatefunc('indent(text, indentchars[, firstline])')
734 @templatefunc('indent(text, indentchars[, firstline])')
735 def indent(context, mapping, args):
735 def indent(context, mapping, args):
736 """Indents all non-empty lines
736 """Indents all non-empty lines
737 with the characters given in the indentchars string. An optional
737 with the characters given in the indentchars string. An optional
738 third parameter will override the indent for the first line only
738 third parameter will override the indent for the first line only
739 if present."""
739 if present."""
740 if not (2 <= len(args) <= 3):
740 if not (2 <= len(args) <= 3):
741 # i18n: "indent" is a keyword
741 # i18n: "indent" is a keyword
742 raise error.ParseError(_("indent() expects two or three arguments"))
742 raise error.ParseError(_("indent() expects two or three arguments"))
743
743
744 text = evalstring(context, mapping, args[0])
744 text = evalstring(context, mapping, args[0])
745 indent = evalstring(context, mapping, args[1])
745 indent = evalstring(context, mapping, args[1])
746
746
747 if len(args) == 3:
747 if len(args) == 3:
748 firstline = evalstring(context, mapping, args[2])
748 firstline = evalstring(context, mapping, args[2])
749 else:
749 else:
750 firstline = indent
750 firstline = indent
751
751
752 # the indent function doesn't indent the first line, so we do it here
752 # the indent function doesn't indent the first line, so we do it here
753 return templatefilters.indent(firstline + text, indent)
753 return templatefilters.indent(firstline + text, indent)
754
754
755 @templatefunc('get(dict, key)')
755 @templatefunc('get(dict, key)')
756 def get(context, mapping, args):
756 def get(context, mapping, args):
757 """Get an attribute/key from an object. Some keywords
757 """Get an attribute/key from an object. Some keywords
758 are complex types. This function allows you to obtain the value of an
758 are complex types. This function allows you to obtain the value of an
759 attribute on these types."""
759 attribute on these types."""
760 if len(args) != 2:
760 if len(args) != 2:
761 # i18n: "get" is a keyword
761 # i18n: "get" is a keyword
762 raise error.ParseError(_("get() expects two arguments"))
762 raise error.ParseError(_("get() expects two arguments"))
763
763
764 dictarg = evalfuncarg(context, mapping, args[0])
764 dictarg = evalfuncarg(context, mapping, args[0])
765 if not util.safehasattr(dictarg, 'get'):
765 if not util.safehasattr(dictarg, 'get'):
766 # i18n: "get" is a keyword
766 # i18n: "get" is a keyword
767 raise error.ParseError(_("get() expects a dict as first argument"))
767 raise error.ParseError(_("get() expects a dict as first argument"))
768
768
769 key = evalfuncarg(context, mapping, args[1])
769 key = evalfuncarg(context, mapping, args[1])
770 return _getdictitem(dictarg, key)
770 return _getdictitem(dictarg, key)
771
771
772 def _getdictitem(dictarg, key):
772 def _getdictitem(dictarg, key):
773 val = dictarg.get(key)
773 val = dictarg.get(key)
774 if val is None:
774 if val is None:
775 return
775 return
776 return templatekw.wraphybridvalue(dictarg, key, val)
776 return templatekw.wraphybridvalue(dictarg, key, val)
777
777
778 @templatefunc('if(expr, then[, else])')
778 @templatefunc('if(expr, then[, else])')
779 def if_(context, mapping, args):
779 def if_(context, mapping, args):
780 """Conditionally execute based on the result of
780 """Conditionally execute based on the result of
781 an expression."""
781 an expression."""
782 if not (2 <= len(args) <= 3):
782 if not (2 <= len(args) <= 3):
783 # i18n: "if" is a keyword
783 # i18n: "if" is a keyword
784 raise error.ParseError(_("if expects two or three arguments"))
784 raise error.ParseError(_("if expects two or three arguments"))
785
785
786 test = evalboolean(context, mapping, args[0])
786 test = evalboolean(context, mapping, args[0])
787 if test:
787 if test:
788 yield evalrawexp(context, mapping, args[1])
788 yield evalrawexp(context, mapping, args[1])
789 elif len(args) == 3:
789 elif len(args) == 3:
790 yield evalrawexp(context, mapping, args[2])
790 yield evalrawexp(context, mapping, args[2])
791
791
792 @templatefunc('ifcontains(needle, haystack, then[, else])')
792 @templatefunc('ifcontains(needle, haystack, then[, else])')
793 def ifcontains(context, mapping, args):
793 def ifcontains(context, mapping, args):
794 """Conditionally execute based
794 """Conditionally execute based
795 on whether the item "needle" is in "haystack"."""
795 on whether the item "needle" is in "haystack"."""
796 if not (3 <= len(args) <= 4):
796 if not (3 <= len(args) <= 4):
797 # i18n: "ifcontains" is a keyword
797 # i18n: "ifcontains" is a keyword
798 raise error.ParseError(_("ifcontains expects three or four arguments"))
798 raise error.ParseError(_("ifcontains expects three or four arguments"))
799
799
800 haystack = evalfuncarg(context, mapping, args[1])
800 haystack = evalfuncarg(context, mapping, args[1])
801 try:
801 try:
802 needle = evalastype(context, mapping, args[0],
802 needle = evalastype(context, mapping, args[0],
803 getattr(haystack, 'keytype', None) or bytes)
803 getattr(haystack, 'keytype', None) or bytes)
804 found = (needle in haystack)
804 found = (needle in haystack)
805 except error.ParseError:
805 except error.ParseError:
806 found = False
806 found = False
807
807
808 if found:
808 if found:
809 yield evalrawexp(context, mapping, args[2])
809 yield evalrawexp(context, mapping, args[2])
810 elif len(args) == 4:
810 elif len(args) == 4:
811 yield evalrawexp(context, mapping, args[3])
811 yield evalrawexp(context, mapping, args[3])
812
812
813 @templatefunc('ifeq(expr1, expr2, then[, else])')
813 @templatefunc('ifeq(expr1, expr2, then[, else])')
814 def ifeq(context, mapping, args):
814 def ifeq(context, mapping, args):
815 """Conditionally execute based on
815 """Conditionally execute based on
816 whether 2 items are equivalent."""
816 whether 2 items are equivalent."""
817 if not (3 <= len(args) <= 4):
817 if not (3 <= len(args) <= 4):
818 # i18n: "ifeq" is a keyword
818 # i18n: "ifeq" is a keyword
819 raise error.ParseError(_("ifeq expects three or four arguments"))
819 raise error.ParseError(_("ifeq expects three or four arguments"))
820
820
821 test = evalstring(context, mapping, args[0])
821 test = evalstring(context, mapping, args[0])
822 match = evalstring(context, mapping, args[1])
822 match = evalstring(context, mapping, args[1])
823 if test == match:
823 if test == match:
824 yield evalrawexp(context, mapping, args[2])
824 yield evalrawexp(context, mapping, args[2])
825 elif len(args) == 4:
825 elif len(args) == 4:
826 yield evalrawexp(context, mapping, args[3])
826 yield evalrawexp(context, mapping, args[3])
827
827
828 @templatefunc('join(list, sep)')
828 @templatefunc('join(list, sep)')
829 def join(context, mapping, args):
829 def join(context, mapping, args):
830 """Join items in a list with a delimiter."""
830 """Join items in a list with a delimiter."""
831 if not (1 <= len(args) <= 2):
831 if not (1 <= len(args) <= 2):
832 # i18n: "join" is a keyword
832 # i18n: "join" is a keyword
833 raise error.ParseError(_("join expects one or two arguments"))
833 raise error.ParseError(_("join expects one or two arguments"))
834
834
835 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
835 # TODO: perhaps this should be evalfuncarg(), but it can't because hgweb
836 # abuses generator as a keyword that returns a list of dicts.
836 # abuses generator as a keyword that returns a list of dicts.
837 joinset = evalrawexp(context, mapping, args[0])
837 joinset = evalrawexp(context, mapping, args[0])
838 joinset = templatekw.unwrapvalue(joinset)
838 joinset = templatekw.unwrapvalue(joinset)
839 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
839 joinfmt = getattr(joinset, 'joinfmt', pycompat.identity)
840 joiner = " "
840 joiner = " "
841 if len(args) > 1:
841 if len(args) > 1:
842 joiner = evalstring(context, mapping, args[1])
842 joiner = evalstring(context, mapping, args[1])
843
843
844 first = True
844 first = True
845 for x in joinset:
845 for x in joinset:
846 if first:
846 if first:
847 first = False
847 first = False
848 else:
848 else:
849 yield joiner
849 yield joiner
850 yield joinfmt(x)
850 yield joinfmt(x)
851
851
852 @templatefunc('label(label, expr)')
852 @templatefunc('label(label, expr)')
853 def label(context, mapping, args):
853 def label(context, mapping, args):
854 """Apply a label to generated content. Content with
854 """Apply a label to generated content. Content with
855 a label applied can result in additional post-processing, such as
855 a label applied can result in additional post-processing, such as
856 automatic colorization."""
856 automatic colorization."""
857 if len(args) != 2:
857 if len(args) != 2:
858 # i18n: "label" is a keyword
858 # i18n: "label" is a keyword
859 raise error.ParseError(_("label expects two arguments"))
859 raise error.ParseError(_("label expects two arguments"))
860
860
861 ui = mapping['ui']
861 ui = mapping['ui']
862 thing = evalstring(context, mapping, args[1])
862 thing = evalstring(context, mapping, args[1])
863 # preserve unknown symbol as literal so effects like 'red', 'bold',
863 # preserve unknown symbol as literal so effects like 'red', 'bold',
864 # etc. don't need to be quoted
864 # etc. don't need to be quoted
865 label = evalstringliteral(context, mapping, args[0])
865 label = evalstringliteral(context, mapping, args[0])
866
866
867 return ui.label(thing, label)
867 return ui.label(thing, label)
868
868
869 @templatefunc('latesttag([pattern])')
869 @templatefunc('latesttag([pattern])')
870 def latesttag(context, mapping, args):
870 def latesttag(context, mapping, args):
871 """The global tags matching the given pattern on the
871 """The global tags matching the given pattern on the
872 most recent globally tagged ancestor of this changeset.
872 most recent globally tagged ancestor of this changeset.
873 If no such tags exist, the "{tag}" template resolves to
873 If no such tags exist, the "{tag}" template resolves to
874 the string "null"."""
874 the string "null"."""
875 if len(args) > 1:
875 if len(args) > 1:
876 # i18n: "latesttag" is a keyword
876 # i18n: "latesttag" is a keyword
877 raise error.ParseError(_("latesttag expects at most one argument"))
877 raise error.ParseError(_("latesttag expects at most one argument"))
878
878
879 pattern = None
879 pattern = None
880 if len(args) == 1:
880 if len(args) == 1:
881 pattern = evalstring(context, mapping, args[0])
881 pattern = evalstring(context, mapping, args[0])
882
882
883 return templatekw.showlatesttags(pattern, **pycompat.strkwargs(mapping))
883 return templatekw.showlatesttags(pattern, **pycompat.strkwargs(mapping))
884
884
885 @templatefunc('localdate(date[, tz])')
885 @templatefunc('localdate(date[, tz])')
886 def localdate(context, mapping, args):
886 def localdate(context, mapping, args):
887 """Converts a date to the specified timezone.
887 """Converts a date to the specified timezone.
888 The default is local date."""
888 The default is local date."""
889 if not (1 <= len(args) <= 2):
889 if not (1 <= len(args) <= 2):
890 # i18n: "localdate" is a keyword
890 # i18n: "localdate" is a keyword
891 raise error.ParseError(_("localdate expects one or two arguments"))
891 raise error.ParseError(_("localdate expects one or two arguments"))
892
892
893 date = evalfuncarg(context, mapping, args[0])
893 date = evalfuncarg(context, mapping, args[0])
894 try:
894 try:
895 date = util.parsedate(date)
895 date = util.parsedate(date)
896 except AttributeError: # not str nor date tuple
896 except AttributeError: # not str nor date tuple
897 # i18n: "localdate" is a keyword
897 # i18n: "localdate" is a keyword
898 raise error.ParseError(_("localdate expects a date information"))
898 raise error.ParseError(_("localdate expects a date information"))
899 if len(args) >= 2:
899 if len(args) >= 2:
900 tzoffset = None
900 tzoffset = None
901 tz = evalfuncarg(context, mapping, args[1])
901 tz = evalfuncarg(context, mapping, args[1])
902 if isinstance(tz, str):
902 if isinstance(tz, str):
903 tzoffset, remainder = util.parsetimezone(tz)
903 tzoffset, remainder = util.parsetimezone(tz)
904 if remainder:
904 if remainder:
905 tzoffset = None
905 tzoffset = None
906 if tzoffset is None:
906 if tzoffset is None:
907 try:
907 try:
908 tzoffset = int(tz)
908 tzoffset = int(tz)
909 except (TypeError, ValueError):
909 except (TypeError, ValueError):
910 # i18n: "localdate" is a keyword
910 # i18n: "localdate" is a keyword
911 raise error.ParseError(_("localdate expects a timezone"))
911 raise error.ParseError(_("localdate expects a timezone"))
912 else:
912 else:
913 tzoffset = util.makedate()[1]
913 tzoffset = util.makedate()[1]
914 return (date[0], tzoffset)
914 return (date[0], tzoffset)
915
915
916 @templatefunc('max(iterable)')
916 @templatefunc('max(iterable)')
917 def max_(context, mapping, args, **kwargs):
917 def max_(context, mapping, args, **kwargs):
918 """Return the max of an iterable"""
918 """Return the max of an iterable"""
919 if len(args) != 1:
919 if len(args) != 1:
920 # i18n: "max" is a keyword
920 # i18n: "max" is a keyword
921 raise error.ParseError(_("max expects one arguments"))
921 raise error.ParseError(_("max expects one argument"))
922
922
923 iterable = evalfuncarg(context, mapping, args[0])
923 iterable = evalfuncarg(context, mapping, args[0])
924 try:
924 try:
925 x = max(iterable)
925 x = max(iterable)
926 except (TypeError, ValueError):
926 except (TypeError, ValueError):
927 # i18n: "max" is a keyword
927 # i18n: "max" is a keyword
928 raise error.ParseError(_("max first argument should be an iterable"))
928 raise error.ParseError(_("max first argument should be an iterable"))
929 return templatekw.wraphybridvalue(iterable, x, x)
929 return templatekw.wraphybridvalue(iterable, x, x)
930
930
931 @templatefunc('min(iterable)')
931 @templatefunc('min(iterable)')
932 def min_(context, mapping, args, **kwargs):
932 def min_(context, mapping, args, **kwargs):
933 """Return the min of an iterable"""
933 """Return the min of an iterable"""
934 if len(args) != 1:
934 if len(args) != 1:
935 # i18n: "min" is a keyword
935 # i18n: "min" is a keyword
936 raise error.ParseError(_("min expects one arguments"))
936 raise error.ParseError(_("min expects one argument"))
937
937
938 iterable = evalfuncarg(context, mapping, args[0])
938 iterable = evalfuncarg(context, mapping, args[0])
939 try:
939 try:
940 x = min(iterable)
940 x = min(iterable)
941 except (TypeError, ValueError):
941 except (TypeError, ValueError):
942 # i18n: "min" is a keyword
942 # i18n: "min" is a keyword
943 raise error.ParseError(_("min first argument should be an iterable"))
943 raise error.ParseError(_("min first argument should be an iterable"))
944 return templatekw.wraphybridvalue(iterable, x, x)
944 return templatekw.wraphybridvalue(iterable, x, x)
945
945
946 @templatefunc('mod(a, b)')
946 @templatefunc('mod(a, b)')
947 def mod(context, mapping, args):
947 def mod(context, mapping, args):
948 """Calculate a mod b such that a / b + a mod b == a"""
948 """Calculate a mod b such that a / b + a mod b == a"""
949 if not len(args) == 2:
949 if not len(args) == 2:
950 # i18n: "mod" is a keyword
950 # i18n: "mod" is a keyword
951 raise error.ParseError(_("mod expects two arguments"))
951 raise error.ParseError(_("mod expects two arguments"))
952
952
953 func = lambda a, b: a % b
953 func = lambda a, b: a % b
954 return runarithmetic(context, mapping, (func, args[0], args[1]))
954 return runarithmetic(context, mapping, (func, args[0], args[1]))
955
955
956 @templatefunc('obsfateoperations(markers)')
956 @templatefunc('obsfateoperations(markers)')
957 def obsfateoperations(context, mapping, args):
957 def obsfateoperations(context, mapping, args):
958 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
958 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
959 if len(args) != 1:
959 if len(args) != 1:
960 # i18n: "obsfateoperations" is a keyword
960 # i18n: "obsfateoperations" is a keyword
961 raise error.ParseError(_("obsfateoperations expects one arguments"))
961 raise error.ParseError(_("obsfateoperations expects one argument"))
962
962
963 markers = evalfuncarg(context, mapping, args[0])
963 markers = evalfuncarg(context, mapping, args[0])
964
964
965 try:
965 try:
966 data = obsutil.markersoperations(markers)
966 data = obsutil.markersoperations(markers)
967 return templatekw.hybridlist(data, name='operation')
967 return templatekw.hybridlist(data, name='operation')
968 except (TypeError, KeyError):
968 except (TypeError, KeyError):
969 # i18n: "obsfateoperations" is a keyword
969 # i18n: "obsfateoperations" is a keyword
970 errmsg = _("obsfateoperations first argument should be an iterable")
970 errmsg = _("obsfateoperations first argument should be an iterable")
971 raise error.ParseError(errmsg)
971 raise error.ParseError(errmsg)
972
972
973 @templatefunc('obsfatedate(markers)')
973 @templatefunc('obsfatedate(markers)')
974 def obsfatedate(context, mapping, args):
974 def obsfatedate(context, mapping, args):
975 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
975 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
976 if len(args) != 1:
976 if len(args) != 1:
977 # i18n: "obsfatedate" is a keyword
977 # i18n: "obsfatedate" is a keyword
978 raise error.ParseError(_("obsfatedate expects one arguments"))
978 raise error.ParseError(_("obsfatedate expects one argument"))
979
979
980 markers = evalfuncarg(context, mapping, args[0])
980 markers = evalfuncarg(context, mapping, args[0])
981
981
982 try:
982 try:
983 data = obsutil.markersdates(markers)
983 data = obsutil.markersdates(markers)
984 return templatekw.hybridlist(data, name='date', fmt='%d %d')
984 return templatekw.hybridlist(data, name='date', fmt='%d %d')
985 except (TypeError, KeyError):
985 except (TypeError, KeyError):
986 # i18n: "obsfatedate" is a keyword
986 # i18n: "obsfatedate" is a keyword
987 errmsg = _("obsfatedate first argument should be an iterable")
987 errmsg = _("obsfatedate first argument should be an iterable")
988 raise error.ParseError(errmsg)
988 raise error.ParseError(errmsg)
989
989
990 @templatefunc('obsfateusers(markers)')
990 @templatefunc('obsfateusers(markers)')
991 def obsfateusers(context, mapping, args):
991 def obsfateusers(context, mapping, args):
992 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
992 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
993 if len(args) != 1:
993 if len(args) != 1:
994 # i18n: "obsfateusers" is a keyword
994 # i18n: "obsfateusers" is a keyword
995 raise error.ParseError(_("obsfateusers expects one arguments"))
995 raise error.ParseError(_("obsfateusers expects one argument"))
996
996
997 markers = evalfuncarg(context, mapping, args[0])
997 markers = evalfuncarg(context, mapping, args[0])
998
998
999 try:
999 try:
1000 data = obsutil.markersusers(markers)
1000 data = obsutil.markersusers(markers)
1001 return templatekw.hybridlist(data, name='user')
1001 return templatekw.hybridlist(data, name='user')
1002 except (TypeError, KeyError, ValueError):
1002 except (TypeError, KeyError, ValueError):
1003 # i18n: "obsfateusers" is a keyword
1003 # i18n: "obsfateusers" is a keyword
1004 msg = _("obsfateusers first argument should be an iterable of "
1004 msg = _("obsfateusers first argument should be an iterable of "
1005 "obsmakers")
1005 "obsmakers")
1006 raise error.ParseError(msg)
1006 raise error.ParseError(msg)
1007
1007
1008 @templatefunc('obsfateverb(successors, markers)')
1008 @templatefunc('obsfateverb(successors, markers)')
1009 def obsfateverb(context, mapping, args):
1009 def obsfateverb(context, mapping, args):
1010 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
1010 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
1011 if len(args) != 2:
1011 if len(args) != 2:
1012 # i18n: "obsfateverb" is a keyword
1012 # i18n: "obsfateverb" is a keyword
1013 raise error.ParseError(_("obsfateverb expects two arguments"))
1013 raise error.ParseError(_("obsfateverb expects two arguments"))
1014
1014
1015 successors = evalfuncarg(context, mapping, args[0])
1015 successors = evalfuncarg(context, mapping, args[0])
1016 markers = evalfuncarg(context, mapping, args[1])
1016 markers = evalfuncarg(context, mapping, args[1])
1017
1017
1018 try:
1018 try:
1019 return obsutil.obsfateverb(successors, markers)
1019 return obsutil.obsfateverb(successors, markers)
1020 except TypeError:
1020 except TypeError:
1021 # i18n: "obsfateverb" is a keyword
1021 # i18n: "obsfateverb" is a keyword
1022 errmsg = _("obsfateverb first argument should be countable")
1022 errmsg = _("obsfateverb first argument should be countable")
1023 raise error.ParseError(errmsg)
1023 raise error.ParseError(errmsg)
1024
1024
1025 @templatefunc('relpath(path)')
1025 @templatefunc('relpath(path)')
1026 def relpath(context, mapping, args):
1026 def relpath(context, mapping, args):
1027 """Convert a repository-absolute path into a filesystem path relative to
1027 """Convert a repository-absolute path into a filesystem path relative to
1028 the current working directory."""
1028 the current working directory."""
1029 if len(args) != 1:
1029 if len(args) != 1:
1030 # i18n: "relpath" is a keyword
1030 # i18n: "relpath" is a keyword
1031 raise error.ParseError(_("relpath expects one argument"))
1031 raise error.ParseError(_("relpath expects one argument"))
1032
1032
1033 repo = mapping['ctx'].repo()
1033 repo = mapping['ctx'].repo()
1034 path = evalstring(context, mapping, args[0])
1034 path = evalstring(context, mapping, args[0])
1035 return repo.pathto(path)
1035 return repo.pathto(path)
1036
1036
1037 @templatefunc('revset(query[, formatargs...])')
1037 @templatefunc('revset(query[, formatargs...])')
1038 def revset(context, mapping, args):
1038 def revset(context, mapping, args):
1039 """Execute a revision set query. See
1039 """Execute a revision set query. See
1040 :hg:`help revset`."""
1040 :hg:`help revset`."""
1041 if not len(args) > 0:
1041 if not len(args) > 0:
1042 # i18n: "revset" is a keyword
1042 # i18n: "revset" is a keyword
1043 raise error.ParseError(_("revset expects one or more arguments"))
1043 raise error.ParseError(_("revset expects one or more arguments"))
1044
1044
1045 raw = evalstring(context, mapping, args[0])
1045 raw = evalstring(context, mapping, args[0])
1046 ctx = mapping['ctx']
1046 ctx = mapping['ctx']
1047 repo = ctx.repo()
1047 repo = ctx.repo()
1048
1048
1049 def query(expr):
1049 def query(expr):
1050 m = revsetmod.match(repo.ui, expr, repo=repo)
1050 m = revsetmod.match(repo.ui, expr, repo=repo)
1051 return m(repo)
1051 return m(repo)
1052
1052
1053 if len(args) > 1:
1053 if len(args) > 1:
1054 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
1054 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
1055 revs = query(revsetlang.formatspec(raw, *formatargs))
1055 revs = query(revsetlang.formatspec(raw, *formatargs))
1056 revs = list(revs)
1056 revs = list(revs)
1057 else:
1057 else:
1058 revsetcache = mapping['cache'].setdefault("revsetcache", {})
1058 revsetcache = mapping['cache'].setdefault("revsetcache", {})
1059 if raw in revsetcache:
1059 if raw in revsetcache:
1060 revs = revsetcache[raw]
1060 revs = revsetcache[raw]
1061 else:
1061 else:
1062 revs = query(raw)
1062 revs = query(raw)
1063 revs = list(revs)
1063 revs = list(revs)
1064 revsetcache[raw] = revs
1064 revsetcache[raw] = revs
1065
1065
1066 return templatekw.showrevslist("revision", revs,
1066 return templatekw.showrevslist("revision", revs,
1067 **pycompat.strkwargs(mapping))
1067 **pycompat.strkwargs(mapping))
1068
1068
1069 @templatefunc('rstdoc(text, style)')
1069 @templatefunc('rstdoc(text, style)')
1070 def rstdoc(context, mapping, args):
1070 def rstdoc(context, mapping, args):
1071 """Format reStructuredText."""
1071 """Format reStructuredText."""
1072 if len(args) != 2:
1072 if len(args) != 2:
1073 # i18n: "rstdoc" is a keyword
1073 # i18n: "rstdoc" is a keyword
1074 raise error.ParseError(_("rstdoc expects two arguments"))
1074 raise error.ParseError(_("rstdoc expects two arguments"))
1075
1075
1076 text = evalstring(context, mapping, args[0])
1076 text = evalstring(context, mapping, args[0])
1077 style = evalstring(context, mapping, args[1])
1077 style = evalstring(context, mapping, args[1])
1078
1078
1079 return minirst.format(text, style=style, keep=['verbose'])
1079 return minirst.format(text, style=style, keep=['verbose'])
1080
1080
1081 @templatefunc('separate(sep, args)', argspec='sep *args')
1081 @templatefunc('separate(sep, args)', argspec='sep *args')
1082 def separate(context, mapping, args):
1082 def separate(context, mapping, args):
1083 """Add a separator between non-empty arguments."""
1083 """Add a separator between non-empty arguments."""
1084 if 'sep' not in args:
1084 if 'sep' not in args:
1085 # i18n: "separate" is a keyword
1085 # i18n: "separate" is a keyword
1086 raise error.ParseError(_("separate expects at least one argument"))
1086 raise error.ParseError(_("separate expects at least one argument"))
1087
1087
1088 sep = evalstring(context, mapping, args['sep'])
1088 sep = evalstring(context, mapping, args['sep'])
1089 first = True
1089 first = True
1090 for arg in args['args']:
1090 for arg in args['args']:
1091 argstr = evalstring(context, mapping, arg)
1091 argstr = evalstring(context, mapping, arg)
1092 if not argstr:
1092 if not argstr:
1093 continue
1093 continue
1094 if first:
1094 if first:
1095 first = False
1095 first = False
1096 else:
1096 else:
1097 yield sep
1097 yield sep
1098 yield argstr
1098 yield argstr
1099
1099
1100 @templatefunc('shortest(node, minlength=4)')
1100 @templatefunc('shortest(node, minlength=4)')
1101 def shortest(context, mapping, args):
1101 def shortest(context, mapping, args):
1102 """Obtain the shortest representation of
1102 """Obtain the shortest representation of
1103 a node."""
1103 a node."""
1104 if not (1 <= len(args) <= 2):
1104 if not (1 <= len(args) <= 2):
1105 # i18n: "shortest" is a keyword
1105 # i18n: "shortest" is a keyword
1106 raise error.ParseError(_("shortest() expects one or two arguments"))
1106 raise error.ParseError(_("shortest() expects one or two arguments"))
1107
1107
1108 node = evalstring(context, mapping, args[0])
1108 node = evalstring(context, mapping, args[0])
1109
1109
1110 minlength = 4
1110 minlength = 4
1111 if len(args) > 1:
1111 if len(args) > 1:
1112 minlength = evalinteger(context, mapping, args[1],
1112 minlength = evalinteger(context, mapping, args[1],
1113 # i18n: "shortest" is a keyword
1113 # i18n: "shortest" is a keyword
1114 _("shortest() expects an integer minlength"))
1114 _("shortest() expects an integer minlength"))
1115
1115
1116 # _partialmatch() of filtered changelog could take O(len(repo)) time,
1116 # _partialmatch() of filtered changelog could take O(len(repo)) time,
1117 # which would be unacceptably slow. so we look for hash collision in
1117 # which would be unacceptably slow. so we look for hash collision in
1118 # unfiltered space, which means some hashes may be slightly longer.
1118 # unfiltered space, which means some hashes may be slightly longer.
1119 cl = mapping['ctx']._repo.unfiltered().changelog
1119 cl = mapping['ctx']._repo.unfiltered().changelog
1120 return cl.shortest(node, minlength)
1120 return cl.shortest(node, minlength)
1121
1121
1122 @templatefunc('strip(text[, chars])')
1122 @templatefunc('strip(text[, chars])')
1123 def strip(context, mapping, args):
1123 def strip(context, mapping, args):
1124 """Strip characters from a string. By default,
1124 """Strip characters from a string. By default,
1125 strips all leading and trailing whitespace."""
1125 strips all leading and trailing whitespace."""
1126 if not (1 <= len(args) <= 2):
1126 if not (1 <= len(args) <= 2):
1127 # i18n: "strip" is a keyword
1127 # i18n: "strip" is a keyword
1128 raise error.ParseError(_("strip expects one or two arguments"))
1128 raise error.ParseError(_("strip expects one or two arguments"))
1129
1129
1130 text = evalstring(context, mapping, args[0])
1130 text = evalstring(context, mapping, args[0])
1131 if len(args) == 2:
1131 if len(args) == 2:
1132 chars = evalstring(context, mapping, args[1])
1132 chars = evalstring(context, mapping, args[1])
1133 return text.strip(chars)
1133 return text.strip(chars)
1134 return text.strip()
1134 return text.strip()
1135
1135
1136 @templatefunc('sub(pattern, replacement, expression)')
1136 @templatefunc('sub(pattern, replacement, expression)')
1137 def sub(context, mapping, args):
1137 def sub(context, mapping, args):
1138 """Perform text substitution
1138 """Perform text substitution
1139 using regular expressions."""
1139 using regular expressions."""
1140 if len(args) != 3:
1140 if len(args) != 3:
1141 # i18n: "sub" is a keyword
1141 # i18n: "sub" is a keyword
1142 raise error.ParseError(_("sub expects three arguments"))
1142 raise error.ParseError(_("sub expects three arguments"))
1143
1143
1144 pat = evalstring(context, mapping, args[0])
1144 pat = evalstring(context, mapping, args[0])
1145 rpl = evalstring(context, mapping, args[1])
1145 rpl = evalstring(context, mapping, args[1])
1146 src = evalstring(context, mapping, args[2])
1146 src = evalstring(context, mapping, args[2])
1147 try:
1147 try:
1148 patre = re.compile(pat)
1148 patre = re.compile(pat)
1149 except re.error:
1149 except re.error:
1150 # i18n: "sub" is a keyword
1150 # i18n: "sub" is a keyword
1151 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1151 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
1152 try:
1152 try:
1153 yield patre.sub(rpl, src)
1153 yield patre.sub(rpl, src)
1154 except re.error:
1154 except re.error:
1155 # i18n: "sub" is a keyword
1155 # i18n: "sub" is a keyword
1156 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1156 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
1157
1157
1158 @templatefunc('startswith(pattern, text)')
1158 @templatefunc('startswith(pattern, text)')
1159 def startswith(context, mapping, args):
1159 def startswith(context, mapping, args):
1160 """Returns the value from the "text" argument
1160 """Returns the value from the "text" argument
1161 if it begins with the content from the "pattern" argument."""
1161 if it begins with the content from the "pattern" argument."""
1162 if len(args) != 2:
1162 if len(args) != 2:
1163 # i18n: "startswith" is a keyword
1163 # i18n: "startswith" is a keyword
1164 raise error.ParseError(_("startswith expects two arguments"))
1164 raise error.ParseError(_("startswith expects two arguments"))
1165
1165
1166 patn = evalstring(context, mapping, args[0])
1166 patn = evalstring(context, mapping, args[0])
1167 text = evalstring(context, mapping, args[1])
1167 text = evalstring(context, mapping, args[1])
1168 if text.startswith(patn):
1168 if text.startswith(patn):
1169 return text
1169 return text
1170 return ''
1170 return ''
1171
1171
1172 @templatefunc('word(number, text[, separator])')
1172 @templatefunc('word(number, text[, separator])')
1173 def word(context, mapping, args):
1173 def word(context, mapping, args):
1174 """Return the nth word from a string."""
1174 """Return the nth word from a string."""
1175 if not (2 <= len(args) <= 3):
1175 if not (2 <= len(args) <= 3):
1176 # i18n: "word" is a keyword
1176 # i18n: "word" is a keyword
1177 raise error.ParseError(_("word expects two or three arguments, got %d")
1177 raise error.ParseError(_("word expects two or three arguments, got %d")
1178 % len(args))
1178 % len(args))
1179
1179
1180 num = evalinteger(context, mapping, args[0],
1180 num = evalinteger(context, mapping, args[0],
1181 # i18n: "word" is a keyword
1181 # i18n: "word" is a keyword
1182 _("word expects an integer index"))
1182 _("word expects an integer index"))
1183 text = evalstring(context, mapping, args[1])
1183 text = evalstring(context, mapping, args[1])
1184 if len(args) == 3:
1184 if len(args) == 3:
1185 splitter = evalstring(context, mapping, args[2])
1185 splitter = evalstring(context, mapping, args[2])
1186 else:
1186 else:
1187 splitter = None
1187 splitter = None
1188
1188
1189 tokens = text.split(splitter)
1189 tokens = text.split(splitter)
1190 if num >= len(tokens) or num < -len(tokens):
1190 if num >= len(tokens) or num < -len(tokens):
1191 return ''
1191 return ''
1192 else:
1192 else:
1193 return tokens[num]
1193 return tokens[num]
1194
1194
1195 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1195 # methods to interpret function arguments or inner expressions (e.g. {_(x)})
1196 exprmethods = {
1196 exprmethods = {
1197 "integer": lambda e, c: (runinteger, e[1]),
1197 "integer": lambda e, c: (runinteger, e[1]),
1198 "string": lambda e, c: (runstring, e[1]),
1198 "string": lambda e, c: (runstring, e[1]),
1199 "symbol": lambda e, c: (runsymbol, e[1]),
1199 "symbol": lambda e, c: (runsymbol, e[1]),
1200 "template": buildtemplate,
1200 "template": buildtemplate,
1201 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1201 "group": lambda e, c: compileexp(e[1], c, exprmethods),
1202 ".": buildmember,
1202 ".": buildmember,
1203 "|": buildfilter,
1203 "|": buildfilter,
1204 "%": buildmap,
1204 "%": buildmap,
1205 "func": buildfunc,
1205 "func": buildfunc,
1206 "keyvalue": buildkeyvaluepair,
1206 "keyvalue": buildkeyvaluepair,
1207 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1207 "+": lambda e, c: buildarithmetic(e, c, lambda a, b: a + b),
1208 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1208 "-": lambda e, c: buildarithmetic(e, c, lambda a, b: a - b),
1209 "negate": buildnegate,
1209 "negate": buildnegate,
1210 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1210 "*": lambda e, c: buildarithmetic(e, c, lambda a, b: a * b),
1211 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1211 "/": lambda e, c: buildarithmetic(e, c, lambda a, b: a // b),
1212 }
1212 }
1213
1213
1214 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1214 # methods to interpret top-level template (e.g. {x}, {x|_}, {x % "y"})
1215 methods = exprmethods.copy()
1215 methods = exprmethods.copy()
1216 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1216 methods["integer"] = exprmethods["symbol"] # '{1}' as variable
1217
1217
1218 class _aliasrules(parser.basealiasrules):
1218 class _aliasrules(parser.basealiasrules):
1219 """Parsing and expansion rule set of template aliases"""
1219 """Parsing and expansion rule set of template aliases"""
1220 _section = _('template alias')
1220 _section = _('template alias')
1221 _parse = staticmethod(_parseexpr)
1221 _parse = staticmethod(_parseexpr)
1222
1222
1223 @staticmethod
1223 @staticmethod
1224 def _trygetfunc(tree):
1224 def _trygetfunc(tree):
1225 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1225 """Return (name, args) if tree is func(...) or ...|filter; otherwise
1226 None"""
1226 None"""
1227 if tree[0] == 'func' and tree[1][0] == 'symbol':
1227 if tree[0] == 'func' and tree[1][0] == 'symbol':
1228 return tree[1][1], getlist(tree[2])
1228 return tree[1][1], getlist(tree[2])
1229 if tree[0] == '|' and tree[2][0] == 'symbol':
1229 if tree[0] == '|' and tree[2][0] == 'symbol':
1230 return tree[2][1], [tree[1]]
1230 return tree[2][1], [tree[1]]
1231
1231
1232 def expandaliases(tree, aliases):
1232 def expandaliases(tree, aliases):
1233 """Return new tree of aliases are expanded"""
1233 """Return new tree of aliases are expanded"""
1234 aliasmap = _aliasrules.buildmap(aliases)
1234 aliasmap = _aliasrules.buildmap(aliases)
1235 return _aliasrules.expand(aliasmap, tree)
1235 return _aliasrules.expand(aliasmap, tree)
1236
1236
1237 # template engine
1237 # template engine
1238
1238
1239 stringify = templatefilters.stringify
1239 stringify = templatefilters.stringify
1240
1240
1241 def _flatten(thing):
1241 def _flatten(thing):
1242 '''yield a single stream from a possibly nested set of iterators'''
1242 '''yield a single stream from a possibly nested set of iterators'''
1243 thing = templatekw.unwraphybrid(thing)
1243 thing = templatekw.unwraphybrid(thing)
1244 if isinstance(thing, bytes):
1244 if isinstance(thing, bytes):
1245 yield thing
1245 yield thing
1246 elif isinstance(thing, str):
1246 elif isinstance(thing, str):
1247 # We can only hit this on Python 3, and it's here to guard
1247 # We can only hit this on Python 3, and it's here to guard
1248 # against infinite recursion.
1248 # against infinite recursion.
1249 raise error.ProgrammingError('Mercurial IO including templates is done'
1249 raise error.ProgrammingError('Mercurial IO including templates is done'
1250 ' with bytes, not strings')
1250 ' with bytes, not strings')
1251 elif thing is None:
1251 elif thing is None:
1252 pass
1252 pass
1253 elif not util.safehasattr(thing, '__iter__'):
1253 elif not util.safehasattr(thing, '__iter__'):
1254 yield pycompat.bytestr(thing)
1254 yield pycompat.bytestr(thing)
1255 else:
1255 else:
1256 for i in thing:
1256 for i in thing:
1257 i = templatekw.unwraphybrid(i)
1257 i = templatekw.unwraphybrid(i)
1258 if isinstance(i, bytes):
1258 if isinstance(i, bytes):
1259 yield i
1259 yield i
1260 elif i is None:
1260 elif i is None:
1261 pass
1261 pass
1262 elif not util.safehasattr(i, '__iter__'):
1262 elif not util.safehasattr(i, '__iter__'):
1263 yield pycompat.bytestr(i)
1263 yield pycompat.bytestr(i)
1264 else:
1264 else:
1265 for j in _flatten(i):
1265 for j in _flatten(i):
1266 yield j
1266 yield j
1267
1267
1268 def unquotestring(s):
1268 def unquotestring(s):
1269 '''unwrap quotes if any; otherwise returns unmodified string'''
1269 '''unwrap quotes if any; otherwise returns unmodified string'''
1270 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1270 if len(s) < 2 or s[0] not in "'\"" or s[0] != s[-1]:
1271 return s
1271 return s
1272 return s[1:-1]
1272 return s[1:-1]
1273
1273
1274 class engine(object):
1274 class engine(object):
1275 '''template expansion engine.
1275 '''template expansion engine.
1276
1276
1277 template expansion works like this. a map file contains key=value
1277 template expansion works like this. a map file contains key=value
1278 pairs. if value is quoted, it is treated as string. otherwise, it
1278 pairs. if value is quoted, it is treated as string. otherwise, it
1279 is treated as name of template file.
1279 is treated as name of template file.
1280
1280
1281 templater is asked to expand a key in map. it looks up key, and
1281 templater is asked to expand a key in map. it looks up key, and
1282 looks for strings like this: {foo}. it expands {foo} by looking up
1282 looks for strings like this: {foo}. it expands {foo} by looking up
1283 foo in map, and substituting it. expansion is recursive: it stops
1283 foo in map, and substituting it. expansion is recursive: it stops
1284 when there is no more {foo} to replace.
1284 when there is no more {foo} to replace.
1285
1285
1286 expansion also allows formatting and filtering.
1286 expansion also allows formatting and filtering.
1287
1287
1288 format uses key to expand each item in list. syntax is
1288 format uses key to expand each item in list. syntax is
1289 {key%format}.
1289 {key%format}.
1290
1290
1291 filter uses function to transform value. syntax is
1291 filter uses function to transform value. syntax is
1292 {key|filter1|filter2|...}.'''
1292 {key|filter1|filter2|...}.'''
1293
1293
1294 def __init__(self, loader, filters=None, defaults=None, aliases=()):
1294 def __init__(self, loader, filters=None, defaults=None, aliases=()):
1295 self._loader = loader
1295 self._loader = loader
1296 if filters is None:
1296 if filters is None:
1297 filters = {}
1297 filters = {}
1298 self._filters = filters
1298 self._filters = filters
1299 if defaults is None:
1299 if defaults is None:
1300 defaults = {}
1300 defaults = {}
1301 self._defaults = defaults
1301 self._defaults = defaults
1302 self._aliasmap = _aliasrules.buildmap(aliases)
1302 self._aliasmap = _aliasrules.buildmap(aliases)
1303 self._cache = {} # key: (func, data)
1303 self._cache = {} # key: (func, data)
1304
1304
1305 def _load(self, t):
1305 def _load(self, t):
1306 '''load, parse, and cache a template'''
1306 '''load, parse, and cache a template'''
1307 if t not in self._cache:
1307 if t not in self._cache:
1308 # put poison to cut recursion while compiling 't'
1308 # put poison to cut recursion while compiling 't'
1309 self._cache[t] = (_runrecursivesymbol, t)
1309 self._cache[t] = (_runrecursivesymbol, t)
1310 try:
1310 try:
1311 x = parse(self._loader(t))
1311 x = parse(self._loader(t))
1312 if self._aliasmap:
1312 if self._aliasmap:
1313 x = _aliasrules.expand(self._aliasmap, x)
1313 x = _aliasrules.expand(self._aliasmap, x)
1314 self._cache[t] = compileexp(x, self, methods)
1314 self._cache[t] = compileexp(x, self, methods)
1315 except: # re-raises
1315 except: # re-raises
1316 del self._cache[t]
1316 del self._cache[t]
1317 raise
1317 raise
1318 return self._cache[t]
1318 return self._cache[t]
1319
1319
1320 def process(self, t, mapping):
1320 def process(self, t, mapping):
1321 '''Perform expansion. t is name of map element to expand.
1321 '''Perform expansion. t is name of map element to expand.
1322 mapping contains added elements for use during expansion. Is a
1322 mapping contains added elements for use during expansion. Is a
1323 generator.'''
1323 generator.'''
1324 func, data = self._load(t)
1324 func, data = self._load(t)
1325 return _flatten(func(self, mapping, data))
1325 return _flatten(func(self, mapping, data))
1326
1326
1327 engines = {'default': engine}
1327 engines = {'default': engine}
1328
1328
1329 def stylelist():
1329 def stylelist():
1330 paths = templatepaths()
1330 paths = templatepaths()
1331 if not paths:
1331 if not paths:
1332 return _('no templates found, try `hg debuginstall` for more info')
1332 return _('no templates found, try `hg debuginstall` for more info')
1333 dirlist = os.listdir(paths[0])
1333 dirlist = os.listdir(paths[0])
1334 stylelist = []
1334 stylelist = []
1335 for file in dirlist:
1335 for file in dirlist:
1336 split = file.split(".")
1336 split = file.split(".")
1337 if split[-1] in ('orig', 'rej'):
1337 if split[-1] in ('orig', 'rej'):
1338 continue
1338 continue
1339 if split[0] == "map-cmdline":
1339 if split[0] == "map-cmdline":
1340 stylelist.append(split[1])
1340 stylelist.append(split[1])
1341 return ", ".join(sorted(stylelist))
1341 return ", ".join(sorted(stylelist))
1342
1342
1343 def _readmapfile(mapfile):
1343 def _readmapfile(mapfile):
1344 """Load template elements from the given map file"""
1344 """Load template elements from the given map file"""
1345 if not os.path.exists(mapfile):
1345 if not os.path.exists(mapfile):
1346 raise error.Abort(_("style '%s' not found") % mapfile,
1346 raise error.Abort(_("style '%s' not found") % mapfile,
1347 hint=_("available styles: %s") % stylelist())
1347 hint=_("available styles: %s") % stylelist())
1348
1348
1349 base = os.path.dirname(mapfile)
1349 base = os.path.dirname(mapfile)
1350 conf = config.config(includepaths=templatepaths())
1350 conf = config.config(includepaths=templatepaths())
1351 conf.read(mapfile, remap={'': 'templates'})
1351 conf.read(mapfile, remap={'': 'templates'})
1352
1352
1353 cache = {}
1353 cache = {}
1354 tmap = {}
1354 tmap = {}
1355 aliases = []
1355 aliases = []
1356
1356
1357 val = conf.get('templates', '__base__')
1357 val = conf.get('templates', '__base__')
1358 if val and val[0] not in "'\"":
1358 if val and val[0] not in "'\"":
1359 # treat as a pointer to a base class for this style
1359 # treat as a pointer to a base class for this style
1360 path = util.normpath(os.path.join(base, val))
1360 path = util.normpath(os.path.join(base, val))
1361
1361
1362 # fallback check in template paths
1362 # fallback check in template paths
1363 if not os.path.exists(path):
1363 if not os.path.exists(path):
1364 for p in templatepaths():
1364 for p in templatepaths():
1365 p2 = util.normpath(os.path.join(p, val))
1365 p2 = util.normpath(os.path.join(p, val))
1366 if os.path.isfile(p2):
1366 if os.path.isfile(p2):
1367 path = p2
1367 path = p2
1368 break
1368 break
1369 p3 = util.normpath(os.path.join(p2, "map"))
1369 p3 = util.normpath(os.path.join(p2, "map"))
1370 if os.path.isfile(p3):
1370 if os.path.isfile(p3):
1371 path = p3
1371 path = p3
1372 break
1372 break
1373
1373
1374 cache, tmap, aliases = _readmapfile(path)
1374 cache, tmap, aliases = _readmapfile(path)
1375
1375
1376 for key, val in conf['templates'].items():
1376 for key, val in conf['templates'].items():
1377 if not val:
1377 if not val:
1378 raise error.ParseError(_('missing value'),
1378 raise error.ParseError(_('missing value'),
1379 conf.source('templates', key))
1379 conf.source('templates', key))
1380 if val[0] in "'\"":
1380 if val[0] in "'\"":
1381 if val[0] != val[-1]:
1381 if val[0] != val[-1]:
1382 raise error.ParseError(_('unmatched quotes'),
1382 raise error.ParseError(_('unmatched quotes'),
1383 conf.source('templates', key))
1383 conf.source('templates', key))
1384 cache[key] = unquotestring(val)
1384 cache[key] = unquotestring(val)
1385 elif key != '__base__':
1385 elif key != '__base__':
1386 val = 'default', val
1386 val = 'default', val
1387 if ':' in val[1]:
1387 if ':' in val[1]:
1388 val = val[1].split(':', 1)
1388 val = val[1].split(':', 1)
1389 tmap[key] = val[0], os.path.join(base, val[1])
1389 tmap[key] = val[0], os.path.join(base, val[1])
1390 aliases.extend(conf['templatealias'].items())
1390 aliases.extend(conf['templatealias'].items())
1391 return cache, tmap, aliases
1391 return cache, tmap, aliases
1392
1392
1393 class TemplateNotFound(error.Abort):
1393 class TemplateNotFound(error.Abort):
1394 pass
1394 pass
1395
1395
1396 class templater(object):
1396 class templater(object):
1397
1397
1398 def __init__(self, filters=None, defaults=None, cache=None, aliases=(),
1398 def __init__(self, filters=None, defaults=None, cache=None, aliases=(),
1399 minchunk=1024, maxchunk=65536):
1399 minchunk=1024, maxchunk=65536):
1400 '''set up template engine.
1400 '''set up template engine.
1401 filters is dict of functions. each transforms a value into another.
1401 filters is dict of functions. each transforms a value into another.
1402 defaults is dict of default map definitions.
1402 defaults is dict of default map definitions.
1403 aliases is list of alias (name, replacement) pairs.
1403 aliases is list of alias (name, replacement) pairs.
1404 '''
1404 '''
1405 if filters is None:
1405 if filters is None:
1406 filters = {}
1406 filters = {}
1407 if defaults is None:
1407 if defaults is None:
1408 defaults = {}
1408 defaults = {}
1409 if cache is None:
1409 if cache is None:
1410 cache = {}
1410 cache = {}
1411 self.cache = cache.copy()
1411 self.cache = cache.copy()
1412 self.map = {}
1412 self.map = {}
1413 self.filters = templatefilters.filters.copy()
1413 self.filters = templatefilters.filters.copy()
1414 self.filters.update(filters)
1414 self.filters.update(filters)
1415 self.defaults = defaults
1415 self.defaults = defaults
1416 self._aliases = aliases
1416 self._aliases = aliases
1417 self.minchunk, self.maxchunk = minchunk, maxchunk
1417 self.minchunk, self.maxchunk = minchunk, maxchunk
1418 self.ecache = {}
1418 self.ecache = {}
1419
1419
1420 @classmethod
1420 @classmethod
1421 def frommapfile(cls, mapfile, filters=None, defaults=None, cache=None,
1421 def frommapfile(cls, mapfile, filters=None, defaults=None, cache=None,
1422 minchunk=1024, maxchunk=65536):
1422 minchunk=1024, maxchunk=65536):
1423 """Create templater from the specified map file"""
1423 """Create templater from the specified map file"""
1424 t = cls(filters, defaults, cache, [], minchunk, maxchunk)
1424 t = cls(filters, defaults, cache, [], minchunk, maxchunk)
1425 cache, tmap, aliases = _readmapfile(mapfile)
1425 cache, tmap, aliases = _readmapfile(mapfile)
1426 t.cache.update(cache)
1426 t.cache.update(cache)
1427 t.map = tmap
1427 t.map = tmap
1428 t._aliases = aliases
1428 t._aliases = aliases
1429 return t
1429 return t
1430
1430
1431 def __contains__(self, key):
1431 def __contains__(self, key):
1432 return key in self.cache or key in self.map
1432 return key in self.cache or key in self.map
1433
1433
1434 def load(self, t):
1434 def load(self, t):
1435 '''Get the template for the given template name. Use a local cache.'''
1435 '''Get the template for the given template name. Use a local cache.'''
1436 if t not in self.cache:
1436 if t not in self.cache:
1437 try:
1437 try:
1438 self.cache[t] = util.readfile(self.map[t][1])
1438 self.cache[t] = util.readfile(self.map[t][1])
1439 except KeyError as inst:
1439 except KeyError as inst:
1440 raise TemplateNotFound(_('"%s" not in template map') %
1440 raise TemplateNotFound(_('"%s" not in template map') %
1441 inst.args[0])
1441 inst.args[0])
1442 except IOError as inst:
1442 except IOError as inst:
1443 raise IOError(inst.args[0], _('template file %s: %s') %
1443 raise IOError(inst.args[0], _('template file %s: %s') %
1444 (self.map[t][1], inst.args[1]))
1444 (self.map[t][1], inst.args[1]))
1445 return self.cache[t]
1445 return self.cache[t]
1446
1446
1447 def render(self, mapping):
1447 def render(self, mapping):
1448 """Render the default unnamed template and return result as string"""
1448 """Render the default unnamed template and return result as string"""
1449 mapping = pycompat.strkwargs(mapping)
1449 mapping = pycompat.strkwargs(mapping)
1450 return stringify(self('', **mapping))
1450 return stringify(self('', **mapping))
1451
1451
1452 def __call__(self, t, **mapping):
1452 def __call__(self, t, **mapping):
1453 mapping = pycompat.byteskwargs(mapping)
1453 mapping = pycompat.byteskwargs(mapping)
1454 ttype = t in self.map and self.map[t][0] or 'default'
1454 ttype = t in self.map and self.map[t][0] or 'default'
1455 if ttype not in self.ecache:
1455 if ttype not in self.ecache:
1456 try:
1456 try:
1457 ecls = engines[ttype]
1457 ecls = engines[ttype]
1458 except KeyError:
1458 except KeyError:
1459 raise error.Abort(_('invalid template engine: %s') % ttype)
1459 raise error.Abort(_('invalid template engine: %s') % ttype)
1460 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1460 self.ecache[ttype] = ecls(self.load, self.filters, self.defaults,
1461 self._aliases)
1461 self._aliases)
1462 proc = self.ecache[ttype]
1462 proc = self.ecache[ttype]
1463
1463
1464 stream = proc.process(t, mapping)
1464 stream = proc.process(t, mapping)
1465 if self.minchunk:
1465 if self.minchunk:
1466 stream = util.increasingchunks(stream, min=self.minchunk,
1466 stream = util.increasingchunks(stream, min=self.minchunk,
1467 max=self.maxchunk)
1467 max=self.maxchunk)
1468 return stream
1468 return stream
1469
1469
1470 def templatepaths():
1470 def templatepaths():
1471 '''return locations used for template files.'''
1471 '''return locations used for template files.'''
1472 pathsrel = ['templates']
1472 pathsrel = ['templates']
1473 paths = [os.path.normpath(os.path.join(util.datapath, f))
1473 paths = [os.path.normpath(os.path.join(util.datapath, f))
1474 for f in pathsrel]
1474 for f in pathsrel]
1475 return [p for p in paths if os.path.isdir(p)]
1475 return [p for p in paths if os.path.isdir(p)]
1476
1476
1477 def templatepath(name):
1477 def templatepath(name):
1478 '''return location of template file. returns None if not found.'''
1478 '''return location of template file. returns None if not found.'''
1479 for p in templatepaths():
1479 for p in templatepaths():
1480 f = os.path.join(p, name)
1480 f = os.path.join(p, name)
1481 if os.path.exists(f):
1481 if os.path.exists(f):
1482 return f
1482 return f
1483 return None
1483 return None
1484
1484
1485 def stylemap(styles, paths=None):
1485 def stylemap(styles, paths=None):
1486 """Return path to mapfile for a given style.
1486 """Return path to mapfile for a given style.
1487
1487
1488 Searches mapfile in the following locations:
1488 Searches mapfile in the following locations:
1489 1. templatepath/style/map
1489 1. templatepath/style/map
1490 2. templatepath/map-style
1490 2. templatepath/map-style
1491 3. templatepath/map
1491 3. templatepath/map
1492 """
1492 """
1493
1493
1494 if paths is None:
1494 if paths is None:
1495 paths = templatepaths()
1495 paths = templatepaths()
1496 elif isinstance(paths, str):
1496 elif isinstance(paths, str):
1497 paths = [paths]
1497 paths = [paths]
1498
1498
1499 if isinstance(styles, str):
1499 if isinstance(styles, str):
1500 styles = [styles]
1500 styles = [styles]
1501
1501
1502 for style in styles:
1502 for style in styles:
1503 # only plain name is allowed to honor template paths
1503 # only plain name is allowed to honor template paths
1504 if (not style
1504 if (not style
1505 or style in (os.curdir, os.pardir)
1505 or style in (os.curdir, os.pardir)
1506 or pycompat.ossep in style
1506 or pycompat.ossep in style
1507 or pycompat.osaltsep and pycompat.osaltsep in style):
1507 or pycompat.osaltsep and pycompat.osaltsep in style):
1508 continue
1508 continue
1509 locations = [os.path.join(style, 'map'), 'map-' + style]
1509 locations = [os.path.join(style, 'map'), 'map-' + style]
1510 locations.append('map')
1510 locations.append('map')
1511
1511
1512 for path in paths:
1512 for path in paths:
1513 for location in locations:
1513 for location in locations:
1514 mapfile = os.path.join(path, location)
1514 mapfile = os.path.join(path, location)
1515 if os.path.isfile(mapfile):
1515 if os.path.isfile(mapfile):
1516 return style, mapfile
1516 return style, mapfile
1517
1517
1518 raise RuntimeError("No hgweb templates found in %r" % paths)
1518 raise RuntimeError("No hgweb templates found in %r" % paths)
1519
1519
1520 def loadfunction(ui, extname, registrarobj):
1520 def loadfunction(ui, extname, registrarobj):
1521 """Load template function from specified registrarobj
1521 """Load template function from specified registrarobj
1522 """
1522 """
1523 for name, func in registrarobj._table.iteritems():
1523 for name, func in registrarobj._table.iteritems():
1524 funcs[name] = func
1524 funcs[name] = func
1525
1525
1526 # tell hggettext to extract docstrings from these functions:
1526 # tell hggettext to extract docstrings from these functions:
1527 i18nfunctions = funcs.values()
1527 i18nfunctions = funcs.values()
@@ -1,456 +1,489
1 $ . "$TESTDIR/histedit-helpers.sh"
1 $ . "$TESTDIR/histedit-helpers.sh"
2
2
3 $ cat >> $HGRCPATH <<EOF
3 $ cat >> $HGRCPATH <<EOF
4 > [extensions]
4 > [extensions]
5 > histedit=
5 > histedit=
6 > EOF
6 > EOF
7
7
8 $ initrepo ()
8 $ initrepo ()
9 > {
9 > {
10 > hg init r
10 > hg init r
11 > cd r
11 > cd r
12 > for x in a b c d e f ; do
12 > for x in a b c d e f ; do
13 > echo $x > $x
13 > echo $x > $x
14 > hg add $x
14 > hg add $x
15 > hg ci -m $x
15 > hg ci -m $x
16 > done
16 > done
17 > }
17 > }
18
18
19 $ initrepo
19 $ initrepo
20
20
21 log before edit
21 log before edit
22 $ hg log --graph
22 $ hg log --graph
23 @ changeset: 5:652413bf663e
23 @ changeset: 5:652413bf663e
24 | tag: tip
24 | tag: tip
25 | user: test
25 | user: test
26 | date: Thu Jan 01 00:00:00 1970 +0000
26 | date: Thu Jan 01 00:00:00 1970 +0000
27 | summary: f
27 | summary: f
28 |
28 |
29 o changeset: 4:e860deea161a
29 o changeset: 4:e860deea161a
30 | user: test
30 | user: test
31 | date: Thu Jan 01 00:00:00 1970 +0000
31 | date: Thu Jan 01 00:00:00 1970 +0000
32 | summary: e
32 | summary: e
33 |
33 |
34 o changeset: 3:055a42cdd887
34 o changeset: 3:055a42cdd887
35 | user: test
35 | user: test
36 | date: Thu Jan 01 00:00:00 1970 +0000
36 | date: Thu Jan 01 00:00:00 1970 +0000
37 | summary: d
37 | summary: d
38 |
38 |
39 o changeset: 2:177f92b77385
39 o changeset: 2:177f92b77385
40 | user: test
40 | user: test
41 | date: Thu Jan 01 00:00:00 1970 +0000
41 | date: Thu Jan 01 00:00:00 1970 +0000
42 | summary: c
42 | summary: c
43 |
43 |
44 o changeset: 1:d2ae7f538514
44 o changeset: 1:d2ae7f538514
45 | user: test
45 | user: test
46 | date: Thu Jan 01 00:00:00 1970 +0000
46 | date: Thu Jan 01 00:00:00 1970 +0000
47 | summary: b
47 | summary: b
48 |
48 |
49 o changeset: 0:cb9a9f314b8b
49 o changeset: 0:cb9a9f314b8b
50 user: test
50 user: test
51 date: Thu Jan 01 00:00:00 1970 +0000
51 date: Thu Jan 01 00:00:00 1970 +0000
52 summary: a
52 summary: a
53
53
54
54
55 show the edit commands offered
55 show the edit commands offered
56 $ HGEDITOR=cat hg histedit 177f92b77385
56 $ HGEDITOR=cat hg histedit 177f92b77385
57 pick 177f92b77385 2 c
57 pick 177f92b77385 2 c
58 pick 055a42cdd887 3 d
58 pick 055a42cdd887 3 d
59 pick e860deea161a 4 e
59 pick e860deea161a 4 e
60 pick 652413bf663e 5 f
60 pick 652413bf663e 5 f
61
61
62 # Edit history between 177f92b77385 and 652413bf663e
62 # Edit history between 177f92b77385 and 652413bf663e
63 #
63 #
64 # Commits are listed from least to most recent
64 # Commits are listed from least to most recent
65 #
65 #
66 # You can reorder changesets by reordering the lines
66 # You can reorder changesets by reordering the lines
67 #
67 #
68 # Commands:
68 # Commands:
69 #
69 #
70 # e, edit = use commit, but stop for amending
70 # e, edit = use commit, but stop for amending
71 # m, mess = edit commit message without changing commit content
71 # m, mess = edit commit message without changing commit content
72 # p, pick = use commit
72 # p, pick = use commit
73 # b, base = checkout changeset and apply further changesets from there
73 # b, base = checkout changeset and apply further changesets from there
74 # d, drop = remove commit from history
74 # d, drop = remove commit from history
75 # f, fold = use commit, but combine it with the one above
75 # f, fold = use commit, but combine it with the one above
76 # r, roll = like fold, but discard this commit's description and date
76 # r, roll = like fold, but discard this commit's description and date
77 #
77 #
78
78
79 edit the history
79 edit the history
80 (use a hacky editor to check histedit-last-edit.txt backup)
80 (use a hacky editor to check histedit-last-edit.txt backup)
81
81
82 $ EDITED="$TESTTMP/editedhistory"
82 $ EDITED="$TESTTMP/editedhistory"
83 $ cat > $EDITED <<EOF
83 $ cat > $EDITED <<EOF
84 > edit 177f92b77385 c
84 > edit 177f92b77385 c
85 > pick e860deea161a e
85 > pick e860deea161a e
86 > pick 652413bf663e f
86 > pick 652413bf663e f
87 > pick 055a42cdd887 d
87 > pick 055a42cdd887 d
88 > EOF
88 > EOF
89 $ HGEDITOR="cat \"$EDITED\" > " hg histedit 177f92b77385 2>&1 | fixbundle
89 $ HGEDITOR="cat \"$EDITED\" > " hg histedit 177f92b77385 2>&1 | fixbundle
90 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
90 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
91 Editing (177f92b77385), you may commit or record as needed now.
91 Editing (177f92b77385), you may commit or record as needed now.
92 (hg histedit --continue to resume)
92 (hg histedit --continue to resume)
93
93
94 rules should end up in .hg/histedit-last-edit.txt:
94 rules should end up in .hg/histedit-last-edit.txt:
95 $ cat .hg/histedit-last-edit.txt
95 $ cat .hg/histedit-last-edit.txt
96 edit 177f92b77385 c
96 edit 177f92b77385 c
97 pick e860deea161a e
97 pick e860deea161a e
98 pick 652413bf663e f
98 pick 652413bf663e f
99 pick 055a42cdd887 d
99 pick 055a42cdd887 d
100
100
101 $ hg histedit --abort
101 $ hg histedit --abort
102 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
102 4 files updated, 0 files merged, 0 files removed, 0 files unresolved
103 $ cat > $EDITED <<EOF
103 $ cat > $EDITED <<EOF
104 > pick 177f92b77385 c
104 > pick 177f92b77385 c
105 > pick e860deea161a e
105 > pick e860deea161a e
106 > pick 652413bf663e f
106 > pick 652413bf663e f
107 > pick 055a42cdd887 d
107 > pick 055a42cdd887 d
108 > EOF
108 > EOF
109 $ HGEDITOR="cat \"$EDITED\" > " hg histedit 177f92b77385 2>&1 | fixbundle
109 $ HGEDITOR="cat \"$EDITED\" > " hg histedit 177f92b77385 2>&1 | fixbundle
110
110
111 log after edit
111 log after edit
112 $ hg log --graph
112 $ hg log --graph
113 @ changeset: 5:07114f51870f
113 @ changeset: 5:07114f51870f
114 | tag: tip
114 | tag: tip
115 | user: test
115 | user: test
116 | date: Thu Jan 01 00:00:00 1970 +0000
116 | date: Thu Jan 01 00:00:00 1970 +0000
117 | summary: d
117 | summary: d
118 |
118 |
119 o changeset: 4:8ade9693061e
119 o changeset: 4:8ade9693061e
120 | user: test
120 | user: test
121 | date: Thu Jan 01 00:00:00 1970 +0000
121 | date: Thu Jan 01 00:00:00 1970 +0000
122 | summary: f
122 | summary: f
123 |
123 |
124 o changeset: 3:d8249471110a
124 o changeset: 3:d8249471110a
125 | user: test
125 | user: test
126 | date: Thu Jan 01 00:00:00 1970 +0000
126 | date: Thu Jan 01 00:00:00 1970 +0000
127 | summary: e
127 | summary: e
128 |
128 |
129 o changeset: 2:177f92b77385
129 o changeset: 2:177f92b77385
130 | user: test
130 | user: test
131 | date: Thu Jan 01 00:00:00 1970 +0000
131 | date: Thu Jan 01 00:00:00 1970 +0000
132 | summary: c
132 | summary: c
133 |
133 |
134 o changeset: 1:d2ae7f538514
134 o changeset: 1:d2ae7f538514
135 | user: test
135 | user: test
136 | date: Thu Jan 01 00:00:00 1970 +0000
136 | date: Thu Jan 01 00:00:00 1970 +0000
137 | summary: b
137 | summary: b
138 |
138 |
139 o changeset: 0:cb9a9f314b8b
139 o changeset: 0:cb9a9f314b8b
140 user: test
140 user: test
141 date: Thu Jan 01 00:00:00 1970 +0000
141 date: Thu Jan 01 00:00:00 1970 +0000
142 summary: a
142 summary: a
143
143
144
144
145 put things back
145 put things back
146
146
147 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF | fixbundle
147 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF | fixbundle
148 > pick 177f92b77385 c
148 > pick 177f92b77385 c
149 > pick 07114f51870f d
149 > pick 07114f51870f d
150 > pick d8249471110a e
150 > pick d8249471110a e
151 > pick 8ade9693061e f
151 > pick 8ade9693061e f
152 > EOF
152 > EOF
153
153
154 $ hg log --graph
154 $ hg log --graph
155 @ changeset: 5:7eca9b5b1148
155 @ changeset: 5:7eca9b5b1148
156 | tag: tip
156 | tag: tip
157 | user: test
157 | user: test
158 | date: Thu Jan 01 00:00:00 1970 +0000
158 | date: Thu Jan 01 00:00:00 1970 +0000
159 | summary: f
159 | summary: f
160 |
160 |
161 o changeset: 4:915da888f2de
161 o changeset: 4:915da888f2de
162 | user: test
162 | user: test
163 | date: Thu Jan 01 00:00:00 1970 +0000
163 | date: Thu Jan 01 00:00:00 1970 +0000
164 | summary: e
164 | summary: e
165 |
165 |
166 o changeset: 3:10517e47bbbb
166 o changeset: 3:10517e47bbbb
167 | user: test
167 | user: test
168 | date: Thu Jan 01 00:00:00 1970 +0000
168 | date: Thu Jan 01 00:00:00 1970 +0000
169 | summary: d
169 | summary: d
170 |
170 |
171 o changeset: 2:177f92b77385
171 o changeset: 2:177f92b77385
172 | user: test
172 | user: test
173 | date: Thu Jan 01 00:00:00 1970 +0000
173 | date: Thu Jan 01 00:00:00 1970 +0000
174 | summary: c
174 | summary: c
175 |
175 |
176 o changeset: 1:d2ae7f538514
176 o changeset: 1:d2ae7f538514
177 | user: test
177 | user: test
178 | date: Thu Jan 01 00:00:00 1970 +0000
178 | date: Thu Jan 01 00:00:00 1970 +0000
179 | summary: b
179 | summary: b
180 |
180 |
181 o changeset: 0:cb9a9f314b8b
181 o changeset: 0:cb9a9f314b8b
182 user: test
182 user: test
183 date: Thu Jan 01 00:00:00 1970 +0000
183 date: Thu Jan 01 00:00:00 1970 +0000
184 summary: a
184 summary: a
185
185
186
186
187 slightly different this time
187 slightly different this time
188
188
189 $ hg histedit 177f92b77385 --commands - << EOF 2>&1 | fixbundle
189 $ hg histedit 177f92b77385 --commands - << EOF 2>&1 | fixbundle
190 > pick 10517e47bbbb d
190 > pick 10517e47bbbb d
191 > pick 7eca9b5b1148 f
191 > pick 7eca9b5b1148 f
192 > pick 915da888f2de e
192 > pick 915da888f2de e
193 > pick 177f92b77385 c
193 > pick 177f92b77385 c
194 > EOF
194 > EOF
195 $ hg log --graph
195 $ hg log --graph
196 @ changeset: 5:38b92f448761
196 @ changeset: 5:38b92f448761
197 | tag: tip
197 | tag: tip
198 | user: test
198 | user: test
199 | date: Thu Jan 01 00:00:00 1970 +0000
199 | date: Thu Jan 01 00:00:00 1970 +0000
200 | summary: c
200 | summary: c
201 |
201 |
202 o changeset: 4:de71b079d9ce
202 o changeset: 4:de71b079d9ce
203 | user: test
203 | user: test
204 | date: Thu Jan 01 00:00:00 1970 +0000
204 | date: Thu Jan 01 00:00:00 1970 +0000
205 | summary: e
205 | summary: e
206 |
206 |
207 o changeset: 3:be9ae3a309c6
207 o changeset: 3:be9ae3a309c6
208 | user: test
208 | user: test
209 | date: Thu Jan 01 00:00:00 1970 +0000
209 | date: Thu Jan 01 00:00:00 1970 +0000
210 | summary: f
210 | summary: f
211 |
211 |
212 o changeset: 2:799205341b6b
212 o changeset: 2:799205341b6b
213 | user: test
213 | user: test
214 | date: Thu Jan 01 00:00:00 1970 +0000
214 | date: Thu Jan 01 00:00:00 1970 +0000
215 | summary: d
215 | summary: d
216 |
216 |
217 o changeset: 1:d2ae7f538514
217 o changeset: 1:d2ae7f538514
218 | user: test
218 | user: test
219 | date: Thu Jan 01 00:00:00 1970 +0000
219 | date: Thu Jan 01 00:00:00 1970 +0000
220 | summary: b
220 | summary: b
221 |
221 |
222 o changeset: 0:cb9a9f314b8b
222 o changeset: 0:cb9a9f314b8b
223 user: test
223 user: test
224 date: Thu Jan 01 00:00:00 1970 +0000
224 date: Thu Jan 01 00:00:00 1970 +0000
225 summary: a
225 summary: a
226
226
227
227
228 keep prevents stripping dead revs
228 keep prevents stripping dead revs
229 $ hg histedit 799205341b6b --keep --commands - 2>&1 << EOF | fixbundle
229 $ hg histedit 799205341b6b --keep --commands - 2>&1 << EOF | fixbundle
230 > pick 799205341b6b d
230 > pick 799205341b6b d
231 > pick be9ae3a309c6 f
231 > pick be9ae3a309c6 f
232 > pick 38b92f448761 c
232 > pick 38b92f448761 c
233 > pick de71b079d9ce e
233 > pick de71b079d9ce e
234 > EOF
234 > EOF
235 $ hg log --graph
235 $ hg log --graph
236 @ changeset: 7:803ef1c6fcfd
236 @ changeset: 7:803ef1c6fcfd
237 | tag: tip
237 | tag: tip
238 | user: test
238 | user: test
239 | date: Thu Jan 01 00:00:00 1970 +0000
239 | date: Thu Jan 01 00:00:00 1970 +0000
240 | summary: e
240 | summary: e
241 |
241 |
242 o changeset: 6:ece0b8d93dda
242 o changeset: 6:ece0b8d93dda
243 | parent: 3:be9ae3a309c6
243 | parent: 3:be9ae3a309c6
244 | user: test
244 | user: test
245 | date: Thu Jan 01 00:00:00 1970 +0000
245 | date: Thu Jan 01 00:00:00 1970 +0000
246 | summary: c
246 | summary: c
247 |
247 |
248 | o changeset: 5:38b92f448761
248 | o changeset: 5:38b92f448761
249 | | user: test
249 | | user: test
250 | | date: Thu Jan 01 00:00:00 1970 +0000
250 | | date: Thu Jan 01 00:00:00 1970 +0000
251 | | summary: c
251 | | summary: c
252 | |
252 | |
253 | o changeset: 4:de71b079d9ce
253 | o changeset: 4:de71b079d9ce
254 |/ user: test
254 |/ user: test
255 | date: Thu Jan 01 00:00:00 1970 +0000
255 | date: Thu Jan 01 00:00:00 1970 +0000
256 | summary: e
256 | summary: e
257 |
257 |
258 o changeset: 3:be9ae3a309c6
258 o changeset: 3:be9ae3a309c6
259 | user: test
259 | user: test
260 | date: Thu Jan 01 00:00:00 1970 +0000
260 | date: Thu Jan 01 00:00:00 1970 +0000
261 | summary: f
261 | summary: f
262 |
262 |
263 o changeset: 2:799205341b6b
263 o changeset: 2:799205341b6b
264 | user: test
264 | user: test
265 | date: Thu Jan 01 00:00:00 1970 +0000
265 | date: Thu Jan 01 00:00:00 1970 +0000
266 | summary: d
266 | summary: d
267 |
267 |
268 o changeset: 1:d2ae7f538514
268 o changeset: 1:d2ae7f538514
269 | user: test
269 | user: test
270 | date: Thu Jan 01 00:00:00 1970 +0000
270 | date: Thu Jan 01 00:00:00 1970 +0000
271 | summary: b
271 | summary: b
272 |
272 |
273 o changeset: 0:cb9a9f314b8b
273 o changeset: 0:cb9a9f314b8b
274 user: test
274 user: test
275 date: Thu Jan 01 00:00:00 1970 +0000
275 date: Thu Jan 01 00:00:00 1970 +0000
276 summary: a
276 summary: a
277
277
278
278
279 try with --rev
279 try with --rev
280 $ hg histedit --commands - --rev -2 2>&1 <<EOF | fixbundle
280 $ hg histedit --commands - --rev -2 2>&1 <<EOF | fixbundle
281 > pick de71b079d9ce e
281 > pick de71b079d9ce e
282 > pick 38b92f448761 c
282 > pick 38b92f448761 c
283 > EOF
283 > EOF
284 hg: parse error: pick "de71b079d9ce" changeset was not a candidate
284 hg: parse error: pick "de71b079d9ce" changeset was not a candidate
285 (only use listed changesets)
285 (only use listed changesets)
286 $ hg log --graph
286 $ hg log --graph
287 @ changeset: 7:803ef1c6fcfd
287 @ changeset: 7:803ef1c6fcfd
288 | tag: tip
288 | tag: tip
289 | user: test
289 | user: test
290 | date: Thu Jan 01 00:00:00 1970 +0000
290 | date: Thu Jan 01 00:00:00 1970 +0000
291 | summary: e
291 | summary: e
292 |
292 |
293 o changeset: 6:ece0b8d93dda
293 o changeset: 6:ece0b8d93dda
294 | parent: 3:be9ae3a309c6
294 | parent: 3:be9ae3a309c6
295 | user: test
295 | user: test
296 | date: Thu Jan 01 00:00:00 1970 +0000
296 | date: Thu Jan 01 00:00:00 1970 +0000
297 | summary: c
297 | summary: c
298 |
298 |
299 | o changeset: 5:38b92f448761
299 | o changeset: 5:38b92f448761
300 | | user: test
300 | | user: test
301 | | date: Thu Jan 01 00:00:00 1970 +0000
301 | | date: Thu Jan 01 00:00:00 1970 +0000
302 | | summary: c
302 | | summary: c
303 | |
303 | |
304 | o changeset: 4:de71b079d9ce
304 | o changeset: 4:de71b079d9ce
305 |/ user: test
305 |/ user: test
306 | date: Thu Jan 01 00:00:00 1970 +0000
306 | date: Thu Jan 01 00:00:00 1970 +0000
307 | summary: e
307 | summary: e
308 |
308 |
309 o changeset: 3:be9ae3a309c6
309 o changeset: 3:be9ae3a309c6
310 | user: test
310 | user: test
311 | date: Thu Jan 01 00:00:00 1970 +0000
311 | date: Thu Jan 01 00:00:00 1970 +0000
312 | summary: f
312 | summary: f
313 |
313 |
314 o changeset: 2:799205341b6b
314 o changeset: 2:799205341b6b
315 | user: test
315 | user: test
316 | date: Thu Jan 01 00:00:00 1970 +0000
316 | date: Thu Jan 01 00:00:00 1970 +0000
317 | summary: d
317 | summary: d
318 |
318 |
319 o changeset: 1:d2ae7f538514
319 o changeset: 1:d2ae7f538514
320 | user: test
320 | user: test
321 | date: Thu Jan 01 00:00:00 1970 +0000
321 | date: Thu Jan 01 00:00:00 1970 +0000
322 | summary: b
322 | summary: b
323 |
323 |
324 o changeset: 0:cb9a9f314b8b
324 o changeset: 0:cb9a9f314b8b
325 user: test
325 user: test
326 date: Thu Jan 01 00:00:00 1970 +0000
326 date: Thu Jan 01 00:00:00 1970 +0000
327 summary: a
327 summary: a
328
328
329 Verify that revsetalias entries work with histedit:
329 Verify that revsetalias entries work with histedit:
330 $ cat >> $HGRCPATH <<EOF
330 $ cat >> $HGRCPATH <<EOF
331 > [revsetalias]
331 > [revsetalias]
332 > grandparent(ARG) = p1(p1(ARG))
332 > grandparent(ARG) = p1(p1(ARG))
333 > EOF
333 > EOF
334 $ echo extra commit >> c
334 $ echo extra commit >> c
335 $ hg ci -m 'extra commit to c'
335 $ hg ci -m 'extra commit to c'
336 $ HGEDITOR=cat hg histedit 'grandparent(.)'
336 $ HGEDITOR=cat hg histedit 'grandparent(.)'
337 pick ece0b8d93dda 6 c
337 pick ece0b8d93dda 6 c
338 pick 803ef1c6fcfd 7 e
338 pick 803ef1c6fcfd 7 e
339 pick 9c863c565126 8 extra commit to c
339 pick 9c863c565126 8 extra commit to c
340
340
341 # Edit history between ece0b8d93dda and 9c863c565126
341 # Edit history between ece0b8d93dda and 9c863c565126
342 #
342 #
343 # Commits are listed from least to most recent
343 # Commits are listed from least to most recent
344 #
344 #
345 # You can reorder changesets by reordering the lines
345 # You can reorder changesets by reordering the lines
346 #
346 #
347 # Commands:
347 # Commands:
348 #
348 #
349 # e, edit = use commit, but stop for amending
349 # e, edit = use commit, but stop for amending
350 # m, mess = edit commit message without changing commit content
350 # m, mess = edit commit message without changing commit content
351 # p, pick = use commit
351 # p, pick = use commit
352 # b, base = checkout changeset and apply further changesets from there
352 # b, base = checkout changeset and apply further changesets from there
353 # d, drop = remove commit from history
353 # d, drop = remove commit from history
354 # f, fold = use commit, but combine it with the one above
354 # f, fold = use commit, but combine it with the one above
355 # r, roll = like fold, but discard this commit's description and date
355 # r, roll = like fold, but discard this commit's description and date
356 #
356 #
357
357
358 should also work if a commit message is missing
358 should also work if a commit message is missing
359 $ BUNDLE="$TESTDIR/missing-comment.hg"
359 $ BUNDLE="$TESTDIR/missing-comment.hg"
360 $ hg init missing
360 $ hg init missing
361 $ cd missing
361 $ cd missing
362 $ hg unbundle $BUNDLE
362 $ hg unbundle $BUNDLE
363 adding changesets
363 adding changesets
364 adding manifests
364 adding manifests
365 adding file changes
365 adding file changes
366 added 3 changesets with 3 changes to 1 files
366 added 3 changesets with 3 changes to 1 files
367 new changesets 141947992243:bd22688093b3
367 new changesets 141947992243:bd22688093b3
368 (run 'hg update' to get a working copy)
368 (run 'hg update' to get a working copy)
369 $ hg co tip
369 $ hg co tip
370 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
370 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
371 $ hg log --graph
371 $ hg log --graph
372 @ changeset: 2:bd22688093b3
372 @ changeset: 2:bd22688093b3
373 | tag: tip
373 | tag: tip
374 | user: Robert Altman <robert.altman@telventDTN.com>
374 | user: Robert Altman <robert.altman@telventDTN.com>
375 | date: Mon Nov 28 16:40:04 2011 +0000
375 | date: Mon Nov 28 16:40:04 2011 +0000
376 | summary: Update file.
376 | summary: Update file.
377 |
377 |
378 o changeset: 1:3b3e956f9171
378 o changeset: 1:3b3e956f9171
379 | user: Robert Altman <robert.altman@telventDTN.com>
379 | user: Robert Altman <robert.altman@telventDTN.com>
380 | date: Mon Nov 28 16:37:57 2011 +0000
380 | date: Mon Nov 28 16:37:57 2011 +0000
381 |
381 |
382 o changeset: 0:141947992243
382 o changeset: 0:141947992243
383 user: Robert Altman <robert.altman@telventDTN.com>
383 user: Robert Altman <robert.altman@telventDTN.com>
384 date: Mon Nov 28 16:35:28 2011 +0000
384 date: Mon Nov 28 16:35:28 2011 +0000
385 summary: Checked in text file
385 summary: Checked in text file
386
386
387 $ hg histedit 0
387 $ hg histedit 0
388 $ cd ..
388 $ cd ..
389
389
390 $ cd ..
390 $ cd ..
391
391
392
392
393 Test to make sure folding renames doesn't cause bogus conflicts (issue4251):
393 Test to make sure folding renames doesn't cause bogus conflicts (issue4251):
394 $ hg init issue4251
394 $ hg init issue4251
395 $ cd issue4251
395 $ cd issue4251
396
396
397 $ mkdir initial-dir
397 $ mkdir initial-dir
398 $ echo foo > initial-dir/initial-file
398 $ echo foo > initial-dir/initial-file
399 $ hg add initial-dir/initial-file
399 $ hg add initial-dir/initial-file
400 $ hg commit -m "initial commit"
400 $ hg commit -m "initial commit"
401
401
402 Move the file to a new directory, and in the same commit, change its content:
402 Move the file to a new directory, and in the same commit, change its content:
403 $ mkdir another-dir
403 $ mkdir another-dir
404 $ hg mv initial-dir/initial-file another-dir/
404 $ hg mv initial-dir/initial-file another-dir/
405 $ echo changed > another-dir/initial-file
405 $ echo changed > another-dir/initial-file
406 $ hg commit -m "moved and changed"
406 $ hg commit -m "moved and changed"
407
407
408 Rename the file:
408 Rename the file:
409 $ hg mv another-dir/initial-file another-dir/renamed-file
409 $ hg mv another-dir/initial-file another-dir/renamed-file
410 $ hg commit -m "renamed"
410 $ hg commit -m "renamed"
411
411
412 Now, let's try to fold the second commit into the first:
412 Now, let's try to fold the second commit into the first:
413 $ cat > editor.sh <<EOF
413 $ cat > editor.sh <<EOF
414 > #!/bin/sh
414 > #!/bin/sh
415 > cat > \$1 <<ENDOF
415 > cat > \$1 <<ENDOF
416 > pick b0f4233702ca 0 initial commit
416 > pick b0f4233702ca 0 initial commit
417 > fold 5e8704a8f2d2 1 moved and changed
417 > fold 5e8704a8f2d2 1 moved and changed
418 > pick 40e7299e8fa7 2 renamed
418 > pick 40e7299e8fa7 2 renamed
419 > ENDOF
419 > ENDOF
420 > EOF
420 > EOF
421
421
422 $ HGEDITOR="sh ./editor.sh" hg histedit 0
422 $ HGEDITOR="sh ./editor.sh" hg histedit 0
423 saved backup bundle to $TESTTMP/issue4251/.hg/strip-backup/b0f4233702ca-4cf5af69-histedit.hg
423 saved backup bundle to $TESTTMP/issue4251/.hg/strip-backup/b0f4233702ca-4cf5af69-histedit.hg
424
424
425 $ hg --config diff.git=yes export 0
425 $ hg --config diff.git=yes export 0
426 # HG changeset patch
426 # HG changeset patch
427 # User test
427 # User test
428 # Date 0 0
428 # Date 0 0
429 # Thu Jan 01 00:00:00 1970 +0000
429 # Thu Jan 01 00:00:00 1970 +0000
430 # Node ID fffadc26f8f85623ce60b028a3f1ccc3730f8530
430 # Node ID fffadc26f8f85623ce60b028a3f1ccc3730f8530
431 # Parent 0000000000000000000000000000000000000000
431 # Parent 0000000000000000000000000000000000000000
432 pick b0f4233702ca 0 initial commit
432 pick b0f4233702ca 0 initial commit
433 fold 5e8704a8f2d2 1 moved and changed
433 fold 5e8704a8f2d2 1 moved and changed
434 pick 40e7299e8fa7 2 renamed
434 pick 40e7299e8fa7 2 renamed
435
435
436 diff --git a/another-dir/initial-file b/another-dir/initial-file
436 diff --git a/another-dir/initial-file b/another-dir/initial-file
437 new file mode 100644
437 new file mode 100644
438 --- /dev/null
438 --- /dev/null
439 +++ b/another-dir/initial-file
439 +++ b/another-dir/initial-file
440 @@ -0,0 +1,1 @@
440 @@ -0,0 +1,1 @@
441 +changed
441 +changed
442
442
443 $ hg --config diff.git=yes export 1
443 $ hg --config diff.git=yes export 1
444 # HG changeset patch
444 # HG changeset patch
445 # User test
445 # User test
446 # Date 0 0
446 # Date 0 0
447 # Thu Jan 01 00:00:00 1970 +0000
447 # Thu Jan 01 00:00:00 1970 +0000
448 # Node ID 9b730d82b00af8a2766facebfa47cc124405a118
448 # Node ID 9b730d82b00af8a2766facebfa47cc124405a118
449 # Parent fffadc26f8f85623ce60b028a3f1ccc3730f8530
449 # Parent fffadc26f8f85623ce60b028a3f1ccc3730f8530
450 renamed
450 renamed
451
451
452 diff --git a/another-dir/initial-file b/another-dir/renamed-file
452 diff --git a/another-dir/initial-file b/another-dir/renamed-file
453 rename from another-dir/initial-file
453 rename from another-dir/initial-file
454 rename to another-dir/renamed-file
454 rename to another-dir/renamed-file
455
455
456 $ cd ..
456 $ cd ..
457
458 Test that branches are preserved and stays active
459 -------------------------------------------------
460
461 $ hg init repo-with-branch
462 $ cd repo-with-branch
463 $ echo a > a
464 $ hg add a
465 $ hg commit -m A
466 $ hg branch foo
467 marked working directory as branch foo
468 (branches are permanent and global, did you want a bookmark?)
469 $ echo a > b
470 $ hg add b
471 $ hg commit -m foo-B
472 $ echo a > c
473 $ hg add c
474 $ hg commit -m foo-C
475
476 $ hg branch
477 foo
478 $ echo "pick efefa76d6dc3 2 foo-C" >> cmd
479 $ echo "pick 7336e7550422 1 foo-B" >> cmd
480
481 $ HGEDITOR=cat hg histedit -r ".^" --commands cmd --quiet
482 $ hg log --template '{rev} {branch}\n'
483 2 foo
484 1 foo
485 0 default
486 $ hg branch
487 foo
488
489 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now