##// END OF EJS Templates
py3: use bytes() instead of str()...
Pulkit Goyal -
r36685:bcfc4e3b default
parent child Browse files
Show More
@@ -1,416 +1,416 b''
1 1 """automatically manage newlines in repository files
2 2
3 3 This extension allows you to manage the type of line endings (CRLF or
4 4 LF) that are used in the repository and in the local working
5 5 directory. That way you can get CRLF line endings on Windows and LF on
6 6 Unix/Mac, thereby letting everybody use their OS native line endings.
7 7
8 8 The extension reads its configuration from a versioned ``.hgeol``
9 9 configuration file found in the root of the working directory. The
10 10 ``.hgeol`` file use the same syntax as all other Mercurial
11 11 configuration files. It uses two sections, ``[patterns]`` and
12 12 ``[repository]``.
13 13
14 14 The ``[patterns]`` section specifies how line endings should be
15 15 converted between the working directory and the repository. The format is
16 16 specified by a file pattern. The first match is used, so put more
17 17 specific patterns first. The available line endings are ``LF``,
18 18 ``CRLF``, and ``BIN``.
19 19
20 20 Files with the declared format of ``CRLF`` or ``LF`` are always
21 21 checked out and stored in the repository in that format and files
22 22 declared to be binary (``BIN``) are left unchanged. Additionally,
23 23 ``native`` is an alias for checking out in the platform's default line
24 24 ending: ``LF`` on Unix (including Mac OS X) and ``CRLF`` on
25 25 Windows. Note that ``BIN`` (do nothing to line endings) is Mercurial's
26 26 default behavior; it is only needed if you need to override a later,
27 27 more general pattern.
28 28
29 29 The optional ``[repository]`` section specifies the line endings to
30 30 use for files stored in the repository. It has a single setting,
31 31 ``native``, which determines the storage line endings for files
32 32 declared as ``native`` in the ``[patterns]`` section. It can be set to
33 33 ``LF`` or ``CRLF``. The default is ``LF``. For example, this means
34 34 that on Windows, files configured as ``native`` (``CRLF`` by default)
35 35 will be converted to ``LF`` when stored in the repository. Files
36 36 declared as ``LF``, ``CRLF``, or ``BIN`` in the ``[patterns]`` section
37 37 are always stored as-is in the repository.
38 38
39 39 Example versioned ``.hgeol`` file::
40 40
41 41 [patterns]
42 42 **.py = native
43 43 **.vcproj = CRLF
44 44 **.txt = native
45 45 Makefile = LF
46 46 **.jpg = BIN
47 47
48 48 [repository]
49 49 native = LF
50 50
51 51 .. note::
52 52
53 53 The rules will first apply when files are touched in the working
54 54 directory, e.g. by updating to null and back to tip to touch all files.
55 55
56 56 The extension uses an optional ``[eol]`` section read from both the
57 57 normal Mercurial configuration files and the ``.hgeol`` file, with the
58 58 latter overriding the former. You can use that section to control the
59 59 overall behavior. There are three settings:
60 60
61 61 - ``eol.native`` (default ``os.linesep``) can be set to ``LF`` or
62 62 ``CRLF`` to override the default interpretation of ``native`` for
63 63 checkout. This can be used with :hg:`archive` on Unix, say, to
64 64 generate an archive where files have line endings for Windows.
65 65
66 66 - ``eol.only-consistent`` (default True) can be set to False to make
67 67 the extension convert files with inconsistent EOLs. Inconsistent
68 68 means that there is both ``CRLF`` and ``LF`` present in the file.
69 69 Such files are normally not touched under the assumption that they
70 70 have mixed EOLs on purpose.
71 71
72 72 - ``eol.fix-trailing-newline`` (default False) can be set to True to
73 73 ensure that converted files end with a EOL character (either ``\\n``
74 74 or ``\\r\\n`` as per the configured patterns).
75 75
76 76 The extension provides ``cleverencode:`` and ``cleverdecode:`` filters
77 77 like the deprecated win32text extension does. This means that you can
78 78 disable win32text and enable eol and your filters will still work. You
79 79 only need to these filters until you have prepared a ``.hgeol`` file.
80 80
81 81 The ``win32text.forbid*`` hooks provided by the win32text extension
82 82 have been unified into a single hook named ``eol.checkheadshook``. The
83 83 hook will lookup the expected line endings from the ``.hgeol`` file,
84 84 which means you must migrate to a ``.hgeol`` file first before using
85 85 the hook. ``eol.checkheadshook`` only checks heads, intermediate
86 86 invalid revisions will be pushed. To forbid them completely, use the
87 87 ``eol.checkallhook`` hook. These hooks are best used as
88 88 ``pretxnchangegroup`` hooks.
89 89
90 90 See :hg:`help patterns` for more information about the glob patterns
91 91 used.
92 92 """
93 93
94 94 from __future__ import absolute_import
95 95
96 96 import os
97 97 import re
98 98 from mercurial.i18n import _
99 99 from mercurial import (
100 100 config,
101 101 error as errormod,
102 102 extensions,
103 103 match,
104 104 pycompat,
105 105 registrar,
106 106 util,
107 107 )
108 108
109 109 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
110 110 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
111 111 # be specifying the version(s) of Mercurial they are tested with, or
112 112 # leave the attribute unspecified.
113 113 testedwith = 'ships-with-hg-core'
114 114
115 115 configtable = {}
116 116 configitem = registrar.configitem(configtable)
117 117
118 118 configitem('eol', 'fix-trailing-newline',
119 119 default=False,
120 120 )
121 121 configitem('eol', 'native',
122 122 default=pycompat.oslinesep,
123 123 )
124 124 configitem('eol', 'only-consistent',
125 125 default=True,
126 126 )
127 127
128 128 # Matches a lone LF, i.e., one that is not part of CRLF.
129 129 singlelf = re.compile('(^|[^\r])\n')
130 130
131 131 def inconsistenteol(data):
132 132 return '\r\n' in data and singlelf.search(data)
133 133
134 134 def tolf(s, params, ui, **kwargs):
135 135 """Filter to convert to LF EOLs."""
136 136 if util.binary(s):
137 137 return s
138 138 if ui.configbool('eol', 'only-consistent') and inconsistenteol(s):
139 139 return s
140 140 if (ui.configbool('eol', 'fix-trailing-newline')
141 141 and s and s[-1] != '\n'):
142 142 s = s + '\n'
143 143 return util.tolf(s)
144 144
145 145 def tocrlf(s, params, ui, **kwargs):
146 146 """Filter to convert to CRLF EOLs."""
147 147 if util.binary(s):
148 148 return s
149 149 if ui.configbool('eol', 'only-consistent') and inconsistenteol(s):
150 150 return s
151 151 if (ui.configbool('eol', 'fix-trailing-newline')
152 152 and s and s[-1] != '\n'):
153 153 s = s + '\n'
154 154 return util.tocrlf(s)
155 155
156 156 def isbinary(s, params):
157 157 """Filter to do nothing with the file."""
158 158 return s
159 159
160 160 filters = {
161 161 'to-lf': tolf,
162 162 'to-crlf': tocrlf,
163 163 'is-binary': isbinary,
164 164 # The following provide backwards compatibility with win32text
165 165 'cleverencode:': tolf,
166 166 'cleverdecode:': tocrlf
167 167 }
168 168
169 169 class eolfile(object):
170 170 def __init__(self, ui, root, data):
171 171 self._decode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
172 172 self._encode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
173 173
174 174 self.cfg = config.config()
175 175 # Our files should not be touched. The pattern must be
176 176 # inserted first override a '** = native' pattern.
177 177 self.cfg.set('patterns', '.hg*', 'BIN', 'eol')
178 178 # We can then parse the user's patterns.
179 179 self.cfg.parse('.hgeol', data)
180 180
181 181 isrepolf = self.cfg.get('repository', 'native') != 'CRLF'
182 182 self._encode['NATIVE'] = isrepolf and 'to-lf' or 'to-crlf'
183 183 iswdlf = ui.config('eol', 'native') in ('LF', '\n')
184 184 self._decode['NATIVE'] = iswdlf and 'to-lf' or 'to-crlf'
185 185
186 186 include = []
187 187 exclude = []
188 188 self.patterns = []
189 189 for pattern, style in self.cfg.items('patterns'):
190 190 key = style.upper()
191 191 if key == 'BIN':
192 192 exclude.append(pattern)
193 193 else:
194 194 include.append(pattern)
195 195 m = match.match(root, '', [pattern])
196 196 self.patterns.append((pattern, key, m))
197 197 # This will match the files for which we need to care
198 198 # about inconsistent newlines.
199 199 self.match = match.match(root, '', [], include, exclude)
200 200
201 201 def copytoui(self, ui):
202 202 for pattern, key, m in self.patterns:
203 203 try:
204 204 ui.setconfig('decode', pattern, self._decode[key], 'eol')
205 205 ui.setconfig('encode', pattern, self._encode[key], 'eol')
206 206 except KeyError:
207 207 ui.warn(_("ignoring unknown EOL style '%s' from %s\n")
208 208 % (key, self.cfg.source('patterns', pattern)))
209 209 # eol.only-consistent can be specified in ~/.hgrc or .hgeol
210 210 for k, v in self.cfg.items('eol'):
211 211 ui.setconfig('eol', k, v, 'eol')
212 212
213 213 def checkrev(self, repo, ctx, files):
214 214 failed = []
215 215 for f in (files or ctx.files()):
216 216 if f not in ctx:
217 217 continue
218 218 for pattern, key, m in self.patterns:
219 219 if not m(f):
220 220 continue
221 221 target = self._encode[key]
222 222 data = ctx[f].data()
223 223 if (target == "to-lf" and "\r\n" in data
224 224 or target == "to-crlf" and singlelf.search(data)):
225 failed.append((f, target, str(ctx)))
225 failed.append((f, target, bytes(ctx)))
226 226 break
227 227 return failed
228 228
229 229 def parseeol(ui, repo, nodes):
230 230 try:
231 231 for node in nodes:
232 232 try:
233 233 if node is None:
234 234 # Cannot use workingctx.data() since it would load
235 235 # and cache the filters before we configure them.
236 236 data = repo.wvfs('.hgeol').read()
237 237 else:
238 238 data = repo[node]['.hgeol'].data()
239 239 return eolfile(ui, repo.root, data)
240 240 except (IOError, LookupError):
241 241 pass
242 242 except errormod.ParseError as inst:
243 243 ui.warn(_("warning: ignoring .hgeol file due to parse error "
244 244 "at %s: %s\n") % (inst.args[1], inst.args[0]))
245 245 return None
246 246
247 247 def ensureenabled(ui):
248 248 """make sure the extension is enabled when used as hook
249 249
250 250 When eol is used through hooks, the extension is never formally loaded and
251 251 enabled. This has some side effect, for example the config declaration is
252 252 never loaded. This function ensure the extension is enabled when running
253 253 hooks.
254 254 """
255 255 if 'eol' in ui._knownconfig:
256 256 return
257 257 ui.setconfig('extensions', 'eol', '', source='internal')
258 258 extensions.loadall(ui, ['eol'])
259 259
260 260 def _checkhook(ui, repo, node, headsonly):
261 261 # Get revisions to check and touched files at the same time
262 262 ensureenabled(ui)
263 263 files = set()
264 264 revs = set()
265 265 for rev in xrange(repo[node].rev(), len(repo)):
266 266 revs.add(rev)
267 267 if headsonly:
268 268 ctx = repo[rev]
269 269 files.update(ctx.files())
270 270 for pctx in ctx.parents():
271 271 revs.discard(pctx.rev())
272 272 failed = []
273 273 for rev in revs:
274 274 ctx = repo[rev]
275 275 eol = parseeol(ui, repo, [ctx.node()])
276 276 if eol:
277 277 failed.extend(eol.checkrev(repo, ctx, files))
278 278
279 279 if failed:
280 280 eols = {'to-lf': 'CRLF', 'to-crlf': 'LF'}
281 281 msgs = []
282 282 for f, target, node in sorted(failed):
283 283 msgs.append(_(" %s in %s should not have %s line endings") %
284 284 (f, node, eols[target]))
285 285 raise errormod.Abort(_("end-of-line check failed:\n") + "\n".join(msgs))
286 286
287 287 def checkallhook(ui, repo, node, hooktype, **kwargs):
288 288 """verify that files have expected EOLs"""
289 289 _checkhook(ui, repo, node, False)
290 290
291 291 def checkheadshook(ui, repo, node, hooktype, **kwargs):
292 292 """verify that files have expected EOLs"""
293 293 _checkhook(ui, repo, node, True)
294 294
295 295 # "checkheadshook" used to be called "hook"
296 296 hook = checkheadshook
297 297
298 298 def preupdate(ui, repo, hooktype, parent1, parent2):
299 299 repo.loadeol([parent1])
300 300 return False
301 301
302 302 def uisetup(ui):
303 303 ui.setconfig('hooks', 'preupdate.eol', preupdate, 'eol')
304 304
305 305 def extsetup(ui):
306 306 try:
307 307 extensions.find('win32text')
308 308 ui.warn(_("the eol extension is incompatible with the "
309 309 "win32text extension\n"))
310 310 except KeyError:
311 311 pass
312 312
313 313
314 314 def reposetup(ui, repo):
315 315 uisetup(repo.ui)
316 316
317 317 if not repo.local():
318 318 return
319 319 for name, fn in filters.iteritems():
320 320 repo.adddatafilter(name, fn)
321 321
322 322 ui.setconfig('patch', 'eol', 'auto', 'eol')
323 323
324 324 class eolrepo(repo.__class__):
325 325
326 326 def loadeol(self, nodes):
327 327 eol = parseeol(self.ui, self, nodes)
328 328 if eol is None:
329 329 return None
330 330 eol.copytoui(self.ui)
331 331 return eol.match
332 332
333 333 def _hgcleardirstate(self):
334 334 self._eolmatch = self.loadeol([None, 'tip'])
335 335 if not self._eolmatch:
336 336 self._eolmatch = util.never
337 337 return
338 338
339 339 oldeol = None
340 340 try:
341 341 cachemtime = os.path.getmtime(self.vfs.join("eol.cache"))
342 342 except OSError:
343 343 cachemtime = 0
344 344 else:
345 345 olddata = self.vfs.read("eol.cache")
346 346 if olddata:
347 347 oldeol = eolfile(self.ui, self.root, olddata)
348 348
349 349 try:
350 350 eolmtime = os.path.getmtime(self.wjoin(".hgeol"))
351 351 except OSError:
352 352 eolmtime = 0
353 353
354 354 if eolmtime > cachemtime:
355 355 self.ui.debug("eol: detected change in .hgeol\n")
356 356
357 357 hgeoldata = self.wvfs.read('.hgeol')
358 358 neweol = eolfile(self.ui, self.root, hgeoldata)
359 359
360 360 wlock = None
361 361 try:
362 362 wlock = self.wlock()
363 363 for f in self.dirstate:
364 364 if self.dirstate[f] != 'n':
365 365 continue
366 366 if oldeol is not None:
367 367 if not oldeol.match(f) and not neweol.match(f):
368 368 continue
369 369 oldkey = None
370 370 for pattern, key, m in oldeol.patterns:
371 371 if m(f):
372 372 oldkey = key
373 373 break
374 374 newkey = None
375 375 for pattern, key, m in neweol.patterns:
376 376 if m(f):
377 377 newkey = key
378 378 break
379 379 if oldkey == newkey:
380 380 continue
381 381 # all normal files need to be looked at again since
382 382 # the new .hgeol file specify a different filter
383 383 self.dirstate.normallookup(f)
384 384 # Write the cache to update mtime and cache .hgeol
385 385 with self.vfs("eol.cache", "w") as f:
386 386 f.write(hgeoldata)
387 387 except errormod.LockUnavailable:
388 388 # If we cannot lock the repository and clear the
389 389 # dirstate, then a commit might not see all files
390 390 # as modified. But if we cannot lock the
391 391 # repository, then we can also not make a commit,
392 392 # so ignore the error.
393 393 pass
394 394 finally:
395 395 if wlock is not None:
396 396 wlock.release()
397 397
398 398 def commitctx(self, ctx, error=False):
399 399 for f in sorted(ctx.added() + ctx.modified()):
400 400 if not self._eolmatch(f):
401 401 continue
402 402 fctx = ctx[f]
403 403 if fctx is None:
404 404 continue
405 405 data = fctx.data()
406 406 if util.binary(data):
407 407 # We should not abort here, since the user should
408 408 # be able to say "** = native" to automatically
409 409 # have all non-binary files taken care of.
410 410 continue
411 411 if inconsistenteol(data):
412 412 raise errormod.Abort(_("inconsistent newline style "
413 413 "in %s\n") % f)
414 414 return super(eolrepo, self).commitctx(ctx, error)
415 415 repo.__class__ = eolrepo
416 416 repo._hgcleardirstate()
@@ -1,520 +1,520 b''
1 1 # journal.py
2 2 #
3 3 # Copyright 2014-2016 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 """track previous positions of bookmarks (EXPERIMENTAL)
8 8
9 9 This extension adds a new command: `hg journal`, which shows you where
10 10 bookmarks were previously located.
11 11
12 12 """
13 13
14 14 from __future__ import absolute_import
15 15
16 16 import collections
17 17 import errno
18 18 import os
19 19 import weakref
20 20
21 21 from mercurial.i18n import _
22 22
23 23 from mercurial import (
24 24 bookmarks,
25 25 cmdutil,
26 26 dispatch,
27 27 encoding,
28 28 error,
29 29 extensions,
30 30 hg,
31 31 localrepo,
32 32 lock,
33 33 logcmdutil,
34 34 node,
35 35 pycompat,
36 36 registrar,
37 37 util,
38 38 )
39 39 from mercurial.utils import dateutil
40 40
41 41 cmdtable = {}
42 42 command = registrar.command(cmdtable)
43 43
44 44 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
45 45 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
46 46 # be specifying the version(s) of Mercurial they are tested with, or
47 47 # leave the attribute unspecified.
48 48 testedwith = 'ships-with-hg-core'
49 49
50 50 # storage format version; increment when the format changes
51 51 storageversion = 0
52 52
53 53 # namespaces
54 54 bookmarktype = 'bookmark'
55 55 wdirparenttype = 'wdirparent'
56 56 # In a shared repository, what shared feature name is used
57 57 # to indicate this namespace is shared with the source?
58 58 sharednamespaces = {
59 59 bookmarktype: hg.sharedbookmarks,
60 60 }
61 61
62 62 # Journal recording, register hooks and storage object
63 63 def extsetup(ui):
64 64 extensions.wrapfunction(dispatch, 'runcommand', runcommand)
65 65 extensions.wrapfunction(bookmarks.bmstore, '_write', recordbookmarks)
66 66 extensions.wrapfilecache(
67 67 localrepo.localrepository, 'dirstate', wrapdirstate)
68 68 extensions.wrapfunction(hg, 'postshare', wrappostshare)
69 69 extensions.wrapfunction(hg, 'copystore', unsharejournal)
70 70
71 71 def reposetup(ui, repo):
72 72 if repo.local():
73 73 repo.journal = journalstorage(repo)
74 74 repo._wlockfreeprefix.add('namejournal')
75 75
76 76 dirstate, cached = localrepo.isfilecached(repo, 'dirstate')
77 77 if cached:
78 78 # already instantiated dirstate isn't yet marked as
79 79 # "journal"-ing, even though repo.dirstate() was already
80 80 # wrapped by own wrapdirstate()
81 81 _setupdirstate(repo, dirstate)
82 82
83 83 def runcommand(orig, lui, repo, cmd, fullargs, *args):
84 84 """Track the command line options for recording in the journal"""
85 85 journalstorage.recordcommand(*fullargs)
86 86 return orig(lui, repo, cmd, fullargs, *args)
87 87
88 88 def _setupdirstate(repo, dirstate):
89 89 dirstate.journalstorage = repo.journal
90 90 dirstate.addparentchangecallback('journal', recorddirstateparents)
91 91
92 92 # hooks to record dirstate changes
93 93 def wrapdirstate(orig, repo):
94 94 """Make journal storage available to the dirstate object"""
95 95 dirstate = orig(repo)
96 96 if util.safehasattr(repo, 'journal'):
97 97 _setupdirstate(repo, dirstate)
98 98 return dirstate
99 99
100 100 def recorddirstateparents(dirstate, old, new):
101 101 """Records all dirstate parent changes in the journal."""
102 102 old = list(old)
103 103 new = list(new)
104 104 if util.safehasattr(dirstate, 'journalstorage'):
105 105 # only record two hashes if there was a merge
106 106 oldhashes = old[:1] if old[1] == node.nullid else old
107 107 newhashes = new[:1] if new[1] == node.nullid else new
108 108 dirstate.journalstorage.record(
109 109 wdirparenttype, '.', oldhashes, newhashes)
110 110
111 111 # hooks to record bookmark changes (both local and remote)
112 112 def recordbookmarks(orig, store, fp):
113 113 """Records all bookmark changes in the journal."""
114 114 repo = store._repo
115 115 if util.safehasattr(repo, 'journal'):
116 116 oldmarks = bookmarks.bmstore(repo)
117 117 for mark, value in store.iteritems():
118 118 oldvalue = oldmarks.get(mark, node.nullid)
119 119 if value != oldvalue:
120 120 repo.journal.record(bookmarktype, mark, oldvalue, value)
121 121 return orig(store, fp)
122 122
123 123 # shared repository support
124 124 def _readsharedfeatures(repo):
125 125 """A set of shared features for this repository"""
126 126 try:
127 127 return set(repo.vfs.read('shared').splitlines())
128 128 except IOError as inst:
129 129 if inst.errno != errno.ENOENT:
130 130 raise
131 131 return set()
132 132
133 133 def _mergeentriesiter(*iterables, **kwargs):
134 134 """Given a set of sorted iterables, yield the next entry in merged order
135 135
136 136 Note that by default entries go from most recent to oldest.
137 137 """
138 138 order = kwargs.pop(r'order', max)
139 139 iterables = [iter(it) for it in iterables]
140 140 # this tracks still active iterables; iterables are deleted as they are
141 141 # exhausted, which is why this is a dictionary and why each entry also
142 142 # stores the key. Entries are mutable so we can store the next value each
143 143 # time.
144 144 iterable_map = {}
145 145 for key, it in enumerate(iterables):
146 146 try:
147 147 iterable_map[key] = [next(it), key, it]
148 148 except StopIteration:
149 149 # empty entry, can be ignored
150 150 pass
151 151
152 152 while iterable_map:
153 153 value, key, it = order(iterable_map.itervalues())
154 154 yield value
155 155 try:
156 156 iterable_map[key][0] = next(it)
157 157 except StopIteration:
158 158 # this iterable is empty, remove it from consideration
159 159 del iterable_map[key]
160 160
161 161 def wrappostshare(orig, sourcerepo, destrepo, **kwargs):
162 162 """Mark this shared working copy as sharing journal information"""
163 163 with destrepo.wlock():
164 164 orig(sourcerepo, destrepo, **kwargs)
165 165 with destrepo.vfs('shared', 'a') as fp:
166 166 fp.write('journal\n')
167 167
168 168 def unsharejournal(orig, ui, repo, repopath):
169 169 """Copy shared journal entries into this repo when unsharing"""
170 170 if (repo.path == repopath and repo.shared() and
171 171 util.safehasattr(repo, 'journal')):
172 172 sharedrepo = hg.sharedreposource(repo)
173 173 sharedfeatures = _readsharedfeatures(repo)
174 174 if sharedrepo and sharedfeatures > {'journal'}:
175 175 # there is a shared repository and there are shared journal entries
176 176 # to copy. move shared date over from source to destination but
177 177 # move the local file first
178 178 if repo.vfs.exists('namejournal'):
179 179 journalpath = repo.vfs.join('namejournal')
180 180 util.rename(journalpath, journalpath + '.bak')
181 181 storage = repo.journal
182 182 local = storage._open(
183 183 repo.vfs, filename='namejournal.bak', _newestfirst=False)
184 184 shared = (
185 185 e for e in storage._open(sharedrepo.vfs, _newestfirst=False)
186 186 if sharednamespaces.get(e.namespace) in sharedfeatures)
187 187 for entry in _mergeentriesiter(local, shared, order=min):
188 188 storage._write(repo.vfs, entry)
189 189
190 190 return orig(ui, repo, repopath)
191 191
192 192 class journalentry(collections.namedtuple(
193 193 u'journalentry',
194 194 u'timestamp user command namespace name oldhashes newhashes')):
195 195 """Individual journal entry
196 196
197 197 * timestamp: a mercurial (time, timezone) tuple
198 198 * user: the username that ran the command
199 199 * namespace: the entry namespace, an opaque string
200 200 * name: the name of the changed item, opaque string with meaning in the
201 201 namespace
202 202 * command: the hg command that triggered this record
203 203 * oldhashes: a tuple of one or more binary hashes for the old location
204 204 * newhashes: a tuple of one or more binary hashes for the new location
205 205
206 206 Handles serialisation from and to the storage format. Fields are
207 207 separated by newlines, hashes are written out in hex separated by commas,
208 208 timestamp and timezone are separated by a space.
209 209
210 210 """
211 211 @classmethod
212 212 def fromstorage(cls, line):
213 213 (time, user, command, namespace, name,
214 214 oldhashes, newhashes) = line.split('\n')
215 215 timestamp, tz = time.split()
216 216 timestamp, tz = float(timestamp), int(tz)
217 217 oldhashes = tuple(node.bin(hash) for hash in oldhashes.split(','))
218 218 newhashes = tuple(node.bin(hash) for hash in newhashes.split(','))
219 219 return cls(
220 220 (timestamp, tz), user, command, namespace, name,
221 221 oldhashes, newhashes)
222 222
223 223 def __bytes__(self):
224 224 """bytes representation for storage"""
225 225 time = ' '.join(map(str, self.timestamp))
226 226 oldhashes = ','.join([node.hex(hash) for hash in self.oldhashes])
227 227 newhashes = ','.join([node.hex(hash) for hash in self.newhashes])
228 228 return '\n'.join((
229 229 time, self.user, self.command, self.namespace, self.name,
230 230 oldhashes, newhashes))
231 231
232 232 __str__ = encoding.strmethod(__bytes__)
233 233
234 234 class journalstorage(object):
235 235 """Storage for journal entries
236 236
237 237 Entries are divided over two files; one with entries that pertain to the
238 238 local working copy *only*, and one with entries that are shared across
239 239 multiple working copies when shared using the share extension.
240 240
241 241 Entries are stored with NUL bytes as separators. See the journalentry
242 242 class for the per-entry structure.
243 243
244 244 The file format starts with an integer version, delimited by a NUL.
245 245
246 246 This storage uses a dedicated lock; this makes it easier to avoid issues
247 247 with adding entries that added when the regular wlock is unlocked (e.g.
248 248 the dirstate).
249 249
250 250 """
251 251 _currentcommand = ()
252 252 _lockref = None
253 253
254 254 def __init__(self, repo):
255 255 self.user = util.getuser()
256 256 self.ui = repo.ui
257 257 self.vfs = repo.vfs
258 258
259 259 # is this working copy using a shared storage?
260 260 self.sharedfeatures = self.sharedvfs = None
261 261 if repo.shared():
262 262 features = _readsharedfeatures(repo)
263 263 sharedrepo = hg.sharedreposource(repo)
264 264 if sharedrepo is not None and 'journal' in features:
265 265 self.sharedvfs = sharedrepo.vfs
266 266 self.sharedfeatures = features
267 267
268 268 # track the current command for recording in journal entries
269 269 @property
270 270 def command(self):
271 271 commandstr = ' '.join(
272 272 map(util.shellquote, journalstorage._currentcommand))
273 273 if '\n' in commandstr:
274 274 # truncate multi-line commands
275 275 commandstr = commandstr.partition('\n')[0] + ' ...'
276 276 return commandstr
277 277
278 278 @classmethod
279 279 def recordcommand(cls, *fullargs):
280 280 """Set the current hg arguments, stored with recorded entries"""
281 281 # Set the current command on the class because we may have started
282 282 # with a non-local repo (cloning for example).
283 283 cls._currentcommand = fullargs
284 284
285 285 def _currentlock(self, lockref):
286 286 """Returns the lock if it's held, or None if it's not.
287 287
288 288 (This is copied from the localrepo class)
289 289 """
290 290 if lockref is None:
291 291 return None
292 292 l = lockref()
293 293 if l is None or not l.held:
294 294 return None
295 295 return l
296 296
297 297 def jlock(self, vfs):
298 298 """Create a lock for the journal file"""
299 299 if self._currentlock(self._lockref) is not None:
300 300 raise error.Abort(_('journal lock does not support nesting'))
301 301 desc = _('journal of %s') % vfs.base
302 302 try:
303 303 l = lock.lock(vfs, 'namejournal.lock', 0, desc=desc)
304 304 except error.LockHeld as inst:
305 305 self.ui.warn(
306 306 _("waiting for lock on %s held by %r\n") % (desc, inst.locker))
307 307 # default to 600 seconds timeout
308 308 l = lock.lock(
309 309 vfs, 'namejournal.lock',
310 310 self.ui.configint("ui", "timeout"), desc=desc)
311 311 self.ui.warn(_("got lock after %s seconds\n") % l.delay)
312 312 self._lockref = weakref.ref(l)
313 313 return l
314 314
315 315 def record(self, namespace, name, oldhashes, newhashes):
316 316 """Record a new journal entry
317 317
318 318 * namespace: an opaque string; this can be used to filter on the type
319 319 of recorded entries.
320 320 * name: the name defining this entry; for bookmarks, this is the
321 321 bookmark name. Can be filtered on when retrieving entries.
322 322 * oldhashes and newhashes: each a single binary hash, or a list of
323 323 binary hashes. These represent the old and new position of the named
324 324 item.
325 325
326 326 """
327 327 if not isinstance(oldhashes, list):
328 328 oldhashes = [oldhashes]
329 329 if not isinstance(newhashes, list):
330 330 newhashes = [newhashes]
331 331
332 332 entry = journalentry(
333 333 dateutil.makedate(), self.user, self.command, namespace, name,
334 334 oldhashes, newhashes)
335 335
336 336 vfs = self.vfs
337 337 if self.sharedvfs is not None:
338 338 # write to the shared repository if this feature is being
339 339 # shared between working copies.
340 340 if sharednamespaces.get(namespace) in self.sharedfeatures:
341 341 vfs = self.sharedvfs
342 342
343 343 self._write(vfs, entry)
344 344
345 345 def _write(self, vfs, entry):
346 346 with self.jlock(vfs):
347 347 version = None
348 348 # open file in amend mode to ensure it is created if missing
349 349 with vfs('namejournal', mode='a+b') as f:
350 350 f.seek(0, os.SEEK_SET)
351 351 # Read just enough bytes to get a version number (up to 2
352 352 # digits plus separator)
353 353 version = f.read(3).partition('\0')[0]
354 354 if version and version != str(storageversion):
355 355 # different version of the storage. Exit early (and not
356 356 # write anything) if this is not a version we can handle or
357 357 # the file is corrupt. In future, perhaps rotate the file
358 358 # instead?
359 359 self.ui.warn(
360 360 _("unsupported journal file version '%s'\n") % version)
361 361 return
362 362 if not version:
363 363 # empty file, write version first
364 364 f.write(str(storageversion) + '\0')
365 365 f.seek(0, os.SEEK_END)
366 f.write(str(entry) + '\0')
366 f.write(bytes(entry) + '\0')
367 367
368 368 def filtered(self, namespace=None, name=None):
369 369 """Yield all journal entries with the given namespace or name
370 370
371 371 Both the namespace and the name are optional; if neither is given all
372 372 entries in the journal are produced.
373 373
374 374 Matching supports regular expressions by using the `re:` prefix
375 375 (use `literal:` to match names or namespaces that start with `re:`)
376 376
377 377 """
378 378 if namespace is not None:
379 379 namespace = util.stringmatcher(namespace)[-1]
380 380 if name is not None:
381 381 name = util.stringmatcher(name)[-1]
382 382 for entry in self:
383 383 if namespace is not None and not namespace(entry.namespace):
384 384 continue
385 385 if name is not None and not name(entry.name):
386 386 continue
387 387 yield entry
388 388
389 389 def __iter__(self):
390 390 """Iterate over the storage
391 391
392 392 Yields journalentry instances for each contained journal record.
393 393
394 394 """
395 395 local = self._open(self.vfs)
396 396
397 397 if self.sharedvfs is None:
398 398 return local
399 399
400 400 # iterate over both local and shared entries, but only those
401 401 # shared entries that are among the currently shared features
402 402 shared = (
403 403 e for e in self._open(self.sharedvfs)
404 404 if sharednamespaces.get(e.namespace) in self.sharedfeatures)
405 405 return _mergeentriesiter(local, shared)
406 406
407 407 def _open(self, vfs, filename='namejournal', _newestfirst=True):
408 408 if not vfs.exists(filename):
409 409 return
410 410
411 411 with vfs(filename) as f:
412 412 raw = f.read()
413 413
414 414 lines = raw.split('\0')
415 415 version = lines and lines[0]
416 416 if version != str(storageversion):
417 417 version = version or _('not available')
418 418 raise error.Abort(_("unknown journal file version '%s'") % version)
419 419
420 420 # Skip the first line, it's a version number. Normally we iterate over
421 421 # these in reverse order to list newest first; only when copying across
422 422 # a shared storage do we forgo reversing.
423 423 lines = lines[1:]
424 424 if _newestfirst:
425 425 lines = reversed(lines)
426 426 for line in lines:
427 427 if not line:
428 428 continue
429 429 yield journalentry.fromstorage(line)
430 430
431 431 # journal reading
432 432 # log options that don't make sense for journal
433 433 _ignoreopts = ('no-merges', 'graph')
434 434 @command(
435 435 'journal', [
436 436 ('', 'all', None, 'show history for all names'),
437 437 ('c', 'commits', None, 'show commit metadata'),
438 438 ] + [opt for opt in cmdutil.logopts if opt[1] not in _ignoreopts],
439 439 '[OPTION]... [BOOKMARKNAME]')
440 440 def journal(ui, repo, *args, **opts):
441 441 """show the previous position of bookmarks and the working copy
442 442
443 443 The journal is used to see the previous commits that bookmarks and the
444 444 working copy pointed to. By default the previous locations for the working
445 445 copy. Passing a bookmark name will show all the previous positions of
446 446 that bookmark. Use the --all switch to show previous locations for all
447 447 bookmarks and the working copy; each line will then include the bookmark
448 448 name, or '.' for the working copy, as well.
449 449
450 450 If `name` starts with `re:`, the remainder of the name is treated as
451 451 a regular expression. To match a name that actually starts with `re:`,
452 452 use the prefix `literal:`.
453 453
454 454 By default hg journal only shows the commit hash and the command that was
455 455 running at that time. -v/--verbose will show the prior hash, the user, and
456 456 the time at which it happened.
457 457
458 458 Use -c/--commits to output log information on each commit hash; at this
459 459 point you can use the usual `--patch`, `--git`, `--stat` and `--template`
460 460 switches to alter the log output for these.
461 461
462 462 `hg journal -T json` can be used to produce machine readable output.
463 463
464 464 """
465 465 opts = pycompat.byteskwargs(opts)
466 466 name = '.'
467 467 if opts.get('all'):
468 468 if args:
469 469 raise error.Abort(
470 470 _("You can't combine --all and filtering on a name"))
471 471 name = None
472 472 if args:
473 473 name = args[0]
474 474
475 475 fm = ui.formatter('journal', opts)
476 476
477 477 if opts.get("template") != "json":
478 478 if name is None:
479 479 displayname = _('the working copy and bookmarks')
480 480 else:
481 481 displayname = "'%s'" % name
482 482 ui.status(_("previous locations of %s:\n") % displayname)
483 483
484 484 limit = logcmdutil.getlimit(opts)
485 485 entry = None
486 486 ui.pager('journal')
487 487 for count, entry in enumerate(repo.journal.filtered(name=name)):
488 488 if count == limit:
489 489 break
490 490 newhashesstr = fm.formatlist(map(fm.hexfunc, entry.newhashes),
491 491 name='node', sep=',')
492 492 oldhashesstr = fm.formatlist(map(fm.hexfunc, entry.oldhashes),
493 493 name='node', sep=',')
494 494
495 495 fm.startitem()
496 496 fm.condwrite(ui.verbose, 'oldhashes', '%s -> ', oldhashesstr)
497 497 fm.write('newhashes', '%s', newhashesstr)
498 498 fm.condwrite(ui.verbose, 'user', ' %-8s', entry.user)
499 499 fm.condwrite(
500 500 opts.get('all') or name.startswith('re:'),
501 501 'name', ' %-8s', entry.name)
502 502
503 503 timestring = fm.formatdate(entry.timestamp, '%Y-%m-%d %H:%M %1%2')
504 504 fm.condwrite(ui.verbose, 'date', ' %s', timestring)
505 505 fm.write('command', ' %s\n', entry.command)
506 506
507 507 if opts.get("commits"):
508 508 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
509 509 for hash in entry.newhashes:
510 510 try:
511 511 ctx = repo[hash]
512 512 displayer.show(ctx)
513 513 except error.RepoLookupError as e:
514 514 fm.write('repolookuperror', "%s\n\n", pycompat.bytestr(e))
515 515 displayer.close()
516 516
517 517 fm.end()
518 518
519 519 if entry is None:
520 520 ui.status(_("no recorded locations\n"))
@@ -1,3656 +1,3656 b''
1 1 # mq.py - patch queues for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''manage a stack of patches
9 9
10 10 This extension lets you work with a stack of patches in a Mercurial
11 11 repository. It manages two stacks of patches - all known patches, and
12 12 applied patches (subset of known patches).
13 13
14 14 Known patches are represented as patch files in the .hg/patches
15 15 directory. Applied patches are both patch files and changesets.
16 16
17 17 Common tasks (use :hg:`help COMMAND` for more details)::
18 18
19 19 create new patch qnew
20 20 import existing patch qimport
21 21
22 22 print patch series qseries
23 23 print applied patches qapplied
24 24
25 25 add known patch to applied stack qpush
26 26 remove patch from applied stack qpop
27 27 refresh contents of top applied patch qrefresh
28 28
29 29 By default, mq will automatically use git patches when required to
30 30 avoid losing file mode changes, copy records, binary files or empty
31 31 files creations or deletions. This behavior can be configured with::
32 32
33 33 [mq]
34 34 git = auto/keep/yes/no
35 35
36 36 If set to 'keep', mq will obey the [diff] section configuration while
37 37 preserving existing git patches upon qrefresh. If set to 'yes' or
38 38 'no', mq will override the [diff] section and always generate git or
39 39 regular patches, possibly losing data in the second case.
40 40
41 41 It may be desirable for mq changesets to be kept in the secret phase (see
42 42 :hg:`help phases`), which can be enabled with the following setting::
43 43
44 44 [mq]
45 45 secret = True
46 46
47 47 You will by default be managing a patch queue named "patches". You can
48 48 create other, independent patch queues with the :hg:`qqueue` command.
49 49
50 50 If the working directory contains uncommitted files, qpush, qpop and
51 51 qgoto abort immediately. If -f/--force is used, the changes are
52 52 discarded. Setting::
53 53
54 54 [mq]
55 55 keepchanges = True
56 56
57 57 make them behave as if --keep-changes were passed, and non-conflicting
58 58 local changes will be tolerated and preserved. If incompatible options
59 59 such as -f/--force or --exact are passed, this setting is ignored.
60 60
61 61 This extension used to provide a strip command. This command now lives
62 62 in the strip extension.
63 63 '''
64 64
65 65 from __future__ import absolute_import, print_function
66 66
67 67 import errno
68 68 import os
69 69 import re
70 70 import shutil
71 71 from mercurial.i18n import _
72 72 from mercurial.node import (
73 73 bin,
74 74 hex,
75 75 nullid,
76 76 nullrev,
77 77 short,
78 78 )
79 79 from mercurial import (
80 80 cmdutil,
81 81 commands,
82 82 dirstateguard,
83 83 encoding,
84 84 error,
85 85 extensions,
86 86 hg,
87 87 localrepo,
88 88 lock as lockmod,
89 89 logcmdutil,
90 90 patch as patchmod,
91 91 phases,
92 92 pycompat,
93 93 registrar,
94 94 revsetlang,
95 95 scmutil,
96 96 smartset,
97 97 subrepoutil,
98 98 util,
99 99 vfs as vfsmod,
100 100 )
101 101 from mercurial.utils import dateutil
102 102
103 103 release = lockmod.release
104 104 seriesopts = [('s', 'summary', None, _('print first line of patch header'))]
105 105
106 106 cmdtable = {}
107 107 command = registrar.command(cmdtable)
108 108 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
109 109 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
110 110 # be specifying the version(s) of Mercurial they are tested with, or
111 111 # leave the attribute unspecified.
112 112 testedwith = 'ships-with-hg-core'
113 113
114 114 configtable = {}
115 115 configitem = registrar.configitem(configtable)
116 116
117 117 configitem('mq', 'git',
118 118 default='auto',
119 119 )
120 120 configitem('mq', 'keepchanges',
121 121 default=False,
122 122 )
123 123 configitem('mq', 'plain',
124 124 default=False,
125 125 )
126 126 configitem('mq', 'secret',
127 127 default=False,
128 128 )
129 129
130 130 # force load strip extension formerly included in mq and import some utility
131 131 try:
132 132 stripext = extensions.find('strip')
133 133 except KeyError:
134 134 # note: load is lazy so we could avoid the try-except,
135 135 # but I (marmoute) prefer this explicit code.
136 136 class dummyui(object):
137 137 def debug(self, msg):
138 138 pass
139 139 stripext = extensions.load(dummyui(), 'strip', '')
140 140
141 141 strip = stripext.strip
142 142 checksubstate = stripext.checksubstate
143 143 checklocalchanges = stripext.checklocalchanges
144 144
145 145
146 146 # Patch names looks like unix-file names.
147 147 # They must be joinable with queue directory and result in the patch path.
148 148 normname = util.normpath
149 149
150 150 class statusentry(object):
151 151 def __init__(self, node, name):
152 152 self.node, self.name = node, name
153 153
154 154 def __bytes__(self):
155 155 return hex(self.node) + ':' + self.name
156 156
157 157 __str__ = encoding.strmethod(__bytes__)
158 158 __repr__ = encoding.strmethod(__bytes__)
159 159
160 160 # The order of the headers in 'hg export' HG patches:
161 161 HGHEADERS = [
162 162 # '# HG changeset patch',
163 163 '# User ',
164 164 '# Date ',
165 165 '# ',
166 166 '# Branch ',
167 167 '# Node ID ',
168 168 '# Parent ', # can occur twice for merges - but that is not relevant for mq
169 169 ]
170 170 # The order of headers in plain 'mail style' patches:
171 171 PLAINHEADERS = {
172 172 'from': 0,
173 173 'date': 1,
174 174 'subject': 2,
175 175 }
176 176
177 177 def inserthgheader(lines, header, value):
178 178 """Assuming lines contains a HG patch header, add a header line with value.
179 179 >>> try: inserthgheader([], b'# Date ', b'z')
180 180 ... except ValueError as inst: print("oops")
181 181 oops
182 182 >>> inserthgheader([b'# HG changeset patch'], b'# Date ', b'z')
183 183 ['# HG changeset patch', '# Date z']
184 184 >>> inserthgheader([b'# HG changeset patch', b''], b'# Date ', b'z')
185 185 ['# HG changeset patch', '# Date z', '']
186 186 >>> inserthgheader([b'# HG changeset patch', b'# User y'], b'# Date ', b'z')
187 187 ['# HG changeset patch', '# User y', '# Date z']
188 188 >>> inserthgheader([b'# HG changeset patch', b'# Date x', b'# User y'],
189 189 ... b'# User ', b'z')
190 190 ['# HG changeset patch', '# Date x', '# User z']
191 191 >>> inserthgheader([b'# HG changeset patch', b'# Date y'], b'# Date ', b'z')
192 192 ['# HG changeset patch', '# Date z']
193 193 >>> inserthgheader([b'# HG changeset patch', b'', b'# Date y'],
194 194 ... b'# Date ', b'z')
195 195 ['# HG changeset patch', '# Date z', '', '# Date y']
196 196 >>> inserthgheader([b'# HG changeset patch', b'# Parent y'],
197 197 ... b'# Date ', b'z')
198 198 ['# HG changeset patch', '# Date z', '# Parent y']
199 199 """
200 200 start = lines.index('# HG changeset patch') + 1
201 201 newindex = HGHEADERS.index(header)
202 202 bestpos = len(lines)
203 203 for i in range(start, len(lines)):
204 204 line = lines[i]
205 205 if not line.startswith('# '):
206 206 bestpos = min(bestpos, i)
207 207 break
208 208 for lineindex, h in enumerate(HGHEADERS):
209 209 if line.startswith(h):
210 210 if lineindex == newindex:
211 211 lines[i] = header + value
212 212 return lines
213 213 if lineindex > newindex:
214 214 bestpos = min(bestpos, i)
215 215 break # next line
216 216 lines.insert(bestpos, header + value)
217 217 return lines
218 218
219 219 def insertplainheader(lines, header, value):
220 220 """For lines containing a plain patch header, add a header line with value.
221 221 >>> insertplainheader([], b'Date', b'z')
222 222 ['Date: z']
223 223 >>> insertplainheader([b''], b'Date', b'z')
224 224 ['Date: z', '']
225 225 >>> insertplainheader([b'x'], b'Date', b'z')
226 226 ['Date: z', '', 'x']
227 227 >>> insertplainheader([b'From: y', b'x'], b'Date', b'z')
228 228 ['From: y', 'Date: z', '', 'x']
229 229 >>> insertplainheader([b' date : x', b' from : y', b''], b'From', b'z')
230 230 [' date : x', 'From: z', '']
231 231 >>> insertplainheader([b'', b'Date: y'], b'Date', b'z')
232 232 ['Date: z', '', 'Date: y']
233 233 >>> insertplainheader([b'foo: bar', b'DATE: z', b'x'], b'From', b'y')
234 234 ['From: y', 'foo: bar', 'DATE: z', '', 'x']
235 235 """
236 236 newprio = PLAINHEADERS[header.lower()]
237 237 bestpos = len(lines)
238 238 for i, line in enumerate(lines):
239 239 if ':' in line:
240 240 lheader = line.split(':', 1)[0].strip().lower()
241 241 lprio = PLAINHEADERS.get(lheader, newprio + 1)
242 242 if lprio == newprio:
243 243 lines[i] = '%s: %s' % (header, value)
244 244 return lines
245 245 if lprio > newprio and i < bestpos:
246 246 bestpos = i
247 247 else:
248 248 if line:
249 249 lines.insert(i, '')
250 250 if i < bestpos:
251 251 bestpos = i
252 252 break
253 253 lines.insert(bestpos, '%s: %s' % (header, value))
254 254 return lines
255 255
256 256 class patchheader(object):
257 257 def __init__(self, pf, plainmode=False):
258 258 def eatdiff(lines):
259 259 while lines:
260 260 l = lines[-1]
261 261 if (l.startswith("diff -") or
262 262 l.startswith("Index:") or
263 263 l.startswith("===========")):
264 264 del lines[-1]
265 265 else:
266 266 break
267 267 def eatempty(lines):
268 268 while lines:
269 269 if not lines[-1].strip():
270 270 del lines[-1]
271 271 else:
272 272 break
273 273
274 274 message = []
275 275 comments = []
276 276 user = None
277 277 date = None
278 278 parent = None
279 279 format = None
280 280 subject = None
281 281 branch = None
282 282 nodeid = None
283 283 diffstart = 0
284 284
285 285 for line in open(pf, 'rb'):
286 286 line = line.rstrip()
287 287 if (line.startswith('diff --git')
288 288 or (diffstart and line.startswith('+++ '))):
289 289 diffstart = 2
290 290 break
291 291 diffstart = 0 # reset
292 292 if line.startswith("--- "):
293 293 diffstart = 1
294 294 continue
295 295 elif format == "hgpatch":
296 296 # parse values when importing the result of an hg export
297 297 if line.startswith("# User "):
298 298 user = line[7:]
299 299 elif line.startswith("# Date "):
300 300 date = line[7:]
301 301 elif line.startswith("# Parent "):
302 302 parent = line[9:].lstrip() # handle double trailing space
303 303 elif line.startswith("# Branch "):
304 304 branch = line[9:]
305 305 elif line.startswith("# Node ID "):
306 306 nodeid = line[10:]
307 307 elif not line.startswith("# ") and line:
308 308 message.append(line)
309 309 format = None
310 310 elif line == '# HG changeset patch':
311 311 message = []
312 312 format = "hgpatch"
313 313 elif (format != "tagdone" and (line.startswith("Subject: ") or
314 314 line.startswith("subject: "))):
315 315 subject = line[9:]
316 316 format = "tag"
317 317 elif (format != "tagdone" and (line.startswith("From: ") or
318 318 line.startswith("from: "))):
319 319 user = line[6:]
320 320 format = "tag"
321 321 elif (format != "tagdone" and (line.startswith("Date: ") or
322 322 line.startswith("date: "))):
323 323 date = line[6:]
324 324 format = "tag"
325 325 elif format == "tag" and line == "":
326 326 # when looking for tags (subject: from: etc) they
327 327 # end once you find a blank line in the source
328 328 format = "tagdone"
329 329 elif message or line:
330 330 message.append(line)
331 331 comments.append(line)
332 332
333 333 eatdiff(message)
334 334 eatdiff(comments)
335 335 # Remember the exact starting line of the patch diffs before consuming
336 336 # empty lines, for external use by TortoiseHg and others
337 337 self.diffstartline = len(comments)
338 338 eatempty(message)
339 339 eatempty(comments)
340 340
341 341 # make sure message isn't empty
342 342 if format and format.startswith("tag") and subject:
343 343 message.insert(0, subject)
344 344
345 345 self.message = message
346 346 self.comments = comments
347 347 self.user = user
348 348 self.date = date
349 349 self.parent = parent
350 350 # nodeid and branch are for external use by TortoiseHg and others
351 351 self.nodeid = nodeid
352 352 self.branch = branch
353 353 self.haspatch = diffstart > 1
354 354 self.plainmode = (plainmode or
355 355 '# HG changeset patch' not in self.comments and
356 356 any(c.startswith('Date: ') or
357 357 c.startswith('From: ')
358 358 for c in self.comments))
359 359
360 360 def setuser(self, user):
361 361 try:
362 362 inserthgheader(self.comments, '# User ', user)
363 363 except ValueError:
364 364 if self.plainmode:
365 365 insertplainheader(self.comments, 'From', user)
366 366 else:
367 367 tmp = ['# HG changeset patch', '# User ' + user]
368 368 self.comments = tmp + self.comments
369 369 self.user = user
370 370
371 371 def setdate(self, date):
372 372 try:
373 373 inserthgheader(self.comments, '# Date ', date)
374 374 except ValueError:
375 375 if self.plainmode:
376 376 insertplainheader(self.comments, 'Date', date)
377 377 else:
378 378 tmp = ['# HG changeset patch', '# Date ' + date]
379 379 self.comments = tmp + self.comments
380 380 self.date = date
381 381
382 382 def setparent(self, parent):
383 383 try:
384 384 inserthgheader(self.comments, '# Parent ', parent)
385 385 except ValueError:
386 386 if not self.plainmode:
387 387 tmp = ['# HG changeset patch', '# Parent ' + parent]
388 388 self.comments = tmp + self.comments
389 389 self.parent = parent
390 390
391 391 def setmessage(self, message):
392 392 if self.comments:
393 393 self._delmsg()
394 394 self.message = [message]
395 395 if message:
396 396 if self.plainmode and self.comments and self.comments[-1]:
397 397 self.comments.append('')
398 398 self.comments.append(message)
399 399
400 400 def __bytes__(self):
401 401 s = '\n'.join(self.comments).rstrip()
402 402 if not s:
403 403 return ''
404 404 return s + '\n\n'
405 405
406 406 __str__ = encoding.strmethod(__bytes__)
407 407
408 408 def _delmsg(self):
409 409 '''Remove existing message, keeping the rest of the comments fields.
410 410 If comments contains 'subject: ', message will prepend
411 411 the field and a blank line.'''
412 412 if self.message:
413 413 subj = 'subject: ' + self.message[0].lower()
414 414 for i in xrange(len(self.comments)):
415 415 if subj == self.comments[i].lower():
416 416 del self.comments[i]
417 417 self.message = self.message[2:]
418 418 break
419 419 ci = 0
420 420 for mi in self.message:
421 421 while mi != self.comments[ci]:
422 422 ci += 1
423 423 del self.comments[ci]
424 424
425 425 def newcommit(repo, phase, *args, **kwargs):
426 426 """helper dedicated to ensure a commit respect mq.secret setting
427 427
428 428 It should be used instead of repo.commit inside the mq source for operation
429 429 creating new changeset.
430 430 """
431 431 repo = repo.unfiltered()
432 432 if phase is None:
433 433 if repo.ui.configbool('mq', 'secret'):
434 434 phase = phases.secret
435 435 overrides = {('ui', 'allowemptycommit'): True}
436 436 if phase is not None:
437 437 overrides[('phases', 'new-commit')] = phase
438 438 with repo.ui.configoverride(overrides, 'mq'):
439 439 repo.ui.setconfig('ui', 'allowemptycommit', True)
440 440 return repo.commit(*args, **kwargs)
441 441
442 442 class AbortNoCleanup(error.Abort):
443 443 pass
444 444
445 445 class queue(object):
446 446 def __init__(self, ui, baseui, path, patchdir=None):
447 447 self.basepath = path
448 448 try:
449 449 with open(os.path.join(path, 'patches.queue'), r'rb') as fh:
450 450 cur = fh.read().rstrip()
451 451
452 452 if not cur:
453 453 curpath = os.path.join(path, 'patches')
454 454 else:
455 455 curpath = os.path.join(path, 'patches-' + cur)
456 456 except IOError:
457 457 curpath = os.path.join(path, 'patches')
458 458 self.path = patchdir or curpath
459 459 self.opener = vfsmod.vfs(self.path)
460 460 self.ui = ui
461 461 self.baseui = baseui
462 462 self.applieddirty = False
463 463 self.seriesdirty = False
464 464 self.added = []
465 465 self.seriespath = "series"
466 466 self.statuspath = "status"
467 467 self.guardspath = "guards"
468 468 self.activeguards = None
469 469 self.guardsdirty = False
470 470 # Handle mq.git as a bool with extended values
471 471 gitmode = ui.config('mq', 'git').lower()
472 472 boolmode = util.parsebool(gitmode)
473 473 if boolmode is not None:
474 474 if boolmode:
475 475 gitmode = 'yes'
476 476 else:
477 477 gitmode = 'no'
478 478 self.gitmode = gitmode
479 479 # deprecated config: mq.plain
480 480 self.plainmode = ui.configbool('mq', 'plain')
481 481 self.checkapplied = True
482 482
483 483 @util.propertycache
484 484 def applied(self):
485 485 def parselines(lines):
486 486 for l in lines:
487 487 entry = l.split(':', 1)
488 488 if len(entry) > 1:
489 489 n, name = entry
490 490 yield statusentry(bin(n), name)
491 491 elif l.strip():
492 492 self.ui.warn(_('malformated mq status line: %s\n') % entry)
493 493 # else we ignore empty lines
494 494 try:
495 495 lines = self.opener.read(self.statuspath).splitlines()
496 496 return list(parselines(lines))
497 497 except IOError as e:
498 498 if e.errno == errno.ENOENT:
499 499 return []
500 500 raise
501 501
502 502 @util.propertycache
503 503 def fullseries(self):
504 504 try:
505 505 return self.opener.read(self.seriespath).splitlines()
506 506 except IOError as e:
507 507 if e.errno == errno.ENOENT:
508 508 return []
509 509 raise
510 510
511 511 @util.propertycache
512 512 def series(self):
513 513 self.parseseries()
514 514 return self.series
515 515
516 516 @util.propertycache
517 517 def seriesguards(self):
518 518 self.parseseries()
519 519 return self.seriesguards
520 520
521 521 def invalidate(self):
522 522 for a in 'applied fullseries series seriesguards'.split():
523 523 if a in self.__dict__:
524 524 delattr(self, a)
525 525 self.applieddirty = False
526 526 self.seriesdirty = False
527 527 self.guardsdirty = False
528 528 self.activeguards = None
529 529
530 530 def diffopts(self, opts=None, patchfn=None, plain=False):
531 531 """Return diff options tweaked for this mq use, possibly upgrading to
532 532 git format, and possibly plain and without lossy options."""
533 533 diffopts = patchmod.difffeatureopts(self.ui, opts,
534 534 git=True, whitespace=not plain, formatchanging=not plain)
535 535 if self.gitmode == 'auto':
536 536 diffopts.upgrade = True
537 537 elif self.gitmode == 'keep':
538 538 pass
539 539 elif self.gitmode in ('yes', 'no'):
540 540 diffopts.git = self.gitmode == 'yes'
541 541 else:
542 542 raise error.Abort(_('mq.git option can be auto/keep/yes/no'
543 543 ' got %s') % self.gitmode)
544 544 if patchfn:
545 545 diffopts = self.patchopts(diffopts, patchfn)
546 546 return diffopts
547 547
548 548 def patchopts(self, diffopts, *patches):
549 549 """Return a copy of input diff options with git set to true if
550 550 referenced patch is a git patch and should be preserved as such.
551 551 """
552 552 diffopts = diffopts.copy()
553 553 if not diffopts.git and self.gitmode == 'keep':
554 554 for patchfn in patches:
555 555 patchf = self.opener(patchfn, 'r')
556 556 # if the patch was a git patch, refresh it as a git patch
557 557 diffopts.git = any(line.startswith('diff --git')
558 558 for line in patchf)
559 559 patchf.close()
560 560 return diffopts
561 561
562 562 def join(self, *p):
563 563 return os.path.join(self.path, *p)
564 564
565 565 def findseries(self, patch):
566 566 def matchpatch(l):
567 567 l = l.split('#', 1)[0]
568 568 return l.strip() == patch
569 569 for index, l in enumerate(self.fullseries):
570 570 if matchpatch(l):
571 571 return index
572 572 return None
573 573
574 574 guard_re = re.compile(br'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)')
575 575
576 576 def parseseries(self):
577 577 self.series = []
578 578 self.seriesguards = []
579 579 for l in self.fullseries:
580 580 h = l.find('#')
581 581 if h == -1:
582 582 patch = l
583 583 comment = ''
584 584 elif h == 0:
585 585 continue
586 586 else:
587 587 patch = l[:h]
588 588 comment = l[h:]
589 589 patch = patch.strip()
590 590 if patch:
591 591 if patch in self.series:
592 592 raise error.Abort(_('%s appears more than once in %s') %
593 593 (patch, self.join(self.seriespath)))
594 594 self.series.append(patch)
595 595 self.seriesguards.append(self.guard_re.findall(comment))
596 596
597 597 def checkguard(self, guard):
598 598 if not guard:
599 599 return _('guard cannot be an empty string')
600 600 bad_chars = '# \t\r\n\f'
601 601 first = guard[0]
602 602 if first in '-+':
603 603 return (_('guard %r starts with invalid character: %r') %
604 604 (guard, first))
605 605 for c in bad_chars:
606 606 if c in guard:
607 607 return _('invalid character in guard %r: %r') % (guard, c)
608 608
609 609 def setactive(self, guards):
610 610 for guard in guards:
611 611 bad = self.checkguard(guard)
612 612 if bad:
613 613 raise error.Abort(bad)
614 614 guards = sorted(set(guards))
615 615 self.ui.debug('active guards: %s\n' % ' '.join(guards))
616 616 self.activeguards = guards
617 617 self.guardsdirty = True
618 618
619 619 def active(self):
620 620 if self.activeguards is None:
621 621 self.activeguards = []
622 622 try:
623 623 guards = self.opener.read(self.guardspath).split()
624 624 except IOError as err:
625 625 if err.errno != errno.ENOENT:
626 626 raise
627 627 guards = []
628 628 for i, guard in enumerate(guards):
629 629 bad = self.checkguard(guard)
630 630 if bad:
631 631 self.ui.warn('%s:%d: %s\n' %
632 632 (self.join(self.guardspath), i + 1, bad))
633 633 else:
634 634 self.activeguards.append(guard)
635 635 return self.activeguards
636 636
637 637 def setguards(self, idx, guards):
638 638 for g in guards:
639 639 if len(g) < 2:
640 640 raise error.Abort(_('guard %r too short') % g)
641 641 if g[0] not in '-+':
642 642 raise error.Abort(_('guard %r starts with invalid char') % g)
643 643 bad = self.checkguard(g[1:])
644 644 if bad:
645 645 raise error.Abort(bad)
646 646 drop = self.guard_re.sub('', self.fullseries[idx])
647 647 self.fullseries[idx] = drop + ''.join([' #' + g for g in guards])
648 648 self.parseseries()
649 649 self.seriesdirty = True
650 650
651 651 def pushable(self, idx):
652 652 if isinstance(idx, bytes):
653 653 idx = self.series.index(idx)
654 654 patchguards = self.seriesguards[idx]
655 655 if not patchguards:
656 656 return True, None
657 657 guards = self.active()
658 658 exactneg = [g for g in patchguards if g[0] == '-' and g[1:] in guards]
659 659 if exactneg:
660 660 return False, repr(exactneg[0])
661 661 pos = [g for g in patchguards if g[0] == '+']
662 662 exactpos = [g for g in pos if g[1:] in guards]
663 663 if pos:
664 664 if exactpos:
665 665 return True, repr(exactpos[0])
666 666 return False, ' '.join(map(repr, pos))
667 667 return True, ''
668 668
669 669 def explainpushable(self, idx, all_patches=False):
670 670 if all_patches:
671 671 write = self.ui.write
672 672 else:
673 673 write = self.ui.warn
674 674
675 675 if all_patches or self.ui.verbose:
676 676 if isinstance(idx, str):
677 677 idx = self.series.index(idx)
678 678 pushable, why = self.pushable(idx)
679 679 if all_patches and pushable:
680 680 if why is None:
681 681 write(_('allowing %s - no guards in effect\n') %
682 682 self.series[idx])
683 683 else:
684 684 if not why:
685 685 write(_('allowing %s - no matching negative guards\n') %
686 686 self.series[idx])
687 687 else:
688 688 write(_('allowing %s - guarded by %s\n') %
689 689 (self.series[idx], why))
690 690 if not pushable:
691 691 if why:
692 692 write(_('skipping %s - guarded by %s\n') %
693 693 (self.series[idx], why))
694 694 else:
695 695 write(_('skipping %s - no matching guards\n') %
696 696 self.series[idx])
697 697
698 698 def savedirty(self):
699 699 def writelist(items, path):
700 700 fp = self.opener(path, 'wb')
701 701 for i in items:
702 702 fp.write("%s\n" % i)
703 703 fp.close()
704 704 if self.applieddirty:
705 705 writelist(map(bytes, self.applied), self.statuspath)
706 706 self.applieddirty = False
707 707 if self.seriesdirty:
708 708 writelist(self.fullseries, self.seriespath)
709 709 self.seriesdirty = False
710 710 if self.guardsdirty:
711 711 writelist(self.activeguards, self.guardspath)
712 712 self.guardsdirty = False
713 713 if self.added:
714 714 qrepo = self.qrepo()
715 715 if qrepo:
716 716 qrepo[None].add(f for f in self.added if f not in qrepo[None])
717 717 self.added = []
718 718
719 719 def removeundo(self, repo):
720 720 undo = repo.sjoin('undo')
721 721 if not os.path.exists(undo):
722 722 return
723 723 try:
724 724 os.unlink(undo)
725 725 except OSError as inst:
726 726 self.ui.warn(_('error removing undo: %s\n') %
727 727 util.forcebytestr(inst))
728 728
729 729 def backup(self, repo, files, copy=False):
730 730 # backup local changes in --force case
731 731 for f in sorted(files):
732 732 absf = repo.wjoin(f)
733 733 if os.path.lexists(absf):
734 734 self.ui.note(_('saving current version of %s as %s\n') %
735 735 (f, scmutil.origpath(self.ui, repo, f)))
736 736
737 737 absorig = scmutil.origpath(self.ui, repo, absf)
738 738 if copy:
739 739 util.copyfile(absf, absorig)
740 740 else:
741 741 util.rename(absf, absorig)
742 742
743 743 def printdiff(self, repo, diffopts, node1, node2=None, files=None,
744 744 fp=None, changes=None, opts=None):
745 745 if opts is None:
746 746 opts = {}
747 747 stat = opts.get('stat')
748 748 m = scmutil.match(repo[node1], files, opts)
749 749 logcmdutil.diffordiffstat(self.ui, repo, diffopts, node1, node2, m,
750 750 changes, stat, fp)
751 751
752 752 def mergeone(self, repo, mergeq, head, patch, rev, diffopts):
753 753 # first try just applying the patch
754 754 (err, n) = self.apply(repo, [patch], update_status=False,
755 755 strict=True, merge=rev)
756 756
757 757 if err == 0:
758 758 return (err, n)
759 759
760 760 if n is None:
761 761 raise error.Abort(_("apply failed for patch %s") % patch)
762 762
763 763 self.ui.warn(_("patch didn't work out, merging %s\n") % patch)
764 764
765 765 # apply failed, strip away that rev and merge.
766 766 hg.clean(repo, head)
767 767 strip(self.ui, repo, [n], update=False, backup=False)
768 768
769 769 ctx = repo[rev]
770 770 ret = hg.merge(repo, rev)
771 771 if ret:
772 772 raise error.Abort(_("update returned %d") % ret)
773 773 n = newcommit(repo, None, ctx.description(), ctx.user(), force=True)
774 774 if n is None:
775 775 raise error.Abort(_("repo commit failed"))
776 776 try:
777 777 ph = patchheader(mergeq.join(patch), self.plainmode)
778 778 except Exception:
779 779 raise error.Abort(_("unable to read %s") % patch)
780 780
781 781 diffopts = self.patchopts(diffopts, patch)
782 782 patchf = self.opener(patch, "w")
783 comments = str(ph)
783 comments = bytes(ph)
784 784 if comments:
785 785 patchf.write(comments)
786 786 self.printdiff(repo, diffopts, head, n, fp=patchf)
787 787 patchf.close()
788 788 self.removeundo(repo)
789 789 return (0, n)
790 790
791 791 def qparents(self, repo, rev=None):
792 792 """return the mq handled parent or p1
793 793
794 794 In some case where mq get himself in being the parent of a merge the
795 795 appropriate parent may be p2.
796 796 (eg: an in progress merge started with mq disabled)
797 797
798 798 If no parent are managed by mq, p1 is returned.
799 799 """
800 800 if rev is None:
801 801 (p1, p2) = repo.dirstate.parents()
802 802 if p2 == nullid:
803 803 return p1
804 804 if not self.applied:
805 805 return None
806 806 return self.applied[-1].node
807 807 p1, p2 = repo.changelog.parents(rev)
808 808 if p2 != nullid and p2 in [x.node for x in self.applied]:
809 809 return p2
810 810 return p1
811 811
812 812 def mergepatch(self, repo, mergeq, series, diffopts):
813 813 if not self.applied:
814 814 # each of the patches merged in will have two parents. This
815 815 # can confuse the qrefresh, qdiff, and strip code because it
816 816 # needs to know which parent is actually in the patch queue.
817 817 # so, we insert a merge marker with only one parent. This way
818 818 # the first patch in the queue is never a merge patch
819 819 #
820 820 pname = ".hg.patches.merge.marker"
821 821 n = newcommit(repo, None, '[mq]: merge marker', force=True)
822 822 self.removeundo(repo)
823 823 self.applied.append(statusentry(n, pname))
824 824 self.applieddirty = True
825 825
826 826 head = self.qparents(repo)
827 827
828 828 for patch in series:
829 829 patch = mergeq.lookup(patch, strict=True)
830 830 if not patch:
831 831 self.ui.warn(_("patch %s does not exist\n") % patch)
832 832 return (1, None)
833 833 pushable, reason = self.pushable(patch)
834 834 if not pushable:
835 835 self.explainpushable(patch, all_patches=True)
836 836 continue
837 837 info = mergeq.isapplied(patch)
838 838 if not info:
839 839 self.ui.warn(_("patch %s is not applied\n") % patch)
840 840 return (1, None)
841 841 rev = info[1]
842 842 err, head = self.mergeone(repo, mergeq, head, patch, rev, diffopts)
843 843 if head:
844 844 self.applied.append(statusentry(head, patch))
845 845 self.applieddirty = True
846 846 if err:
847 847 return (err, head)
848 848 self.savedirty()
849 849 return (0, head)
850 850
851 851 def patch(self, repo, patchfile):
852 852 '''Apply patchfile to the working directory.
853 853 patchfile: name of patch file'''
854 854 files = set()
855 855 try:
856 856 fuzz = patchmod.patch(self.ui, repo, patchfile, strip=1,
857 857 files=files, eolmode=None)
858 858 return (True, list(files), fuzz)
859 859 except Exception as inst:
860 860 self.ui.note(util.forcebytestr(inst) + '\n')
861 861 if not self.ui.verbose:
862 862 self.ui.warn(_("patch failed, unable to continue (try -v)\n"))
863 863 self.ui.traceback()
864 864 return (False, list(files), False)
865 865
866 866 def apply(self, repo, series, list=False, update_status=True,
867 867 strict=False, patchdir=None, merge=None, all_files=None,
868 868 tobackup=None, keepchanges=False):
869 869 wlock = lock = tr = None
870 870 try:
871 871 wlock = repo.wlock()
872 872 lock = repo.lock()
873 873 tr = repo.transaction("qpush")
874 874 try:
875 875 ret = self._apply(repo, series, list, update_status,
876 876 strict, patchdir, merge, all_files=all_files,
877 877 tobackup=tobackup, keepchanges=keepchanges)
878 878 tr.close()
879 879 self.savedirty()
880 880 return ret
881 881 except AbortNoCleanup:
882 882 tr.close()
883 883 self.savedirty()
884 884 raise
885 885 except: # re-raises
886 886 try:
887 887 tr.abort()
888 888 finally:
889 889 self.invalidate()
890 890 raise
891 891 finally:
892 892 release(tr, lock, wlock)
893 893 self.removeundo(repo)
894 894
895 895 def _apply(self, repo, series, list=False, update_status=True,
896 896 strict=False, patchdir=None, merge=None, all_files=None,
897 897 tobackup=None, keepchanges=False):
898 898 """returns (error, hash)
899 899
900 900 error = 1 for unable to read, 2 for patch failed, 3 for patch
901 901 fuzz. tobackup is None or a set of files to backup before they
902 902 are modified by a patch.
903 903 """
904 904 # TODO unify with commands.py
905 905 if not patchdir:
906 906 patchdir = self.path
907 907 err = 0
908 908 n = None
909 909 for patchname in series:
910 910 pushable, reason = self.pushable(patchname)
911 911 if not pushable:
912 912 self.explainpushable(patchname, all_patches=True)
913 913 continue
914 914 self.ui.status(_("applying %s\n") % patchname)
915 915 pf = os.path.join(patchdir, patchname)
916 916
917 917 try:
918 918 ph = patchheader(self.join(patchname), self.plainmode)
919 919 except IOError:
920 920 self.ui.warn(_("unable to read %s\n") % patchname)
921 921 err = 1
922 922 break
923 923
924 924 message = ph.message
925 925 if not message:
926 926 # The commit message should not be translated
927 927 message = "imported patch %s\n" % patchname
928 928 else:
929 929 if list:
930 930 # The commit message should not be translated
931 931 message.append("\nimported patch %s" % patchname)
932 932 message = '\n'.join(message)
933 933
934 934 if ph.haspatch:
935 935 if tobackup:
936 936 touched = patchmod.changedfiles(self.ui, repo, pf)
937 937 touched = set(touched) & tobackup
938 938 if touched and keepchanges:
939 939 raise AbortNoCleanup(
940 940 _("conflicting local changes found"),
941 941 hint=_("did you forget to qrefresh?"))
942 942 self.backup(repo, touched, copy=True)
943 943 tobackup = tobackup - touched
944 944 (patcherr, files, fuzz) = self.patch(repo, pf)
945 945 if all_files is not None:
946 946 all_files.update(files)
947 947 patcherr = not patcherr
948 948 else:
949 949 self.ui.warn(_("patch %s is empty\n") % patchname)
950 950 patcherr, files, fuzz = 0, [], 0
951 951
952 952 if merge and files:
953 953 # Mark as removed/merged and update dirstate parent info
954 954 removed = []
955 955 merged = []
956 956 for f in files:
957 957 if os.path.lexists(repo.wjoin(f)):
958 958 merged.append(f)
959 959 else:
960 960 removed.append(f)
961 961 with repo.dirstate.parentchange():
962 962 for f in removed:
963 963 repo.dirstate.remove(f)
964 964 for f in merged:
965 965 repo.dirstate.merge(f)
966 966 p1, p2 = repo.dirstate.parents()
967 967 repo.setparents(p1, merge)
968 968
969 969 if all_files and '.hgsubstate' in all_files:
970 970 wctx = repo[None]
971 971 pctx = repo['.']
972 972 overwrite = False
973 973 mergedsubstate = subrepoutil.submerge(repo, pctx, wctx, wctx,
974 974 overwrite)
975 975 files += mergedsubstate.keys()
976 976
977 977 match = scmutil.matchfiles(repo, files or [])
978 978 oldtip = repo['tip']
979 979 n = newcommit(repo, None, message, ph.user, ph.date, match=match,
980 980 force=True)
981 981 if repo['tip'] == oldtip:
982 982 raise error.Abort(_("qpush exactly duplicates child changeset"))
983 983 if n is None:
984 984 raise error.Abort(_("repository commit failed"))
985 985
986 986 if update_status:
987 987 self.applied.append(statusentry(n, patchname))
988 988
989 989 if patcherr:
990 990 self.ui.warn(_("patch failed, rejects left in working "
991 991 "directory\n"))
992 992 err = 2
993 993 break
994 994
995 995 if fuzz and strict:
996 996 self.ui.warn(_("fuzz found when applying patch, stopping\n"))
997 997 err = 3
998 998 break
999 999 return (err, n)
1000 1000
1001 1001 def _cleanup(self, patches, numrevs, keep=False):
1002 1002 if not keep:
1003 1003 r = self.qrepo()
1004 1004 if r:
1005 1005 r[None].forget(patches)
1006 1006 for p in patches:
1007 1007 try:
1008 1008 os.unlink(self.join(p))
1009 1009 except OSError as inst:
1010 1010 if inst.errno != errno.ENOENT:
1011 1011 raise
1012 1012
1013 1013 qfinished = []
1014 1014 if numrevs:
1015 1015 qfinished = self.applied[:numrevs]
1016 1016 del self.applied[:numrevs]
1017 1017 self.applieddirty = True
1018 1018
1019 1019 unknown = []
1020 1020
1021 1021 for (i, p) in sorted([(self.findseries(p), p) for p in patches],
1022 1022 reverse=True):
1023 1023 if i is not None:
1024 1024 del self.fullseries[i]
1025 1025 else:
1026 1026 unknown.append(p)
1027 1027
1028 1028 if unknown:
1029 1029 if numrevs:
1030 1030 rev = dict((entry.name, entry.node) for entry in qfinished)
1031 1031 for p in unknown:
1032 1032 msg = _('revision %s refers to unknown patches: %s\n')
1033 1033 self.ui.warn(msg % (short(rev[p]), p))
1034 1034 else:
1035 1035 msg = _('unknown patches: %s\n')
1036 1036 raise error.Abort(''.join(msg % p for p in unknown))
1037 1037
1038 1038 self.parseseries()
1039 1039 self.seriesdirty = True
1040 1040 return [entry.node for entry in qfinished]
1041 1041
1042 1042 def _revpatches(self, repo, revs):
1043 1043 firstrev = repo[self.applied[0].node].rev()
1044 1044 patches = []
1045 1045 for i, rev in enumerate(revs):
1046 1046
1047 1047 if rev < firstrev:
1048 1048 raise error.Abort(_('revision %d is not managed') % rev)
1049 1049
1050 1050 ctx = repo[rev]
1051 1051 base = self.applied[i].node
1052 1052 if ctx.node() != base:
1053 1053 msg = _('cannot delete revision %d above applied patches')
1054 1054 raise error.Abort(msg % rev)
1055 1055
1056 1056 patch = self.applied[i].name
1057 1057 for fmt in ('[mq]: %s', 'imported patch %s'):
1058 1058 if ctx.description() == fmt % patch:
1059 1059 msg = _('patch %s finalized without changeset message\n')
1060 1060 repo.ui.status(msg % patch)
1061 1061 break
1062 1062
1063 1063 patches.append(patch)
1064 1064 return patches
1065 1065
1066 1066 def finish(self, repo, revs):
1067 1067 # Manually trigger phase computation to ensure phasedefaults is
1068 1068 # executed before we remove the patches.
1069 1069 repo._phasecache
1070 1070 patches = self._revpatches(repo, sorted(revs))
1071 1071 qfinished = self._cleanup(patches, len(patches))
1072 1072 if qfinished and repo.ui.configbool('mq', 'secret'):
1073 1073 # only use this logic when the secret option is added
1074 1074 oldqbase = repo[qfinished[0]]
1075 1075 tphase = phases.newcommitphase(repo.ui)
1076 1076 if oldqbase.phase() > tphase and oldqbase.p1().phase() <= tphase:
1077 1077 with repo.transaction('qfinish') as tr:
1078 1078 phases.advanceboundary(repo, tr, tphase, qfinished)
1079 1079
1080 1080 def delete(self, repo, patches, opts):
1081 1081 if not patches and not opts.get('rev'):
1082 1082 raise error.Abort(_('qdelete requires at least one revision or '
1083 1083 'patch name'))
1084 1084
1085 1085 realpatches = []
1086 1086 for patch in patches:
1087 1087 patch = self.lookup(patch, strict=True)
1088 1088 info = self.isapplied(patch)
1089 1089 if info:
1090 1090 raise error.Abort(_("cannot delete applied patch %s") % patch)
1091 1091 if patch not in self.series:
1092 1092 raise error.Abort(_("patch %s not in series file") % patch)
1093 1093 if patch not in realpatches:
1094 1094 realpatches.append(patch)
1095 1095
1096 1096 numrevs = 0
1097 1097 if opts.get('rev'):
1098 1098 if not self.applied:
1099 1099 raise error.Abort(_('no patches applied'))
1100 1100 revs = scmutil.revrange(repo, opts.get('rev'))
1101 1101 revs.sort()
1102 1102 revpatches = self._revpatches(repo, revs)
1103 1103 realpatches += revpatches
1104 1104 numrevs = len(revpatches)
1105 1105
1106 1106 self._cleanup(realpatches, numrevs, opts.get('keep'))
1107 1107
1108 1108 def checktoppatch(self, repo):
1109 1109 '''check that working directory is at qtip'''
1110 1110 if self.applied:
1111 1111 top = self.applied[-1].node
1112 1112 patch = self.applied[-1].name
1113 1113 if repo.dirstate.p1() != top:
1114 1114 raise error.Abort(_("working directory revision is not qtip"))
1115 1115 return top, patch
1116 1116 return None, None
1117 1117
1118 1118 def putsubstate2changes(self, substatestate, changes):
1119 1119 for files in changes[:3]:
1120 1120 if '.hgsubstate' in files:
1121 1121 return # already listed up
1122 1122 # not yet listed up
1123 1123 if substatestate in 'a?':
1124 1124 changes[1].append('.hgsubstate')
1125 1125 elif substatestate in 'r':
1126 1126 changes[2].append('.hgsubstate')
1127 1127 else: # modified
1128 1128 changes[0].append('.hgsubstate')
1129 1129
1130 1130 def checklocalchanges(self, repo, force=False, refresh=True):
1131 1131 excsuffix = ''
1132 1132 if refresh:
1133 1133 excsuffix = ', qrefresh first'
1134 1134 # plain versions for i18n tool to detect them
1135 1135 _("local changes found, qrefresh first")
1136 1136 _("local changed subrepos found, qrefresh first")
1137 1137 return checklocalchanges(repo, force, excsuffix)
1138 1138
1139 1139 _reserved = ('series', 'status', 'guards', '.', '..')
1140 1140 def checkreservedname(self, name):
1141 1141 if name in self._reserved:
1142 1142 raise error.Abort(_('"%s" cannot be used as the name of a patch')
1143 1143 % name)
1144 1144 if name != name.strip():
1145 1145 # whitespace is stripped by parseseries()
1146 1146 raise error.Abort(_('patch name cannot begin or end with '
1147 1147 'whitespace'))
1148 1148 for prefix in ('.hg', '.mq'):
1149 1149 if name.startswith(prefix):
1150 1150 raise error.Abort(_('patch name cannot begin with "%s"')
1151 1151 % prefix)
1152 1152 for c in ('#', ':', '\r', '\n'):
1153 1153 if c in name:
1154 1154 raise error.Abort(_('%r cannot be used in the name of a patch')
1155 1155 % c)
1156 1156
1157 1157 def checkpatchname(self, name, force=False):
1158 1158 self.checkreservedname(name)
1159 1159 if not force and os.path.exists(self.join(name)):
1160 1160 if os.path.isdir(self.join(name)):
1161 1161 raise error.Abort(_('"%s" already exists as a directory')
1162 1162 % name)
1163 1163 else:
1164 1164 raise error.Abort(_('patch "%s" already exists') % name)
1165 1165
1166 1166 def makepatchname(self, title, fallbackname):
1167 1167 """Return a suitable filename for title, adding a suffix to make
1168 1168 it unique in the existing list"""
1169 1169 namebase = re.sub('[\s\W_]+', '_', title.lower()).strip('_')
1170 1170 namebase = namebase[:75] # avoid too long name (issue5117)
1171 1171 if namebase:
1172 1172 try:
1173 1173 self.checkreservedname(namebase)
1174 1174 except error.Abort:
1175 1175 namebase = fallbackname
1176 1176 else:
1177 1177 namebase = fallbackname
1178 1178 name = namebase
1179 1179 i = 0
1180 1180 while True:
1181 1181 if name not in self.fullseries:
1182 1182 try:
1183 1183 self.checkpatchname(name)
1184 1184 break
1185 1185 except error.Abort:
1186 1186 pass
1187 1187 i += 1
1188 1188 name = '%s__%d' % (namebase, i)
1189 1189 return name
1190 1190
1191 1191 def checkkeepchanges(self, keepchanges, force):
1192 1192 if force and keepchanges:
1193 1193 raise error.Abort(_('cannot use both --force and --keep-changes'))
1194 1194
1195 1195 def new(self, repo, patchfn, *pats, **opts):
1196 1196 """options:
1197 1197 msg: a string or a no-argument function returning a string
1198 1198 """
1199 1199 opts = pycompat.byteskwargs(opts)
1200 1200 msg = opts.get('msg')
1201 1201 edit = opts.get('edit')
1202 1202 editform = opts.get('editform', 'mq.qnew')
1203 1203 user = opts.get('user')
1204 1204 date = opts.get('date')
1205 1205 if date:
1206 1206 date = dateutil.parsedate(date)
1207 1207 diffopts = self.diffopts({'git': opts.get('git')}, plain=True)
1208 1208 if opts.get('checkname', True):
1209 1209 self.checkpatchname(patchfn)
1210 1210 inclsubs = checksubstate(repo)
1211 1211 if inclsubs:
1212 1212 substatestate = repo.dirstate['.hgsubstate']
1213 1213 if opts.get('include') or opts.get('exclude') or pats:
1214 1214 # detect missing files in pats
1215 1215 def badfn(f, msg):
1216 1216 if f != '.hgsubstate': # .hgsubstate is auto-created
1217 1217 raise error.Abort('%s: %s' % (f, msg))
1218 1218 match = scmutil.match(repo[None], pats, opts, badfn=badfn)
1219 1219 changes = repo.status(match=match)
1220 1220 else:
1221 1221 changes = self.checklocalchanges(repo, force=True)
1222 1222 commitfiles = list(inclsubs)
1223 1223 for files in changes[:3]:
1224 1224 commitfiles.extend(files)
1225 1225 match = scmutil.matchfiles(repo, commitfiles)
1226 1226 if len(repo[None].parents()) > 1:
1227 1227 raise error.Abort(_('cannot manage merge changesets'))
1228 1228 self.checktoppatch(repo)
1229 1229 insert = self.fullseriesend()
1230 1230 with repo.wlock():
1231 1231 try:
1232 1232 # if patch file write fails, abort early
1233 1233 p = self.opener(patchfn, "w")
1234 1234 except IOError as e:
1235 1235 raise error.Abort(_('cannot write patch "%s": %s')
1236 1236 % (patchfn, encoding.strtolocal(e.strerror)))
1237 1237 try:
1238 1238 defaultmsg = "[mq]: %s" % patchfn
1239 1239 editor = cmdutil.getcommiteditor(editform=editform)
1240 1240 if edit:
1241 1241 def finishdesc(desc):
1242 1242 if desc.rstrip():
1243 1243 return desc
1244 1244 else:
1245 1245 return defaultmsg
1246 1246 # i18n: this message is shown in editor with "HG: " prefix
1247 1247 extramsg = _('Leave message empty to use default message.')
1248 1248 editor = cmdutil.getcommiteditor(finishdesc=finishdesc,
1249 1249 extramsg=extramsg,
1250 1250 editform=editform)
1251 1251 commitmsg = msg
1252 1252 else:
1253 1253 commitmsg = msg or defaultmsg
1254 1254
1255 1255 n = newcommit(repo, None, commitmsg, user, date, match=match,
1256 1256 force=True, editor=editor)
1257 1257 if n is None:
1258 1258 raise error.Abort(_("repo commit failed"))
1259 1259 try:
1260 1260 self.fullseries[insert:insert] = [patchfn]
1261 1261 self.applied.append(statusentry(n, patchfn))
1262 1262 self.parseseries()
1263 1263 self.seriesdirty = True
1264 1264 self.applieddirty = True
1265 1265 nctx = repo[n]
1266 1266 ph = patchheader(self.join(patchfn), self.plainmode)
1267 1267 if user:
1268 1268 ph.setuser(user)
1269 1269 if date:
1270 1270 ph.setdate('%d %d' % date)
1271 1271 ph.setparent(hex(nctx.p1().node()))
1272 1272 msg = nctx.description().strip()
1273 1273 if msg == defaultmsg.strip():
1274 1274 msg = ''
1275 1275 ph.setmessage(msg)
1276 1276 p.write(bytes(ph))
1277 1277 if commitfiles:
1278 1278 parent = self.qparents(repo, n)
1279 1279 if inclsubs:
1280 1280 self.putsubstate2changes(substatestate, changes)
1281 1281 chunks = patchmod.diff(repo, node1=parent, node2=n,
1282 1282 changes=changes, opts=diffopts)
1283 1283 for chunk in chunks:
1284 1284 p.write(chunk)
1285 1285 p.close()
1286 1286 r = self.qrepo()
1287 1287 if r:
1288 1288 r[None].add([patchfn])
1289 1289 except: # re-raises
1290 1290 repo.rollback()
1291 1291 raise
1292 1292 except Exception:
1293 1293 patchpath = self.join(patchfn)
1294 1294 try:
1295 1295 os.unlink(patchpath)
1296 1296 except OSError:
1297 1297 self.ui.warn(_('error unlinking %s\n') % patchpath)
1298 1298 raise
1299 1299 self.removeundo(repo)
1300 1300
1301 1301 def isapplied(self, patch):
1302 1302 """returns (index, rev, patch)"""
1303 1303 for i, a in enumerate(self.applied):
1304 1304 if a.name == patch:
1305 1305 return (i, a.node, a.name)
1306 1306 return None
1307 1307
1308 1308 # if the exact patch name does not exist, we try a few
1309 1309 # variations. If strict is passed, we try only #1
1310 1310 #
1311 1311 # 1) a number (as string) to indicate an offset in the series file
1312 1312 # 2) a unique substring of the patch name was given
1313 1313 # 3) patchname[-+]num to indicate an offset in the series file
1314 1314 def lookup(self, patch, strict=False):
1315 1315 def partialname(s):
1316 1316 if s in self.series:
1317 1317 return s
1318 1318 matches = [x for x in self.series if s in x]
1319 1319 if len(matches) > 1:
1320 1320 self.ui.warn(_('patch name "%s" is ambiguous:\n') % s)
1321 1321 for m in matches:
1322 1322 self.ui.warn(' %s\n' % m)
1323 1323 return None
1324 1324 if matches:
1325 1325 return matches[0]
1326 1326 if self.series and self.applied:
1327 1327 if s == 'qtip':
1328 1328 return self.series[self.seriesend(True) - 1]
1329 1329 if s == 'qbase':
1330 1330 return self.series[0]
1331 1331 return None
1332 1332
1333 1333 if patch in self.series:
1334 1334 return patch
1335 1335
1336 1336 if not os.path.isfile(self.join(patch)):
1337 1337 try:
1338 1338 sno = int(patch)
1339 1339 except (ValueError, OverflowError):
1340 1340 pass
1341 1341 else:
1342 1342 if -len(self.series) <= sno < len(self.series):
1343 1343 return self.series[sno]
1344 1344
1345 1345 if not strict:
1346 1346 res = partialname(patch)
1347 1347 if res:
1348 1348 return res
1349 1349 minus = patch.rfind('-')
1350 1350 if minus >= 0:
1351 1351 res = partialname(patch[:minus])
1352 1352 if res:
1353 1353 i = self.series.index(res)
1354 1354 try:
1355 1355 off = int(patch[minus + 1:] or 1)
1356 1356 except (ValueError, OverflowError):
1357 1357 pass
1358 1358 else:
1359 1359 if i - off >= 0:
1360 1360 return self.series[i - off]
1361 1361 plus = patch.rfind('+')
1362 1362 if plus >= 0:
1363 1363 res = partialname(patch[:plus])
1364 1364 if res:
1365 1365 i = self.series.index(res)
1366 1366 try:
1367 1367 off = int(patch[plus + 1:] or 1)
1368 1368 except (ValueError, OverflowError):
1369 1369 pass
1370 1370 else:
1371 1371 if i + off < len(self.series):
1372 1372 return self.series[i + off]
1373 1373 raise error.Abort(_("patch %s not in series") % patch)
1374 1374
1375 1375 def push(self, repo, patch=None, force=False, list=False, mergeq=None,
1376 1376 all=False, move=False, exact=False, nobackup=False,
1377 1377 keepchanges=False):
1378 1378 self.checkkeepchanges(keepchanges, force)
1379 1379 diffopts = self.diffopts()
1380 1380 with repo.wlock():
1381 1381 heads = []
1382 1382 for hs in repo.branchmap().itervalues():
1383 1383 heads.extend(hs)
1384 1384 if not heads:
1385 1385 heads = [nullid]
1386 1386 if repo.dirstate.p1() not in heads and not exact:
1387 1387 self.ui.status(_("(working directory not at a head)\n"))
1388 1388
1389 1389 if not self.series:
1390 1390 self.ui.warn(_('no patches in series\n'))
1391 1391 return 0
1392 1392
1393 1393 # Suppose our series file is: A B C and the current 'top'
1394 1394 # patch is B. qpush C should be performed (moving forward)
1395 1395 # qpush B is a NOP (no change) qpush A is an error (can't
1396 1396 # go backwards with qpush)
1397 1397 if patch:
1398 1398 patch = self.lookup(patch)
1399 1399 info = self.isapplied(patch)
1400 1400 if info and info[0] >= len(self.applied) - 1:
1401 1401 self.ui.warn(
1402 1402 _('qpush: %s is already at the top\n') % patch)
1403 1403 return 0
1404 1404
1405 1405 pushable, reason = self.pushable(patch)
1406 1406 if pushable:
1407 1407 if self.series.index(patch) < self.seriesend():
1408 1408 raise error.Abort(
1409 1409 _("cannot push to a previous patch: %s") % patch)
1410 1410 else:
1411 1411 if reason:
1412 1412 reason = _('guarded by %s') % reason
1413 1413 else:
1414 1414 reason = _('no matching guards')
1415 1415 self.ui.warn(_("cannot push '%s' - %s\n") % (patch, reason))
1416 1416 return 1
1417 1417 elif all:
1418 1418 patch = self.series[-1]
1419 1419 if self.isapplied(patch):
1420 1420 self.ui.warn(_('all patches are currently applied\n'))
1421 1421 return 0
1422 1422
1423 1423 # Following the above example, starting at 'top' of B:
1424 1424 # qpush should be performed (pushes C), but a subsequent
1425 1425 # qpush without an argument is an error (nothing to
1426 1426 # apply). This allows a loop of "...while hg qpush..." to
1427 1427 # work as it detects an error when done
1428 1428 start = self.seriesend()
1429 1429 if start == len(self.series):
1430 1430 self.ui.warn(_('patch series already fully applied\n'))
1431 1431 return 1
1432 1432 if not force and not keepchanges:
1433 1433 self.checklocalchanges(repo, refresh=self.applied)
1434 1434
1435 1435 if exact:
1436 1436 if keepchanges:
1437 1437 raise error.Abort(
1438 1438 _("cannot use --exact and --keep-changes together"))
1439 1439 if move:
1440 1440 raise error.Abort(_('cannot use --exact and --move '
1441 1441 'together'))
1442 1442 if self.applied:
1443 1443 raise error.Abort(_('cannot push --exact with applied '
1444 1444 'patches'))
1445 1445 root = self.series[start]
1446 1446 target = patchheader(self.join(root), self.plainmode).parent
1447 1447 if not target:
1448 1448 raise error.Abort(
1449 1449 _("%s does not have a parent recorded") % root)
1450 1450 if not repo[target] == repo['.']:
1451 1451 hg.update(repo, target)
1452 1452
1453 1453 if move:
1454 1454 if not patch:
1455 1455 raise error.Abort(_("please specify the patch to move"))
1456 1456 for fullstart, rpn in enumerate(self.fullseries):
1457 1457 # strip markers for patch guards
1458 1458 if self.guard_re.split(rpn, 1)[0] == self.series[start]:
1459 1459 break
1460 1460 for i, rpn in enumerate(self.fullseries[fullstart:]):
1461 1461 # strip markers for patch guards
1462 1462 if self.guard_re.split(rpn, 1)[0] == patch:
1463 1463 break
1464 1464 index = fullstart + i
1465 1465 assert index < len(self.fullseries)
1466 1466 fullpatch = self.fullseries[index]
1467 1467 del self.fullseries[index]
1468 1468 self.fullseries.insert(fullstart, fullpatch)
1469 1469 self.parseseries()
1470 1470 self.seriesdirty = True
1471 1471
1472 1472 self.applieddirty = True
1473 1473 if start > 0:
1474 1474 self.checktoppatch(repo)
1475 1475 if not patch:
1476 1476 patch = self.series[start]
1477 1477 end = start + 1
1478 1478 else:
1479 1479 end = self.series.index(patch, start) + 1
1480 1480
1481 1481 tobackup = set()
1482 1482 if (not nobackup and force) or keepchanges:
1483 1483 status = self.checklocalchanges(repo, force=True)
1484 1484 if keepchanges:
1485 1485 tobackup.update(status.modified + status.added +
1486 1486 status.removed + status.deleted)
1487 1487 else:
1488 1488 tobackup.update(status.modified + status.added)
1489 1489
1490 1490 s = self.series[start:end]
1491 1491 all_files = set()
1492 1492 try:
1493 1493 if mergeq:
1494 1494 ret = self.mergepatch(repo, mergeq, s, diffopts)
1495 1495 else:
1496 1496 ret = self.apply(repo, s, list, all_files=all_files,
1497 1497 tobackup=tobackup, keepchanges=keepchanges)
1498 1498 except AbortNoCleanup:
1499 1499 raise
1500 1500 except: # re-raises
1501 1501 self.ui.warn(_('cleaning up working directory...\n'))
1502 1502 cmdutil.revert(self.ui, repo, repo['.'],
1503 1503 repo.dirstate.parents(), no_backup=True)
1504 1504 # only remove unknown files that we know we touched or
1505 1505 # created while patching
1506 1506 for f in all_files:
1507 1507 if f not in repo.dirstate:
1508 1508 repo.wvfs.unlinkpath(f, ignoremissing=True)
1509 1509 self.ui.warn(_('done\n'))
1510 1510 raise
1511 1511
1512 1512 if not self.applied:
1513 1513 return ret[0]
1514 1514 top = self.applied[-1].name
1515 1515 if ret[0] and ret[0] > 1:
1516 1516 msg = _("errors during apply, please fix and qrefresh %s\n")
1517 1517 self.ui.write(msg % top)
1518 1518 else:
1519 1519 self.ui.write(_("now at: %s\n") % top)
1520 1520 return ret[0]
1521 1521
1522 1522 def pop(self, repo, patch=None, force=False, update=True, all=False,
1523 1523 nobackup=False, keepchanges=False):
1524 1524 self.checkkeepchanges(keepchanges, force)
1525 1525 with repo.wlock():
1526 1526 if patch:
1527 1527 # index, rev, patch
1528 1528 info = self.isapplied(patch)
1529 1529 if not info:
1530 1530 patch = self.lookup(patch)
1531 1531 info = self.isapplied(patch)
1532 1532 if not info:
1533 1533 raise error.Abort(_("patch %s is not applied") % patch)
1534 1534
1535 1535 if not self.applied:
1536 1536 # Allow qpop -a to work repeatedly,
1537 1537 # but not qpop without an argument
1538 1538 self.ui.warn(_("no patches applied\n"))
1539 1539 return not all
1540 1540
1541 1541 if all:
1542 1542 start = 0
1543 1543 elif patch:
1544 1544 start = info[0] + 1
1545 1545 else:
1546 1546 start = len(self.applied) - 1
1547 1547
1548 1548 if start >= len(self.applied):
1549 1549 self.ui.warn(_("qpop: %s is already at the top\n") % patch)
1550 1550 return
1551 1551
1552 1552 if not update:
1553 1553 parents = repo.dirstate.parents()
1554 1554 rr = [x.node for x in self.applied]
1555 1555 for p in parents:
1556 1556 if p in rr:
1557 1557 self.ui.warn(_("qpop: forcing dirstate update\n"))
1558 1558 update = True
1559 1559 else:
1560 1560 parents = [p.node() for p in repo[None].parents()]
1561 1561 update = any(entry.node in parents
1562 1562 for entry in self.applied[start:])
1563 1563
1564 1564 tobackup = set()
1565 1565 if update:
1566 1566 s = self.checklocalchanges(repo, force=force or keepchanges)
1567 1567 if force:
1568 1568 if not nobackup:
1569 1569 tobackup.update(s.modified + s.added)
1570 1570 elif keepchanges:
1571 1571 tobackup.update(s.modified + s.added +
1572 1572 s.removed + s.deleted)
1573 1573
1574 1574 self.applieddirty = True
1575 1575 end = len(self.applied)
1576 1576 rev = self.applied[start].node
1577 1577
1578 1578 try:
1579 1579 heads = repo.changelog.heads(rev)
1580 1580 except error.LookupError:
1581 1581 node = short(rev)
1582 1582 raise error.Abort(_('trying to pop unknown node %s') % node)
1583 1583
1584 1584 if heads != [self.applied[-1].node]:
1585 1585 raise error.Abort(_("popping would remove a revision not "
1586 1586 "managed by this patch queue"))
1587 1587 if not repo[self.applied[-1].node].mutable():
1588 1588 raise error.Abort(
1589 1589 _("popping would remove a public revision"),
1590 1590 hint=_("see 'hg help phases' for details"))
1591 1591
1592 1592 # we know there are no local changes, so we can make a simplified
1593 1593 # form of hg.update.
1594 1594 if update:
1595 1595 qp = self.qparents(repo, rev)
1596 1596 ctx = repo[qp]
1597 1597 m, a, r, d = repo.status(qp, '.')[:4]
1598 1598 if d:
1599 1599 raise error.Abort(_("deletions found between repo revs"))
1600 1600
1601 1601 tobackup = set(a + m + r) & tobackup
1602 1602 if keepchanges and tobackup:
1603 1603 raise error.Abort(_("local changes found, qrefresh first"))
1604 1604 self.backup(repo, tobackup)
1605 1605 with repo.dirstate.parentchange():
1606 1606 for f in a:
1607 1607 repo.wvfs.unlinkpath(f, ignoremissing=True)
1608 1608 repo.dirstate.drop(f)
1609 1609 for f in m + r:
1610 1610 fctx = ctx[f]
1611 1611 repo.wwrite(f, fctx.data(), fctx.flags())
1612 1612 repo.dirstate.normal(f)
1613 1613 repo.setparents(qp, nullid)
1614 1614 for patch in reversed(self.applied[start:end]):
1615 1615 self.ui.status(_("popping %s\n") % patch.name)
1616 1616 del self.applied[start:end]
1617 1617 strip(self.ui, repo, [rev], update=False, backup=False)
1618 1618 for s, state in repo['.'].substate.items():
1619 1619 repo['.'].sub(s).get(state)
1620 1620 if self.applied:
1621 1621 self.ui.write(_("now at: %s\n") % self.applied[-1].name)
1622 1622 else:
1623 1623 self.ui.write(_("patch queue now empty\n"))
1624 1624
1625 1625 def diff(self, repo, pats, opts):
1626 1626 top, patch = self.checktoppatch(repo)
1627 1627 if not top:
1628 1628 self.ui.write(_("no patches applied\n"))
1629 1629 return
1630 1630 qp = self.qparents(repo, top)
1631 1631 if opts.get('reverse'):
1632 1632 node1, node2 = None, qp
1633 1633 else:
1634 1634 node1, node2 = qp, None
1635 1635 diffopts = self.diffopts(opts, patch)
1636 1636 self.printdiff(repo, diffopts, node1, node2, files=pats, opts=opts)
1637 1637
1638 1638 def refresh(self, repo, pats=None, **opts):
1639 1639 opts = pycompat.byteskwargs(opts)
1640 1640 if not self.applied:
1641 1641 self.ui.write(_("no patches applied\n"))
1642 1642 return 1
1643 1643 msg = opts.get('msg', '').rstrip()
1644 1644 edit = opts.get('edit')
1645 1645 editform = opts.get('editform', 'mq.qrefresh')
1646 1646 newuser = opts.get('user')
1647 1647 newdate = opts.get('date')
1648 1648 if newdate:
1649 1649 newdate = '%d %d' % dateutil.parsedate(newdate)
1650 1650 wlock = repo.wlock()
1651 1651
1652 1652 try:
1653 1653 self.checktoppatch(repo)
1654 1654 (top, patchfn) = (self.applied[-1].node, self.applied[-1].name)
1655 1655 if repo.changelog.heads(top) != [top]:
1656 1656 raise error.Abort(_("cannot qrefresh a revision with children"))
1657 1657 if not repo[top].mutable():
1658 1658 raise error.Abort(_("cannot qrefresh public revision"),
1659 1659 hint=_("see 'hg help phases' for details"))
1660 1660
1661 1661 cparents = repo.changelog.parents(top)
1662 1662 patchparent = self.qparents(repo, top)
1663 1663
1664 1664 inclsubs = checksubstate(repo, hex(patchparent))
1665 1665 if inclsubs:
1666 1666 substatestate = repo.dirstate['.hgsubstate']
1667 1667
1668 1668 ph = patchheader(self.join(patchfn), self.plainmode)
1669 1669 diffopts = self.diffopts({'git': opts.get('git')}, patchfn,
1670 1670 plain=True)
1671 1671 if newuser:
1672 1672 ph.setuser(newuser)
1673 1673 if newdate:
1674 1674 ph.setdate(newdate)
1675 1675 ph.setparent(hex(patchparent))
1676 1676
1677 1677 # only commit new patch when write is complete
1678 1678 patchf = self.opener(patchfn, 'w', atomictemp=True)
1679 1679
1680 1680 # update the dirstate in place, strip off the qtip commit
1681 1681 # and then commit.
1682 1682 #
1683 1683 # this should really read:
1684 1684 # mm, dd, aa = repo.status(top, patchparent)[:3]
1685 1685 # but we do it backwards to take advantage of manifest/changelog
1686 1686 # caching against the next repo.status call
1687 1687 mm, aa, dd = repo.status(patchparent, top)[:3]
1688 1688 changes = repo.changelog.read(top)
1689 1689 man = repo.manifestlog[changes[0]].read()
1690 1690 aaa = aa[:]
1691 1691 match1 = scmutil.match(repo[None], pats, opts)
1692 1692 # in short mode, we only diff the files included in the
1693 1693 # patch already plus specified files
1694 1694 if opts.get('short'):
1695 1695 # if amending a patch, we start with existing
1696 1696 # files plus specified files - unfiltered
1697 1697 match = scmutil.matchfiles(repo, mm + aa + dd + match1.files())
1698 1698 # filter with include/exclude options
1699 1699 match1 = scmutil.match(repo[None], opts=opts)
1700 1700 else:
1701 1701 match = scmutil.matchall(repo)
1702 1702 m, a, r, d = repo.status(match=match)[:4]
1703 1703 mm = set(mm)
1704 1704 aa = set(aa)
1705 1705 dd = set(dd)
1706 1706
1707 1707 # we might end up with files that were added between
1708 1708 # qtip and the dirstate parent, but then changed in the
1709 1709 # local dirstate. in this case, we want them to only
1710 1710 # show up in the added section
1711 1711 for x in m:
1712 1712 if x not in aa:
1713 1713 mm.add(x)
1714 1714 # we might end up with files added by the local dirstate that
1715 1715 # were deleted by the patch. In this case, they should only
1716 1716 # show up in the changed section.
1717 1717 for x in a:
1718 1718 if x in dd:
1719 1719 dd.remove(x)
1720 1720 mm.add(x)
1721 1721 else:
1722 1722 aa.add(x)
1723 1723 # make sure any files deleted in the local dirstate
1724 1724 # are not in the add or change column of the patch
1725 1725 forget = []
1726 1726 for x in d + r:
1727 1727 if x in aa:
1728 1728 aa.remove(x)
1729 1729 forget.append(x)
1730 1730 continue
1731 1731 else:
1732 1732 mm.discard(x)
1733 1733 dd.add(x)
1734 1734
1735 1735 m = list(mm)
1736 1736 r = list(dd)
1737 1737 a = list(aa)
1738 1738
1739 1739 # create 'match' that includes the files to be recommitted.
1740 1740 # apply match1 via repo.status to ensure correct case handling.
1741 1741 cm, ca, cr, cd = repo.status(patchparent, match=match1)[:4]
1742 1742 allmatches = set(cm + ca + cr + cd)
1743 1743 refreshchanges = [x.intersection(allmatches) for x in (mm, aa, dd)]
1744 1744
1745 1745 files = set(inclsubs)
1746 1746 for x in refreshchanges:
1747 1747 files.update(x)
1748 1748 match = scmutil.matchfiles(repo, files)
1749 1749
1750 1750 bmlist = repo[top].bookmarks()
1751 1751
1752 1752 dsguard = None
1753 1753 try:
1754 1754 dsguard = dirstateguard.dirstateguard(repo, 'mq.refresh')
1755 1755 if diffopts.git or diffopts.upgrade:
1756 1756 copies = {}
1757 1757 for dst in a:
1758 1758 src = repo.dirstate.copied(dst)
1759 1759 # during qfold, the source file for copies may
1760 1760 # be removed. Treat this as a simple add.
1761 1761 if src is not None and src in repo.dirstate:
1762 1762 copies.setdefault(src, []).append(dst)
1763 1763 repo.dirstate.add(dst)
1764 1764 # remember the copies between patchparent and qtip
1765 1765 for dst in aaa:
1766 1766 f = repo.file(dst)
1767 1767 src = f.renamed(man[dst])
1768 1768 if src:
1769 1769 copies.setdefault(src[0], []).extend(
1770 1770 copies.get(dst, []))
1771 1771 if dst in a:
1772 1772 copies[src[0]].append(dst)
1773 1773 # we can't copy a file created by the patch itself
1774 1774 if dst in copies:
1775 1775 del copies[dst]
1776 1776 for src, dsts in copies.iteritems():
1777 1777 for dst in dsts:
1778 1778 repo.dirstate.copy(src, dst)
1779 1779 else:
1780 1780 for dst in a:
1781 1781 repo.dirstate.add(dst)
1782 1782 # Drop useless copy information
1783 1783 for f in list(repo.dirstate.copies()):
1784 1784 repo.dirstate.copy(None, f)
1785 1785 for f in r:
1786 1786 repo.dirstate.remove(f)
1787 1787 # if the patch excludes a modified file, mark that
1788 1788 # file with mtime=0 so status can see it.
1789 1789 mm = []
1790 1790 for i in xrange(len(m) - 1, -1, -1):
1791 1791 if not match1(m[i]):
1792 1792 mm.append(m[i])
1793 1793 del m[i]
1794 1794 for f in m:
1795 1795 repo.dirstate.normal(f)
1796 1796 for f in mm:
1797 1797 repo.dirstate.normallookup(f)
1798 1798 for f in forget:
1799 1799 repo.dirstate.drop(f)
1800 1800
1801 1801 user = ph.user or changes[1]
1802 1802
1803 1803 oldphase = repo[top].phase()
1804 1804
1805 1805 # assumes strip can roll itself back if interrupted
1806 1806 repo.setparents(*cparents)
1807 1807 self.applied.pop()
1808 1808 self.applieddirty = True
1809 1809 strip(self.ui, repo, [top], update=False, backup=False)
1810 1810 dsguard.close()
1811 1811 finally:
1812 1812 release(dsguard)
1813 1813
1814 1814 try:
1815 1815 # might be nice to attempt to roll back strip after this
1816 1816
1817 1817 defaultmsg = "[mq]: %s" % patchfn
1818 1818 editor = cmdutil.getcommiteditor(editform=editform)
1819 1819 if edit:
1820 1820 def finishdesc(desc):
1821 1821 if desc.rstrip():
1822 1822 ph.setmessage(desc)
1823 1823 return desc
1824 1824 return defaultmsg
1825 1825 # i18n: this message is shown in editor with "HG: " prefix
1826 1826 extramsg = _('Leave message empty to use default message.')
1827 1827 editor = cmdutil.getcommiteditor(finishdesc=finishdesc,
1828 1828 extramsg=extramsg,
1829 1829 editform=editform)
1830 1830 message = msg or "\n".join(ph.message)
1831 1831 elif not msg:
1832 1832 if not ph.message:
1833 1833 message = defaultmsg
1834 1834 else:
1835 1835 message = "\n".join(ph.message)
1836 1836 else:
1837 1837 message = msg
1838 1838 ph.setmessage(msg)
1839 1839
1840 1840 # Ensure we create a new changeset in the same phase than
1841 1841 # the old one.
1842 1842 lock = tr = None
1843 1843 try:
1844 1844 lock = repo.lock()
1845 1845 tr = repo.transaction('mq')
1846 1846 n = newcommit(repo, oldphase, message, user, ph.date,
1847 1847 match=match, force=True, editor=editor)
1848 1848 # only write patch after a successful commit
1849 1849 c = [list(x) for x in refreshchanges]
1850 1850 if inclsubs:
1851 1851 self.putsubstate2changes(substatestate, c)
1852 1852 chunks = patchmod.diff(repo, patchparent,
1853 1853 changes=c, opts=diffopts)
1854 1854 comments = bytes(ph)
1855 1855 if comments:
1856 1856 patchf.write(comments)
1857 1857 for chunk in chunks:
1858 1858 patchf.write(chunk)
1859 1859 patchf.close()
1860 1860
1861 1861 marks = repo._bookmarks
1862 1862 marks.applychanges(repo, tr, [(bm, n) for bm in bmlist])
1863 1863 tr.close()
1864 1864
1865 1865 self.applied.append(statusentry(n, patchfn))
1866 1866 finally:
1867 1867 lockmod.release(tr, lock)
1868 1868 except: # re-raises
1869 1869 ctx = repo[cparents[0]]
1870 1870 repo.dirstate.rebuild(ctx.node(), ctx.manifest())
1871 1871 self.savedirty()
1872 1872 self.ui.warn(_('qrefresh interrupted while patch was popped! '
1873 1873 '(revert --all, qpush to recover)\n'))
1874 1874 raise
1875 1875 finally:
1876 1876 wlock.release()
1877 1877 self.removeundo(repo)
1878 1878
1879 1879 def init(self, repo, create=False):
1880 1880 if not create and os.path.isdir(self.path):
1881 1881 raise error.Abort(_("patch queue directory already exists"))
1882 1882 try:
1883 1883 os.mkdir(self.path)
1884 1884 except OSError as inst:
1885 1885 if inst.errno != errno.EEXIST or not create:
1886 1886 raise
1887 1887 if create:
1888 1888 return self.qrepo(create=True)
1889 1889
1890 1890 def unapplied(self, repo, patch=None):
1891 1891 if patch and patch not in self.series:
1892 1892 raise error.Abort(_("patch %s is not in series file") % patch)
1893 1893 if not patch:
1894 1894 start = self.seriesend()
1895 1895 else:
1896 1896 start = self.series.index(patch) + 1
1897 1897 unapplied = []
1898 1898 for i in xrange(start, len(self.series)):
1899 1899 pushable, reason = self.pushable(i)
1900 1900 if pushable:
1901 1901 unapplied.append((i, self.series[i]))
1902 1902 self.explainpushable(i)
1903 1903 return unapplied
1904 1904
1905 1905 def qseries(self, repo, missing=None, start=0, length=None, status=None,
1906 1906 summary=False):
1907 1907 def displayname(pfx, patchname, state):
1908 1908 if pfx:
1909 1909 self.ui.write(pfx)
1910 1910 if summary:
1911 1911 ph = patchheader(self.join(patchname), self.plainmode)
1912 1912 if ph.message:
1913 1913 msg = ph.message[0]
1914 1914 else:
1915 1915 msg = ''
1916 1916
1917 1917 if self.ui.formatted():
1918 1918 width = self.ui.termwidth() - len(pfx) - len(patchname) - 2
1919 1919 if width > 0:
1920 1920 msg = util.ellipsis(msg, width)
1921 1921 else:
1922 1922 msg = ''
1923 1923 self.ui.write(patchname, label='qseries.' + state)
1924 1924 self.ui.write(': ')
1925 1925 self.ui.write(msg, label='qseries.message.' + state)
1926 1926 else:
1927 1927 self.ui.write(patchname, label='qseries.' + state)
1928 1928 self.ui.write('\n')
1929 1929
1930 1930 applied = set([p.name for p in self.applied])
1931 1931 if length is None:
1932 1932 length = len(self.series) - start
1933 1933 if not missing:
1934 1934 if self.ui.verbose:
1935 1935 idxwidth = len(str(start + length - 1))
1936 1936 for i in xrange(start, start + length):
1937 1937 patch = self.series[i]
1938 1938 if patch in applied:
1939 1939 char, state = 'A', 'applied'
1940 1940 elif self.pushable(i)[0]:
1941 1941 char, state = 'U', 'unapplied'
1942 1942 else:
1943 1943 char, state = 'G', 'guarded'
1944 1944 pfx = ''
1945 1945 if self.ui.verbose:
1946 1946 pfx = '%*d %s ' % (idxwidth, i, char)
1947 1947 elif status and status != char:
1948 1948 continue
1949 1949 displayname(pfx, patch, state)
1950 1950 else:
1951 1951 msng_list = []
1952 1952 for root, dirs, files in os.walk(self.path):
1953 1953 d = root[len(self.path) + 1:]
1954 1954 for f in files:
1955 1955 fl = os.path.join(d, f)
1956 1956 if (fl not in self.series and
1957 1957 fl not in (self.statuspath, self.seriespath,
1958 1958 self.guardspath)
1959 1959 and not fl.startswith('.')):
1960 1960 msng_list.append(fl)
1961 1961 for x in sorted(msng_list):
1962 1962 pfx = self.ui.verbose and ('D ') or ''
1963 1963 displayname(pfx, x, 'missing')
1964 1964
1965 1965 def issaveline(self, l):
1966 1966 if l.name == '.hg.patches.save.line':
1967 1967 return True
1968 1968
1969 1969 def qrepo(self, create=False):
1970 1970 ui = self.baseui.copy()
1971 1971 # copy back attributes set by ui.pager()
1972 1972 if self.ui.pageractive and not ui.pageractive:
1973 1973 ui.pageractive = self.ui.pageractive
1974 1974 # internal config: ui.formatted
1975 1975 ui.setconfig('ui', 'formatted',
1976 1976 self.ui.config('ui', 'formatted'), 'mqpager')
1977 1977 ui.setconfig('ui', 'interactive',
1978 1978 self.ui.config('ui', 'interactive'), 'mqpager')
1979 1979 if create or os.path.isdir(self.join(".hg")):
1980 1980 return hg.repository(ui, path=self.path, create=create)
1981 1981
1982 1982 def restore(self, repo, rev, delete=None, qupdate=None):
1983 1983 desc = repo[rev].description().strip()
1984 1984 lines = desc.splitlines()
1985 1985 i = 0
1986 1986 datastart = None
1987 1987 series = []
1988 1988 applied = []
1989 1989 qpp = None
1990 1990 for i, line in enumerate(lines):
1991 1991 if line == 'Patch Data:':
1992 1992 datastart = i + 1
1993 1993 elif line.startswith('Dirstate:'):
1994 1994 l = line.rstrip()
1995 1995 l = l[10:].split(' ')
1996 1996 qpp = [bin(x) for x in l]
1997 1997 elif datastart is not None:
1998 1998 l = line.rstrip()
1999 1999 n, name = l.split(':', 1)
2000 2000 if n:
2001 2001 applied.append(statusentry(bin(n), name))
2002 2002 else:
2003 2003 series.append(l)
2004 2004 if datastart is None:
2005 2005 self.ui.warn(_("no saved patch data found\n"))
2006 2006 return 1
2007 2007 self.ui.warn(_("restoring status: %s\n") % lines[0])
2008 2008 self.fullseries = series
2009 2009 self.applied = applied
2010 2010 self.parseseries()
2011 2011 self.seriesdirty = True
2012 2012 self.applieddirty = True
2013 2013 heads = repo.changelog.heads()
2014 2014 if delete:
2015 2015 if rev not in heads:
2016 2016 self.ui.warn(_("save entry has children, leaving it alone\n"))
2017 2017 else:
2018 2018 self.ui.warn(_("removing save entry %s\n") % short(rev))
2019 2019 pp = repo.dirstate.parents()
2020 2020 if rev in pp:
2021 2021 update = True
2022 2022 else:
2023 2023 update = False
2024 2024 strip(self.ui, repo, [rev], update=update, backup=False)
2025 2025 if qpp:
2026 2026 self.ui.warn(_("saved queue repository parents: %s %s\n") %
2027 2027 (short(qpp[0]), short(qpp[1])))
2028 2028 if qupdate:
2029 2029 self.ui.status(_("updating queue directory\n"))
2030 2030 r = self.qrepo()
2031 2031 if not r:
2032 2032 self.ui.warn(_("unable to load queue repository\n"))
2033 2033 return 1
2034 2034 hg.clean(r, qpp[0])
2035 2035
2036 2036 def save(self, repo, msg=None):
2037 2037 if not self.applied:
2038 2038 self.ui.warn(_("save: no patches applied, exiting\n"))
2039 2039 return 1
2040 2040 if self.issaveline(self.applied[-1]):
2041 2041 self.ui.warn(_("status is already saved\n"))
2042 2042 return 1
2043 2043
2044 2044 if not msg:
2045 2045 msg = _("hg patches saved state")
2046 2046 else:
2047 2047 msg = "hg patches: " + msg.rstrip('\r\n')
2048 2048 r = self.qrepo()
2049 2049 if r:
2050 2050 pp = r.dirstate.parents()
2051 2051 msg += "\nDirstate: %s %s" % (hex(pp[0]), hex(pp[1]))
2052 2052 msg += "\n\nPatch Data:\n"
2053 2053 msg += ''.join('%s\n' % x for x in self.applied)
2054 2054 msg += ''.join(':%s\n' % x for x in self.fullseries)
2055 2055 n = repo.commit(msg, force=True)
2056 2056 if not n:
2057 2057 self.ui.warn(_("repo commit failed\n"))
2058 2058 return 1
2059 2059 self.applied.append(statusentry(n, '.hg.patches.save.line'))
2060 2060 self.applieddirty = True
2061 2061 self.removeundo(repo)
2062 2062
2063 2063 def fullseriesend(self):
2064 2064 if self.applied:
2065 2065 p = self.applied[-1].name
2066 2066 end = self.findseries(p)
2067 2067 if end is None:
2068 2068 return len(self.fullseries)
2069 2069 return end + 1
2070 2070 return 0
2071 2071
2072 2072 def seriesend(self, all_patches=False):
2073 2073 """If all_patches is False, return the index of the next pushable patch
2074 2074 in the series, or the series length. If all_patches is True, return the
2075 2075 index of the first patch past the last applied one.
2076 2076 """
2077 2077 end = 0
2078 2078 def nextpatch(start):
2079 2079 if all_patches or start >= len(self.series):
2080 2080 return start
2081 2081 for i in xrange(start, len(self.series)):
2082 2082 p, reason = self.pushable(i)
2083 2083 if p:
2084 2084 return i
2085 2085 self.explainpushable(i)
2086 2086 return len(self.series)
2087 2087 if self.applied:
2088 2088 p = self.applied[-1].name
2089 2089 try:
2090 2090 end = self.series.index(p)
2091 2091 except ValueError:
2092 2092 return 0
2093 2093 return nextpatch(end + 1)
2094 2094 return nextpatch(end)
2095 2095
2096 2096 def appliedname(self, index):
2097 2097 pname = self.applied[index].name
2098 2098 if not self.ui.verbose:
2099 2099 p = pname
2100 2100 else:
2101 2101 p = str(self.series.index(pname)) + " " + pname
2102 2102 return p
2103 2103
2104 2104 def qimport(self, repo, files, patchname=None, rev=None, existing=None,
2105 2105 force=None, git=False):
2106 2106 def checkseries(patchname):
2107 2107 if patchname in self.series:
2108 2108 raise error.Abort(_('patch %s is already in the series file')
2109 2109 % patchname)
2110 2110
2111 2111 if rev:
2112 2112 if files:
2113 2113 raise error.Abort(_('option "-r" not valid when importing '
2114 2114 'files'))
2115 2115 rev = scmutil.revrange(repo, rev)
2116 2116 rev.sort(reverse=True)
2117 2117 elif not files:
2118 2118 raise error.Abort(_('no files or revisions specified'))
2119 2119 if (len(files) > 1 or len(rev) > 1) and patchname:
2120 2120 raise error.Abort(_('option "-n" not valid when importing multiple '
2121 2121 'patches'))
2122 2122 imported = []
2123 2123 if rev:
2124 2124 # If mq patches are applied, we can only import revisions
2125 2125 # that form a linear path to qbase.
2126 2126 # Otherwise, they should form a linear path to a head.
2127 2127 heads = repo.changelog.heads(repo.changelog.node(rev.first()))
2128 2128 if len(heads) > 1:
2129 2129 raise error.Abort(_('revision %d is the root of more than one '
2130 2130 'branch') % rev.last())
2131 2131 if self.applied:
2132 2132 base = repo.changelog.node(rev.first())
2133 2133 if base in [n.node for n in self.applied]:
2134 2134 raise error.Abort(_('revision %d is already managed')
2135 2135 % rev.first())
2136 2136 if heads != [self.applied[-1].node]:
2137 2137 raise error.Abort(_('revision %d is not the parent of '
2138 2138 'the queue') % rev.first())
2139 2139 base = repo.changelog.rev(self.applied[0].node)
2140 2140 lastparent = repo.changelog.parentrevs(base)[0]
2141 2141 else:
2142 2142 if heads != [repo.changelog.node(rev.first())]:
2143 2143 raise error.Abort(_('revision %d has unmanaged children')
2144 2144 % rev.first())
2145 2145 lastparent = None
2146 2146
2147 2147 diffopts = self.diffopts({'git': git})
2148 2148 with repo.transaction('qimport') as tr:
2149 2149 for r in rev:
2150 2150 if not repo[r].mutable():
2151 2151 raise error.Abort(_('revision %d is not mutable') % r,
2152 2152 hint=_("see 'hg help phases' "
2153 2153 'for details'))
2154 2154 p1, p2 = repo.changelog.parentrevs(r)
2155 2155 n = repo.changelog.node(r)
2156 2156 if p2 != nullrev:
2157 2157 raise error.Abort(_('cannot import merge revision %d')
2158 2158 % r)
2159 2159 if lastparent and lastparent != r:
2160 2160 raise error.Abort(_('revision %d is not the parent of '
2161 2161 '%d')
2162 2162 % (r, lastparent))
2163 2163 lastparent = p1
2164 2164
2165 2165 if not patchname:
2166 2166 patchname = self.makepatchname(
2167 2167 repo[r].description().split('\n', 1)[0],
2168 2168 '%d.diff' % r)
2169 2169 checkseries(patchname)
2170 2170 self.checkpatchname(patchname, force)
2171 2171 self.fullseries.insert(0, patchname)
2172 2172
2173 2173 patchf = self.opener(patchname, "w")
2174 2174 cmdutil.export(repo, [n], fp=patchf, opts=diffopts)
2175 2175 patchf.close()
2176 2176
2177 2177 se = statusentry(n, patchname)
2178 2178 self.applied.insert(0, se)
2179 2179
2180 2180 self.added.append(patchname)
2181 2181 imported.append(patchname)
2182 2182 patchname = None
2183 2183 if rev and repo.ui.configbool('mq', 'secret'):
2184 2184 # if we added anything with --rev, move the secret root
2185 2185 phases.retractboundary(repo, tr, phases.secret, [n])
2186 2186 self.parseseries()
2187 2187 self.applieddirty = True
2188 2188 self.seriesdirty = True
2189 2189
2190 2190 for i, filename in enumerate(files):
2191 2191 if existing:
2192 2192 if filename == '-':
2193 2193 raise error.Abort(_('-e is incompatible with import from -')
2194 2194 )
2195 2195 filename = normname(filename)
2196 2196 self.checkreservedname(filename)
2197 2197 if util.url(filename).islocal():
2198 2198 originpath = self.join(filename)
2199 2199 if not os.path.isfile(originpath):
2200 2200 raise error.Abort(
2201 2201 _("patch %s does not exist") % filename)
2202 2202
2203 2203 if patchname:
2204 2204 self.checkpatchname(patchname, force)
2205 2205
2206 2206 self.ui.write(_('renaming %s to %s\n')
2207 2207 % (filename, patchname))
2208 2208 util.rename(originpath, self.join(patchname))
2209 2209 else:
2210 2210 patchname = filename
2211 2211
2212 2212 else:
2213 2213 if filename == '-' and not patchname:
2214 2214 raise error.Abort(_('need --name to import a patch from -'))
2215 2215 elif not patchname:
2216 2216 patchname = normname(os.path.basename(filename.rstrip('/')))
2217 2217 self.checkpatchname(patchname, force)
2218 2218 try:
2219 2219 if filename == '-':
2220 2220 text = self.ui.fin.read()
2221 2221 else:
2222 2222 fp = hg.openpath(self.ui, filename)
2223 2223 text = fp.read()
2224 2224 fp.close()
2225 2225 except (OSError, IOError):
2226 2226 raise error.Abort(_("unable to read file %s") % filename)
2227 2227 patchf = self.opener(patchname, "w")
2228 2228 patchf.write(text)
2229 2229 patchf.close()
2230 2230 if not force:
2231 2231 checkseries(patchname)
2232 2232 if patchname not in self.series:
2233 2233 index = self.fullseriesend() + i
2234 2234 self.fullseries[index:index] = [patchname]
2235 2235 self.parseseries()
2236 2236 self.seriesdirty = True
2237 2237 self.ui.warn(_("adding %s to series file\n") % patchname)
2238 2238 self.added.append(patchname)
2239 2239 imported.append(patchname)
2240 2240 patchname = None
2241 2241
2242 2242 self.removeundo(repo)
2243 2243 return imported
2244 2244
2245 2245 def fixkeepchangesopts(ui, opts):
2246 2246 if (not ui.configbool('mq', 'keepchanges') or opts.get('force')
2247 2247 or opts.get('exact')):
2248 2248 return opts
2249 2249 opts = dict(opts)
2250 2250 opts['keep_changes'] = True
2251 2251 return opts
2252 2252
2253 2253 @command("qdelete|qremove|qrm",
2254 2254 [('k', 'keep', None, _('keep patch file')),
2255 2255 ('r', 'rev', [],
2256 2256 _('stop managing a revision (DEPRECATED)'), _('REV'))],
2257 2257 _('hg qdelete [-k] [PATCH]...'))
2258 2258 def delete(ui, repo, *patches, **opts):
2259 2259 """remove patches from queue
2260 2260
2261 2261 The patches must not be applied, and at least one patch is required. Exact
2262 2262 patch identifiers must be given. With -k/--keep, the patch files are
2263 2263 preserved in the patch directory.
2264 2264
2265 2265 To stop managing a patch and move it into permanent history,
2266 2266 use the :hg:`qfinish` command."""
2267 2267 q = repo.mq
2268 2268 q.delete(repo, patches, pycompat.byteskwargs(opts))
2269 2269 q.savedirty()
2270 2270 return 0
2271 2271
2272 2272 @command("qapplied",
2273 2273 [('1', 'last', None, _('show only the preceding applied patch'))
2274 2274 ] + seriesopts,
2275 2275 _('hg qapplied [-1] [-s] [PATCH]'))
2276 2276 def applied(ui, repo, patch=None, **opts):
2277 2277 """print the patches already applied
2278 2278
2279 2279 Returns 0 on success."""
2280 2280
2281 2281 q = repo.mq
2282 2282 opts = pycompat.byteskwargs(opts)
2283 2283
2284 2284 if patch:
2285 2285 if patch not in q.series:
2286 2286 raise error.Abort(_("patch %s is not in series file") % patch)
2287 2287 end = q.series.index(patch) + 1
2288 2288 else:
2289 2289 end = q.seriesend(True)
2290 2290
2291 2291 if opts.get('last') and not end:
2292 2292 ui.write(_("no patches applied\n"))
2293 2293 return 1
2294 2294 elif opts.get('last') and end == 1:
2295 2295 ui.write(_("only one patch applied\n"))
2296 2296 return 1
2297 2297 elif opts.get('last'):
2298 2298 start = end - 2
2299 2299 end = 1
2300 2300 else:
2301 2301 start = 0
2302 2302
2303 2303 q.qseries(repo, length=end, start=start, status='A',
2304 2304 summary=opts.get('summary'))
2305 2305
2306 2306
2307 2307 @command("qunapplied",
2308 2308 [('1', 'first', None, _('show only the first patch'))] + seriesopts,
2309 2309 _('hg qunapplied [-1] [-s] [PATCH]'))
2310 2310 def unapplied(ui, repo, patch=None, **opts):
2311 2311 """print the patches not yet applied
2312 2312
2313 2313 Returns 0 on success."""
2314 2314
2315 2315 q = repo.mq
2316 2316 opts = pycompat.byteskwargs(opts)
2317 2317 if patch:
2318 2318 if patch not in q.series:
2319 2319 raise error.Abort(_("patch %s is not in series file") % patch)
2320 2320 start = q.series.index(patch) + 1
2321 2321 else:
2322 2322 start = q.seriesend(True)
2323 2323
2324 2324 if start == len(q.series) and opts.get('first'):
2325 2325 ui.write(_("all patches applied\n"))
2326 2326 return 1
2327 2327
2328 2328 if opts.get('first'):
2329 2329 length = 1
2330 2330 else:
2331 2331 length = None
2332 2332 q.qseries(repo, start=start, length=length, status='U',
2333 2333 summary=opts.get('summary'))
2334 2334
2335 2335 @command("qimport",
2336 2336 [('e', 'existing', None, _('import file in patch directory')),
2337 2337 ('n', 'name', '',
2338 2338 _('name of patch file'), _('NAME')),
2339 2339 ('f', 'force', None, _('overwrite existing files')),
2340 2340 ('r', 'rev', [],
2341 2341 _('place existing revisions under mq control'), _('REV')),
2342 2342 ('g', 'git', None, _('use git extended diff format')),
2343 2343 ('P', 'push', None, _('qpush after importing'))],
2344 2344 _('hg qimport [-e] [-n NAME] [-f] [-g] [-P] [-r REV]... [FILE]...'))
2345 2345 def qimport(ui, repo, *filename, **opts):
2346 2346 """import a patch or existing changeset
2347 2347
2348 2348 The patch is inserted into the series after the last applied
2349 2349 patch. If no patches have been applied, qimport prepends the patch
2350 2350 to the series.
2351 2351
2352 2352 The patch will have the same name as its source file unless you
2353 2353 give it a new one with -n/--name.
2354 2354
2355 2355 You can register an existing patch inside the patch directory with
2356 2356 the -e/--existing flag.
2357 2357
2358 2358 With -f/--force, an existing patch of the same name will be
2359 2359 overwritten.
2360 2360
2361 2361 An existing changeset may be placed under mq control with -r/--rev
2362 2362 (e.g. qimport --rev . -n patch will place the current revision
2363 2363 under mq control). With -g/--git, patches imported with --rev will
2364 2364 use the git diff format. See the diffs help topic for information
2365 2365 on why this is important for preserving rename/copy information
2366 2366 and permission changes. Use :hg:`qfinish` to remove changesets
2367 2367 from mq control.
2368 2368
2369 2369 To import a patch from standard input, pass - as the patch file.
2370 2370 When importing from standard input, a patch name must be specified
2371 2371 using the --name flag.
2372 2372
2373 2373 To import an existing patch while renaming it::
2374 2374
2375 2375 hg qimport -e existing-patch -n new-name
2376 2376
2377 2377 Returns 0 if import succeeded.
2378 2378 """
2379 2379 opts = pycompat.byteskwargs(opts)
2380 2380 with repo.lock(): # cause this may move phase
2381 2381 q = repo.mq
2382 2382 try:
2383 2383 imported = q.qimport(
2384 2384 repo, filename, patchname=opts.get('name'),
2385 2385 existing=opts.get('existing'), force=opts.get('force'),
2386 2386 rev=opts.get('rev'), git=opts.get('git'))
2387 2387 finally:
2388 2388 q.savedirty()
2389 2389
2390 2390 if imported and opts.get('push') and not opts.get('rev'):
2391 2391 return q.push(repo, imported[-1])
2392 2392 return 0
2393 2393
2394 2394 def qinit(ui, repo, create):
2395 2395 """initialize a new queue repository
2396 2396
2397 2397 This command also creates a series file for ordering patches, and
2398 2398 an mq-specific .hgignore file in the queue repository, to exclude
2399 2399 the status and guards files (these contain mostly transient state).
2400 2400
2401 2401 Returns 0 if initialization succeeded."""
2402 2402 q = repo.mq
2403 2403 r = q.init(repo, create)
2404 2404 q.savedirty()
2405 2405 if r:
2406 2406 if not os.path.exists(r.wjoin('.hgignore')):
2407 2407 fp = r.wvfs('.hgignore', 'w')
2408 2408 fp.write('^\\.hg\n')
2409 2409 fp.write('^\\.mq\n')
2410 2410 fp.write('syntax: glob\n')
2411 2411 fp.write('status\n')
2412 2412 fp.write('guards\n')
2413 2413 fp.close()
2414 2414 if not os.path.exists(r.wjoin('series')):
2415 2415 r.wvfs('series', 'w').close()
2416 2416 r[None].add(['.hgignore', 'series'])
2417 2417 commands.add(ui, r)
2418 2418 return 0
2419 2419
2420 2420 @command("^qinit",
2421 2421 [('c', 'create-repo', None, _('create queue repository'))],
2422 2422 _('hg qinit [-c]'))
2423 2423 def init(ui, repo, **opts):
2424 2424 """init a new queue repository (DEPRECATED)
2425 2425
2426 2426 The queue repository is unversioned by default. If
2427 2427 -c/--create-repo is specified, qinit will create a separate nested
2428 2428 repository for patches (qinit -c may also be run later to convert
2429 2429 an unversioned patch repository into a versioned one). You can use
2430 2430 qcommit to commit changes to this queue repository.
2431 2431
2432 2432 This command is deprecated. Without -c, it's implied by other relevant
2433 2433 commands. With -c, use :hg:`init --mq` instead."""
2434 2434 return qinit(ui, repo, create=opts.get(r'create_repo'))
2435 2435
2436 2436 @command("qclone",
2437 2437 [('', 'pull', None, _('use pull protocol to copy metadata')),
2438 2438 ('U', 'noupdate', None,
2439 2439 _('do not update the new working directories')),
2440 2440 ('', 'uncompressed', None,
2441 2441 _('use uncompressed transfer (fast over LAN)')),
2442 2442 ('p', 'patches', '',
2443 2443 _('location of source patch repository'), _('REPO')),
2444 2444 ] + cmdutil.remoteopts,
2445 2445 _('hg qclone [OPTION]... SOURCE [DEST]'),
2446 2446 norepo=True)
2447 2447 def clone(ui, source, dest=None, **opts):
2448 2448 '''clone main and patch repository at same time
2449 2449
2450 2450 If source is local, destination will have no patches applied. If
2451 2451 source is remote, this command can not check if patches are
2452 2452 applied in source, so cannot guarantee that patches are not
2453 2453 applied in destination. If you clone remote repository, be sure
2454 2454 before that it has no patches applied.
2455 2455
2456 2456 Source patch repository is looked for in <src>/.hg/patches by
2457 2457 default. Use -p <url> to change.
2458 2458
2459 2459 The patch directory must be a nested Mercurial repository, as
2460 2460 would be created by :hg:`init --mq`.
2461 2461
2462 2462 Return 0 on success.
2463 2463 '''
2464 2464 opts = pycompat.byteskwargs(opts)
2465 2465 def patchdir(repo):
2466 2466 """compute a patch repo url from a repo object"""
2467 2467 url = repo.url()
2468 2468 if url.endswith('/'):
2469 2469 url = url[:-1]
2470 2470 return url + '/.hg/patches'
2471 2471
2472 2472 # main repo (destination and sources)
2473 2473 if dest is None:
2474 2474 dest = hg.defaultdest(source)
2475 2475 sr = hg.peer(ui, opts, ui.expandpath(source))
2476 2476
2477 2477 # patches repo (source only)
2478 2478 if opts.get('patches'):
2479 2479 patchespath = ui.expandpath(opts.get('patches'))
2480 2480 else:
2481 2481 patchespath = patchdir(sr)
2482 2482 try:
2483 2483 hg.peer(ui, opts, patchespath)
2484 2484 except error.RepoError:
2485 2485 raise error.Abort(_('versioned patch repository not found'
2486 2486 ' (see init --mq)'))
2487 2487 qbase, destrev = None, None
2488 2488 if sr.local():
2489 2489 repo = sr.local()
2490 2490 if repo.mq.applied and repo[qbase].phase() != phases.secret:
2491 2491 qbase = repo.mq.applied[0].node
2492 2492 if not hg.islocal(dest):
2493 2493 heads = set(repo.heads())
2494 2494 destrev = list(heads.difference(repo.heads(qbase)))
2495 2495 destrev.append(repo.changelog.parents(qbase)[0])
2496 2496 elif sr.capable('lookup'):
2497 2497 try:
2498 2498 qbase = sr.lookup('qbase')
2499 2499 except error.RepoError:
2500 2500 pass
2501 2501
2502 2502 ui.note(_('cloning main repository\n'))
2503 2503 sr, dr = hg.clone(ui, opts, sr.url(), dest,
2504 2504 pull=opts.get('pull'),
2505 2505 rev=destrev,
2506 2506 update=False,
2507 2507 stream=opts.get('uncompressed'))
2508 2508
2509 2509 ui.note(_('cloning patch repository\n'))
2510 2510 hg.clone(ui, opts, opts.get('patches') or patchdir(sr), patchdir(dr),
2511 2511 pull=opts.get('pull'), update=not opts.get('noupdate'),
2512 2512 stream=opts.get('uncompressed'))
2513 2513
2514 2514 if dr.local():
2515 2515 repo = dr.local()
2516 2516 if qbase:
2517 2517 ui.note(_('stripping applied patches from destination '
2518 2518 'repository\n'))
2519 2519 strip(ui, repo, [qbase], update=False, backup=None)
2520 2520 if not opts.get('noupdate'):
2521 2521 ui.note(_('updating destination repository\n'))
2522 2522 hg.update(repo, repo.changelog.tip())
2523 2523
2524 2524 @command("qcommit|qci",
2525 2525 commands.table["^commit|ci"][1],
2526 2526 _('hg qcommit [OPTION]... [FILE]...'),
2527 2527 inferrepo=True)
2528 2528 def commit(ui, repo, *pats, **opts):
2529 2529 """commit changes in the queue repository (DEPRECATED)
2530 2530
2531 2531 This command is deprecated; use :hg:`commit --mq` instead."""
2532 2532 q = repo.mq
2533 2533 r = q.qrepo()
2534 2534 if not r:
2535 2535 raise error.Abort('no queue repository')
2536 2536 commands.commit(r.ui, r, *pats, **opts)
2537 2537
2538 2538 @command("qseries",
2539 2539 [('m', 'missing', None, _('print patches not in series')),
2540 2540 ] + seriesopts,
2541 2541 _('hg qseries [-ms]'))
2542 2542 def series(ui, repo, **opts):
2543 2543 """print the entire series file
2544 2544
2545 2545 Returns 0 on success."""
2546 2546 repo.mq.qseries(repo, missing=opts.get(r'missing'),
2547 2547 summary=opts.get(r'summary'))
2548 2548 return 0
2549 2549
2550 2550 @command("qtop", seriesopts, _('hg qtop [-s]'))
2551 2551 def top(ui, repo, **opts):
2552 2552 """print the name of the current patch
2553 2553
2554 2554 Returns 0 on success."""
2555 2555 q = repo.mq
2556 2556 if q.applied:
2557 2557 t = q.seriesend(True)
2558 2558 else:
2559 2559 t = 0
2560 2560
2561 2561 if t:
2562 2562 q.qseries(repo, start=t - 1, length=1, status='A',
2563 2563 summary=opts.get(r'summary'))
2564 2564 else:
2565 2565 ui.write(_("no patches applied\n"))
2566 2566 return 1
2567 2567
2568 2568 @command("qnext", seriesopts, _('hg qnext [-s]'))
2569 2569 def next(ui, repo, **opts):
2570 2570 """print the name of the next pushable patch
2571 2571
2572 2572 Returns 0 on success."""
2573 2573 q = repo.mq
2574 2574 end = q.seriesend()
2575 2575 if end == len(q.series):
2576 2576 ui.write(_("all patches applied\n"))
2577 2577 return 1
2578 2578 q.qseries(repo, start=end, length=1, summary=opts.get(r'summary'))
2579 2579
2580 2580 @command("qprev", seriesopts, _('hg qprev [-s]'))
2581 2581 def prev(ui, repo, **opts):
2582 2582 """print the name of the preceding applied patch
2583 2583
2584 2584 Returns 0 on success."""
2585 2585 q = repo.mq
2586 2586 l = len(q.applied)
2587 2587 if l == 1:
2588 2588 ui.write(_("only one patch applied\n"))
2589 2589 return 1
2590 2590 if not l:
2591 2591 ui.write(_("no patches applied\n"))
2592 2592 return 1
2593 2593 idx = q.series.index(q.applied[-2].name)
2594 2594 q.qseries(repo, start=idx, length=1, status='A',
2595 2595 summary=opts.get(r'summary'))
2596 2596
2597 2597 def setupheaderopts(ui, opts):
2598 2598 if not opts.get('user') and opts.get('currentuser'):
2599 2599 opts['user'] = ui.username()
2600 2600 if not opts.get('date') and opts.get('currentdate'):
2601 2601 opts['date'] = "%d %d" % dateutil.makedate()
2602 2602
2603 2603 @command("^qnew",
2604 2604 [('e', 'edit', None, _('invoke editor on commit messages')),
2605 2605 ('f', 'force', None, _('import uncommitted changes (DEPRECATED)')),
2606 2606 ('g', 'git', None, _('use git extended diff format')),
2607 2607 ('U', 'currentuser', None, _('add "From: <current user>" to patch')),
2608 2608 ('u', 'user', '',
2609 2609 _('add "From: <USER>" to patch'), _('USER')),
2610 2610 ('D', 'currentdate', None, _('add "Date: <current date>" to patch')),
2611 2611 ('d', 'date', '',
2612 2612 _('add "Date: <DATE>" to patch'), _('DATE'))
2613 2613 ] + cmdutil.walkopts + cmdutil.commitopts,
2614 2614 _('hg qnew [-e] [-m TEXT] [-l FILE] PATCH [FILE]...'),
2615 2615 inferrepo=True)
2616 2616 def new(ui, repo, patch, *args, **opts):
2617 2617 """create a new patch
2618 2618
2619 2619 qnew creates a new patch on top of the currently-applied patch (if
2620 2620 any). The patch will be initialized with any outstanding changes
2621 2621 in the working directory. You may also use -I/--include,
2622 2622 -X/--exclude, and/or a list of files after the patch name to add
2623 2623 only changes to matching files to the new patch, leaving the rest
2624 2624 as uncommitted modifications.
2625 2625
2626 2626 -u/--user and -d/--date can be used to set the (given) user and
2627 2627 date, respectively. -U/--currentuser and -D/--currentdate set user
2628 2628 to current user and date to current date.
2629 2629
2630 2630 -e/--edit, -m/--message or -l/--logfile set the patch header as
2631 2631 well as the commit message. If none is specified, the header is
2632 2632 empty and the commit message is '[mq]: PATCH'.
2633 2633
2634 2634 Use the -g/--git option to keep the patch in the git extended diff
2635 2635 format. Read the diffs help topic for more information on why this
2636 2636 is important for preserving permission changes and copy/rename
2637 2637 information.
2638 2638
2639 2639 Returns 0 on successful creation of a new patch.
2640 2640 """
2641 2641 opts = pycompat.byteskwargs(opts)
2642 2642 msg = cmdutil.logmessage(ui, opts)
2643 2643 q = repo.mq
2644 2644 opts['msg'] = msg
2645 2645 setupheaderopts(ui, opts)
2646 2646 q.new(repo, patch, *args, **pycompat.strkwargs(opts))
2647 2647 q.savedirty()
2648 2648 return 0
2649 2649
2650 2650 @command("^qrefresh",
2651 2651 [('e', 'edit', None, _('invoke editor on commit messages')),
2652 2652 ('g', 'git', None, _('use git extended diff format')),
2653 2653 ('s', 'short', None,
2654 2654 _('refresh only files already in the patch and specified files')),
2655 2655 ('U', 'currentuser', None,
2656 2656 _('add/update author field in patch with current user')),
2657 2657 ('u', 'user', '',
2658 2658 _('add/update author field in patch with given user'), _('USER')),
2659 2659 ('D', 'currentdate', None,
2660 2660 _('add/update date field in patch with current date')),
2661 2661 ('d', 'date', '',
2662 2662 _('add/update date field in patch with given date'), _('DATE'))
2663 2663 ] + cmdutil.walkopts + cmdutil.commitopts,
2664 2664 _('hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] [FILE]...'),
2665 2665 inferrepo=True)
2666 2666 def refresh(ui, repo, *pats, **opts):
2667 2667 """update the current patch
2668 2668
2669 2669 If any file patterns are provided, the refreshed patch will
2670 2670 contain only the modifications that match those patterns; the
2671 2671 remaining modifications will remain in the working directory.
2672 2672
2673 2673 If -s/--short is specified, files currently included in the patch
2674 2674 will be refreshed just like matched files and remain in the patch.
2675 2675
2676 2676 If -e/--edit is specified, Mercurial will start your configured editor for
2677 2677 you to enter a message. In case qrefresh fails, you will find a backup of
2678 2678 your message in ``.hg/last-message.txt``.
2679 2679
2680 2680 hg add/remove/copy/rename work as usual, though you might want to
2681 2681 use git-style patches (-g/--git or [diff] git=1) to track copies
2682 2682 and renames. See the diffs help topic for more information on the
2683 2683 git diff format.
2684 2684
2685 2685 Returns 0 on success.
2686 2686 """
2687 2687 opts = pycompat.byteskwargs(opts)
2688 2688 q = repo.mq
2689 2689 message = cmdutil.logmessage(ui, opts)
2690 2690 setupheaderopts(ui, opts)
2691 2691 with repo.wlock():
2692 2692 ret = q.refresh(repo, pats, msg=message, **pycompat.strkwargs(opts))
2693 2693 q.savedirty()
2694 2694 return ret
2695 2695
2696 2696 @command("^qdiff",
2697 2697 cmdutil.diffopts + cmdutil.diffopts2 + cmdutil.walkopts,
2698 2698 _('hg qdiff [OPTION]... [FILE]...'),
2699 2699 inferrepo=True)
2700 2700 def diff(ui, repo, *pats, **opts):
2701 2701 """diff of the current patch and subsequent modifications
2702 2702
2703 2703 Shows a diff which includes the current patch as well as any
2704 2704 changes which have been made in the working directory since the
2705 2705 last refresh (thus showing what the current patch would become
2706 2706 after a qrefresh).
2707 2707
2708 2708 Use :hg:`diff` if you only want to see the changes made since the
2709 2709 last qrefresh, or :hg:`export qtip` if you want to see changes
2710 2710 made by the current patch without including changes made since the
2711 2711 qrefresh.
2712 2712
2713 2713 Returns 0 on success.
2714 2714 """
2715 2715 ui.pager('qdiff')
2716 2716 repo.mq.diff(repo, pats, pycompat.byteskwargs(opts))
2717 2717 return 0
2718 2718
2719 2719 @command('qfold',
2720 2720 [('e', 'edit', None, _('invoke editor on commit messages')),
2721 2721 ('k', 'keep', None, _('keep folded patch files')),
2722 2722 ] + cmdutil.commitopts,
2723 2723 _('hg qfold [-e] [-k] [-m TEXT] [-l FILE] PATCH...'))
2724 2724 def fold(ui, repo, *files, **opts):
2725 2725 """fold the named patches into the current patch
2726 2726
2727 2727 Patches must not yet be applied. Each patch will be successively
2728 2728 applied to the current patch in the order given. If all the
2729 2729 patches apply successfully, the current patch will be refreshed
2730 2730 with the new cumulative patch, and the folded patches will be
2731 2731 deleted. With -k/--keep, the folded patch files will not be
2732 2732 removed afterwards.
2733 2733
2734 2734 The header for each folded patch will be concatenated with the
2735 2735 current patch header, separated by a line of ``* * *``.
2736 2736
2737 2737 Returns 0 on success."""
2738 2738 opts = pycompat.byteskwargs(opts)
2739 2739 q = repo.mq
2740 2740 if not files:
2741 2741 raise error.Abort(_('qfold requires at least one patch name'))
2742 2742 if not q.checktoppatch(repo)[0]:
2743 2743 raise error.Abort(_('no patches applied'))
2744 2744 q.checklocalchanges(repo)
2745 2745
2746 2746 message = cmdutil.logmessage(ui, opts)
2747 2747
2748 2748 parent = q.lookup('qtip')
2749 2749 patches = []
2750 2750 messages = []
2751 2751 for f in files:
2752 2752 p = q.lookup(f)
2753 2753 if p in patches or p == parent:
2754 2754 ui.warn(_('skipping already folded patch %s\n') % p)
2755 2755 if q.isapplied(p):
2756 2756 raise error.Abort(_('qfold cannot fold already applied patch %s')
2757 2757 % p)
2758 2758 patches.append(p)
2759 2759
2760 2760 for p in patches:
2761 2761 if not message:
2762 2762 ph = patchheader(q.join(p), q.plainmode)
2763 2763 if ph.message:
2764 2764 messages.append(ph.message)
2765 2765 pf = q.join(p)
2766 2766 (patchsuccess, files, fuzz) = q.patch(repo, pf)
2767 2767 if not patchsuccess:
2768 2768 raise error.Abort(_('error folding patch %s') % p)
2769 2769
2770 2770 if not message:
2771 2771 ph = patchheader(q.join(parent), q.plainmode)
2772 2772 message = ph.message
2773 2773 for msg in messages:
2774 2774 if msg:
2775 2775 if message:
2776 2776 message.append('* * *')
2777 2777 message.extend(msg)
2778 2778 message = '\n'.join(message)
2779 2779
2780 2780 diffopts = q.patchopts(q.diffopts(), *patches)
2781 2781 with repo.wlock():
2782 2782 q.refresh(repo, msg=message, git=diffopts.git, edit=opts.get('edit'),
2783 2783 editform='mq.qfold')
2784 2784 q.delete(repo, patches, opts)
2785 2785 q.savedirty()
2786 2786
2787 2787 @command("qgoto",
2788 2788 [('', 'keep-changes', None,
2789 2789 _('tolerate non-conflicting local changes')),
2790 2790 ('f', 'force', None, _('overwrite any local changes')),
2791 2791 ('', 'no-backup', None, _('do not save backup copies of files'))],
2792 2792 _('hg qgoto [OPTION]... PATCH'))
2793 2793 def goto(ui, repo, patch, **opts):
2794 2794 '''push or pop patches until named patch is at top of stack
2795 2795
2796 2796 Returns 0 on success.'''
2797 2797 opts = pycompat.byteskwargs(opts)
2798 2798 opts = fixkeepchangesopts(ui, opts)
2799 2799 q = repo.mq
2800 2800 patch = q.lookup(patch)
2801 2801 nobackup = opts.get('no_backup')
2802 2802 keepchanges = opts.get('keep_changes')
2803 2803 if q.isapplied(patch):
2804 2804 ret = q.pop(repo, patch, force=opts.get('force'), nobackup=nobackup,
2805 2805 keepchanges=keepchanges)
2806 2806 else:
2807 2807 ret = q.push(repo, patch, force=opts.get('force'), nobackup=nobackup,
2808 2808 keepchanges=keepchanges)
2809 2809 q.savedirty()
2810 2810 return ret
2811 2811
2812 2812 @command("qguard",
2813 2813 [('l', 'list', None, _('list all patches and guards')),
2814 2814 ('n', 'none', None, _('drop all guards'))],
2815 2815 _('hg qguard [-l] [-n] [PATCH] [-- [+GUARD]... [-GUARD]...]'))
2816 2816 def guard(ui, repo, *args, **opts):
2817 2817 '''set or print guards for a patch
2818 2818
2819 2819 Guards control whether a patch can be pushed. A patch with no
2820 2820 guards is always pushed. A patch with a positive guard ("+foo") is
2821 2821 pushed only if the :hg:`qselect` command has activated it. A patch with
2822 2822 a negative guard ("-foo") is never pushed if the :hg:`qselect` command
2823 2823 has activated it.
2824 2824
2825 2825 With no arguments, print the currently active guards.
2826 2826 With arguments, set guards for the named patch.
2827 2827
2828 2828 .. note::
2829 2829
2830 2830 Specifying negative guards now requires '--'.
2831 2831
2832 2832 To set guards on another patch::
2833 2833
2834 2834 hg qguard other.patch -- +2.6.17 -stable
2835 2835
2836 2836 Returns 0 on success.
2837 2837 '''
2838 2838 def status(idx):
2839 2839 guards = q.seriesguards[idx] or ['unguarded']
2840 2840 if q.series[idx] in applied:
2841 2841 state = 'applied'
2842 2842 elif q.pushable(idx)[0]:
2843 2843 state = 'unapplied'
2844 2844 else:
2845 2845 state = 'guarded'
2846 2846 label = 'qguard.patch qguard.%s qseries.%s' % (state, state)
2847 2847 ui.write('%s: ' % ui.label(q.series[idx], label))
2848 2848
2849 2849 for i, guard in enumerate(guards):
2850 2850 if guard.startswith('+'):
2851 2851 ui.write(guard, label='qguard.positive')
2852 2852 elif guard.startswith('-'):
2853 2853 ui.write(guard, label='qguard.negative')
2854 2854 else:
2855 2855 ui.write(guard, label='qguard.unguarded')
2856 2856 if i != len(guards) - 1:
2857 2857 ui.write(' ')
2858 2858 ui.write('\n')
2859 2859 q = repo.mq
2860 2860 applied = set(p.name for p in q.applied)
2861 2861 patch = None
2862 2862 args = list(args)
2863 2863 if opts.get(r'list'):
2864 2864 if args or opts.get('none'):
2865 2865 raise error.Abort(_('cannot mix -l/--list with options or '
2866 2866 'arguments'))
2867 2867 for i in xrange(len(q.series)):
2868 2868 status(i)
2869 2869 return
2870 2870 if not args or args[0][0:1] in '-+':
2871 2871 if not q.applied:
2872 2872 raise error.Abort(_('no patches applied'))
2873 2873 patch = q.applied[-1].name
2874 2874 if patch is None and args[0][0:1] not in '-+':
2875 2875 patch = args.pop(0)
2876 2876 if patch is None:
2877 2877 raise error.Abort(_('no patch to work with'))
2878 2878 if args or opts.get('none'):
2879 2879 idx = q.findseries(patch)
2880 2880 if idx is None:
2881 2881 raise error.Abort(_('no patch named %s') % patch)
2882 2882 q.setguards(idx, args)
2883 2883 q.savedirty()
2884 2884 else:
2885 2885 status(q.series.index(q.lookup(patch)))
2886 2886
2887 2887 @command("qheader", [], _('hg qheader [PATCH]'))
2888 2888 def header(ui, repo, patch=None):
2889 2889 """print the header of the topmost or specified patch
2890 2890
2891 2891 Returns 0 on success."""
2892 2892 q = repo.mq
2893 2893
2894 2894 if patch:
2895 2895 patch = q.lookup(patch)
2896 2896 else:
2897 2897 if not q.applied:
2898 2898 ui.write(_('no patches applied\n'))
2899 2899 return 1
2900 2900 patch = q.lookup('qtip')
2901 2901 ph = patchheader(q.join(patch), q.plainmode)
2902 2902
2903 2903 ui.write('\n'.join(ph.message) + '\n')
2904 2904
2905 2905 def lastsavename(path):
2906 2906 (directory, base) = os.path.split(path)
2907 2907 names = os.listdir(directory)
2908 2908 namere = re.compile("%s.([0-9]+)" % base)
2909 2909 maxindex = None
2910 2910 maxname = None
2911 2911 for f in names:
2912 2912 m = namere.match(f)
2913 2913 if m:
2914 2914 index = int(m.group(1))
2915 2915 if maxindex is None or index > maxindex:
2916 2916 maxindex = index
2917 2917 maxname = f
2918 2918 if maxname:
2919 2919 return (os.path.join(directory, maxname), maxindex)
2920 2920 return (None, None)
2921 2921
2922 2922 def savename(path):
2923 2923 (last, index) = lastsavename(path)
2924 2924 if last is None:
2925 2925 index = 0
2926 2926 newpath = path + ".%d" % (index + 1)
2927 2927 return newpath
2928 2928
2929 2929 @command("^qpush",
2930 2930 [('', 'keep-changes', None,
2931 2931 _('tolerate non-conflicting local changes')),
2932 2932 ('f', 'force', None, _('apply on top of local changes')),
2933 2933 ('e', 'exact', None,
2934 2934 _('apply the target patch to its recorded parent')),
2935 2935 ('l', 'list', None, _('list patch name in commit text')),
2936 2936 ('a', 'all', None, _('apply all patches')),
2937 2937 ('m', 'merge', None, _('merge from another queue (DEPRECATED)')),
2938 2938 ('n', 'name', '',
2939 2939 _('merge queue name (DEPRECATED)'), _('NAME')),
2940 2940 ('', 'move', None,
2941 2941 _('reorder patch series and apply only the patch')),
2942 2942 ('', 'no-backup', None, _('do not save backup copies of files'))],
2943 2943 _('hg qpush [-f] [-l] [-a] [--move] [PATCH | INDEX]'))
2944 2944 def push(ui, repo, patch=None, **opts):
2945 2945 """push the next patch onto the stack
2946 2946
2947 2947 By default, abort if the working directory contains uncommitted
2948 2948 changes. With --keep-changes, abort only if the uncommitted files
2949 2949 overlap with patched files. With -f/--force, backup and patch over
2950 2950 uncommitted changes.
2951 2951
2952 2952 Return 0 on success.
2953 2953 """
2954 2954 q = repo.mq
2955 2955 mergeq = None
2956 2956
2957 2957 opts = pycompat.byteskwargs(opts)
2958 2958 opts = fixkeepchangesopts(ui, opts)
2959 2959 if opts.get('merge'):
2960 2960 if opts.get('name'):
2961 2961 newpath = repo.vfs.join(opts.get('name'))
2962 2962 else:
2963 2963 newpath, i = lastsavename(q.path)
2964 2964 if not newpath:
2965 2965 ui.warn(_("no saved queues found, please use -n\n"))
2966 2966 return 1
2967 2967 mergeq = queue(ui, repo.baseui, repo.path, newpath)
2968 2968 ui.warn(_("merging with queue at: %s\n") % mergeq.path)
2969 2969 ret = q.push(repo, patch, force=opts.get('force'), list=opts.get('list'),
2970 2970 mergeq=mergeq, all=opts.get('all'), move=opts.get('move'),
2971 2971 exact=opts.get('exact'), nobackup=opts.get('no_backup'),
2972 2972 keepchanges=opts.get('keep_changes'))
2973 2973 return ret
2974 2974
2975 2975 @command("^qpop",
2976 2976 [('a', 'all', None, _('pop all patches')),
2977 2977 ('n', 'name', '',
2978 2978 _('queue name to pop (DEPRECATED)'), _('NAME')),
2979 2979 ('', 'keep-changes', None,
2980 2980 _('tolerate non-conflicting local changes')),
2981 2981 ('f', 'force', None, _('forget any local changes to patched files')),
2982 2982 ('', 'no-backup', None, _('do not save backup copies of files'))],
2983 2983 _('hg qpop [-a] [-f] [PATCH | INDEX]'))
2984 2984 def pop(ui, repo, patch=None, **opts):
2985 2985 """pop the current patch off the stack
2986 2986
2987 2987 Without argument, pops off the top of the patch stack. If given a
2988 2988 patch name, keeps popping off patches until the named patch is at
2989 2989 the top of the stack.
2990 2990
2991 2991 By default, abort if the working directory contains uncommitted
2992 2992 changes. With --keep-changes, abort only if the uncommitted files
2993 2993 overlap with patched files. With -f/--force, backup and discard
2994 2994 changes made to such files.
2995 2995
2996 2996 Return 0 on success.
2997 2997 """
2998 2998 opts = pycompat.byteskwargs(opts)
2999 2999 opts = fixkeepchangesopts(ui, opts)
3000 3000 localupdate = True
3001 3001 if opts.get('name'):
3002 3002 q = queue(ui, repo.baseui, repo.path, repo.vfs.join(opts.get('name')))
3003 3003 ui.warn(_('using patch queue: %s\n') % q.path)
3004 3004 localupdate = False
3005 3005 else:
3006 3006 q = repo.mq
3007 3007 ret = q.pop(repo, patch, force=opts.get('force'), update=localupdate,
3008 3008 all=opts.get('all'), nobackup=opts.get('no_backup'),
3009 3009 keepchanges=opts.get('keep_changes'))
3010 3010 q.savedirty()
3011 3011 return ret
3012 3012
3013 3013 @command("qrename|qmv", [], _('hg qrename PATCH1 [PATCH2]'))
3014 3014 def rename(ui, repo, patch, name=None, **opts):
3015 3015 """rename a patch
3016 3016
3017 3017 With one argument, renames the current patch to PATCH1.
3018 3018 With two arguments, renames PATCH1 to PATCH2.
3019 3019
3020 3020 Returns 0 on success."""
3021 3021 q = repo.mq
3022 3022 if not name:
3023 3023 name = patch
3024 3024 patch = None
3025 3025
3026 3026 if patch:
3027 3027 patch = q.lookup(patch)
3028 3028 else:
3029 3029 if not q.applied:
3030 3030 ui.write(_('no patches applied\n'))
3031 3031 return
3032 3032 patch = q.lookup('qtip')
3033 3033 absdest = q.join(name)
3034 3034 if os.path.isdir(absdest):
3035 3035 name = normname(os.path.join(name, os.path.basename(patch)))
3036 3036 absdest = q.join(name)
3037 3037 q.checkpatchname(name)
3038 3038
3039 3039 ui.note(_('renaming %s to %s\n') % (patch, name))
3040 3040 i = q.findseries(patch)
3041 3041 guards = q.guard_re.findall(q.fullseries[i])
3042 3042 q.fullseries[i] = name + ''.join([' #' + g for g in guards])
3043 3043 q.parseseries()
3044 3044 q.seriesdirty = True
3045 3045
3046 3046 info = q.isapplied(patch)
3047 3047 if info:
3048 3048 q.applied[info[0]] = statusentry(info[1], name)
3049 3049 q.applieddirty = True
3050 3050
3051 3051 destdir = os.path.dirname(absdest)
3052 3052 if not os.path.isdir(destdir):
3053 3053 os.makedirs(destdir)
3054 3054 util.rename(q.join(patch), absdest)
3055 3055 r = q.qrepo()
3056 3056 if r and patch in r.dirstate:
3057 3057 wctx = r[None]
3058 3058 with r.wlock():
3059 3059 if r.dirstate[patch] == 'a':
3060 3060 r.dirstate.drop(patch)
3061 3061 r.dirstate.add(name)
3062 3062 else:
3063 3063 wctx.copy(patch, name)
3064 3064 wctx.forget([patch])
3065 3065
3066 3066 q.savedirty()
3067 3067
3068 3068 @command("qrestore",
3069 3069 [('d', 'delete', None, _('delete save entry')),
3070 3070 ('u', 'update', None, _('update queue working directory'))],
3071 3071 _('hg qrestore [-d] [-u] REV'))
3072 3072 def restore(ui, repo, rev, **opts):
3073 3073 """restore the queue state saved by a revision (DEPRECATED)
3074 3074
3075 3075 This command is deprecated, use :hg:`rebase` instead."""
3076 3076 rev = repo.lookup(rev)
3077 3077 q = repo.mq
3078 3078 q.restore(repo, rev, delete=opts.get(r'delete'),
3079 3079 qupdate=opts.get(r'update'))
3080 3080 q.savedirty()
3081 3081 return 0
3082 3082
3083 3083 @command("qsave",
3084 3084 [('c', 'copy', None, _('copy patch directory')),
3085 3085 ('n', 'name', '',
3086 3086 _('copy directory name'), _('NAME')),
3087 3087 ('e', 'empty', None, _('clear queue status file')),
3088 3088 ('f', 'force', None, _('force copy'))] + cmdutil.commitopts,
3089 3089 _('hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]'))
3090 3090 def save(ui, repo, **opts):
3091 3091 """save current queue state (DEPRECATED)
3092 3092
3093 3093 This command is deprecated, use :hg:`rebase` instead."""
3094 3094 q = repo.mq
3095 3095 opts = pycompat.byteskwargs(opts)
3096 3096 message = cmdutil.logmessage(ui, opts)
3097 3097 ret = q.save(repo, msg=message)
3098 3098 if ret:
3099 3099 return ret
3100 3100 q.savedirty() # save to .hg/patches before copying
3101 3101 if opts.get('copy'):
3102 3102 path = q.path
3103 3103 if opts.get('name'):
3104 3104 newpath = os.path.join(q.basepath, opts.get('name'))
3105 3105 if os.path.exists(newpath):
3106 3106 if not os.path.isdir(newpath):
3107 3107 raise error.Abort(_('destination %s exists and is not '
3108 3108 'a directory') % newpath)
3109 3109 if not opts.get('force'):
3110 3110 raise error.Abort(_('destination %s exists, '
3111 3111 'use -f to force') % newpath)
3112 3112 else:
3113 3113 newpath = savename(path)
3114 3114 ui.warn(_("copy %s to %s\n") % (path, newpath))
3115 3115 util.copyfiles(path, newpath)
3116 3116 if opts.get('empty'):
3117 3117 del q.applied[:]
3118 3118 q.applieddirty = True
3119 3119 q.savedirty()
3120 3120 return 0
3121 3121
3122 3122
3123 3123 @command("qselect",
3124 3124 [('n', 'none', None, _('disable all guards')),
3125 3125 ('s', 'series', None, _('list all guards in series file')),
3126 3126 ('', 'pop', None, _('pop to before first guarded applied patch')),
3127 3127 ('', 'reapply', None, _('pop, then reapply patches'))],
3128 3128 _('hg qselect [OPTION]... [GUARD]...'))
3129 3129 def select(ui, repo, *args, **opts):
3130 3130 '''set or print guarded patches to push
3131 3131
3132 3132 Use the :hg:`qguard` command to set or print guards on patch, then use
3133 3133 qselect to tell mq which guards to use. A patch will be pushed if
3134 3134 it has no guards or any positive guards match the currently
3135 3135 selected guard, but will not be pushed if any negative guards
3136 3136 match the current guard. For example::
3137 3137
3138 3138 qguard foo.patch -- -stable (negative guard)
3139 3139 qguard bar.patch +stable (positive guard)
3140 3140 qselect stable
3141 3141
3142 3142 This activates the "stable" guard. mq will skip foo.patch (because
3143 3143 it has a negative match) but push bar.patch (because it has a
3144 3144 positive match).
3145 3145
3146 3146 With no arguments, prints the currently active guards.
3147 3147 With one argument, sets the active guard.
3148 3148
3149 3149 Use -n/--none to deactivate guards (no other arguments needed).
3150 3150 When no guards are active, patches with positive guards are
3151 3151 skipped and patches with negative guards are pushed.
3152 3152
3153 3153 qselect can change the guards on applied patches. It does not pop
3154 3154 guarded patches by default. Use --pop to pop back to the last
3155 3155 applied patch that is not guarded. Use --reapply (which implies
3156 3156 --pop) to push back to the current patch afterwards, but skip
3157 3157 guarded patches.
3158 3158
3159 3159 Use -s/--series to print a list of all guards in the series file
3160 3160 (no other arguments needed). Use -v for more information.
3161 3161
3162 3162 Returns 0 on success.'''
3163 3163
3164 3164 q = repo.mq
3165 3165 opts = pycompat.byteskwargs(opts)
3166 3166 guards = q.active()
3167 3167 pushable = lambda i: q.pushable(q.applied[i].name)[0]
3168 3168 if args or opts.get('none'):
3169 3169 old_unapplied = q.unapplied(repo)
3170 3170 old_guarded = [i for i in xrange(len(q.applied)) if not pushable(i)]
3171 3171 q.setactive(args)
3172 3172 q.savedirty()
3173 3173 if not args:
3174 3174 ui.status(_('guards deactivated\n'))
3175 3175 if not opts.get('pop') and not opts.get('reapply'):
3176 3176 unapplied = q.unapplied(repo)
3177 3177 guarded = [i for i in xrange(len(q.applied)) if not pushable(i)]
3178 3178 if len(unapplied) != len(old_unapplied):
3179 3179 ui.status(_('number of unguarded, unapplied patches has '
3180 3180 'changed from %d to %d\n') %
3181 3181 (len(old_unapplied), len(unapplied)))
3182 3182 if len(guarded) != len(old_guarded):
3183 3183 ui.status(_('number of guarded, applied patches has changed '
3184 3184 'from %d to %d\n') %
3185 3185 (len(old_guarded), len(guarded)))
3186 3186 elif opts.get('series'):
3187 3187 guards = {}
3188 3188 noguards = 0
3189 3189 for gs in q.seriesguards:
3190 3190 if not gs:
3191 3191 noguards += 1
3192 3192 for g in gs:
3193 3193 guards.setdefault(g, 0)
3194 3194 guards[g] += 1
3195 3195 if ui.verbose:
3196 3196 guards['NONE'] = noguards
3197 3197 guards = list(guards.items())
3198 3198 guards.sort(key=lambda x: x[0][1:])
3199 3199 if guards:
3200 3200 ui.note(_('guards in series file:\n'))
3201 3201 for guard, count in guards:
3202 3202 ui.note('%2d ' % count)
3203 3203 ui.write(guard, '\n')
3204 3204 else:
3205 3205 ui.note(_('no guards in series file\n'))
3206 3206 else:
3207 3207 if guards:
3208 3208 ui.note(_('active guards:\n'))
3209 3209 for g in guards:
3210 3210 ui.write(g, '\n')
3211 3211 else:
3212 3212 ui.write(_('no active guards\n'))
3213 3213 reapply = opts.get('reapply') and q.applied and q.applied[-1].name
3214 3214 popped = False
3215 3215 if opts.get('pop') or opts.get('reapply'):
3216 3216 for i in xrange(len(q.applied)):
3217 3217 if not pushable(i):
3218 3218 ui.status(_('popping guarded patches\n'))
3219 3219 popped = True
3220 3220 if i == 0:
3221 3221 q.pop(repo, all=True)
3222 3222 else:
3223 3223 q.pop(repo, q.applied[i - 1].name)
3224 3224 break
3225 3225 if popped:
3226 3226 try:
3227 3227 if reapply:
3228 3228 ui.status(_('reapplying unguarded patches\n'))
3229 3229 q.push(repo, reapply)
3230 3230 finally:
3231 3231 q.savedirty()
3232 3232
3233 3233 @command("qfinish",
3234 3234 [('a', 'applied', None, _('finish all applied changesets'))],
3235 3235 _('hg qfinish [-a] [REV]...'))
3236 3236 def finish(ui, repo, *revrange, **opts):
3237 3237 """move applied patches into repository history
3238 3238
3239 3239 Finishes the specified revisions (corresponding to applied
3240 3240 patches) by moving them out of mq control into regular repository
3241 3241 history.
3242 3242
3243 3243 Accepts a revision range or the -a/--applied option. If --applied
3244 3244 is specified, all applied mq revisions are removed from mq
3245 3245 control. Otherwise, the given revisions must be at the base of the
3246 3246 stack of applied patches.
3247 3247
3248 3248 This can be especially useful if your changes have been applied to
3249 3249 an upstream repository, or if you are about to push your changes
3250 3250 to upstream.
3251 3251
3252 3252 Returns 0 on success.
3253 3253 """
3254 3254 if not opts.get(r'applied') and not revrange:
3255 3255 raise error.Abort(_('no revisions specified'))
3256 3256 elif opts.get(r'applied'):
3257 3257 revrange = ('qbase::qtip',) + revrange
3258 3258
3259 3259 q = repo.mq
3260 3260 if not q.applied:
3261 3261 ui.status(_('no patches applied\n'))
3262 3262 return 0
3263 3263
3264 3264 revs = scmutil.revrange(repo, revrange)
3265 3265 if repo['.'].rev() in revs and repo[None].files():
3266 3266 ui.warn(_('warning: uncommitted changes in the working directory\n'))
3267 3267 # queue.finish may changes phases but leave the responsibility to lock the
3268 3268 # repo to the caller to avoid deadlock with wlock. This command code is
3269 3269 # responsibility for this locking.
3270 3270 with repo.lock():
3271 3271 q.finish(repo, revs)
3272 3272 q.savedirty()
3273 3273 return 0
3274 3274
3275 3275 @command("qqueue",
3276 3276 [('l', 'list', False, _('list all available queues')),
3277 3277 ('', 'active', False, _('print name of active queue')),
3278 3278 ('c', 'create', False, _('create new queue')),
3279 3279 ('', 'rename', False, _('rename active queue')),
3280 3280 ('', 'delete', False, _('delete reference to queue')),
3281 3281 ('', 'purge', False, _('delete queue, and remove patch dir')),
3282 3282 ],
3283 3283 _('[OPTION] [QUEUE]'))
3284 3284 def qqueue(ui, repo, name=None, **opts):
3285 3285 '''manage multiple patch queues
3286 3286
3287 3287 Supports switching between different patch queues, as well as creating
3288 3288 new patch queues and deleting existing ones.
3289 3289
3290 3290 Omitting a queue name or specifying -l/--list will show you the registered
3291 3291 queues - by default the "normal" patches queue is registered. The currently
3292 3292 active queue will be marked with "(active)". Specifying --active will print
3293 3293 only the name of the active queue.
3294 3294
3295 3295 To create a new queue, use -c/--create. The queue is automatically made
3296 3296 active, except in the case where there are applied patches from the
3297 3297 currently active queue in the repository. Then the queue will only be
3298 3298 created and switching will fail.
3299 3299
3300 3300 To delete an existing queue, use --delete. You cannot delete the currently
3301 3301 active queue.
3302 3302
3303 3303 Returns 0 on success.
3304 3304 '''
3305 3305 q = repo.mq
3306 3306 _defaultqueue = 'patches'
3307 3307 _allqueues = 'patches.queues'
3308 3308 _activequeue = 'patches.queue'
3309 3309
3310 3310 def _getcurrent():
3311 3311 cur = os.path.basename(q.path)
3312 3312 if cur.startswith('patches-'):
3313 3313 cur = cur[8:]
3314 3314 return cur
3315 3315
3316 3316 def _noqueues():
3317 3317 try:
3318 3318 fh = repo.vfs(_allqueues, 'r')
3319 3319 fh.close()
3320 3320 except IOError:
3321 3321 return True
3322 3322
3323 3323 return False
3324 3324
3325 3325 def _getqueues():
3326 3326 current = _getcurrent()
3327 3327
3328 3328 try:
3329 3329 fh = repo.vfs(_allqueues, 'r')
3330 3330 queues = [queue.strip() for queue in fh if queue.strip()]
3331 3331 fh.close()
3332 3332 if current not in queues:
3333 3333 queues.append(current)
3334 3334 except IOError:
3335 3335 queues = [_defaultqueue]
3336 3336
3337 3337 return sorted(queues)
3338 3338
3339 3339 def _setactive(name):
3340 3340 if q.applied:
3341 3341 raise error.Abort(_('new queue created, but cannot make active '
3342 3342 'as patches are applied'))
3343 3343 _setactivenocheck(name)
3344 3344
3345 3345 def _setactivenocheck(name):
3346 3346 fh = repo.vfs(_activequeue, 'w')
3347 3347 if name != 'patches':
3348 3348 fh.write(name)
3349 3349 fh.close()
3350 3350
3351 3351 def _addqueue(name):
3352 3352 fh = repo.vfs(_allqueues, 'a')
3353 3353 fh.write('%s\n' % (name,))
3354 3354 fh.close()
3355 3355
3356 3356 def _queuedir(name):
3357 3357 if name == 'patches':
3358 3358 return repo.vfs.join('patches')
3359 3359 else:
3360 3360 return repo.vfs.join('patches-' + name)
3361 3361
3362 3362 def _validname(name):
3363 3363 for n in name:
3364 3364 if n in ':\\/.':
3365 3365 return False
3366 3366 return True
3367 3367
3368 3368 def _delete(name):
3369 3369 if name not in existing:
3370 3370 raise error.Abort(_('cannot delete queue that does not exist'))
3371 3371
3372 3372 current = _getcurrent()
3373 3373
3374 3374 if name == current:
3375 3375 raise error.Abort(_('cannot delete currently active queue'))
3376 3376
3377 3377 fh = repo.vfs('patches.queues.new', 'w')
3378 3378 for queue in existing:
3379 3379 if queue == name:
3380 3380 continue
3381 3381 fh.write('%s\n' % (queue,))
3382 3382 fh.close()
3383 3383 repo.vfs.rename('patches.queues.new', _allqueues)
3384 3384
3385 3385 opts = pycompat.byteskwargs(opts)
3386 3386 if not name or opts.get('list') or opts.get('active'):
3387 3387 current = _getcurrent()
3388 3388 if opts.get('active'):
3389 3389 ui.write('%s\n' % (current,))
3390 3390 return
3391 3391 for queue in _getqueues():
3392 3392 ui.write('%s' % (queue,))
3393 3393 if queue == current and not ui.quiet:
3394 3394 ui.write(_(' (active)\n'))
3395 3395 else:
3396 3396 ui.write('\n')
3397 3397 return
3398 3398
3399 3399 if not _validname(name):
3400 3400 raise error.Abort(
3401 3401 _('invalid queue name, may not contain the characters ":\\/."'))
3402 3402
3403 3403 with repo.wlock():
3404 3404 existing = _getqueues()
3405 3405
3406 3406 if opts.get('create'):
3407 3407 if name in existing:
3408 3408 raise error.Abort(_('queue "%s" already exists') % name)
3409 3409 if _noqueues():
3410 3410 _addqueue(_defaultqueue)
3411 3411 _addqueue(name)
3412 3412 _setactive(name)
3413 3413 elif opts.get('rename'):
3414 3414 current = _getcurrent()
3415 3415 if name == current:
3416 3416 raise error.Abort(_('can\'t rename "%s" to its current name')
3417 3417 % name)
3418 3418 if name in existing:
3419 3419 raise error.Abort(_('queue "%s" already exists') % name)
3420 3420
3421 3421 olddir = _queuedir(current)
3422 3422 newdir = _queuedir(name)
3423 3423
3424 3424 if os.path.exists(newdir):
3425 3425 raise error.Abort(_('non-queue directory "%s" already exists') %
3426 3426 newdir)
3427 3427
3428 3428 fh = repo.vfs('patches.queues.new', 'w')
3429 3429 for queue in existing:
3430 3430 if queue == current:
3431 3431 fh.write('%s\n' % (name,))
3432 3432 if os.path.exists(olddir):
3433 3433 util.rename(olddir, newdir)
3434 3434 else:
3435 3435 fh.write('%s\n' % (queue,))
3436 3436 fh.close()
3437 3437 repo.vfs.rename('patches.queues.new', _allqueues)
3438 3438 _setactivenocheck(name)
3439 3439 elif opts.get('delete'):
3440 3440 _delete(name)
3441 3441 elif opts.get('purge'):
3442 3442 if name in existing:
3443 3443 _delete(name)
3444 3444 qdir = _queuedir(name)
3445 3445 if os.path.exists(qdir):
3446 3446 shutil.rmtree(qdir)
3447 3447 else:
3448 3448 if name not in existing:
3449 3449 raise error.Abort(_('use --create to create a new queue'))
3450 3450 _setactive(name)
3451 3451
3452 3452 def mqphasedefaults(repo, roots):
3453 3453 """callback used to set mq changeset as secret when no phase data exists"""
3454 3454 if repo.mq.applied:
3455 3455 if repo.ui.configbool('mq', 'secret'):
3456 3456 mqphase = phases.secret
3457 3457 else:
3458 3458 mqphase = phases.draft
3459 3459 qbase = repo[repo.mq.applied[0].node]
3460 3460 roots[mqphase].add(qbase.node())
3461 3461 return roots
3462 3462
3463 3463 def reposetup(ui, repo):
3464 3464 class mqrepo(repo.__class__):
3465 3465 @localrepo.unfilteredpropertycache
3466 3466 def mq(self):
3467 3467 return queue(self.ui, self.baseui, self.path)
3468 3468
3469 3469 def invalidateall(self):
3470 3470 super(mqrepo, self).invalidateall()
3471 3471 if localrepo.hasunfilteredcache(self, 'mq'):
3472 3472 # recreate mq in case queue path was changed
3473 3473 delattr(self.unfiltered(), 'mq')
3474 3474
3475 3475 def abortifwdirpatched(self, errmsg, force=False):
3476 3476 if self.mq.applied and self.mq.checkapplied and not force:
3477 3477 parents = self.dirstate.parents()
3478 3478 patches = [s.node for s in self.mq.applied]
3479 3479 if parents[0] in patches or parents[1] in patches:
3480 3480 raise error.Abort(errmsg)
3481 3481
3482 3482 def commit(self, text="", user=None, date=None, match=None,
3483 3483 force=False, editor=False, extra=None):
3484 3484 if extra is None:
3485 3485 extra = {}
3486 3486 self.abortifwdirpatched(
3487 3487 _('cannot commit over an applied mq patch'),
3488 3488 force)
3489 3489
3490 3490 return super(mqrepo, self).commit(text, user, date, match, force,
3491 3491 editor, extra)
3492 3492
3493 3493 def checkpush(self, pushop):
3494 3494 if self.mq.applied and self.mq.checkapplied and not pushop.force:
3495 3495 outapplied = [e.node for e in self.mq.applied]
3496 3496 if pushop.revs:
3497 3497 # Assume applied patches have no non-patch descendants and
3498 3498 # are not on remote already. Filtering any changeset not
3499 3499 # pushed.
3500 3500 heads = set(pushop.revs)
3501 3501 for node in reversed(outapplied):
3502 3502 if node in heads:
3503 3503 break
3504 3504 else:
3505 3505 outapplied.pop()
3506 3506 # looking for pushed and shared changeset
3507 3507 for node in outapplied:
3508 3508 if self[node].phase() < phases.secret:
3509 3509 raise error.Abort(_('source has mq patches applied'))
3510 3510 # no non-secret patches pushed
3511 3511 super(mqrepo, self).checkpush(pushop)
3512 3512
3513 3513 def _findtags(self):
3514 3514 '''augment tags from base class with patch tags'''
3515 3515 result = super(mqrepo, self)._findtags()
3516 3516
3517 3517 q = self.mq
3518 3518 if not q.applied:
3519 3519 return result
3520 3520
3521 3521 mqtags = [(patch.node, patch.name) for patch in q.applied]
3522 3522
3523 3523 try:
3524 3524 # for now ignore filtering business
3525 3525 self.unfiltered().changelog.rev(mqtags[-1][0])
3526 3526 except error.LookupError:
3527 3527 self.ui.warn(_('mq status file refers to unknown node %s\n')
3528 3528 % short(mqtags[-1][0]))
3529 3529 return result
3530 3530
3531 3531 # do not add fake tags for filtered revisions
3532 3532 included = self.changelog.hasnode
3533 3533 mqtags = [mqt for mqt in mqtags if included(mqt[0])]
3534 3534 if not mqtags:
3535 3535 return result
3536 3536
3537 3537 mqtags.append((mqtags[-1][0], 'qtip'))
3538 3538 mqtags.append((mqtags[0][0], 'qbase'))
3539 3539 mqtags.append((self.changelog.parents(mqtags[0][0])[0], 'qparent'))
3540 3540 tags = result[0]
3541 3541 for patch in mqtags:
3542 3542 if patch[1] in tags:
3543 3543 self.ui.warn(_('tag %s overrides mq patch of the same '
3544 3544 'name\n') % patch[1])
3545 3545 else:
3546 3546 tags[patch[1]] = patch[0]
3547 3547
3548 3548 return result
3549 3549
3550 3550 if repo.local():
3551 3551 repo.__class__ = mqrepo
3552 3552
3553 3553 repo._phasedefaults.append(mqphasedefaults)
3554 3554
3555 3555 def mqimport(orig, ui, repo, *args, **kwargs):
3556 3556 if (util.safehasattr(repo, 'abortifwdirpatched')
3557 3557 and not kwargs.get(r'no_commit', False)):
3558 3558 repo.abortifwdirpatched(_('cannot import over an applied patch'),
3559 3559 kwargs.get(r'force'))
3560 3560 return orig(ui, repo, *args, **kwargs)
3561 3561
3562 3562 def mqinit(orig, ui, *args, **kwargs):
3563 3563 mq = kwargs.pop(r'mq', None)
3564 3564
3565 3565 if not mq:
3566 3566 return orig(ui, *args, **kwargs)
3567 3567
3568 3568 if args:
3569 3569 repopath = args[0]
3570 3570 if not hg.islocal(repopath):
3571 3571 raise error.Abort(_('only a local queue repository '
3572 3572 'may be initialized'))
3573 3573 else:
3574 3574 repopath = cmdutil.findrepo(pycompat.getcwd())
3575 3575 if not repopath:
3576 3576 raise error.Abort(_('there is no Mercurial repository here '
3577 3577 '(.hg not found)'))
3578 3578 repo = hg.repository(ui, repopath)
3579 3579 return qinit(ui, repo, True)
3580 3580
3581 3581 def mqcommand(orig, ui, repo, *args, **kwargs):
3582 3582 """Add --mq option to operate on patch repository instead of main"""
3583 3583
3584 3584 # some commands do not like getting unknown options
3585 3585 mq = kwargs.pop(r'mq', None)
3586 3586
3587 3587 if not mq:
3588 3588 return orig(ui, repo, *args, **kwargs)
3589 3589
3590 3590 q = repo.mq
3591 3591 r = q.qrepo()
3592 3592 if not r:
3593 3593 raise error.Abort(_('no queue repository'))
3594 3594 return orig(r.ui, r, *args, **kwargs)
3595 3595
3596 3596 def summaryhook(ui, repo):
3597 3597 q = repo.mq
3598 3598 m = []
3599 3599 a, u = len(q.applied), len(q.unapplied(repo))
3600 3600 if a:
3601 3601 m.append(ui.label(_("%d applied"), 'qseries.applied') % a)
3602 3602 if u:
3603 3603 m.append(ui.label(_("%d unapplied"), 'qseries.unapplied') % u)
3604 3604 if m:
3605 3605 # i18n: column positioning for "hg summary"
3606 3606 ui.write(_("mq: %s\n") % ', '.join(m))
3607 3607 else:
3608 3608 # i18n: column positioning for "hg summary"
3609 3609 ui.note(_("mq: (empty queue)\n"))
3610 3610
3611 3611 revsetpredicate = registrar.revsetpredicate()
3612 3612
3613 3613 @revsetpredicate('mq()')
3614 3614 def revsetmq(repo, subset, x):
3615 3615 """Changesets managed by MQ.
3616 3616 """
3617 3617 revsetlang.getargs(x, 0, 0, _("mq takes no arguments"))
3618 3618 applied = set([repo[r.node].rev() for r in repo.mq.applied])
3619 3619 return smartset.baseset([r for r in subset if r in applied])
3620 3620
3621 3621 # tell hggettext to extract docstrings from these functions:
3622 3622 i18nfunctions = [revsetmq]
3623 3623
3624 3624 def extsetup(ui):
3625 3625 # Ensure mq wrappers are called first, regardless of extension load order by
3626 3626 # NOT wrapping in uisetup() and instead deferring to init stage two here.
3627 3627 mqopt = [('', 'mq', None, _("operate on patch repository"))]
3628 3628
3629 3629 extensions.wrapcommand(commands.table, 'import', mqimport)
3630 3630 cmdutil.summaryhooks.add('mq', summaryhook)
3631 3631
3632 3632 entry = extensions.wrapcommand(commands.table, 'init', mqinit)
3633 3633 entry[1].extend(mqopt)
3634 3634
3635 3635 def dotable(cmdtable):
3636 3636 for cmd, entry in cmdtable.iteritems():
3637 3637 cmd = cmdutil.parsealiases(cmd)[0]
3638 3638 func = entry[0]
3639 3639 if func.norepo:
3640 3640 continue
3641 3641 entry = extensions.wrapcommand(cmdtable, cmd, mqcommand)
3642 3642 entry[1].extend(mqopt)
3643 3643
3644 3644 dotable(commands.table)
3645 3645
3646 3646 for extname, extmodule in extensions.extensions():
3647 3647 if extmodule.__file__ != __file__:
3648 3648 dotable(getattr(extmodule, 'cmdtable', {}))
3649 3649
3650 3650 colortable = {'qguard.negative': 'red',
3651 3651 'qguard.positive': 'yellow',
3652 3652 'qguard.unguarded': 'green',
3653 3653 'qseries.applied': 'blue bold underline',
3654 3654 'qseries.guarded': 'black bold',
3655 3655 'qseries.missing': 'red bold',
3656 3656 'qseries.unapplied': 'black bold'}
General Comments 0
You need to be logged in to leave comments. Login now