##// END OF EJS Templates
merge with stable
Matt Mackall -
r20012:a1d88278 merge default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,528 +1,547 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # check-code - a style and portability checker for Mercurial
4 4 #
5 5 # Copyright 2010 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 import re, glob, os, sys
11 11 import keyword
12 12 import optparse
13 13 try:
14 14 import re2
15 15 except ImportError:
16 16 re2 = None
17 17
18 18 def compilere(pat, multiline=False):
19 19 if multiline:
20 20 pat = '(?m)' + pat
21 21 if re2:
22 22 try:
23 23 return re2.compile(pat)
24 24 except re2.error:
25 25 pass
26 26 return re.compile(pat)
27 27
28 28 def repquote(m):
29 t = re.sub(r"\w", "x", m.group('text'))
30 t = re.sub(r"[^\s\nx]", "o", t)
29 fromc = '.:'
30 tochr = 'pq'
31 def encodechr(i):
32 if i > 255:
33 return 'u'
34 c = chr(i)
35 if c in ' \n':
36 return c
37 if c.isalpha():
38 return 'x'
39 if c.isdigit():
40 return 'n'
41 try:
42 return tochr[fromc.find(c)]
43 except (ValueError, IndexError):
44 return 'o'
45 t = m.group('text')
46 tt = ''.join(encodechr(i) for i in xrange(256))
47 t = t.translate(tt)
31 48 return m.group('quote') + t + m.group('quote')
32 49
33 50 def reppython(m):
34 51 comment = m.group('comment')
35 52 if comment:
36 53 l = len(comment.rstrip())
37 54 return "#" * l + comment[l:]
38 55 return repquote(m)
39 56
40 57 def repcomment(m):
41 58 return m.group(1) + "#" * len(m.group(2))
42 59
43 60 def repccomment(m):
44 61 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
45 62 return m.group(1) + t + "*/"
46 63
47 64 def repcallspaces(m):
48 65 t = re.sub(r"\n\s+", "\n", m.group(2))
49 66 return m.group(1) + t
50 67
51 68 def repinclude(m):
52 69 return m.group(1) + "<foo>"
53 70
54 71 def rephere(m):
55 72 t = re.sub(r"\S", "x", m.group(2))
56 73 return m.group(1) + t
57 74
58 75
59 76 testpats = [
60 77 [
61 78 (r'pushd|popd', "don't use 'pushd' or 'popd', use 'cd'"),
62 79 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
63 80 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
64 81 (r'(?<!hg )grep.*-a', "don't use 'grep -a', use in-line python"),
65 82 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
66 83 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
67 84 (r'echo -n', "don't use 'echo -n', use printf"),
68 85 (r'(^| )wc[^|]*$\n(?!.*\(re\))', "filter wc output"),
69 86 (r'head -c', "don't use 'head -c', use 'dd'"),
70 87 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
71 88 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
72 89 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
73 90 (r'printf.*[^\\]\\([1-9]|0\d)', "don't use 'printf \NNN', use Python"),
74 91 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
75 92 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
76 93 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
77 94 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
78 95 "use egrep for extended grep syntax"),
79 96 (r'/bin/', "don't use explicit paths for tools"),
80 97 (r'[^\n]\Z', "no trailing newline"),
81 98 (r'export.*=', "don't export and assign at once"),
82 99 (r'^source\b', "don't use 'source', use '.'"),
83 100 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
84 101 (r'ls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
85 102 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
86 103 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
87 104 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
88 105 (r'^alias\b.*=', "don't use alias, use a function"),
89 106 (r'if\s*!', "don't use '!' to negate exit status"),
90 107 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
91 108 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
92 109 (r'^( *)\t', "don't use tabs to indent"),
93 110 (r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
94 111 "put a backslash-escaped newline after sed 'i' command"),
95 112 ],
96 113 # warnings
97 114 [
98 115 (r'^function', "don't use 'function', use old style"),
99 116 (r'^diff.*-\w*N', "don't use 'diff -N'"),
100 117 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`"),
101 118 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
102 119 (r'kill (`|\$\()', "don't use kill, use killdaemons.py")
103 120 ]
104 121 ]
105 122
106 123 testfilters = [
107 124 (r"( *)(#([^\n]*\S)?)", repcomment),
108 125 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
109 126 ]
110 127
111 128 winglobmsg = "use (glob) to match Windows paths too"
112 129 uprefix = r"^ \$ "
113 130 utestpats = [
114 131 [
115 132 (r'^(\S.*|| [$>] .*)[ \t]\n', "trailing whitespace on non-output"),
116 133 (uprefix + r'.*\|\s*sed[^|>\n]*\n',
117 134 "use regex test output patterns instead of sed"),
118 135 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
119 136 (uprefix + r'.*(?<!\[)\$\?', "explicit exit code checks unnecessary"),
120 137 (uprefix + r'.*\|\| echo.*(fail|error)',
121 138 "explicit exit code checks unnecessary"),
122 139 (uprefix + r'set -e', "don't use set -e"),
123 140 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
124 141 (r'^ saved backup bundle to \$TESTTMP.*\.hg$', winglobmsg),
125 142 (r'^ changeset .* references (corrupted|missing) \$TESTTMP/.*[^)]$',
126 143 winglobmsg),
127 144 (r'^ pulling from \$TESTTMP/.*[^)]$', winglobmsg, '\$TESTTMP/unix-repo$'),
128 145 (r'^ reverting .*/.*[^)]$', winglobmsg, '\$TESTTMP/unix-repo$'),
129 146 (r'^ cloning subrepo \S+/.*[^)]$', winglobmsg, '\$TESTTMP/unix-repo$'),
130 147 (r'^ pushing to \$TESTTMP/.*[^)]$', winglobmsg, '\$TESTTMP/unix-repo$'),
131 148 (r'^ pushing subrepo \S+/\S+ to.*[^)]$', winglobmsg,
132 149 '\$TESTTMP/unix-repo$'),
133 150 (r'^ moving \S+/.*[^)]$', winglobmsg),
134 151 (r'^ no changes made to subrepo since.*/.*[^)]$',
135 152 winglobmsg, '\$TESTTMP/unix-repo$'),
136 153 (r'^ .*: largefile \S+ not available from file:.*/.*[^)]$',
137 154 winglobmsg, '\$TESTTMP/unix-repo$'),
138 155 ],
139 156 # warnings
140 157 [
141 158 (r'^ [^*?/\n]* \(glob\)$',
142 159 "glob match with no glob character (?*/)"),
143 160 ]
144 161 ]
145 162
146 163 for i in [0, 1]:
147 164 for p, m in testpats[i]:
148 165 if p.startswith(r'^'):
149 166 p = r"^ [$>] (%s)" % p[1:]
150 167 else:
151 168 p = r"^ [$>] .*(%s)" % p
152 169 utestpats[i].append((p, m))
153 170
154 171 utestfilters = [
155 172 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
156 173 (r"( *)(#([^\n]*\S)?)", repcomment),
157 174 ]
158 175
159 176 pypats = [
160 177 [
161 178 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
162 179 "tuple parameter unpacking not available in Python 3+"),
163 180 (r'lambda\s*\(.*,.*\)',
164 181 "tuple parameter unpacking not available in Python 3+"),
165 182 (r'import (.+,[^.]+\.[^.]+|[^.]+\.[^.]+,)',
166 183 '2to3 can\'t always rewrite "import qux, foo.bar", '
167 184 'use "import foo.bar" on its own line instead.'),
168 185 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
169 186 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
170 187 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
171 188 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
172 189 (r'^\s*\t', "don't use tabs"),
173 190 (r'\S;\s*\n', "semicolon"),
174 191 (r'[^_]_\("[^"]+"\s*%', "don't use % inside _()"),
175 192 (r"[^_]_\('[^']+'\s*%", "don't use % inside _()"),
176 193 (r'(\w|\)),\w', "missing whitespace after ,"),
177 194 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
178 195 (r'^\s+(\w|\.)+=\w[^,()\n]*$', "missing whitespace in assignment"),
179 196 (r'(\s+)try:\n((?:\n|\1\s.*\n)+?)\1except.*?:\n'
180 197 r'((?:\n|\1\s.*\n)+?)\1finally:', 'no try/except/finally in Python 2.4'),
181 198 (r'(?<!def)(\s+|^|\()next\(.+\)',
182 199 'no next(foo) in Python 2.4 and 2.5, use foo.next() instead'),
183 200 (r'(\s+)try:\n((?:\n|\1\s.*\n)*?)\1\s*yield\b.*?'
184 201 r'((?:\n|\1\s.*\n)+?)\1finally:',
185 202 'no yield inside try/finally in Python 2.4'),
186 203 (r'.{81}', "line too long"),
187 204 (r' x+[xo][\'"]\n\s+[\'"]x', 'string join across lines with no space'),
188 205 (r'[^\n]\Z', "no trailing newline"),
189 206 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
190 207 # (r'^\s+[^_ \n][^_. \n]+_[^_\n]+\s*=',
191 208 # "don't use underbars in identifiers"),
192 209 (r'^\s+(self\.)?[A-za-z][a-z0-9]+[A-Z]\w* = ',
193 210 "don't use camelcase in identifiers"),
194 211 (r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
195 212 "linebreak after :"),
196 213 (r'class\s[^( \n]+:', "old-style class, use class foo(object)"),
197 214 (r'class\s[^( \n]+\(\):',
198 215 "class foo() not available in Python 2.4, use class foo(object)"),
199 216 (r'\b(%s)\(' % '|'.join(keyword.kwlist),
200 217 "Python keyword is not a function"),
201 218 (r',]', "unneeded trailing ',' in list"),
202 219 # (r'class\s[A-Z][^\(]*\((?!Exception)',
203 220 # "don't capitalize non-exception classes"),
204 221 # (r'in range\(', "use xrange"),
205 222 # (r'^\s*print\s+', "avoid using print in core and extensions"),
206 223 (r'[\x80-\xff]', "non-ASCII character literal"),
207 224 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
208 225 (r'^\s*with\s+', "with not available in Python 2.4"),
209 226 (r'\.isdisjoint\(', "set.isdisjoint not available in Python 2.4"),
210 227 (r'^\s*except.* as .*:', "except as not available in Python 2.4"),
211 228 (r'^\s*os\.path\.relpath', "relpath not available in Python 2.4"),
212 229 (r'(?<!def)\s+(any|all|format)\(',
213 230 "any/all/format not available in Python 2.4"),
214 231 (r'(?<!def)\s+(callable)\(',
215 232 "callable not available in Python 3, use getattr(f, '__call__', None)"),
216 233 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
217 234 (r'^\s*(%s)\s\s' % '|'.join(keyword.kwlist),
218 235 "gratuitous whitespace after Python keyword"),
219 236 (r'([\(\[][ \t]\S)|(\S[ \t][\)\]])', "gratuitous whitespace in () or []"),
220 237 # (r'\s\s=', "gratuitous whitespace before ="),
221 238 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
222 239 "missing whitespace around operator"),
223 240 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
224 241 "missing whitespace around operator"),
225 242 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
226 243 "missing whitespace around operator"),
227 244 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]',
228 245 "wrong whitespace around ="),
229 246 (r'\([^()]*( =[^=]|[^<>!=]= )',
230 247 "no whitespace around = for named parameters"),
231 248 (r'raise Exception', "don't raise generic exceptions"),
232 249 (r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
233 250 "don't use old-style two-argument raise, use Exception(message)"),
234 251 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
235 252 (r' [=!]=\s+(True|False|None)',
236 253 "comparison with singleton, use 'is' or 'is not' instead"),
237 254 (r'^\s*(while|if) [01]:',
238 255 "use True/False for constant Boolean expression"),
239 256 (r'(?:(?<!def)\s+|\()hasattr',
240 257 'hasattr(foo, bar) is broken, use util.safehasattr(foo, bar) instead'),
241 258 (r'opener\([^)]*\).read\(',
242 259 "use opener.read() instead"),
243 260 (r'BaseException', 'not in Python 2.4, use Exception'),
244 261 (r'os\.path\.relpath', 'os.path.relpath is not in Python 2.5'),
245 262 (r'opener\([^)]*\).write\(',
246 263 "use opener.write() instead"),
247 264 (r'[\s\(](open|file)\([^)]*\)\.read\(',
248 265 "use util.readfile() instead"),
249 266 (r'[\s\(](open|file)\([^)]*\)\.write\(',
250 267 "use util.writefile() instead"),
251 268 (r'^[\s\(]*(open(er)?|file)\([^)]*\)',
252 269 "always assign an opened file to a variable, and close it afterwards"),
253 270 (r'[\s\(](open|file)\([^)]*\)\.',
254 271 "always assign an opened file to a variable, and close it afterwards"),
255 272 (r'(?i)descendent', "the proper spelling is descendAnt"),
256 273 (r'\.debug\(\_', "don't mark debug messages for translation"),
257 274 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
258 275 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
259 276 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
260 277 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
261 278 "missing _() in ui message (use () to hide false-positives)"),
262 279 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
263 280 ],
264 281 # warnings
265 282 [
283 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
266 284 ]
267 285 ]
268 286
269 287 pyfilters = [
270 288 (r"""(?msx)(?P<comment>\#.*?$)|
271 289 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
272 290 (?P<text>(([^\\]|\\.)*?))
273 291 (?P=quote))""", reppython),
274 292 ]
275 293
276 294 txtfilters = []
277 295
278 296 txtpats = [
279 297 [
280 298 ('\s$', 'trailing whitespace'),
281 299 ],
282 300 []
283 301 ]
284 302
285 303 cpats = [
286 304 [
287 305 (r'//', "don't use //-style comments"),
288 306 (r'^ ', "don't use spaces to indent"),
289 307 (r'\S\t', "don't use tabs except for indent"),
290 308 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
291 309 (r'.{81}', "line too long"),
292 310 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
293 311 (r'return\(', "return is not a function"),
294 312 (r' ;', "no space before ;"),
295 313 (r'[)][{]', "space between ) and {"),
296 314 (r'\w+\* \w+', "use int *foo, not int* foo"),
297 315 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
298 316 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
299 317 (r'\w,\w', "missing whitespace after ,"),
300 318 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
301 319 (r'^#\s+\w', "use #foo, not # foo"),
302 320 (r'[^\n]\Z', "no trailing newline"),
303 321 (r'^\s*#import\b', "use only #include in standard C code"),
304 322 ],
305 323 # warnings
306 324 []
307 325 ]
308 326
309 327 cfilters = [
310 328 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
311 329 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
312 330 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
313 331 (r'(\()([^)]+\))', repcallspaces),
314 332 ]
315 333
316 334 inutilpats = [
317 335 [
318 336 (r'\bui\.', "don't use ui in util"),
319 337 ],
320 338 # warnings
321 339 []
322 340 ]
323 341
324 342 inrevlogpats = [
325 343 [
326 344 (r'\brepo\.', "don't use repo in revlog"),
327 345 ],
328 346 # warnings
329 347 []
330 348 ]
331 349
332 350 checks = [
333 351 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
334 352 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
335 353 ('c', r'.*\.[ch]$', cfilters, cpats),
336 354 ('unified test', r'.*\.t$', utestfilters, utestpats),
337 355 ('layering violation repo in revlog', r'mercurial/revlog\.py', pyfilters,
338 356 inrevlogpats),
339 357 ('layering violation ui in util', r'mercurial/util\.py', pyfilters,
340 358 inutilpats),
341 359 ('txt', r'.*\.txt$', txtfilters, txtpats),
342 360 ]
343 361
344 362 def _preparepats():
345 363 for c in checks:
346 364 failandwarn = c[-1]
347 365 for pats in failandwarn:
348 366 for i, pseq in enumerate(pats):
349 367 # fix-up regexes for multi-line searches
350 368 p = pseq[0]
351 369 # \s doesn't match \n
352 370 p = re.sub(r'(?<!\\)\\s', r'[ \\t]', p)
353 371 # [^...] doesn't match newline
354 372 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
355 373
356 374 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
357 375 filters = c[2]
358 376 for i, flt in enumerate(filters):
359 377 filters[i] = re.compile(flt[0]), flt[1]
360 378 _preparepats()
361 379
362 380 class norepeatlogger(object):
363 381 def __init__(self):
364 382 self._lastseen = None
365 383
366 384 def log(self, fname, lineno, line, msg, blame):
367 385 """print error related a to given line of a given file.
368 386
369 387 The faulty line will also be printed but only once in the case
370 388 of multiple errors.
371 389
372 390 :fname: filename
373 391 :lineno: line number
374 392 :line: actual content of the line
375 393 :msg: error message
376 394 """
377 395 msgid = fname, lineno, line
378 396 if msgid != self._lastseen:
379 397 if blame:
380 398 print "%s:%d (%s):" % (fname, lineno, blame)
381 399 else:
382 400 print "%s:%d:" % (fname, lineno)
383 401 print " > %s" % line
384 402 self._lastseen = msgid
385 403 print " " + msg
386 404
387 405 _defaultlogger = norepeatlogger()
388 406
389 407 def getblame(f):
390 408 lines = []
391 409 for l in os.popen('hg annotate -un %s' % f):
392 410 start, line = l.split(':', 1)
393 411 user, rev = start.split()
394 412 lines.append((line[1:-1], user, rev))
395 413 return lines
396 414
397 415 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
398 416 blame=False, debug=False, lineno=True):
399 417 """checks style and portability of a given file
400 418
401 419 :f: filepath
402 420 :logfunc: function used to report error
403 421 logfunc(filename, linenumber, linecontent, errormessage)
404 422 :maxerr: number of error to display before aborting.
405 423 Set to false (default) to report all errors
406 424
407 425 return True if no error is found, False otherwise.
408 426 """
409 427 blamecache = None
410 428 result = True
411 429 for name, match, filters, pats in checks:
412 430 if debug:
413 431 print name, f
414 432 fc = 0
415 433 if not re.match(match, f):
416 434 if debug:
417 435 print "Skipping %s for %s it doesn't match %s" % (
418 436 name, match, f)
419 437 continue
420 438 try:
421 439 fp = open(f)
422 440 except IOError, e:
423 441 print "Skipping %s, %s" % (f, str(e).split(':', 1)[0])
424 442 continue
425 443 pre = post = fp.read()
426 444 fp.close()
427 445 if "no-" "check-code" in pre:
428 446 if debug:
429 447 print "Skipping %s for %s it has no-" "check-code" % (
430 448 name, f)
431 449 break
432 450 for p, r in filters:
433 451 post = re.sub(p, r, post)
434 452 nerrs = len(pats[0]) # nerr elements are errors
435 453 if warnings:
436 454 pats = pats[0] + pats[1]
437 455 else:
438 456 pats = pats[0]
439 457 # print post # uncomment to show filtered version
440 458
441 459 if debug:
442 460 print "Checking %s for %s" % (name, f)
443 461
444 462 prelines = None
445 463 errors = []
446 464 for i, pat in enumerate(pats):
447 465 if len(pat) == 3:
448 466 p, msg, ignore = pat
449 467 else:
450 468 p, msg = pat
451 469 ignore = None
470 if i >= nerrs:
471 msg = "warning: " + msg
452 472
453 473 pos = 0
454 474 n = 0
455 475 for m in p.finditer(post):
456 476 if prelines is None:
457 477 prelines = pre.splitlines()
458 478 postlines = post.splitlines(True)
459 if i >= nerrs:
460 msg = "warning: " + msg
461 479
462 480 start = m.start()
463 481 while n < len(postlines):
464 482 step = len(postlines[n])
465 483 if pos + step > start:
466 484 break
467 485 pos += step
468 486 n += 1
469 487 l = prelines[n]
470 488
471 489 if "check-code" "-ignore" in l:
472 490 if debug:
473 491 print "Skipping %s for %s:%s (check-code" "-ignore)" % (
474 492 name, f, n)
475 493 continue
476 494 elif ignore and re.search(ignore, l, re.MULTILINE):
477 495 continue
478 496 bd = ""
479 497 if blame:
480 498 bd = 'working directory'
481 499 if not blamecache:
482 500 blamecache = getblame(f)
483 501 if n < len(blamecache):
484 502 bl, bu, br = blamecache[n]
485 503 if bl == l:
486 504 bd = '%s@%s' % (bu, br)
505
487 506 errors.append((f, lineno and n + 1, l, msg, bd))
488 507 result = False
489 508
490 509 errors.sort()
491 510 for e in errors:
492 511 logfunc(*e)
493 512 fc += 1
494 513 if maxerr and fc >= maxerr:
495 514 print " (too many errors, giving up)"
496 515 break
497 516
498 517 return result
499 518
500 519 if __name__ == "__main__":
501 520 parser = optparse.OptionParser("%prog [options] [files]")
502 521 parser.add_option("-w", "--warnings", action="store_true",
503 522 help="include warning-level checks")
504 523 parser.add_option("-p", "--per-file", type="int",
505 524 help="max warnings per file")
506 525 parser.add_option("-b", "--blame", action="store_true",
507 526 help="use annotate to generate blame info")
508 527 parser.add_option("", "--debug", action="store_true",
509 528 help="show debug information")
510 529 parser.add_option("", "--nolineno", action="store_false",
511 530 dest='lineno', help="don't show line numbers")
512 531
513 532 parser.set_defaults(per_file=15, warnings=False, blame=False, debug=False,
514 533 lineno=True)
515 534 (options, args) = parser.parse_args()
516 535
517 536 if len(args) == 0:
518 537 check = glob.glob("*")
519 538 else:
520 539 check = args
521 540
522 541 ret = 0
523 542 for f in check:
524 543 if not checkfile(f, maxerr=options.per_file, warnings=options.warnings,
525 544 blame=options.blame, debug=options.debug,
526 545 lineno=options.lineno):
527 546 ret = 1
528 547 sys.exit(ret)
@@ -1,349 +1,350 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 copy. 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 copy 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 behaviour; 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 The rules will first apply when files are touched in the working
53 54 copy, e.g. by updating to null and back to tip to touch all files.
54 55
55 56 The extension uses an optional ``[eol]`` section read from both the
56 57 normal Mercurial configuration files and the ``.hgeol`` file, with the
57 58 latter overriding the former. You can use that section to control the
58 59 overall behavior. There are three settings:
59 60
60 61 - ``eol.native`` (default ``os.linesep``) can be set to ``LF`` or
61 62 ``CRLF`` to override the default interpretation of ``native`` for
62 63 checkout. This can be used with :hg:`archive` on Unix, say, to
63 64 generate an archive where files have line endings for Windows.
64 65
65 66 - ``eol.only-consistent`` (default True) can be set to False to make
66 67 the extension convert files with inconsistent EOLs. Inconsistent
67 68 means that there is both ``CRLF`` and ``LF`` present in the file.
68 69 Such files are normally not touched under the assumption that they
69 70 have mixed EOLs on purpose.
70 71
71 72 - ``eol.fix-trailing-newline`` (default False) can be set to True to
72 73 ensure that converted files end with a EOL character (either ``\\n``
73 74 or ``\\r\\n`` as per the configured patterns).
74 75
75 76 The extension provides ``cleverencode:`` and ``cleverdecode:`` filters
76 77 like the deprecated win32text extension does. This means that you can
77 78 disable win32text and enable eol and your filters will still work. You
78 79 only need to these filters until you have prepared a ``.hgeol`` file.
79 80
80 81 The ``win32text.forbid*`` hooks provided by the win32text extension
81 82 have been unified into a single hook named ``eol.checkheadshook``. The
82 83 hook will lookup the expected line endings from the ``.hgeol`` file,
83 84 which means you must migrate to a ``.hgeol`` file first before using
84 85 the hook. ``eol.checkheadshook`` only checks heads, intermediate
85 86 invalid revisions will be pushed. To forbid them completely, use the
86 87 ``eol.checkallhook`` hook. These hooks are best used as
87 88 ``pretxnchangegroup`` hooks.
88 89
89 90 See :hg:`help patterns` for more information about the glob patterns
90 91 used.
91 92 """
92 93
93 94 from mercurial.i18n import _
94 95 from mercurial import util, config, extensions, match, error
95 96 import re, os
96 97
97 98 testedwith = 'internal'
98 99
99 100 # Matches a lone LF, i.e., one that is not part of CRLF.
100 101 singlelf = re.compile('(^|[^\r])\n')
101 102 # Matches a single EOL which can either be a CRLF where repeated CR
102 103 # are removed or a LF. We do not care about old Macintosh files, so a
103 104 # stray CR is an error.
104 105 eolre = re.compile('\r*\n')
105 106
106 107
107 108 def inconsistenteol(data):
108 109 return '\r\n' in data and singlelf.search(data)
109 110
110 111 def tolf(s, params, ui, **kwargs):
111 112 """Filter to convert to LF EOLs."""
112 113 if util.binary(s):
113 114 return s
114 115 if ui.configbool('eol', 'only-consistent', True) and inconsistenteol(s):
115 116 return s
116 117 if (ui.configbool('eol', 'fix-trailing-newline', False)
117 118 and s and s[-1] != '\n'):
118 119 s = s + '\n'
119 120 return eolre.sub('\n', s)
120 121
121 122 def tocrlf(s, params, ui, **kwargs):
122 123 """Filter to convert to CRLF EOLs."""
123 124 if util.binary(s):
124 125 return s
125 126 if ui.configbool('eol', 'only-consistent', True) and inconsistenteol(s):
126 127 return s
127 128 if (ui.configbool('eol', 'fix-trailing-newline', False)
128 129 and s and s[-1] != '\n'):
129 130 s = s + '\n'
130 131 return eolre.sub('\r\n', s)
131 132
132 133 def isbinary(s, params):
133 134 """Filter to do nothing with the file."""
134 135 return s
135 136
136 137 filters = {
137 138 'to-lf': tolf,
138 139 'to-crlf': tocrlf,
139 140 'is-binary': isbinary,
140 141 # The following provide backwards compatibility with win32text
141 142 'cleverencode:': tolf,
142 143 'cleverdecode:': tocrlf
143 144 }
144 145
145 146 class eolfile(object):
146 147 def __init__(self, ui, root, data):
147 148 self._decode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
148 149 self._encode = {'LF': 'to-lf', 'CRLF': 'to-crlf', 'BIN': 'is-binary'}
149 150
150 151 self.cfg = config.config()
151 152 # Our files should not be touched. The pattern must be
152 153 # inserted first override a '** = native' pattern.
153 154 self.cfg.set('patterns', '.hg*', 'BIN')
154 155 # We can then parse the user's patterns.
155 156 self.cfg.parse('.hgeol', data)
156 157
157 158 isrepolf = self.cfg.get('repository', 'native') != 'CRLF'
158 159 self._encode['NATIVE'] = isrepolf and 'to-lf' or 'to-crlf'
159 160 iswdlf = ui.config('eol', 'native', os.linesep) in ('LF', '\n')
160 161 self._decode['NATIVE'] = iswdlf and 'to-lf' or 'to-crlf'
161 162
162 163 include = []
163 164 exclude = []
164 165 for pattern, style in self.cfg.items('patterns'):
165 166 key = style.upper()
166 167 if key == 'BIN':
167 168 exclude.append(pattern)
168 169 else:
169 170 include.append(pattern)
170 171 # This will match the files for which we need to care
171 172 # about inconsistent newlines.
172 173 self.match = match.match(root, '', [], include, exclude)
173 174
174 175 def copytoui(self, ui):
175 176 for pattern, style in self.cfg.items('patterns'):
176 177 key = style.upper()
177 178 try:
178 179 ui.setconfig('decode', pattern, self._decode[key])
179 180 ui.setconfig('encode', pattern, self._encode[key])
180 181 except KeyError:
181 182 ui.warn(_("ignoring unknown EOL style '%s' from %s\n")
182 183 % (style, self.cfg.source('patterns', pattern)))
183 184 # eol.only-consistent can be specified in ~/.hgrc or .hgeol
184 185 for k, v in self.cfg.items('eol'):
185 186 ui.setconfig('eol', k, v)
186 187
187 188 def checkrev(self, repo, ctx, files):
188 189 failed = []
189 190 for f in (files or ctx.files()):
190 191 if f not in ctx:
191 192 continue
192 193 for pattern, style in self.cfg.items('patterns'):
193 194 if not match.match(repo.root, '', [pattern])(f):
194 195 continue
195 196 target = self._encode[style.upper()]
196 197 data = ctx[f].data()
197 198 if (target == "to-lf" and "\r\n" in data
198 199 or target == "to-crlf" and singlelf.search(data)):
199 200 failed.append((str(ctx), target, f))
200 201 break
201 202 return failed
202 203
203 204 def parseeol(ui, repo, nodes):
204 205 try:
205 206 for node in nodes:
206 207 try:
207 208 if node is None:
208 209 # Cannot use workingctx.data() since it would load
209 210 # and cache the filters before we configure them.
210 211 data = repo.wfile('.hgeol').read()
211 212 else:
212 213 data = repo[node]['.hgeol'].data()
213 214 return eolfile(ui, repo.root, data)
214 215 except (IOError, LookupError):
215 216 pass
216 217 except error.ParseError, inst:
217 218 ui.warn(_("warning: ignoring .hgeol file due to parse error "
218 219 "at %s: %s\n") % (inst.args[1], inst.args[0]))
219 220 return None
220 221
221 222 def _checkhook(ui, repo, node, headsonly):
222 223 # Get revisions to check and touched files at the same time
223 224 files = set()
224 225 revs = set()
225 226 for rev in xrange(repo[node].rev(), len(repo)):
226 227 revs.add(rev)
227 228 if headsonly:
228 229 ctx = repo[rev]
229 230 files.update(ctx.files())
230 231 for pctx in ctx.parents():
231 232 revs.discard(pctx.rev())
232 233 failed = []
233 234 for rev in revs:
234 235 ctx = repo[rev]
235 236 eol = parseeol(ui, repo, [ctx.node()])
236 237 if eol:
237 238 failed.extend(eol.checkrev(repo, ctx, files))
238 239
239 240 if failed:
240 241 eols = {'to-lf': 'CRLF', 'to-crlf': 'LF'}
241 242 msgs = []
242 243 for node, target, f in failed:
243 244 msgs.append(_(" %s in %s should not have %s line endings") %
244 245 (f, node, eols[target]))
245 246 raise util.Abort(_("end-of-line check failed:\n") + "\n".join(msgs))
246 247
247 248 def checkallhook(ui, repo, node, hooktype, **kwargs):
248 249 """verify that files have expected EOLs"""
249 250 _checkhook(ui, repo, node, False)
250 251
251 252 def checkheadshook(ui, repo, node, hooktype, **kwargs):
252 253 """verify that files have expected EOLs"""
253 254 _checkhook(ui, repo, node, True)
254 255
255 256 # "checkheadshook" used to be called "hook"
256 257 hook = checkheadshook
257 258
258 259 def preupdate(ui, repo, hooktype, parent1, parent2):
259 260 repo.loadeol([parent1])
260 261 return False
261 262
262 263 def uisetup(ui):
263 264 ui.setconfig('hooks', 'preupdate.eol', preupdate)
264 265
265 266 def extsetup(ui):
266 267 try:
267 268 extensions.find('win32text')
268 269 ui.warn(_("the eol extension is incompatible with the "
269 270 "win32text extension\n"))
270 271 except KeyError:
271 272 pass
272 273
273 274
274 275 def reposetup(ui, repo):
275 276 uisetup(repo.ui)
276 277
277 278 if not repo.local():
278 279 return
279 280 for name, fn in filters.iteritems():
280 281 repo.adddatafilter(name, fn)
281 282
282 283 ui.setconfig('patch', 'eol', 'auto')
283 284
284 285 class eolrepo(repo.__class__):
285 286
286 287 def loadeol(self, nodes):
287 288 eol = parseeol(self.ui, self, nodes)
288 289 if eol is None:
289 290 return None
290 291 eol.copytoui(self.ui)
291 292 return eol.match
292 293
293 294 def _hgcleardirstate(self):
294 295 self._eolfile = self.loadeol([None, 'tip'])
295 296 if not self._eolfile:
296 297 self._eolfile = util.never
297 298 return
298 299
299 300 try:
300 301 cachemtime = os.path.getmtime(self.join("eol.cache"))
301 302 except OSError:
302 303 cachemtime = 0
303 304
304 305 try:
305 306 eolmtime = os.path.getmtime(self.wjoin(".hgeol"))
306 307 except OSError:
307 308 eolmtime = 0
308 309
309 310 if eolmtime > cachemtime:
310 311 self.ui.debug("eol: detected change in .hgeol\n")
311 312 wlock = None
312 313 try:
313 314 wlock = self.wlock()
314 315 for f in self.dirstate:
315 316 if self.dirstate[f] == 'n':
316 317 # all normal files need to be looked at
317 318 # again since the new .hgeol file might no
318 319 # longer match a file it matched before
319 320 self.dirstate.normallookup(f)
320 321 # Create or touch the cache to update mtime
321 322 self.opener("eol.cache", "w").close()
322 323 wlock.release()
323 324 except error.LockUnavailable:
324 325 # If we cannot lock the repository and clear the
325 326 # dirstate, then a commit might not see all files
326 327 # as modified. But if we cannot lock the
327 328 # repository, then we can also not make a commit,
328 329 # so ignore the error.
329 330 pass
330 331
331 332 def commitctx(self, ctx, error=False):
332 333 for f in sorted(ctx.added() + ctx.modified()):
333 334 if not self._eolfile(f):
334 335 continue
335 336 try:
336 337 data = ctx[f].data()
337 338 except IOError:
338 339 continue
339 340 if util.binary(data):
340 341 # We should not abort here, since the user should
341 342 # be able to say "** = native" to automatically
342 343 # have all non-binary files taken care of.
343 344 continue
344 345 if inconsistenteol(data):
345 346 raise util.Abort(_("inconsistent newline style "
346 347 "in %s\n" % f))
347 348 return super(eolrepo, self).commitctx(ctx, error)
348 349 repo.__class__ = eolrepo
349 350 repo._hgcleardirstate()
@@ -1,731 +1,732 b''
1 1 # keyword.py - $Keyword$ expansion for Mercurial
2 2 #
3 3 # Copyright 2007-2012 Christian Ebert <blacktrash@gmx.net>
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 # $Id$
9 9 #
10 10 # Keyword expansion hack against the grain of a Distributed SCM
11 11 #
12 12 # There are many good reasons why this is not needed in a distributed
13 13 # SCM, still it may be useful in very small projects based on single
14 14 # files (like LaTeX packages), that are mostly addressed to an
15 15 # audience not running a version control system.
16 16 #
17 17 # For in-depth discussion refer to
18 18 # <http://mercurial.selenic.com/wiki/KeywordPlan>.
19 19 #
20 20 # Keyword expansion is based on Mercurial's changeset template mappings.
21 21 #
22 22 # Binary files are not touched.
23 23 #
24 24 # Files to act upon/ignore are specified in the [keyword] section.
25 25 # Customized keyword template mappings in the [keywordmaps] section.
26 26 #
27 27 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
28 28
29 29 '''expand keywords in tracked files
30 30
31 31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
32 32 tracked text files selected by your configuration.
33 33
34 34 Keywords are only expanded in local repositories and not stored in the
35 35 change history. The mechanism can be regarded as a convenience for the
36 36 current user or for archive distribution.
37 37
38 38 Keywords expand to the changeset data pertaining to the latest change
39 39 relative to the working directory parent of each file.
40 40
41 41 Configuration is done in the [keyword], [keywordset] and [keywordmaps]
42 42 sections of hgrc files.
43 43
44 44 Example::
45 45
46 46 [keyword]
47 47 # expand keywords in every python file except those matching "x*"
48 48 **.py =
49 49 x* = ignore
50 50
51 51 [keywordset]
52 52 # prefer svn- over cvs-like default keywordmaps
53 53 svn = True
54 54
55 55 .. note::
56
56 57 The more specific you are in your filename patterns the less you
57 58 lose speed in huge repositories.
58 59
59 60 For [keywordmaps] template mapping and expansion demonstration and
60 61 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
61 62 available templates and filters.
62 63
63 64 Three additional date template filters are provided:
64 65
65 66 :``utcdate``: "2006/09/18 15:13:13"
66 67 :``svnutcdate``: "2006-09-18 15:13:13Z"
67 68 :``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
68 69
69 70 The default template mappings (view with :hg:`kwdemo -d`) can be
70 71 replaced with customized keywords and templates. Again, run
71 72 :hg:`kwdemo` to control the results of your configuration changes.
72 73
73 74 Before changing/disabling active keywords, you must run :hg:`kwshrink`
74 75 to avoid storing expanded keywords in the change history.
75 76
76 77 To force expansion after enabling it, or a configuration change, run
77 78 :hg:`kwexpand`.
78 79
79 80 Expansions spanning more than one line and incremental expansions,
80 81 like CVS' $Log$, are not supported. A keyword template map "Log =
81 82 {desc}" expands to the first line of the changeset description.
82 83 '''
83 84
84 85 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
85 86 from mercurial import localrepo, match, patch, templatefilters, templater, util
86 87 from mercurial import scmutil
87 88 from mercurial.hgweb import webcommands
88 89 from mercurial.i18n import _
89 90 import os, re, shutil, tempfile
90 91
91 92 commands.optionalrepo += ' kwdemo'
92 93 commands.inferrepo += ' kwexpand kwfiles kwshrink'
93 94
94 95 cmdtable = {}
95 96 command = cmdutil.command(cmdtable)
96 97 testedwith = 'internal'
97 98
98 99 # hg commands that do not act on keywords
99 100 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
100 101 ' outgoing push tip verify convert email glog')
101 102
102 103 # hg commands that trigger expansion only when writing to working dir,
103 104 # not when reading filelog, and unexpand when reading from working dir
104 105 restricted = 'merge kwexpand kwshrink record qrecord resolve transplant'
105 106
106 107 # names of extensions using dorecord
107 108 recordextensions = 'record'
108 109
109 110 colortable = {
110 111 'kwfiles.enabled': 'green bold',
111 112 'kwfiles.deleted': 'cyan bold underline',
112 113 'kwfiles.enabledunknown': 'green',
113 114 'kwfiles.ignored': 'bold',
114 115 'kwfiles.ignoredunknown': 'none'
115 116 }
116 117
117 118 # date like in cvs' $Date
118 119 def utcdate(text):
119 120 ''':utcdate: Date. Returns a UTC-date in this format: "2009/08/18 11:00:13".
120 121 '''
121 122 return util.datestr((util.parsedate(text)[0], 0), '%Y/%m/%d %H:%M:%S')
122 123 # date like in svn's $Date
123 124 def svnisodate(text):
124 125 ''':svnisodate: Date. Returns a date in this format: "2009-08-18 13:00:13
125 126 +0200 (Tue, 18 Aug 2009)".
126 127 '''
127 128 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
128 129 # date like in svn's $Id
129 130 def svnutcdate(text):
130 131 ''':svnutcdate: Date. Returns a UTC-date in this format: "2009-08-18
131 132 11:00:13Z".
132 133 '''
133 134 return util.datestr((util.parsedate(text)[0], 0), '%Y-%m-%d %H:%M:%SZ')
134 135
135 136 templatefilters.filters.update({'utcdate': utcdate,
136 137 'svnisodate': svnisodate,
137 138 'svnutcdate': svnutcdate})
138 139
139 140 # make keyword tools accessible
140 141 kwtools = {'templater': None, 'hgcmd': ''}
141 142
142 143 def _defaultkwmaps(ui):
143 144 '''Returns default keywordmaps according to keywordset configuration.'''
144 145 templates = {
145 146 'Revision': '{node|short}',
146 147 'Author': '{author|user}',
147 148 }
148 149 kwsets = ({
149 150 'Date': '{date|utcdate}',
150 151 'RCSfile': '{file|basename},v',
151 152 'RCSFile': '{file|basename},v', # kept for backwards compatibility
152 153 # with hg-keyword
153 154 'Source': '{root}/{file},v',
154 155 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
155 156 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
156 157 }, {
157 158 'Date': '{date|svnisodate}',
158 159 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
159 160 'LastChangedRevision': '{node|short}',
160 161 'LastChangedBy': '{author|user}',
161 162 'LastChangedDate': '{date|svnisodate}',
162 163 })
163 164 templates.update(kwsets[ui.configbool('keywordset', 'svn')])
164 165 return templates
165 166
166 167 def _shrinktext(text, subfunc):
167 168 '''Helper for keyword expansion removal in text.
168 169 Depending on subfunc also returns number of substitutions.'''
169 170 return subfunc(r'$\1$', text)
170 171
171 172 def _preselect(wstatus, changed):
172 173 '''Retrieves modified and added files from a working directory state
173 174 and returns the subset of each contained in given changed files
174 175 retrieved from a change context.'''
175 176 modified, added = wstatus[:2]
176 177 modified = [f for f in modified if f in changed]
177 178 added = [f for f in added if f in changed]
178 179 return modified, added
179 180
180 181
181 182 class kwtemplater(object):
182 183 '''
183 184 Sets up keyword templates, corresponding keyword regex, and
184 185 provides keyword substitution functions.
185 186 '''
186 187
187 188 def __init__(self, ui, repo, inc, exc):
188 189 self.ui = ui
189 190 self.repo = repo
190 191 self.match = match.match(repo.root, '', [], inc, exc)
191 192 self.restrict = kwtools['hgcmd'] in restricted.split()
192 193 self.postcommit = False
193 194
194 195 kwmaps = self.ui.configitems('keywordmaps')
195 196 if kwmaps: # override default templates
196 197 self.templates = dict((k, templater.parsestring(v, False))
197 198 for k, v in kwmaps)
198 199 else:
199 200 self.templates = _defaultkwmaps(self.ui)
200 201
201 202 @util.propertycache
202 203 def escape(self):
203 204 '''Returns bar-separated and escaped keywords.'''
204 205 return '|'.join(map(re.escape, self.templates.keys()))
205 206
206 207 @util.propertycache
207 208 def rekw(self):
208 209 '''Returns regex for unexpanded keywords.'''
209 210 return re.compile(r'\$(%s)\$' % self.escape)
210 211
211 212 @util.propertycache
212 213 def rekwexp(self):
213 214 '''Returns regex for expanded keywords.'''
214 215 return re.compile(r'\$(%s): [^$\n\r]*? \$' % self.escape)
215 216
216 217 def substitute(self, data, path, ctx, subfunc):
217 218 '''Replaces keywords in data with expanded template.'''
218 219 def kwsub(mobj):
219 220 kw = mobj.group(1)
220 221 ct = cmdutil.changeset_templater(self.ui, self.repo,
221 222 False, None, '', False)
222 223 ct.use_template(self.templates[kw])
223 224 self.ui.pushbuffer()
224 225 ct.show(ctx, root=self.repo.root, file=path)
225 226 ekw = templatefilters.firstline(self.ui.popbuffer())
226 227 return '$%s: %s $' % (kw, ekw)
227 228 return subfunc(kwsub, data)
228 229
229 230 def linkctx(self, path, fileid):
230 231 '''Similar to filelog.linkrev, but returns a changectx.'''
231 232 return self.repo.filectx(path, fileid=fileid).changectx()
232 233
233 234 def expand(self, path, node, data):
234 235 '''Returns data with keywords expanded.'''
235 236 if not self.restrict and self.match(path) and not util.binary(data):
236 237 ctx = self.linkctx(path, node)
237 238 return self.substitute(data, path, ctx, self.rekw.sub)
238 239 return data
239 240
240 241 def iskwfile(self, cand, ctx):
241 242 '''Returns subset of candidates which are configured for keyword
242 243 expansion but are not symbolic links.'''
243 244 return [f for f in cand if self.match(f) and 'l' not in ctx.flags(f)]
244 245
245 246 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
246 247 '''Overwrites selected files expanding/shrinking keywords.'''
247 248 if self.restrict or lookup or self.postcommit: # exclude kw_copy
248 249 candidates = self.iskwfile(candidates, ctx)
249 250 if not candidates:
250 251 return
251 252 kwcmd = self.restrict and lookup # kwexpand/kwshrink
252 253 if self.restrict or expand and lookup:
253 254 mf = ctx.manifest()
254 255 if self.restrict or rekw:
255 256 re_kw = self.rekw
256 257 else:
257 258 re_kw = self.rekwexp
258 259 if expand:
259 260 msg = _('overwriting %s expanding keywords\n')
260 261 else:
261 262 msg = _('overwriting %s shrinking keywords\n')
262 263 for f in candidates:
263 264 if self.restrict:
264 265 data = self.repo.file(f).read(mf[f])
265 266 else:
266 267 data = self.repo.wread(f)
267 268 if util.binary(data):
268 269 continue
269 270 if expand:
270 271 if lookup:
271 272 ctx = self.linkctx(f, mf[f])
272 273 data, found = self.substitute(data, f, ctx, re_kw.subn)
273 274 elif self.restrict:
274 275 found = re_kw.search(data)
275 276 else:
276 277 data, found = _shrinktext(data, re_kw.subn)
277 278 if found:
278 279 self.ui.note(msg % f)
279 280 fp = self.repo.wopener(f, "wb", atomictemp=True)
280 281 fp.write(data)
281 282 fp.close()
282 283 if kwcmd:
283 284 self.repo.dirstate.normal(f)
284 285 elif self.postcommit:
285 286 self.repo.dirstate.normallookup(f)
286 287
287 288 def shrink(self, fname, text):
288 289 '''Returns text with all keyword substitutions removed.'''
289 290 if self.match(fname) and not util.binary(text):
290 291 return _shrinktext(text, self.rekwexp.sub)
291 292 return text
292 293
293 294 def shrinklines(self, fname, lines):
294 295 '''Returns lines with keyword substitutions removed.'''
295 296 if self.match(fname):
296 297 text = ''.join(lines)
297 298 if not util.binary(text):
298 299 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
299 300 return lines
300 301
301 302 def wread(self, fname, data):
302 303 '''If in restricted mode returns data read from wdir with
303 304 keyword substitutions removed.'''
304 305 if self.restrict:
305 306 return self.shrink(fname, data)
306 307 return data
307 308
308 309 class kwfilelog(filelog.filelog):
309 310 '''
310 311 Subclass of filelog to hook into its read, add, cmp methods.
311 312 Keywords are "stored" unexpanded, and processed on reading.
312 313 '''
313 314 def __init__(self, opener, kwt, path):
314 315 super(kwfilelog, self).__init__(opener, path)
315 316 self.kwt = kwt
316 317 self.path = path
317 318
318 319 def read(self, node):
319 320 '''Expands keywords when reading filelog.'''
320 321 data = super(kwfilelog, self).read(node)
321 322 if self.renamed(node):
322 323 return data
323 324 return self.kwt.expand(self.path, node, data)
324 325
325 326 def add(self, text, meta, tr, link, p1=None, p2=None):
326 327 '''Removes keyword substitutions when adding to filelog.'''
327 328 text = self.kwt.shrink(self.path, text)
328 329 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
329 330
330 331 def cmp(self, node, text):
331 332 '''Removes keyword substitutions for comparison.'''
332 333 text = self.kwt.shrink(self.path, text)
333 334 return super(kwfilelog, self).cmp(node, text)
334 335
335 336 def _status(ui, repo, wctx, kwt, *pats, **opts):
336 337 '''Bails out if [keyword] configuration is not active.
337 338 Returns status of working directory.'''
338 339 if kwt:
339 340 return repo.status(match=scmutil.match(wctx, pats, opts), clean=True,
340 341 unknown=opts.get('unknown') or opts.get('all'))
341 342 if ui.configitems('keyword'):
342 343 raise util.Abort(_('[keyword] patterns cannot match'))
343 344 raise util.Abort(_('no [keyword] patterns configured'))
344 345
345 346 def _kwfwrite(ui, repo, expand, *pats, **opts):
346 347 '''Selects files and passes them to kwtemplater.overwrite.'''
347 348 wctx = repo[None]
348 349 if len(wctx.parents()) > 1:
349 350 raise util.Abort(_('outstanding uncommitted merge'))
350 351 kwt = kwtools['templater']
351 352 wlock = repo.wlock()
352 353 try:
353 354 status = _status(ui, repo, wctx, kwt, *pats, **opts)
354 355 modified, added, removed, deleted, unknown, ignored, clean = status
355 356 if modified or added or removed or deleted:
356 357 raise util.Abort(_('outstanding uncommitted changes'))
357 358 kwt.overwrite(wctx, clean, True, expand)
358 359 finally:
359 360 wlock.release()
360 361
361 362 @command('kwdemo',
362 363 [('d', 'default', None, _('show default keyword template maps')),
363 364 ('f', 'rcfile', '',
364 365 _('read maps from rcfile'), _('FILE'))],
365 366 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'))
366 367 def demo(ui, repo, *args, **opts):
367 368 '''print [keywordmaps] configuration and an expansion example
368 369
369 370 Show current, custom, or default keyword template maps and their
370 371 expansions.
371 372
372 373 Extend the current configuration by specifying maps as arguments
373 374 and using -f/--rcfile to source an external hgrc file.
374 375
375 376 Use -d/--default to disable current configuration.
376 377
377 378 See :hg:`help templates` for information on templates and filters.
378 379 '''
379 380 def demoitems(section, items):
380 381 ui.write('[%s]\n' % section)
381 382 for k, v in sorted(items):
382 383 ui.write('%s = %s\n' % (k, v))
383 384
384 385 fn = 'demo.txt'
385 386 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
386 387 ui.note(_('creating temporary repository at %s\n') % tmpdir)
387 388 repo = localrepo.localrepository(repo.baseui, tmpdir, True)
388 389 ui.setconfig('keyword', fn, '')
389 390 svn = ui.configbool('keywordset', 'svn')
390 391 # explicitly set keywordset for demo output
391 392 ui.setconfig('keywordset', 'svn', svn)
392 393
393 394 uikwmaps = ui.configitems('keywordmaps')
394 395 if args or opts.get('rcfile'):
395 396 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
396 397 if uikwmaps:
397 398 ui.status(_('\textending current template maps\n'))
398 399 if opts.get('default') or not uikwmaps:
399 400 if svn:
400 401 ui.status(_('\toverriding default svn keywordset\n'))
401 402 else:
402 403 ui.status(_('\toverriding default cvs keywordset\n'))
403 404 if opts.get('rcfile'):
404 405 ui.readconfig(opts.get('rcfile'))
405 406 if args:
406 407 # simulate hgrc parsing
407 408 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
408 409 fp = repo.opener('hgrc', 'w')
409 410 fp.writelines(rcmaps)
410 411 fp.close()
411 412 ui.readconfig(repo.join('hgrc'))
412 413 kwmaps = dict(ui.configitems('keywordmaps'))
413 414 elif opts.get('default'):
414 415 if svn:
415 416 ui.status(_('\n\tconfiguration using default svn keywordset\n'))
416 417 else:
417 418 ui.status(_('\n\tconfiguration using default cvs keywordset\n'))
418 419 kwmaps = _defaultkwmaps(ui)
419 420 if uikwmaps:
420 421 ui.status(_('\tdisabling current template maps\n'))
421 422 for k, v in kwmaps.iteritems():
422 423 ui.setconfig('keywordmaps', k, v)
423 424 else:
424 425 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
425 426 if uikwmaps:
426 427 kwmaps = dict(uikwmaps)
427 428 else:
428 429 kwmaps = _defaultkwmaps(ui)
429 430
430 431 uisetup(ui)
431 432 reposetup(ui, repo)
432 433 ui.write('[extensions]\nkeyword =\n')
433 434 demoitems('keyword', ui.configitems('keyword'))
434 435 demoitems('keywordset', ui.configitems('keywordset'))
435 436 demoitems('keywordmaps', kwmaps.iteritems())
436 437 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
437 438 repo.wopener.write(fn, keywords)
438 439 repo[None].add([fn])
439 440 ui.note(_('\nkeywords written to %s:\n') % fn)
440 441 ui.note(keywords)
441 442 repo.dirstate.setbranch('demobranch')
442 443 for name, cmd in ui.configitems('hooks'):
443 444 if name.split('.', 1)[0].find('commit') > -1:
444 445 repo.ui.setconfig('hooks', name, '')
445 446 msg = _('hg keyword configuration and expansion example')
446 447 ui.note("hg ci -m '%s'\n" % msg) # check-code-ignore
447 448 repo.commit(text=msg)
448 449 ui.status(_('\n\tkeywords expanded\n'))
449 450 ui.write(repo.wread(fn))
450 451 shutil.rmtree(tmpdir, ignore_errors=True)
451 452
452 453 @command('kwexpand', commands.walkopts, _('hg kwexpand [OPTION]... [FILE]...'))
453 454 def expand(ui, repo, *pats, **opts):
454 455 '''expand keywords in the working directory
455 456
456 457 Run after (re)enabling keyword expansion.
457 458
458 459 kwexpand refuses to run if given files contain local changes.
459 460 '''
460 461 # 3rd argument sets expansion to True
461 462 _kwfwrite(ui, repo, True, *pats, **opts)
462 463
463 464 @command('kwfiles',
464 465 [('A', 'all', None, _('show keyword status flags of all files')),
465 466 ('i', 'ignore', None, _('show files excluded from expansion')),
466 467 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
467 468 ] + commands.walkopts,
468 469 _('hg kwfiles [OPTION]... [FILE]...'))
469 470 def files(ui, repo, *pats, **opts):
470 471 '''show files configured for keyword expansion
471 472
472 473 List which files in the working directory are matched by the
473 474 [keyword] configuration patterns.
474 475
475 476 Useful to prevent inadvertent keyword expansion and to speed up
476 477 execution by including only files that are actual candidates for
477 478 expansion.
478 479
479 480 See :hg:`help keyword` on how to construct patterns both for
480 481 inclusion and exclusion of files.
481 482
482 483 With -A/--all and -v/--verbose the codes used to show the status
483 484 of files are::
484 485
485 486 K = keyword expansion candidate
486 487 k = keyword expansion candidate (not tracked)
487 488 I = ignored
488 489 i = ignored (not tracked)
489 490 '''
490 491 kwt = kwtools['templater']
491 492 wctx = repo[None]
492 493 status = _status(ui, repo, wctx, kwt, *pats, **opts)
493 494 cwd = pats and repo.getcwd() or ''
494 495 modified, added, removed, deleted, unknown, ignored, clean = status
495 496 files = []
496 497 if not opts.get('unknown') or opts.get('all'):
497 498 files = sorted(modified + added + clean)
498 499 kwfiles = kwt.iskwfile(files, wctx)
499 500 kwdeleted = kwt.iskwfile(deleted, wctx)
500 501 kwunknown = kwt.iskwfile(unknown, wctx)
501 502 if not opts.get('ignore') or opts.get('all'):
502 503 showfiles = kwfiles, kwdeleted, kwunknown
503 504 else:
504 505 showfiles = [], [], []
505 506 if opts.get('all') or opts.get('ignore'):
506 507 showfiles += ([f for f in files if f not in kwfiles],
507 508 [f for f in unknown if f not in kwunknown])
508 509 kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split()
509 510 kwstates = zip(kwlabels, 'K!kIi', showfiles)
510 511 fm = ui.formatter('kwfiles', opts)
511 512 fmt = '%.0s%s\n'
512 513 if opts.get('all') or ui.verbose:
513 514 fmt = '%s %s\n'
514 515 for kwstate, char, filenames in kwstates:
515 516 label = 'kwfiles.' + kwstate
516 517 for f in filenames:
517 518 fm.startitem()
518 519 fm.write('kwstatus path', fmt, char,
519 520 repo.pathto(f, cwd), label=label)
520 521 fm.end()
521 522
522 523 @command('kwshrink', commands.walkopts, _('hg kwshrink [OPTION]... [FILE]...'))
523 524 def shrink(ui, repo, *pats, **opts):
524 525 '''revert expanded keywords in the working directory
525 526
526 527 Must be run before changing/disabling active keywords.
527 528
528 529 kwshrink refuses to run if given files contain local changes.
529 530 '''
530 531 # 3rd argument sets expansion to False
531 532 _kwfwrite(ui, repo, False, *pats, **opts)
532 533
533 534
534 535 def uisetup(ui):
535 536 ''' Monkeypatches dispatch._parse to retrieve user command.'''
536 537
537 538 def kwdispatch_parse(orig, ui, args):
538 539 '''Monkeypatch dispatch._parse to obtain running hg command.'''
539 540 cmd, func, args, options, cmdoptions = orig(ui, args)
540 541 kwtools['hgcmd'] = cmd
541 542 return cmd, func, args, options, cmdoptions
542 543
543 544 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
544 545
545 546 def reposetup(ui, repo):
546 547 '''Sets up repo as kwrepo for keyword substitution.
547 548 Overrides file method to return kwfilelog instead of filelog
548 549 if file matches user configuration.
549 550 Wraps commit to overwrite configured files with updated
550 551 keyword substitutions.
551 552 Monkeypatches patch and webcommands.'''
552 553
553 554 try:
554 555 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
555 556 or '.hg' in util.splitpath(repo.root)
556 557 or repo._url.startswith('bundle:')):
557 558 return
558 559 except AttributeError:
559 560 pass
560 561
561 562 inc, exc = [], ['.hg*']
562 563 for pat, opt in ui.configitems('keyword'):
563 564 if opt != 'ignore':
564 565 inc.append(pat)
565 566 else:
566 567 exc.append(pat)
567 568 if not inc:
568 569 return
569 570
570 571 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
571 572
572 573 class kwrepo(repo.__class__):
573 574 def file(self, f):
574 575 if f[0] == '/':
575 576 f = f[1:]
576 577 return kwfilelog(self.sopener, kwt, f)
577 578
578 579 def wread(self, filename):
579 580 data = super(kwrepo, self).wread(filename)
580 581 return kwt.wread(filename, data)
581 582
582 583 def commit(self, *args, **opts):
583 584 # use custom commitctx for user commands
584 585 # other extensions can still wrap repo.commitctx directly
585 586 self.commitctx = self.kwcommitctx
586 587 try:
587 588 return super(kwrepo, self).commit(*args, **opts)
588 589 finally:
589 590 del self.commitctx
590 591
591 592 def kwcommitctx(self, ctx, error=False):
592 593 n = super(kwrepo, self).commitctx(ctx, error)
593 594 # no lock needed, only called from repo.commit() which already locks
594 595 if not kwt.postcommit:
595 596 restrict = kwt.restrict
596 597 kwt.restrict = True
597 598 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
598 599 False, True)
599 600 kwt.restrict = restrict
600 601 return n
601 602
602 603 def rollback(self, dryrun=False, force=False):
603 604 wlock = self.wlock()
604 605 try:
605 606 if not dryrun:
606 607 changed = self['.'].files()
607 608 ret = super(kwrepo, self).rollback(dryrun, force)
608 609 if not dryrun:
609 610 ctx = self['.']
610 611 modified, added = _preselect(self[None].status(), changed)
611 612 kwt.overwrite(ctx, modified, True, True)
612 613 kwt.overwrite(ctx, added, True, False)
613 614 return ret
614 615 finally:
615 616 wlock.release()
616 617
617 618 # monkeypatches
618 619 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
619 620 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
620 621 rejects or conflicts due to expanded keywords in working dir.'''
621 622 orig(self, ui, gp, backend, store, eolmode)
622 623 # shrink keywords read from working dir
623 624 self.lines = kwt.shrinklines(self.fname, self.lines)
624 625
625 626 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
626 627 opts=None, prefix=''):
627 628 '''Monkeypatch patch.diff to avoid expansion.'''
628 629 kwt.restrict = True
629 630 return orig(repo, node1, node2, match, changes, opts, prefix)
630 631
631 632 def kwweb_skip(orig, web, req, tmpl):
632 633 '''Wraps webcommands.x turning off keyword expansion.'''
633 634 kwt.match = util.never
634 635 return orig(web, req, tmpl)
635 636
636 637 def kw_amend(orig, ui, repo, commitfunc, old, extra, pats, opts):
637 638 '''Wraps cmdutil.amend expanding keywords after amend.'''
638 639 wlock = repo.wlock()
639 640 try:
640 641 kwt.postcommit = True
641 642 newid = orig(ui, repo, commitfunc, old, extra, pats, opts)
642 643 if newid != old.node():
643 644 ctx = repo[newid]
644 645 kwt.restrict = True
645 646 kwt.overwrite(ctx, ctx.files(), False, True)
646 647 kwt.restrict = False
647 648 return newid
648 649 finally:
649 650 wlock.release()
650 651
651 652 def kw_copy(orig, ui, repo, pats, opts, rename=False):
652 653 '''Wraps cmdutil.copy so that copy/rename destinations do not
653 654 contain expanded keywords.
654 655 Note that the source of a regular file destination may also be a
655 656 symlink:
656 657 hg cp sym x -> x is symlink
657 658 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
658 659 For the latter we have to follow the symlink to find out whether its
659 660 target is configured for expansion and we therefore must unexpand the
660 661 keywords in the destination.'''
661 662 wlock = repo.wlock()
662 663 try:
663 664 orig(ui, repo, pats, opts, rename)
664 665 if opts.get('dry_run'):
665 666 return
666 667 wctx = repo[None]
667 668 cwd = repo.getcwd()
668 669
669 670 def haskwsource(dest):
670 671 '''Returns true if dest is a regular file and configured for
671 672 expansion or a symlink which points to a file configured for
672 673 expansion. '''
673 674 source = repo.dirstate.copied(dest)
674 675 if 'l' in wctx.flags(source):
675 676 source = scmutil.canonpath(repo.root, cwd,
676 677 os.path.realpath(source))
677 678 return kwt.match(source)
678 679
679 680 candidates = [f for f in repo.dirstate.copies() if
680 681 'l' not in wctx.flags(f) and haskwsource(f)]
681 682 kwt.overwrite(wctx, candidates, False, False)
682 683 finally:
683 684 wlock.release()
684 685
685 686 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
686 687 '''Wraps record.dorecord expanding keywords after recording.'''
687 688 wlock = repo.wlock()
688 689 try:
689 690 # record returns 0 even when nothing has changed
690 691 # therefore compare nodes before and after
691 692 kwt.postcommit = True
692 693 ctx = repo['.']
693 694 wstatus = repo[None].status()
694 695 ret = orig(ui, repo, commitfunc, *pats, **opts)
695 696 recctx = repo['.']
696 697 if ctx != recctx:
697 698 modified, added = _preselect(wstatus, recctx.files())
698 699 kwt.restrict = False
699 700 kwt.overwrite(recctx, modified, False, True)
700 701 kwt.overwrite(recctx, added, False, True, True)
701 702 kwt.restrict = True
702 703 return ret
703 704 finally:
704 705 wlock.release()
705 706
706 707 def kwfilectx_cmp(orig, self, fctx):
707 708 # keyword affects data size, comparing wdir and filelog size does
708 709 # not make sense
709 710 if (fctx._filerev is None and
710 711 (self._repo._encodefilterpats or
711 712 kwt.match(fctx.path()) and 'l' not in fctx.flags() or
712 713 self.size() - 4 == fctx.size()) or
713 714 self.size() == fctx.size()):
714 715 return self._filelog.cmp(self._filenode, fctx.data())
715 716 return True
716 717
717 718 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
718 719 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
719 720 extensions.wrapfunction(patch, 'diff', kw_diff)
720 721 extensions.wrapfunction(cmdutil, 'amend', kw_amend)
721 722 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
722 723 for c in 'annotate changeset rev filediff diff'.split():
723 724 extensions.wrapfunction(webcommands, c, kwweb_skip)
724 725 for name in recordextensions.split():
725 726 try:
726 727 record = extensions.find(name)
727 728 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
728 729 except KeyError:
729 730 pass
730 731
731 732 repo.__class__ = kwrepo
@@ -1,3447 +1,3448 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 behaviour 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 mercurial.i18n import _
66 66 from mercurial.node import bin, hex, short, nullid, nullrev
67 67 from mercurial.lock import release
68 68 from mercurial import commands, cmdutil, hg, scmutil, util, revset
69 69 from mercurial import extensions, error, phases
70 70 from mercurial import patch as patchmod
71 71 from mercurial import localrepo
72 72 from mercurial import subrepo
73 73 import os, re, errno, shutil
74 74
75 75 commands.norepo += " qclone"
76 76
77 77 seriesopts = [('s', 'summary', None, _('print first line of patch header'))]
78 78
79 79 cmdtable = {}
80 80 command = cmdutil.command(cmdtable)
81 81 testedwith = 'internal'
82 82
83 83 # force load strip extension formerly included in mq and import some utility
84 84 try:
85 85 stripext = extensions.find('strip')
86 86 except KeyError:
87 87 # note: load is lazy so we could avoid the try-except,
88 88 # but I (marmoute) prefer this explicit code.
89 89 class dummyui(object):
90 90 def debug(self, msg):
91 91 pass
92 92 stripext = extensions.load(dummyui(), 'strip', '')
93 93
94 94 strip = stripext.strip
95 95 checksubstate = stripext.checksubstate
96 96 checklocalchanges = stripext.checklocalchanges
97 97
98 98
99 99 # Patch names looks like unix-file names.
100 100 # They must be joinable with queue directory and result in the patch path.
101 101 normname = util.normpath
102 102
103 103 class statusentry(object):
104 104 def __init__(self, node, name):
105 105 self.node, self.name = node, name
106 106 def __repr__(self):
107 107 return hex(self.node) + ':' + self.name
108 108
109 109 class patchheader(object):
110 110 def __init__(self, pf, plainmode=False):
111 111 def eatdiff(lines):
112 112 while lines:
113 113 l = lines[-1]
114 114 if (l.startswith("diff -") or
115 115 l.startswith("Index:") or
116 116 l.startswith("===========")):
117 117 del lines[-1]
118 118 else:
119 119 break
120 120 def eatempty(lines):
121 121 while lines:
122 122 if not lines[-1].strip():
123 123 del lines[-1]
124 124 else:
125 125 break
126 126
127 127 message = []
128 128 comments = []
129 129 user = None
130 130 date = None
131 131 parent = None
132 132 format = None
133 133 subject = None
134 134 branch = None
135 135 nodeid = None
136 136 diffstart = 0
137 137
138 138 for line in file(pf):
139 139 line = line.rstrip()
140 140 if (line.startswith('diff --git')
141 141 or (diffstart and line.startswith('+++ '))):
142 142 diffstart = 2
143 143 break
144 144 diffstart = 0 # reset
145 145 if line.startswith("--- "):
146 146 diffstart = 1
147 147 continue
148 148 elif format == "hgpatch":
149 149 # parse values when importing the result of an hg export
150 150 if line.startswith("# User "):
151 151 user = line[7:]
152 152 elif line.startswith("# Date "):
153 153 date = line[7:]
154 154 elif line.startswith("# Parent "):
155 155 parent = line[9:].lstrip()
156 156 elif line.startswith("# Branch "):
157 157 branch = line[9:]
158 158 elif line.startswith("# Node ID "):
159 159 nodeid = line[10:]
160 160 elif not line.startswith("# ") and line:
161 161 message.append(line)
162 162 format = None
163 163 elif line == '# HG changeset patch':
164 164 message = []
165 165 format = "hgpatch"
166 166 elif (format != "tagdone" and (line.startswith("Subject: ") or
167 167 line.startswith("subject: "))):
168 168 subject = line[9:]
169 169 format = "tag"
170 170 elif (format != "tagdone" and (line.startswith("From: ") or
171 171 line.startswith("from: "))):
172 172 user = line[6:]
173 173 format = "tag"
174 174 elif (format != "tagdone" and (line.startswith("Date: ") or
175 175 line.startswith("date: "))):
176 176 date = line[6:]
177 177 format = "tag"
178 178 elif format == "tag" and line == "":
179 179 # when looking for tags (subject: from: etc) they
180 180 # end once you find a blank line in the source
181 181 format = "tagdone"
182 182 elif message or line:
183 183 message.append(line)
184 184 comments.append(line)
185 185
186 186 eatdiff(message)
187 187 eatdiff(comments)
188 188 # Remember the exact starting line of the patch diffs before consuming
189 189 # empty lines, for external use by TortoiseHg and others
190 190 self.diffstartline = len(comments)
191 191 eatempty(message)
192 192 eatempty(comments)
193 193
194 194 # make sure message isn't empty
195 195 if format and format.startswith("tag") and subject:
196 196 message.insert(0, "")
197 197 message.insert(0, subject)
198 198
199 199 self.message = message
200 200 self.comments = comments
201 201 self.user = user
202 202 self.date = date
203 203 self.parent = parent
204 204 # nodeid and branch are for external use by TortoiseHg and others
205 205 self.nodeid = nodeid
206 206 self.branch = branch
207 207 self.haspatch = diffstart > 1
208 208 self.plainmode = plainmode
209 209
210 210 def setuser(self, user):
211 211 if not self.updateheader(['From: ', '# User '], user):
212 212 try:
213 213 patchheaderat = self.comments.index('# HG changeset patch')
214 214 self.comments.insert(patchheaderat + 1, '# User ' + user)
215 215 except ValueError:
216 216 if self.plainmode or self._hasheader(['Date: ']):
217 217 self.comments = ['From: ' + user] + self.comments
218 218 else:
219 219 tmp = ['# HG changeset patch', '# User ' + user, '']
220 220 self.comments = tmp + self.comments
221 221 self.user = user
222 222
223 223 def setdate(self, date):
224 224 if not self.updateheader(['Date: ', '# Date '], date):
225 225 try:
226 226 patchheaderat = self.comments.index('# HG changeset patch')
227 227 self.comments.insert(patchheaderat + 1, '# Date ' + date)
228 228 except ValueError:
229 229 if self.plainmode or self._hasheader(['From: ']):
230 230 self.comments = ['Date: ' + date] + self.comments
231 231 else:
232 232 tmp = ['# HG changeset patch', '# Date ' + date, '']
233 233 self.comments = tmp + self.comments
234 234 self.date = date
235 235
236 236 def setparent(self, parent):
237 237 if not self.updateheader(['# Parent '], parent):
238 238 try:
239 239 patchheaderat = self.comments.index('# HG changeset patch')
240 240 self.comments.insert(patchheaderat + 1, '# Parent ' + parent)
241 241 except ValueError:
242 242 pass
243 243 self.parent = parent
244 244
245 245 def setmessage(self, message):
246 246 if self.comments:
247 247 self._delmsg()
248 248 self.message = [message]
249 249 self.comments += self.message
250 250
251 251 def updateheader(self, prefixes, new):
252 252 '''Update all references to a field in the patch header.
253 253 Return whether the field is present.'''
254 254 res = False
255 255 for prefix in prefixes:
256 256 for i in xrange(len(self.comments)):
257 257 if self.comments[i].startswith(prefix):
258 258 self.comments[i] = prefix + new
259 259 res = True
260 260 break
261 261 return res
262 262
263 263 def _hasheader(self, prefixes):
264 264 '''Check if a header starts with any of the given prefixes.'''
265 265 for prefix in prefixes:
266 266 for comment in self.comments:
267 267 if comment.startswith(prefix):
268 268 return True
269 269 return False
270 270
271 271 def __str__(self):
272 272 if not self.comments:
273 273 return ''
274 274 return '\n'.join(self.comments) + '\n\n'
275 275
276 276 def _delmsg(self):
277 277 '''Remove existing message, keeping the rest of the comments fields.
278 278 If comments contains 'subject: ', message will prepend
279 279 the field and a blank line.'''
280 280 if self.message:
281 281 subj = 'subject: ' + self.message[0].lower()
282 282 for i in xrange(len(self.comments)):
283 283 if subj == self.comments[i].lower():
284 284 del self.comments[i]
285 285 self.message = self.message[2:]
286 286 break
287 287 ci = 0
288 288 for mi in self.message:
289 289 while mi != self.comments[ci]:
290 290 ci += 1
291 291 del self.comments[ci]
292 292
293 293 def newcommit(repo, phase, *args, **kwargs):
294 294 """helper dedicated to ensure a commit respect mq.secret setting
295 295
296 296 It should be used instead of repo.commit inside the mq source for operation
297 297 creating new changeset.
298 298 """
299 299 repo = repo.unfiltered()
300 300 if phase is None:
301 301 if repo.ui.configbool('mq', 'secret', False):
302 302 phase = phases.secret
303 303 if phase is not None:
304 304 backup = repo.ui.backupconfig('phases', 'new-commit')
305 305 try:
306 306 if phase is not None:
307 307 repo.ui.setconfig('phases', 'new-commit', phase)
308 308 return repo.commit(*args, **kwargs)
309 309 finally:
310 310 if phase is not None:
311 311 repo.ui.restoreconfig(backup)
312 312
313 313 class AbortNoCleanup(error.Abort):
314 314 pass
315 315
316 316 class queue(object):
317 317 def __init__(self, ui, baseui, path, patchdir=None):
318 318 self.basepath = path
319 319 try:
320 320 fh = open(os.path.join(path, 'patches.queue'))
321 321 cur = fh.read().rstrip()
322 322 fh.close()
323 323 if not cur:
324 324 curpath = os.path.join(path, 'patches')
325 325 else:
326 326 curpath = os.path.join(path, 'patches-' + cur)
327 327 except IOError:
328 328 curpath = os.path.join(path, 'patches')
329 329 self.path = patchdir or curpath
330 330 self.opener = scmutil.opener(self.path)
331 331 self.ui = ui
332 332 self.baseui = baseui
333 333 self.applieddirty = False
334 334 self.seriesdirty = False
335 335 self.added = []
336 336 self.seriespath = "series"
337 337 self.statuspath = "status"
338 338 self.guardspath = "guards"
339 339 self.activeguards = None
340 340 self.guardsdirty = False
341 341 # Handle mq.git as a bool with extended values
342 342 try:
343 343 gitmode = ui.configbool('mq', 'git', None)
344 344 if gitmode is None:
345 345 raise error.ConfigError
346 346 self.gitmode = gitmode and 'yes' or 'no'
347 347 except error.ConfigError:
348 348 self.gitmode = ui.config('mq', 'git', 'auto').lower()
349 349 self.plainmode = ui.configbool('mq', 'plain', False)
350 350 self.checkapplied = True
351 351
352 352 @util.propertycache
353 353 def applied(self):
354 354 def parselines(lines):
355 355 for l in lines:
356 356 entry = l.split(':', 1)
357 357 if len(entry) > 1:
358 358 n, name = entry
359 359 yield statusentry(bin(n), name)
360 360 elif l.strip():
361 361 self.ui.warn(_('malformated mq status line: %s\n') % entry)
362 362 # else we ignore empty lines
363 363 try:
364 364 lines = self.opener.read(self.statuspath).splitlines()
365 365 return list(parselines(lines))
366 366 except IOError, e:
367 367 if e.errno == errno.ENOENT:
368 368 return []
369 369 raise
370 370
371 371 @util.propertycache
372 372 def fullseries(self):
373 373 try:
374 374 return self.opener.read(self.seriespath).splitlines()
375 375 except IOError, e:
376 376 if e.errno == errno.ENOENT:
377 377 return []
378 378 raise
379 379
380 380 @util.propertycache
381 381 def series(self):
382 382 self.parseseries()
383 383 return self.series
384 384
385 385 @util.propertycache
386 386 def seriesguards(self):
387 387 self.parseseries()
388 388 return self.seriesguards
389 389
390 390 def invalidate(self):
391 391 for a in 'applied fullseries series seriesguards'.split():
392 392 if a in self.__dict__:
393 393 delattr(self, a)
394 394 self.applieddirty = False
395 395 self.seriesdirty = False
396 396 self.guardsdirty = False
397 397 self.activeguards = None
398 398
399 399 def diffopts(self, opts={}, patchfn=None):
400 400 diffopts = patchmod.diffopts(self.ui, opts)
401 401 if self.gitmode == 'auto':
402 402 diffopts.upgrade = True
403 403 elif self.gitmode == 'keep':
404 404 pass
405 405 elif self.gitmode in ('yes', 'no'):
406 406 diffopts.git = self.gitmode == 'yes'
407 407 else:
408 408 raise util.Abort(_('mq.git option can be auto/keep/yes/no'
409 409 ' got %s') % self.gitmode)
410 410 if patchfn:
411 411 diffopts = self.patchopts(diffopts, patchfn)
412 412 return diffopts
413 413
414 414 def patchopts(self, diffopts, *patches):
415 415 """Return a copy of input diff options with git set to true if
416 416 referenced patch is a git patch and should be preserved as such.
417 417 """
418 418 diffopts = diffopts.copy()
419 419 if not diffopts.git and self.gitmode == 'keep':
420 420 for patchfn in patches:
421 421 patchf = self.opener(patchfn, 'r')
422 422 # if the patch was a git patch, refresh it as a git patch
423 423 for line in patchf:
424 424 if line.startswith('diff --git'):
425 425 diffopts.git = True
426 426 break
427 427 patchf.close()
428 428 return diffopts
429 429
430 430 def join(self, *p):
431 431 return os.path.join(self.path, *p)
432 432
433 433 def findseries(self, patch):
434 434 def matchpatch(l):
435 435 l = l.split('#', 1)[0]
436 436 return l.strip() == patch
437 437 for index, l in enumerate(self.fullseries):
438 438 if matchpatch(l):
439 439 return index
440 440 return None
441 441
442 442 guard_re = re.compile(r'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)')
443 443
444 444 def parseseries(self):
445 445 self.series = []
446 446 self.seriesguards = []
447 447 for l in self.fullseries:
448 448 h = l.find('#')
449 449 if h == -1:
450 450 patch = l
451 451 comment = ''
452 452 elif h == 0:
453 453 continue
454 454 else:
455 455 patch = l[:h]
456 456 comment = l[h:]
457 457 patch = patch.strip()
458 458 if patch:
459 459 if patch in self.series:
460 460 raise util.Abort(_('%s appears more than once in %s') %
461 461 (patch, self.join(self.seriespath)))
462 462 self.series.append(patch)
463 463 self.seriesguards.append(self.guard_re.findall(comment))
464 464
465 465 def checkguard(self, guard):
466 466 if not guard:
467 467 return _('guard cannot be an empty string')
468 468 bad_chars = '# \t\r\n\f'
469 469 first = guard[0]
470 470 if first in '-+':
471 471 return (_('guard %r starts with invalid character: %r') %
472 472 (guard, first))
473 473 for c in bad_chars:
474 474 if c in guard:
475 475 return _('invalid character in guard %r: %r') % (guard, c)
476 476
477 477 def setactive(self, guards):
478 478 for guard in guards:
479 479 bad = self.checkguard(guard)
480 480 if bad:
481 481 raise util.Abort(bad)
482 482 guards = sorted(set(guards))
483 483 self.ui.debug('active guards: %s\n' % ' '.join(guards))
484 484 self.activeguards = guards
485 485 self.guardsdirty = True
486 486
487 487 def active(self):
488 488 if self.activeguards is None:
489 489 self.activeguards = []
490 490 try:
491 491 guards = self.opener.read(self.guardspath).split()
492 492 except IOError, err:
493 493 if err.errno != errno.ENOENT:
494 494 raise
495 495 guards = []
496 496 for i, guard in enumerate(guards):
497 497 bad = self.checkguard(guard)
498 498 if bad:
499 499 self.ui.warn('%s:%d: %s\n' %
500 500 (self.join(self.guardspath), i + 1, bad))
501 501 else:
502 502 self.activeguards.append(guard)
503 503 return self.activeguards
504 504
505 505 def setguards(self, idx, guards):
506 506 for g in guards:
507 507 if len(g) < 2:
508 508 raise util.Abort(_('guard %r too short') % g)
509 509 if g[0] not in '-+':
510 510 raise util.Abort(_('guard %r starts with invalid char') % g)
511 511 bad = self.checkguard(g[1:])
512 512 if bad:
513 513 raise util.Abort(bad)
514 514 drop = self.guard_re.sub('', self.fullseries[idx])
515 515 self.fullseries[idx] = drop + ''.join([' #' + g for g in guards])
516 516 self.parseseries()
517 517 self.seriesdirty = True
518 518
519 519 def pushable(self, idx):
520 520 if isinstance(idx, str):
521 521 idx = self.series.index(idx)
522 522 patchguards = self.seriesguards[idx]
523 523 if not patchguards:
524 524 return True, None
525 525 guards = self.active()
526 526 exactneg = [g for g in patchguards if g[0] == '-' and g[1:] in guards]
527 527 if exactneg:
528 528 return False, repr(exactneg[0])
529 529 pos = [g for g in patchguards if g[0] == '+']
530 530 exactpos = [g for g in pos if g[1:] in guards]
531 531 if pos:
532 532 if exactpos:
533 533 return True, repr(exactpos[0])
534 534 return False, ' '.join(map(repr, pos))
535 535 return True, ''
536 536
537 537 def explainpushable(self, idx, all_patches=False):
538 538 write = all_patches and self.ui.write or self.ui.warn
539 539 if all_patches or self.ui.verbose:
540 540 if isinstance(idx, str):
541 541 idx = self.series.index(idx)
542 542 pushable, why = self.pushable(idx)
543 543 if all_patches and pushable:
544 544 if why is None:
545 545 write(_('allowing %s - no guards in effect\n') %
546 546 self.series[idx])
547 547 else:
548 548 if not why:
549 549 write(_('allowing %s - no matching negative guards\n') %
550 550 self.series[idx])
551 551 else:
552 552 write(_('allowing %s - guarded by %s\n') %
553 553 (self.series[idx], why))
554 554 if not pushable:
555 555 if why:
556 556 write(_('skipping %s - guarded by %s\n') %
557 557 (self.series[idx], why))
558 558 else:
559 559 write(_('skipping %s - no matching guards\n') %
560 560 self.series[idx])
561 561
562 562 def savedirty(self):
563 563 def writelist(items, path):
564 564 fp = self.opener(path, 'w')
565 565 for i in items:
566 566 fp.write("%s\n" % i)
567 567 fp.close()
568 568 if self.applieddirty:
569 569 writelist(map(str, self.applied), self.statuspath)
570 570 self.applieddirty = False
571 571 if self.seriesdirty:
572 572 writelist(self.fullseries, self.seriespath)
573 573 self.seriesdirty = False
574 574 if self.guardsdirty:
575 575 writelist(self.activeguards, self.guardspath)
576 576 self.guardsdirty = False
577 577 if self.added:
578 578 qrepo = self.qrepo()
579 579 if qrepo:
580 580 qrepo[None].add(f for f in self.added if f not in qrepo[None])
581 581 self.added = []
582 582
583 583 def removeundo(self, repo):
584 584 undo = repo.sjoin('undo')
585 585 if not os.path.exists(undo):
586 586 return
587 587 try:
588 588 os.unlink(undo)
589 589 except OSError, inst:
590 590 self.ui.warn(_('error removing undo: %s\n') % str(inst))
591 591
592 592 def backup(self, repo, files, copy=False):
593 593 # backup local changes in --force case
594 594 for f in sorted(files):
595 595 absf = repo.wjoin(f)
596 596 if os.path.lexists(absf):
597 597 self.ui.note(_('saving current version of %s as %s\n') %
598 598 (f, f + '.orig'))
599 599 if copy:
600 600 util.copyfile(absf, absf + '.orig')
601 601 else:
602 602 util.rename(absf, absf + '.orig')
603 603
604 604 def printdiff(self, repo, diffopts, node1, node2=None, files=None,
605 605 fp=None, changes=None, opts={}):
606 606 stat = opts.get('stat')
607 607 m = scmutil.match(repo[node1], files, opts)
608 608 cmdutil.diffordiffstat(self.ui, repo, diffopts, node1, node2, m,
609 609 changes, stat, fp)
610 610
611 611 def mergeone(self, repo, mergeq, head, patch, rev, diffopts):
612 612 # first try just applying the patch
613 613 (err, n) = self.apply(repo, [patch], update_status=False,
614 614 strict=True, merge=rev)
615 615
616 616 if err == 0:
617 617 return (err, n)
618 618
619 619 if n is None:
620 620 raise util.Abort(_("apply failed for patch %s") % patch)
621 621
622 622 self.ui.warn(_("patch didn't work out, merging %s\n") % patch)
623 623
624 624 # apply failed, strip away that rev and merge.
625 625 hg.clean(repo, head)
626 626 strip(self.ui, repo, [n], update=False, backup='strip')
627 627
628 628 ctx = repo[rev]
629 629 ret = hg.merge(repo, rev)
630 630 if ret:
631 631 raise util.Abort(_("update returned %d") % ret)
632 632 n = newcommit(repo, None, ctx.description(), ctx.user(), force=True)
633 633 if n is None:
634 634 raise util.Abort(_("repo commit failed"))
635 635 try:
636 636 ph = patchheader(mergeq.join(patch), self.plainmode)
637 637 except Exception:
638 638 raise util.Abort(_("unable to read %s") % patch)
639 639
640 640 diffopts = self.patchopts(diffopts, patch)
641 641 patchf = self.opener(patch, "w")
642 642 comments = str(ph)
643 643 if comments:
644 644 patchf.write(comments)
645 645 self.printdiff(repo, diffopts, head, n, fp=patchf)
646 646 patchf.close()
647 647 self.removeundo(repo)
648 648 return (0, n)
649 649
650 650 def qparents(self, repo, rev=None):
651 651 """return the mq handled parent or p1
652 652
653 653 In some case where mq get himself in being the parent of a merge the
654 654 appropriate parent may be p2.
655 655 (eg: an in progress merge started with mq disabled)
656 656
657 657 If no parent are managed by mq, p1 is returned.
658 658 """
659 659 if rev is None:
660 660 (p1, p2) = repo.dirstate.parents()
661 661 if p2 == nullid:
662 662 return p1
663 663 if not self.applied:
664 664 return None
665 665 return self.applied[-1].node
666 666 p1, p2 = repo.changelog.parents(rev)
667 667 if p2 != nullid and p2 in [x.node for x in self.applied]:
668 668 return p2
669 669 return p1
670 670
671 671 def mergepatch(self, repo, mergeq, series, diffopts):
672 672 if not self.applied:
673 673 # each of the patches merged in will have two parents. This
674 674 # can confuse the qrefresh, qdiff, and strip code because it
675 675 # needs to know which parent is actually in the patch queue.
676 676 # so, we insert a merge marker with only one parent. This way
677 677 # the first patch in the queue is never a merge patch
678 678 #
679 679 pname = ".hg.patches.merge.marker"
680 680 n = newcommit(repo, None, '[mq]: merge marker', force=True)
681 681 self.removeundo(repo)
682 682 self.applied.append(statusentry(n, pname))
683 683 self.applieddirty = True
684 684
685 685 head = self.qparents(repo)
686 686
687 687 for patch in series:
688 688 patch = mergeq.lookup(patch, strict=True)
689 689 if not patch:
690 690 self.ui.warn(_("patch %s does not exist\n") % patch)
691 691 return (1, None)
692 692 pushable, reason = self.pushable(patch)
693 693 if not pushable:
694 694 self.explainpushable(patch, all_patches=True)
695 695 continue
696 696 info = mergeq.isapplied(patch)
697 697 if not info:
698 698 self.ui.warn(_("patch %s is not applied\n") % patch)
699 699 return (1, None)
700 700 rev = info[1]
701 701 err, head = self.mergeone(repo, mergeq, head, patch, rev, diffopts)
702 702 if head:
703 703 self.applied.append(statusentry(head, patch))
704 704 self.applieddirty = True
705 705 if err:
706 706 return (err, head)
707 707 self.savedirty()
708 708 return (0, head)
709 709
710 710 def patch(self, repo, patchfile):
711 711 '''Apply patchfile to the working directory.
712 712 patchfile: name of patch file'''
713 713 files = set()
714 714 try:
715 715 fuzz = patchmod.patch(self.ui, repo, patchfile, strip=1,
716 716 files=files, eolmode=None)
717 717 return (True, list(files), fuzz)
718 718 except Exception, inst:
719 719 self.ui.note(str(inst) + '\n')
720 720 if not self.ui.verbose:
721 721 self.ui.warn(_("patch failed, unable to continue (try -v)\n"))
722 722 self.ui.traceback()
723 723 return (False, list(files), False)
724 724
725 725 def apply(self, repo, series, list=False, update_status=True,
726 726 strict=False, patchdir=None, merge=None, all_files=None,
727 727 tobackup=None, keepchanges=False):
728 728 wlock = lock = tr = None
729 729 try:
730 730 wlock = repo.wlock()
731 731 lock = repo.lock()
732 732 tr = repo.transaction("qpush")
733 733 try:
734 734 ret = self._apply(repo, series, list, update_status,
735 735 strict, patchdir, merge, all_files=all_files,
736 736 tobackup=tobackup, keepchanges=keepchanges)
737 737 tr.close()
738 738 self.savedirty()
739 739 return ret
740 740 except AbortNoCleanup:
741 741 tr.close()
742 742 self.savedirty()
743 743 return 2, repo.dirstate.p1()
744 744 except: # re-raises
745 745 try:
746 746 tr.abort()
747 747 finally:
748 748 repo.invalidate()
749 749 repo.dirstate.invalidate()
750 750 self.invalidate()
751 751 raise
752 752 finally:
753 753 release(tr, lock, wlock)
754 754 self.removeundo(repo)
755 755
756 756 def _apply(self, repo, series, list=False, update_status=True,
757 757 strict=False, patchdir=None, merge=None, all_files=None,
758 758 tobackup=None, keepchanges=False):
759 759 """returns (error, hash)
760 760
761 761 error = 1 for unable to read, 2 for patch failed, 3 for patch
762 762 fuzz. tobackup is None or a set of files to backup before they
763 763 are modified by a patch.
764 764 """
765 765 # TODO unify with commands.py
766 766 if not patchdir:
767 767 patchdir = self.path
768 768 err = 0
769 769 n = None
770 770 for patchname in series:
771 771 pushable, reason = self.pushable(patchname)
772 772 if not pushable:
773 773 self.explainpushable(patchname, all_patches=True)
774 774 continue
775 775 self.ui.status(_("applying %s\n") % patchname)
776 776 pf = os.path.join(patchdir, patchname)
777 777
778 778 try:
779 779 ph = patchheader(self.join(patchname), self.plainmode)
780 780 except IOError:
781 781 self.ui.warn(_("unable to read %s\n") % patchname)
782 782 err = 1
783 783 break
784 784
785 785 message = ph.message
786 786 if not message:
787 787 # The commit message should not be translated
788 788 message = "imported patch %s\n" % patchname
789 789 else:
790 790 if list:
791 791 # The commit message should not be translated
792 792 message.append("\nimported patch %s" % patchname)
793 793 message = '\n'.join(message)
794 794
795 795 if ph.haspatch:
796 796 if tobackup:
797 797 touched = patchmod.changedfiles(self.ui, repo, pf)
798 798 touched = set(touched) & tobackup
799 799 if touched and keepchanges:
800 800 raise AbortNoCleanup(
801 801 _("local changes found, refresh first"))
802 802 self.backup(repo, touched, copy=True)
803 803 tobackup = tobackup - touched
804 804 (patcherr, files, fuzz) = self.patch(repo, pf)
805 805 if all_files is not None:
806 806 all_files.update(files)
807 807 patcherr = not patcherr
808 808 else:
809 809 self.ui.warn(_("patch %s is empty\n") % patchname)
810 810 patcherr, files, fuzz = 0, [], 0
811 811
812 812 if merge and files:
813 813 # Mark as removed/merged and update dirstate parent info
814 814 removed = []
815 815 merged = []
816 816 for f in files:
817 817 if os.path.lexists(repo.wjoin(f)):
818 818 merged.append(f)
819 819 else:
820 820 removed.append(f)
821 821 for f in removed:
822 822 repo.dirstate.remove(f)
823 823 for f in merged:
824 824 repo.dirstate.merge(f)
825 825 p1, p2 = repo.dirstate.parents()
826 826 repo.setparents(p1, merge)
827 827
828 828 if all_files and '.hgsubstate' in all_files:
829 829 wctx = repo['.']
830 830 mctx = actx = repo[None]
831 831 overwrite = False
832 832 mergedsubstate = subrepo.submerge(repo, wctx, mctx, actx,
833 833 overwrite)
834 834 files += mergedsubstate.keys()
835 835
836 836 match = scmutil.matchfiles(repo, files or [])
837 837 oldtip = repo['tip']
838 838 n = newcommit(repo, None, message, ph.user, ph.date, match=match,
839 839 force=True)
840 840 if repo['tip'] == oldtip:
841 841 raise util.Abort(_("qpush exactly duplicates child changeset"))
842 842 if n is None:
843 843 raise util.Abort(_("repository commit failed"))
844 844
845 845 if update_status:
846 846 self.applied.append(statusentry(n, patchname))
847 847
848 848 if patcherr:
849 849 self.ui.warn(_("patch failed, rejects left in working dir\n"))
850 850 err = 2
851 851 break
852 852
853 853 if fuzz and strict:
854 854 self.ui.warn(_("fuzz found when applying patch, stopping\n"))
855 855 err = 3
856 856 break
857 857 return (err, n)
858 858
859 859 def _cleanup(self, patches, numrevs, keep=False):
860 860 if not keep:
861 861 r = self.qrepo()
862 862 if r:
863 863 r[None].forget(patches)
864 864 for p in patches:
865 865 try:
866 866 os.unlink(self.join(p))
867 867 except OSError, inst:
868 868 if inst.errno != errno.ENOENT:
869 869 raise
870 870
871 871 qfinished = []
872 872 if numrevs:
873 873 qfinished = self.applied[:numrevs]
874 874 del self.applied[:numrevs]
875 875 self.applieddirty = True
876 876
877 877 unknown = []
878 878
879 879 for (i, p) in sorted([(self.findseries(p), p) for p in patches],
880 880 reverse=True):
881 881 if i is not None:
882 882 del self.fullseries[i]
883 883 else:
884 884 unknown.append(p)
885 885
886 886 if unknown:
887 887 if numrevs:
888 888 rev = dict((entry.name, entry.node) for entry in qfinished)
889 889 for p in unknown:
890 890 msg = _('revision %s refers to unknown patches: %s\n')
891 891 self.ui.warn(msg % (short(rev[p]), p))
892 892 else:
893 893 msg = _('unknown patches: %s\n')
894 894 raise util.Abort(''.join(msg % p for p in unknown))
895 895
896 896 self.parseseries()
897 897 self.seriesdirty = True
898 898 return [entry.node for entry in qfinished]
899 899
900 900 def _revpatches(self, repo, revs):
901 901 firstrev = repo[self.applied[0].node].rev()
902 902 patches = []
903 903 for i, rev in enumerate(revs):
904 904
905 905 if rev < firstrev:
906 906 raise util.Abort(_('revision %d is not managed') % rev)
907 907
908 908 ctx = repo[rev]
909 909 base = self.applied[i].node
910 910 if ctx.node() != base:
911 911 msg = _('cannot delete revision %d above applied patches')
912 912 raise util.Abort(msg % rev)
913 913
914 914 patch = self.applied[i].name
915 915 for fmt in ('[mq]: %s', 'imported patch %s'):
916 916 if ctx.description() == fmt % patch:
917 917 msg = _('patch %s finalized without changeset message\n')
918 918 repo.ui.status(msg % patch)
919 919 break
920 920
921 921 patches.append(patch)
922 922 return patches
923 923
924 924 def finish(self, repo, revs):
925 925 # Manually trigger phase computation to ensure phasedefaults is
926 926 # executed before we remove the patches.
927 927 repo._phasecache
928 928 patches = self._revpatches(repo, sorted(revs))
929 929 qfinished = self._cleanup(patches, len(patches))
930 930 if qfinished and repo.ui.configbool('mq', 'secret', False):
931 931 # only use this logic when the secret option is added
932 932 oldqbase = repo[qfinished[0]]
933 933 tphase = repo.ui.config('phases', 'new-commit', phases.draft)
934 934 if oldqbase.phase() > tphase and oldqbase.p1().phase() <= tphase:
935 935 phases.advanceboundary(repo, tphase, qfinished)
936 936
937 937 def delete(self, repo, patches, opts):
938 938 if not patches and not opts.get('rev'):
939 939 raise util.Abort(_('qdelete requires at least one revision or '
940 940 'patch name'))
941 941
942 942 realpatches = []
943 943 for patch in patches:
944 944 patch = self.lookup(patch, strict=True)
945 945 info = self.isapplied(patch)
946 946 if info:
947 947 raise util.Abort(_("cannot delete applied patch %s") % patch)
948 948 if patch not in self.series:
949 949 raise util.Abort(_("patch %s not in series file") % patch)
950 950 if patch not in realpatches:
951 951 realpatches.append(patch)
952 952
953 953 numrevs = 0
954 954 if opts.get('rev'):
955 955 if not self.applied:
956 956 raise util.Abort(_('no patches applied'))
957 957 revs = scmutil.revrange(repo, opts.get('rev'))
958 958 if len(revs) > 1 and revs[0] > revs[1]:
959 959 revs.reverse()
960 960 revpatches = self._revpatches(repo, revs)
961 961 realpatches += revpatches
962 962 numrevs = len(revpatches)
963 963
964 964 self._cleanup(realpatches, numrevs, opts.get('keep'))
965 965
966 966 def checktoppatch(self, repo):
967 967 '''check that working directory is at qtip'''
968 968 if self.applied:
969 969 top = self.applied[-1].node
970 970 patch = self.applied[-1].name
971 971 if repo.dirstate.p1() != top:
972 972 raise util.Abort(_("working directory revision is not qtip"))
973 973 return top, patch
974 974 return None, None
975 975
976 976 def putsubstate2changes(self, substatestate, changes):
977 977 for files in changes[:3]:
978 978 if '.hgsubstate' in files:
979 979 return # already listed up
980 980 # not yet listed up
981 981 if substatestate in 'a?':
982 982 changes[1].append('.hgsubstate')
983 983 elif substatestate in 'r':
984 984 changes[2].append('.hgsubstate')
985 985 else: # modified
986 986 changes[0].append('.hgsubstate')
987 987
988 988 def checklocalchanges(self, repo, force=False, refresh=True):
989 989 excsuffix = ''
990 990 if refresh:
991 991 excsuffix = ', refresh first'
992 992 # plain versions for i18n tool to detect them
993 993 _("local changes found, refresh first")
994 994 _("local changed subrepos found, refresh first")
995 995 return checklocalchanges(repo, force, excsuffix)
996 996
997 997 _reserved = ('series', 'status', 'guards', '.', '..')
998 998 def checkreservedname(self, name):
999 999 if name in self._reserved:
1000 1000 raise util.Abort(_('"%s" cannot be used as the name of a patch')
1001 1001 % name)
1002 1002 for prefix in ('.hg', '.mq'):
1003 1003 if name.startswith(prefix):
1004 1004 raise util.Abort(_('patch name cannot begin with "%s"')
1005 1005 % prefix)
1006 1006 for c in ('#', ':'):
1007 1007 if c in name:
1008 1008 raise util.Abort(_('"%s" cannot be used in the name of a patch')
1009 1009 % c)
1010 1010
1011 1011 def checkpatchname(self, name, force=False):
1012 1012 self.checkreservedname(name)
1013 1013 if not force and os.path.exists(self.join(name)):
1014 1014 if os.path.isdir(self.join(name)):
1015 1015 raise util.Abort(_('"%s" already exists as a directory')
1016 1016 % name)
1017 1017 else:
1018 1018 raise util.Abort(_('patch "%s" already exists') % name)
1019 1019
1020 1020 def checkkeepchanges(self, keepchanges, force):
1021 1021 if force and keepchanges:
1022 1022 raise util.Abort(_('cannot use both --force and --keep-changes'))
1023 1023
1024 1024 def new(self, repo, patchfn, *pats, **opts):
1025 1025 """options:
1026 1026 msg: a string or a no-argument function returning a string
1027 1027 """
1028 1028 msg = opts.get('msg')
1029 1029 user = opts.get('user')
1030 1030 date = opts.get('date')
1031 1031 if date:
1032 1032 date = util.parsedate(date)
1033 1033 diffopts = self.diffopts({'git': opts.get('git')})
1034 1034 if opts.get('checkname', True):
1035 1035 self.checkpatchname(patchfn)
1036 1036 inclsubs = checksubstate(repo)
1037 1037 if inclsubs:
1038 1038 inclsubs.append('.hgsubstate')
1039 1039 substatestate = repo.dirstate['.hgsubstate']
1040 1040 if opts.get('include') or opts.get('exclude') or pats:
1041 1041 if inclsubs:
1042 1042 pats = list(pats or []) + inclsubs
1043 1043 match = scmutil.match(repo[None], pats, opts)
1044 1044 # detect missing files in pats
1045 1045 def badfn(f, msg):
1046 1046 if f != '.hgsubstate': # .hgsubstate is auto-created
1047 1047 raise util.Abort('%s: %s' % (f, msg))
1048 1048 match.bad = badfn
1049 1049 changes = repo.status(match=match)
1050 1050 m, a, r, d = changes[:4]
1051 1051 else:
1052 1052 changes = self.checklocalchanges(repo, force=True)
1053 1053 m, a, r, d = changes
1054 1054 match = scmutil.matchfiles(repo, m + a + r + inclsubs)
1055 1055 if len(repo[None].parents()) > 1:
1056 1056 raise util.Abort(_('cannot manage merge changesets'))
1057 1057 commitfiles = m + a + r
1058 1058 self.checktoppatch(repo)
1059 1059 insert = self.fullseriesend()
1060 1060 wlock = repo.wlock()
1061 1061 try:
1062 1062 try:
1063 1063 # if patch file write fails, abort early
1064 1064 p = self.opener(patchfn, "w")
1065 1065 except IOError, e:
1066 1066 raise util.Abort(_('cannot write patch "%s": %s')
1067 1067 % (patchfn, e.strerror))
1068 1068 try:
1069 1069 if self.plainmode:
1070 1070 if user:
1071 1071 p.write("From: " + user + "\n")
1072 1072 if not date:
1073 1073 p.write("\n")
1074 1074 if date:
1075 1075 p.write("Date: %d %d\n\n" % date)
1076 1076 else:
1077 1077 p.write("# HG changeset patch\n")
1078 1078 p.write("# Parent "
1079 1079 + hex(repo[None].p1().node()) + "\n")
1080 1080 if user:
1081 1081 p.write("# User " + user + "\n")
1082 1082 if date:
1083 1083 p.write("# Date %s %s\n\n" % date)
1084 1084 if util.safehasattr(msg, '__call__'):
1085 1085 msg = msg()
1086 1086 commitmsg = msg and msg or ("[mq]: %s" % patchfn)
1087 1087 n = newcommit(repo, None, commitmsg, user, date, match=match,
1088 1088 force=True)
1089 1089 if n is None:
1090 1090 raise util.Abort(_("repo commit failed"))
1091 1091 try:
1092 1092 self.fullseries[insert:insert] = [patchfn]
1093 1093 self.applied.append(statusentry(n, patchfn))
1094 1094 self.parseseries()
1095 1095 self.seriesdirty = True
1096 1096 self.applieddirty = True
1097 1097 if msg:
1098 1098 msg = msg + "\n\n"
1099 1099 p.write(msg)
1100 1100 if commitfiles:
1101 1101 parent = self.qparents(repo, n)
1102 1102 if inclsubs:
1103 1103 self.putsubstate2changes(substatestate, changes)
1104 1104 chunks = patchmod.diff(repo, node1=parent, node2=n,
1105 1105 changes=changes, opts=diffopts)
1106 1106 for chunk in chunks:
1107 1107 p.write(chunk)
1108 1108 p.close()
1109 1109 r = self.qrepo()
1110 1110 if r:
1111 1111 r[None].add([patchfn])
1112 1112 except: # re-raises
1113 1113 repo.rollback()
1114 1114 raise
1115 1115 except Exception:
1116 1116 patchpath = self.join(patchfn)
1117 1117 try:
1118 1118 os.unlink(patchpath)
1119 1119 except OSError:
1120 1120 self.ui.warn(_('error unlinking %s\n') % patchpath)
1121 1121 raise
1122 1122 self.removeundo(repo)
1123 1123 finally:
1124 1124 release(wlock)
1125 1125
1126 1126 def isapplied(self, patch):
1127 1127 """returns (index, rev, patch)"""
1128 1128 for i, a in enumerate(self.applied):
1129 1129 if a.name == patch:
1130 1130 return (i, a.node, a.name)
1131 1131 return None
1132 1132
1133 1133 # if the exact patch name does not exist, we try a few
1134 1134 # variations. If strict is passed, we try only #1
1135 1135 #
1136 1136 # 1) a number (as string) to indicate an offset in the series file
1137 1137 # 2) a unique substring of the patch name was given
1138 1138 # 3) patchname[-+]num to indicate an offset in the series file
1139 1139 def lookup(self, patch, strict=False):
1140 1140 def partialname(s):
1141 1141 if s in self.series:
1142 1142 return s
1143 1143 matches = [x for x in self.series if s in x]
1144 1144 if len(matches) > 1:
1145 1145 self.ui.warn(_('patch name "%s" is ambiguous:\n') % s)
1146 1146 for m in matches:
1147 1147 self.ui.warn(' %s\n' % m)
1148 1148 return None
1149 1149 if matches:
1150 1150 return matches[0]
1151 1151 if self.series and self.applied:
1152 1152 if s == 'qtip':
1153 1153 return self.series[self.seriesend(True) - 1]
1154 1154 if s == 'qbase':
1155 1155 return self.series[0]
1156 1156 return None
1157 1157
1158 1158 if patch in self.series:
1159 1159 return patch
1160 1160
1161 1161 if not os.path.isfile(self.join(patch)):
1162 1162 try:
1163 1163 sno = int(patch)
1164 1164 except (ValueError, OverflowError):
1165 1165 pass
1166 1166 else:
1167 1167 if -len(self.series) <= sno < len(self.series):
1168 1168 return self.series[sno]
1169 1169
1170 1170 if not strict:
1171 1171 res = partialname(patch)
1172 1172 if res:
1173 1173 return res
1174 1174 minus = patch.rfind('-')
1175 1175 if minus >= 0:
1176 1176 res = partialname(patch[:minus])
1177 1177 if res:
1178 1178 i = self.series.index(res)
1179 1179 try:
1180 1180 off = int(patch[minus + 1:] or 1)
1181 1181 except (ValueError, OverflowError):
1182 1182 pass
1183 1183 else:
1184 1184 if i - off >= 0:
1185 1185 return self.series[i - off]
1186 1186 plus = patch.rfind('+')
1187 1187 if plus >= 0:
1188 1188 res = partialname(patch[:plus])
1189 1189 if res:
1190 1190 i = self.series.index(res)
1191 1191 try:
1192 1192 off = int(patch[plus + 1:] or 1)
1193 1193 except (ValueError, OverflowError):
1194 1194 pass
1195 1195 else:
1196 1196 if i + off < len(self.series):
1197 1197 return self.series[i + off]
1198 1198 raise util.Abort(_("patch %s not in series") % patch)
1199 1199
1200 1200 def push(self, repo, patch=None, force=False, list=False, mergeq=None,
1201 1201 all=False, move=False, exact=False, nobackup=False,
1202 1202 keepchanges=False):
1203 1203 self.checkkeepchanges(keepchanges, force)
1204 1204 diffopts = self.diffopts()
1205 1205 wlock = repo.wlock()
1206 1206 try:
1207 1207 heads = [h for hs in repo.branchmap().itervalues() for h in hs]
1208 1208 if not heads:
1209 1209 heads = [nullid]
1210 1210 if repo.dirstate.p1() not in heads and not exact:
1211 1211 self.ui.status(_("(working directory not at a head)\n"))
1212 1212
1213 1213 if not self.series:
1214 1214 self.ui.warn(_('no patches in series\n'))
1215 1215 return 0
1216 1216
1217 1217 # Suppose our series file is: A B C and the current 'top'
1218 1218 # patch is B. qpush C should be performed (moving forward)
1219 1219 # qpush B is a NOP (no change) qpush A is an error (can't
1220 1220 # go backwards with qpush)
1221 1221 if patch:
1222 1222 patch = self.lookup(patch)
1223 1223 info = self.isapplied(patch)
1224 1224 if info and info[0] >= len(self.applied) - 1:
1225 1225 self.ui.warn(
1226 1226 _('qpush: %s is already at the top\n') % patch)
1227 1227 return 0
1228 1228
1229 1229 pushable, reason = self.pushable(patch)
1230 1230 if pushable:
1231 1231 if self.series.index(patch) < self.seriesend():
1232 1232 raise util.Abort(
1233 1233 _("cannot push to a previous patch: %s") % patch)
1234 1234 else:
1235 1235 if reason:
1236 1236 reason = _('guarded by %s') % reason
1237 1237 else:
1238 1238 reason = _('no matching guards')
1239 1239 self.ui.warn(_("cannot push '%s' - %s\n") % (patch, reason))
1240 1240 return 1
1241 1241 elif all:
1242 1242 patch = self.series[-1]
1243 1243 if self.isapplied(patch):
1244 1244 self.ui.warn(_('all patches are currently applied\n'))
1245 1245 return 0
1246 1246
1247 1247 # Following the above example, starting at 'top' of B:
1248 1248 # qpush should be performed (pushes C), but a subsequent
1249 1249 # qpush without an argument is an error (nothing to
1250 1250 # apply). This allows a loop of "...while hg qpush..." to
1251 1251 # work as it detects an error when done
1252 1252 start = self.seriesend()
1253 1253 if start == len(self.series):
1254 1254 self.ui.warn(_('patch series already fully applied\n'))
1255 1255 return 1
1256 1256 if not force and not keepchanges:
1257 1257 self.checklocalchanges(repo, refresh=self.applied)
1258 1258
1259 1259 if exact:
1260 1260 if keepchanges:
1261 1261 raise util.Abort(
1262 1262 _("cannot use --exact and --keep-changes together"))
1263 1263 if move:
1264 1264 raise util.Abort(_('cannot use --exact and --move '
1265 1265 'together'))
1266 1266 if self.applied:
1267 1267 raise util.Abort(_('cannot push --exact with applied '
1268 1268 'patches'))
1269 1269 root = self.series[start]
1270 1270 target = patchheader(self.join(root), self.plainmode).parent
1271 1271 if not target:
1272 1272 raise util.Abort(
1273 1273 _("%s does not have a parent recorded") % root)
1274 1274 if not repo[target] == repo['.']:
1275 1275 hg.update(repo, target)
1276 1276
1277 1277 if move:
1278 1278 if not patch:
1279 1279 raise util.Abort(_("please specify the patch to move"))
1280 1280 for fullstart, rpn in enumerate(self.fullseries):
1281 1281 # strip markers for patch guards
1282 1282 if self.guard_re.split(rpn, 1)[0] == self.series[start]:
1283 1283 break
1284 1284 for i, rpn in enumerate(self.fullseries[fullstart:]):
1285 1285 # strip markers for patch guards
1286 1286 if self.guard_re.split(rpn, 1)[0] == patch:
1287 1287 break
1288 1288 index = fullstart + i
1289 1289 assert index < len(self.fullseries)
1290 1290 fullpatch = self.fullseries[index]
1291 1291 del self.fullseries[index]
1292 1292 self.fullseries.insert(fullstart, fullpatch)
1293 1293 self.parseseries()
1294 1294 self.seriesdirty = True
1295 1295
1296 1296 self.applieddirty = True
1297 1297 if start > 0:
1298 1298 self.checktoppatch(repo)
1299 1299 if not patch:
1300 1300 patch = self.series[start]
1301 1301 end = start + 1
1302 1302 else:
1303 1303 end = self.series.index(patch, start) + 1
1304 1304
1305 1305 tobackup = set()
1306 1306 if (not nobackup and force) or keepchanges:
1307 1307 m, a, r, d = self.checklocalchanges(repo, force=True)
1308 1308 if keepchanges:
1309 1309 tobackup.update(m + a + r + d)
1310 1310 else:
1311 1311 tobackup.update(m + a)
1312 1312
1313 1313 s = self.series[start:end]
1314 1314 all_files = set()
1315 1315 try:
1316 1316 if mergeq:
1317 1317 ret = self.mergepatch(repo, mergeq, s, diffopts)
1318 1318 else:
1319 1319 ret = self.apply(repo, s, list, all_files=all_files,
1320 1320 tobackup=tobackup, keepchanges=keepchanges)
1321 1321 except: # re-raises
1322 1322 self.ui.warn(_('cleaning up working directory...'))
1323 1323 node = repo.dirstate.p1()
1324 1324 hg.revert(repo, node, None)
1325 1325 # only remove unknown files that we know we touched or
1326 1326 # created while patching
1327 1327 for f in all_files:
1328 1328 if f not in repo.dirstate:
1329 1329 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
1330 1330 self.ui.warn(_('done\n'))
1331 1331 raise
1332 1332
1333 1333 if not self.applied:
1334 1334 return ret[0]
1335 1335 top = self.applied[-1].name
1336 1336 if ret[0] and ret[0] > 1:
1337 1337 msg = _("errors during apply, please fix and refresh %s\n")
1338 1338 self.ui.write(msg % top)
1339 1339 else:
1340 1340 self.ui.write(_("now at: %s\n") % top)
1341 1341 return ret[0]
1342 1342
1343 1343 finally:
1344 1344 wlock.release()
1345 1345
1346 1346 def pop(self, repo, patch=None, force=False, update=True, all=False,
1347 1347 nobackup=False, keepchanges=False):
1348 1348 self.checkkeepchanges(keepchanges, force)
1349 1349 wlock = repo.wlock()
1350 1350 try:
1351 1351 if patch:
1352 1352 # index, rev, patch
1353 1353 info = self.isapplied(patch)
1354 1354 if not info:
1355 1355 patch = self.lookup(patch)
1356 1356 info = self.isapplied(patch)
1357 1357 if not info:
1358 1358 raise util.Abort(_("patch %s is not applied") % patch)
1359 1359
1360 1360 if not self.applied:
1361 1361 # Allow qpop -a to work repeatedly,
1362 1362 # but not qpop without an argument
1363 1363 self.ui.warn(_("no patches applied\n"))
1364 1364 return not all
1365 1365
1366 1366 if all:
1367 1367 start = 0
1368 1368 elif patch:
1369 1369 start = info[0] + 1
1370 1370 else:
1371 1371 start = len(self.applied) - 1
1372 1372
1373 1373 if start >= len(self.applied):
1374 1374 self.ui.warn(_("qpop: %s is already at the top\n") % patch)
1375 1375 return
1376 1376
1377 1377 if not update:
1378 1378 parents = repo.dirstate.parents()
1379 1379 rr = [x.node for x in self.applied]
1380 1380 for p in parents:
1381 1381 if p in rr:
1382 1382 self.ui.warn(_("qpop: forcing dirstate update\n"))
1383 1383 update = True
1384 1384 else:
1385 1385 parents = [p.node() for p in repo[None].parents()]
1386 1386 needupdate = False
1387 1387 for entry in self.applied[start:]:
1388 1388 if entry.node in parents:
1389 1389 needupdate = True
1390 1390 break
1391 1391 update = needupdate
1392 1392
1393 1393 tobackup = set()
1394 1394 if update:
1395 1395 m, a, r, d = self.checklocalchanges(
1396 1396 repo, force=force or keepchanges)
1397 1397 if force:
1398 1398 if not nobackup:
1399 1399 tobackup.update(m + a)
1400 1400 elif keepchanges:
1401 1401 tobackup.update(m + a + r + d)
1402 1402
1403 1403 self.applieddirty = True
1404 1404 end = len(self.applied)
1405 1405 rev = self.applied[start].node
1406 1406
1407 1407 try:
1408 1408 heads = repo.changelog.heads(rev)
1409 1409 except error.LookupError:
1410 1410 node = short(rev)
1411 1411 raise util.Abort(_('trying to pop unknown node %s') % node)
1412 1412
1413 1413 if heads != [self.applied[-1].node]:
1414 1414 raise util.Abort(_("popping would remove a revision not "
1415 1415 "managed by this patch queue"))
1416 1416 if not repo[self.applied[-1].node].mutable():
1417 1417 raise util.Abort(
1418 1418 _("popping would remove an immutable revision"),
1419 1419 hint=_('see "hg help phases" for details'))
1420 1420
1421 1421 # we know there are no local changes, so we can make a simplified
1422 1422 # form of hg.update.
1423 1423 if update:
1424 1424 qp = self.qparents(repo, rev)
1425 1425 ctx = repo[qp]
1426 1426 m, a, r, d = repo.status(qp, '.')[:4]
1427 1427 if d:
1428 1428 raise util.Abort(_("deletions found between repo revs"))
1429 1429
1430 1430 tobackup = set(a + m + r) & tobackup
1431 1431 if keepchanges and tobackup:
1432 1432 raise util.Abort(_("local changes found, refresh first"))
1433 1433 self.backup(repo, tobackup)
1434 1434
1435 1435 for f in a:
1436 1436 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
1437 1437 repo.dirstate.drop(f)
1438 1438 for f in m + r:
1439 1439 fctx = ctx[f]
1440 1440 repo.wwrite(f, fctx.data(), fctx.flags())
1441 1441 repo.dirstate.normal(f)
1442 1442 repo.setparents(qp, nullid)
1443 1443 for patch in reversed(self.applied[start:end]):
1444 1444 self.ui.status(_("popping %s\n") % patch.name)
1445 1445 del self.applied[start:end]
1446 1446 strip(self.ui, repo, [rev], update=False, backup='strip')
1447 1447 for s, state in repo['.'].substate.items():
1448 1448 repo['.'].sub(s).get(state)
1449 1449 if self.applied:
1450 1450 self.ui.write(_("now at: %s\n") % self.applied[-1].name)
1451 1451 else:
1452 1452 self.ui.write(_("patch queue now empty\n"))
1453 1453 finally:
1454 1454 wlock.release()
1455 1455
1456 1456 def diff(self, repo, pats, opts):
1457 1457 top, patch = self.checktoppatch(repo)
1458 1458 if not top:
1459 1459 self.ui.write(_("no patches applied\n"))
1460 1460 return
1461 1461 qp = self.qparents(repo, top)
1462 1462 if opts.get('reverse'):
1463 1463 node1, node2 = None, qp
1464 1464 else:
1465 1465 node1, node2 = qp, None
1466 1466 diffopts = self.diffopts(opts, patch)
1467 1467 self.printdiff(repo, diffopts, node1, node2, files=pats, opts=opts)
1468 1468
1469 1469 def refresh(self, repo, pats=None, **opts):
1470 1470 if not self.applied:
1471 1471 self.ui.write(_("no patches applied\n"))
1472 1472 return 1
1473 1473 msg = opts.get('msg', '').rstrip()
1474 1474 newuser = opts.get('user')
1475 1475 newdate = opts.get('date')
1476 1476 if newdate:
1477 1477 newdate = '%d %d' % util.parsedate(newdate)
1478 1478 wlock = repo.wlock()
1479 1479
1480 1480 try:
1481 1481 self.checktoppatch(repo)
1482 1482 (top, patchfn) = (self.applied[-1].node, self.applied[-1].name)
1483 1483 if repo.changelog.heads(top) != [top]:
1484 1484 raise util.Abort(_("cannot refresh a revision with children"))
1485 1485 if not repo[top].mutable():
1486 1486 raise util.Abort(_("cannot refresh immutable revision"),
1487 1487 hint=_('see "hg help phases" for details'))
1488 1488
1489 1489 cparents = repo.changelog.parents(top)
1490 1490 patchparent = self.qparents(repo, top)
1491 1491
1492 1492 inclsubs = checksubstate(repo, hex(patchparent))
1493 1493 if inclsubs:
1494 1494 inclsubs.append('.hgsubstate')
1495 1495 substatestate = repo.dirstate['.hgsubstate']
1496 1496
1497 1497 ph = patchheader(self.join(patchfn), self.plainmode)
1498 1498 diffopts = self.diffopts({'git': opts.get('git')}, patchfn)
1499 1499 if msg:
1500 1500 ph.setmessage(msg)
1501 1501 if newuser:
1502 1502 ph.setuser(newuser)
1503 1503 if newdate:
1504 1504 ph.setdate(newdate)
1505 1505 ph.setparent(hex(patchparent))
1506 1506
1507 1507 # only commit new patch when write is complete
1508 1508 patchf = self.opener(patchfn, 'w', atomictemp=True)
1509 1509
1510 1510 comments = str(ph)
1511 1511 if comments:
1512 1512 patchf.write(comments)
1513 1513
1514 1514 # update the dirstate in place, strip off the qtip commit
1515 1515 # and then commit.
1516 1516 #
1517 1517 # this should really read:
1518 1518 # mm, dd, aa = repo.status(top, patchparent)[:3]
1519 1519 # but we do it backwards to take advantage of manifest/changelog
1520 1520 # caching against the next repo.status call
1521 1521 mm, aa, dd = repo.status(patchparent, top)[:3]
1522 1522 changes = repo.changelog.read(top)
1523 1523 man = repo.manifest.read(changes[0])
1524 1524 aaa = aa[:]
1525 1525 matchfn = scmutil.match(repo[None], pats, opts)
1526 1526 # in short mode, we only diff the files included in the
1527 1527 # patch already plus specified files
1528 1528 if opts.get('short'):
1529 1529 # if amending a patch, we start with existing
1530 1530 # files plus specified files - unfiltered
1531 1531 match = scmutil.matchfiles(repo, mm + aa + dd + matchfn.files())
1532 1532 # filter with include/exclude options
1533 1533 matchfn = scmutil.match(repo[None], opts=opts)
1534 1534 else:
1535 1535 match = scmutil.matchall(repo)
1536 1536 m, a, r, d = repo.status(match=match)[:4]
1537 1537 mm = set(mm)
1538 1538 aa = set(aa)
1539 1539 dd = set(dd)
1540 1540
1541 1541 # we might end up with files that were added between
1542 1542 # qtip and the dirstate parent, but then changed in the
1543 1543 # local dirstate. in this case, we want them to only
1544 1544 # show up in the added section
1545 1545 for x in m:
1546 1546 if x not in aa:
1547 1547 mm.add(x)
1548 1548 # we might end up with files added by the local dirstate that
1549 1549 # were deleted by the patch. In this case, they should only
1550 1550 # show up in the changed section.
1551 1551 for x in a:
1552 1552 if x in dd:
1553 1553 dd.remove(x)
1554 1554 mm.add(x)
1555 1555 else:
1556 1556 aa.add(x)
1557 1557 # make sure any files deleted in the local dirstate
1558 1558 # are not in the add or change column of the patch
1559 1559 forget = []
1560 1560 for x in d + r:
1561 1561 if x in aa:
1562 1562 aa.remove(x)
1563 1563 forget.append(x)
1564 1564 continue
1565 1565 else:
1566 1566 mm.discard(x)
1567 1567 dd.add(x)
1568 1568
1569 1569 m = list(mm)
1570 1570 r = list(dd)
1571 1571 a = list(aa)
1572 1572
1573 1573 # create 'match' that includes the files to be recommitted.
1574 1574 # apply matchfn via repo.status to ensure correct case handling.
1575 1575 cm, ca, cr, cd = repo.status(patchparent, match=matchfn)[:4]
1576 1576 allmatches = set(cm + ca + cr + cd)
1577 1577 refreshchanges = [x.intersection(allmatches) for x in (mm, aa, dd)]
1578 1578
1579 1579 files = set(inclsubs)
1580 1580 for x in refreshchanges:
1581 1581 files.update(x)
1582 1582 match = scmutil.matchfiles(repo, files)
1583 1583
1584 1584 bmlist = repo[top].bookmarks()
1585 1585
1586 1586 try:
1587 1587 if diffopts.git or diffopts.upgrade:
1588 1588 copies = {}
1589 1589 for dst in a:
1590 1590 src = repo.dirstate.copied(dst)
1591 1591 # during qfold, the source file for copies may
1592 1592 # be removed. Treat this as a simple add.
1593 1593 if src is not None and src in repo.dirstate:
1594 1594 copies.setdefault(src, []).append(dst)
1595 1595 repo.dirstate.add(dst)
1596 1596 # remember the copies between patchparent and qtip
1597 1597 for dst in aaa:
1598 1598 f = repo.file(dst)
1599 1599 src = f.renamed(man[dst])
1600 1600 if src:
1601 1601 copies.setdefault(src[0], []).extend(
1602 1602 copies.get(dst, []))
1603 1603 if dst in a:
1604 1604 copies[src[0]].append(dst)
1605 1605 # we can't copy a file created by the patch itself
1606 1606 if dst in copies:
1607 1607 del copies[dst]
1608 1608 for src, dsts in copies.iteritems():
1609 1609 for dst in dsts:
1610 1610 repo.dirstate.copy(src, dst)
1611 1611 else:
1612 1612 for dst in a:
1613 1613 repo.dirstate.add(dst)
1614 1614 # Drop useless copy information
1615 1615 for f in list(repo.dirstate.copies()):
1616 1616 repo.dirstate.copy(None, f)
1617 1617 for f in r:
1618 1618 repo.dirstate.remove(f)
1619 1619 # if the patch excludes a modified file, mark that
1620 1620 # file with mtime=0 so status can see it.
1621 1621 mm = []
1622 1622 for i in xrange(len(m) - 1, -1, -1):
1623 1623 if not matchfn(m[i]):
1624 1624 mm.append(m[i])
1625 1625 del m[i]
1626 1626 for f in m:
1627 1627 repo.dirstate.normal(f)
1628 1628 for f in mm:
1629 1629 repo.dirstate.normallookup(f)
1630 1630 for f in forget:
1631 1631 repo.dirstate.drop(f)
1632 1632
1633 1633 if not msg:
1634 1634 if not ph.message:
1635 1635 message = "[mq]: %s\n" % patchfn
1636 1636 else:
1637 1637 message = "\n".join(ph.message)
1638 1638 else:
1639 1639 message = msg
1640 1640
1641 1641 user = ph.user or changes[1]
1642 1642
1643 1643 oldphase = repo[top].phase()
1644 1644
1645 1645 # assumes strip can roll itself back if interrupted
1646 1646 repo.setparents(*cparents)
1647 1647 self.applied.pop()
1648 1648 self.applieddirty = True
1649 1649 strip(self.ui, repo, [top], update=False, backup='strip')
1650 1650 except: # re-raises
1651 1651 repo.dirstate.invalidate()
1652 1652 raise
1653 1653
1654 1654 try:
1655 1655 # might be nice to attempt to roll back strip after this
1656 1656
1657 1657 # Ensure we create a new changeset in the same phase than
1658 1658 # the old one.
1659 1659 n = newcommit(repo, oldphase, message, user, ph.date,
1660 1660 match=match, force=True)
1661 1661 # only write patch after a successful commit
1662 1662 c = [list(x) for x in refreshchanges]
1663 1663 if inclsubs:
1664 1664 self.putsubstate2changes(substatestate, c)
1665 1665 chunks = patchmod.diff(repo, patchparent,
1666 1666 changes=c, opts=diffopts)
1667 1667 for chunk in chunks:
1668 1668 patchf.write(chunk)
1669 1669 patchf.close()
1670 1670
1671 1671 marks = repo._bookmarks
1672 1672 for bm in bmlist:
1673 1673 marks[bm] = n
1674 1674 marks.write()
1675 1675
1676 1676 self.applied.append(statusentry(n, patchfn))
1677 1677 except: # re-raises
1678 1678 ctx = repo[cparents[0]]
1679 1679 repo.dirstate.rebuild(ctx.node(), ctx.manifest())
1680 1680 self.savedirty()
1681 1681 self.ui.warn(_('refresh interrupted while patch was popped! '
1682 1682 '(revert --all, qpush to recover)\n'))
1683 1683 raise
1684 1684 finally:
1685 1685 wlock.release()
1686 1686 self.removeundo(repo)
1687 1687
1688 1688 def init(self, repo, create=False):
1689 1689 if not create and os.path.isdir(self.path):
1690 1690 raise util.Abort(_("patch queue directory already exists"))
1691 1691 try:
1692 1692 os.mkdir(self.path)
1693 1693 except OSError, inst:
1694 1694 if inst.errno != errno.EEXIST or not create:
1695 1695 raise
1696 1696 if create:
1697 1697 return self.qrepo(create=True)
1698 1698
1699 1699 def unapplied(self, repo, patch=None):
1700 1700 if patch and patch not in self.series:
1701 1701 raise util.Abort(_("patch %s is not in series file") % patch)
1702 1702 if not patch:
1703 1703 start = self.seriesend()
1704 1704 else:
1705 1705 start = self.series.index(patch) + 1
1706 1706 unapplied = []
1707 1707 for i in xrange(start, len(self.series)):
1708 1708 pushable, reason = self.pushable(i)
1709 1709 if pushable:
1710 1710 unapplied.append((i, self.series[i]))
1711 1711 self.explainpushable(i)
1712 1712 return unapplied
1713 1713
1714 1714 def qseries(self, repo, missing=None, start=0, length=None, status=None,
1715 1715 summary=False):
1716 1716 def displayname(pfx, patchname, state):
1717 1717 if pfx:
1718 1718 self.ui.write(pfx)
1719 1719 if summary:
1720 1720 ph = patchheader(self.join(patchname), self.plainmode)
1721 1721 msg = ph.message and ph.message[0] or ''
1722 1722 if self.ui.formatted():
1723 1723 width = self.ui.termwidth() - len(pfx) - len(patchname) - 2
1724 1724 if width > 0:
1725 1725 msg = util.ellipsis(msg, width)
1726 1726 else:
1727 1727 msg = ''
1728 1728 self.ui.write(patchname, label='qseries.' + state)
1729 1729 self.ui.write(': ')
1730 1730 self.ui.write(msg, label='qseries.message.' + state)
1731 1731 else:
1732 1732 self.ui.write(patchname, label='qseries.' + state)
1733 1733 self.ui.write('\n')
1734 1734
1735 1735 applied = set([p.name for p in self.applied])
1736 1736 if length is None:
1737 1737 length = len(self.series) - start
1738 1738 if not missing:
1739 1739 if self.ui.verbose:
1740 1740 idxwidth = len(str(start + length - 1))
1741 1741 for i in xrange(start, start + length):
1742 1742 patch = self.series[i]
1743 1743 if patch in applied:
1744 1744 char, state = 'A', 'applied'
1745 1745 elif self.pushable(i)[0]:
1746 1746 char, state = 'U', 'unapplied'
1747 1747 else:
1748 1748 char, state = 'G', 'guarded'
1749 1749 pfx = ''
1750 1750 if self.ui.verbose:
1751 1751 pfx = '%*d %s ' % (idxwidth, i, char)
1752 1752 elif status and status != char:
1753 1753 continue
1754 1754 displayname(pfx, patch, state)
1755 1755 else:
1756 1756 msng_list = []
1757 1757 for root, dirs, files in os.walk(self.path):
1758 1758 d = root[len(self.path) + 1:]
1759 1759 for f in files:
1760 1760 fl = os.path.join(d, f)
1761 1761 if (fl not in self.series and
1762 1762 fl not in (self.statuspath, self.seriespath,
1763 1763 self.guardspath)
1764 1764 and not fl.startswith('.')):
1765 1765 msng_list.append(fl)
1766 1766 for x in sorted(msng_list):
1767 1767 pfx = self.ui.verbose and ('D ') or ''
1768 1768 displayname(pfx, x, 'missing')
1769 1769
1770 1770 def issaveline(self, l):
1771 1771 if l.name == '.hg.patches.save.line':
1772 1772 return True
1773 1773
1774 1774 def qrepo(self, create=False):
1775 1775 ui = self.baseui.copy()
1776 1776 if create or os.path.isdir(self.join(".hg")):
1777 1777 return hg.repository(ui, path=self.path, create=create)
1778 1778
1779 1779 def restore(self, repo, rev, delete=None, qupdate=None):
1780 1780 desc = repo[rev].description().strip()
1781 1781 lines = desc.splitlines()
1782 1782 i = 0
1783 1783 datastart = None
1784 1784 series = []
1785 1785 applied = []
1786 1786 qpp = None
1787 1787 for i, line in enumerate(lines):
1788 1788 if line == 'Patch Data:':
1789 1789 datastart = i + 1
1790 1790 elif line.startswith('Dirstate:'):
1791 1791 l = line.rstrip()
1792 1792 l = l[10:].split(' ')
1793 1793 qpp = [bin(x) for x in l]
1794 1794 elif datastart is not None:
1795 1795 l = line.rstrip()
1796 1796 n, name = l.split(':', 1)
1797 1797 if n:
1798 1798 applied.append(statusentry(bin(n), name))
1799 1799 else:
1800 1800 series.append(l)
1801 1801 if datastart is None:
1802 1802 self.ui.warn(_("no saved patch data found\n"))
1803 1803 return 1
1804 1804 self.ui.warn(_("restoring status: %s\n") % lines[0])
1805 1805 self.fullseries = series
1806 1806 self.applied = applied
1807 1807 self.parseseries()
1808 1808 self.seriesdirty = True
1809 1809 self.applieddirty = True
1810 1810 heads = repo.changelog.heads()
1811 1811 if delete:
1812 1812 if rev not in heads:
1813 1813 self.ui.warn(_("save entry has children, leaving it alone\n"))
1814 1814 else:
1815 1815 self.ui.warn(_("removing save entry %s\n") % short(rev))
1816 1816 pp = repo.dirstate.parents()
1817 1817 if rev in pp:
1818 1818 update = True
1819 1819 else:
1820 1820 update = False
1821 1821 strip(self.ui, repo, [rev], update=update, backup='strip')
1822 1822 if qpp:
1823 1823 self.ui.warn(_("saved queue repository parents: %s %s\n") %
1824 1824 (short(qpp[0]), short(qpp[1])))
1825 1825 if qupdate:
1826 1826 self.ui.status(_("updating queue directory\n"))
1827 1827 r = self.qrepo()
1828 1828 if not r:
1829 1829 self.ui.warn(_("unable to load queue repository\n"))
1830 1830 return 1
1831 1831 hg.clean(r, qpp[0])
1832 1832
1833 1833 def save(self, repo, msg=None):
1834 1834 if not self.applied:
1835 1835 self.ui.warn(_("save: no patches applied, exiting\n"))
1836 1836 return 1
1837 1837 if self.issaveline(self.applied[-1]):
1838 1838 self.ui.warn(_("status is already saved\n"))
1839 1839 return 1
1840 1840
1841 1841 if not msg:
1842 1842 msg = _("hg patches saved state")
1843 1843 else:
1844 1844 msg = "hg patches: " + msg.rstrip('\r\n')
1845 1845 r = self.qrepo()
1846 1846 if r:
1847 1847 pp = r.dirstate.parents()
1848 1848 msg += "\nDirstate: %s %s" % (hex(pp[0]), hex(pp[1]))
1849 1849 msg += "\n\nPatch Data:\n"
1850 1850 msg += ''.join('%s\n' % x for x in self.applied)
1851 1851 msg += ''.join(':%s\n' % x for x in self.fullseries)
1852 1852 n = repo.commit(msg, force=True)
1853 1853 if not n:
1854 1854 self.ui.warn(_("repo commit failed\n"))
1855 1855 return 1
1856 1856 self.applied.append(statusentry(n, '.hg.patches.save.line'))
1857 1857 self.applieddirty = True
1858 1858 self.removeundo(repo)
1859 1859
1860 1860 def fullseriesend(self):
1861 1861 if self.applied:
1862 1862 p = self.applied[-1].name
1863 1863 end = self.findseries(p)
1864 1864 if end is None:
1865 1865 return len(self.fullseries)
1866 1866 return end + 1
1867 1867 return 0
1868 1868
1869 1869 def seriesend(self, all_patches=False):
1870 1870 """If all_patches is False, return the index of the next pushable patch
1871 1871 in the series, or the series length. If all_patches is True, return the
1872 1872 index of the first patch past the last applied one.
1873 1873 """
1874 1874 end = 0
1875 1875 def nextpatch(start):
1876 1876 if all_patches or start >= len(self.series):
1877 1877 return start
1878 1878 for i in xrange(start, len(self.series)):
1879 1879 p, reason = self.pushable(i)
1880 1880 if p:
1881 1881 return i
1882 1882 self.explainpushable(i)
1883 1883 return len(self.series)
1884 1884 if self.applied:
1885 1885 p = self.applied[-1].name
1886 1886 try:
1887 1887 end = self.series.index(p)
1888 1888 except ValueError:
1889 1889 return 0
1890 1890 return nextpatch(end + 1)
1891 1891 return nextpatch(end)
1892 1892
1893 1893 def appliedname(self, index):
1894 1894 pname = self.applied[index].name
1895 1895 if not self.ui.verbose:
1896 1896 p = pname
1897 1897 else:
1898 1898 p = str(self.series.index(pname)) + " " + pname
1899 1899 return p
1900 1900
1901 1901 def qimport(self, repo, files, patchname=None, rev=None, existing=None,
1902 1902 force=None, git=False):
1903 1903 def checkseries(patchname):
1904 1904 if patchname in self.series:
1905 1905 raise util.Abort(_('patch %s is already in the series file')
1906 1906 % patchname)
1907 1907
1908 1908 if rev:
1909 1909 if files:
1910 1910 raise util.Abort(_('option "-r" not valid when importing '
1911 1911 'files'))
1912 1912 rev = scmutil.revrange(repo, rev)
1913 1913 rev.sort(reverse=True)
1914 1914 elif not files:
1915 1915 raise util.Abort(_('no files or revisions specified'))
1916 1916 if (len(files) > 1 or len(rev) > 1) and patchname:
1917 1917 raise util.Abort(_('option "-n" not valid when importing multiple '
1918 1918 'patches'))
1919 1919 imported = []
1920 1920 if rev:
1921 1921 # If mq patches are applied, we can only import revisions
1922 1922 # that form a linear path to qbase.
1923 1923 # Otherwise, they should form a linear path to a head.
1924 1924 heads = repo.changelog.heads(repo.changelog.node(rev[-1]))
1925 1925 if len(heads) > 1:
1926 1926 raise util.Abort(_('revision %d is the root of more than one '
1927 1927 'branch') % rev[-1])
1928 1928 if self.applied:
1929 1929 base = repo.changelog.node(rev[0])
1930 1930 if base in [n.node for n in self.applied]:
1931 1931 raise util.Abort(_('revision %d is already managed')
1932 1932 % rev[0])
1933 1933 if heads != [self.applied[-1].node]:
1934 1934 raise util.Abort(_('revision %d is not the parent of '
1935 1935 'the queue') % rev[0])
1936 1936 base = repo.changelog.rev(self.applied[0].node)
1937 1937 lastparent = repo.changelog.parentrevs(base)[0]
1938 1938 else:
1939 1939 if heads != [repo.changelog.node(rev[0])]:
1940 1940 raise util.Abort(_('revision %d has unmanaged children')
1941 1941 % rev[0])
1942 1942 lastparent = None
1943 1943
1944 1944 diffopts = self.diffopts({'git': git})
1945 1945 for r in rev:
1946 1946 if not repo[r].mutable():
1947 1947 raise util.Abort(_('revision %d is not mutable') % r,
1948 1948 hint=_('see "hg help phases" for details'))
1949 1949 p1, p2 = repo.changelog.parentrevs(r)
1950 1950 n = repo.changelog.node(r)
1951 1951 if p2 != nullrev:
1952 1952 raise util.Abort(_('cannot import merge revision %d') % r)
1953 1953 if lastparent and lastparent != r:
1954 1954 raise util.Abort(_('revision %d is not the parent of %d')
1955 1955 % (r, lastparent))
1956 1956 lastparent = p1
1957 1957
1958 1958 if not patchname:
1959 1959 patchname = normname('%d.diff' % r)
1960 1960 checkseries(patchname)
1961 1961 self.checkpatchname(patchname, force)
1962 1962 self.fullseries.insert(0, patchname)
1963 1963
1964 1964 patchf = self.opener(patchname, "w")
1965 1965 cmdutil.export(repo, [n], fp=patchf, opts=diffopts)
1966 1966 patchf.close()
1967 1967
1968 1968 se = statusentry(n, patchname)
1969 1969 self.applied.insert(0, se)
1970 1970
1971 1971 self.added.append(patchname)
1972 1972 imported.append(patchname)
1973 1973 patchname = None
1974 1974 if rev and repo.ui.configbool('mq', 'secret', False):
1975 1975 # if we added anything with --rev, we must move the secret root
1976 1976 phases.retractboundary(repo, phases.secret, [n])
1977 1977 self.parseseries()
1978 1978 self.applieddirty = True
1979 1979 self.seriesdirty = True
1980 1980
1981 1981 for i, filename in enumerate(files):
1982 1982 if existing:
1983 1983 if filename == '-':
1984 1984 raise util.Abort(_('-e is incompatible with import from -'))
1985 1985 filename = normname(filename)
1986 1986 self.checkreservedname(filename)
1987 1987 originpath = self.join(filename)
1988 1988 if not os.path.isfile(originpath):
1989 1989 raise util.Abort(_("patch %s does not exist") % filename)
1990 1990
1991 1991 if patchname:
1992 1992 self.checkpatchname(patchname, force)
1993 1993
1994 1994 self.ui.write(_('renaming %s to %s\n')
1995 1995 % (filename, patchname))
1996 1996 util.rename(originpath, self.join(patchname))
1997 1997 else:
1998 1998 patchname = filename
1999 1999
2000 2000 else:
2001 2001 if filename == '-' and not patchname:
2002 2002 raise util.Abort(_('need --name to import a patch from -'))
2003 2003 elif not patchname:
2004 2004 patchname = normname(os.path.basename(filename.rstrip('/')))
2005 2005 self.checkpatchname(patchname, force)
2006 2006 try:
2007 2007 if filename == '-':
2008 2008 text = self.ui.fin.read()
2009 2009 else:
2010 2010 fp = hg.openpath(self.ui, filename)
2011 2011 text = fp.read()
2012 2012 fp.close()
2013 2013 except (OSError, IOError):
2014 2014 raise util.Abort(_("unable to read file %s") % filename)
2015 2015 patchf = self.opener(patchname, "w")
2016 2016 patchf.write(text)
2017 2017 patchf.close()
2018 2018 if not force:
2019 2019 checkseries(patchname)
2020 2020 if patchname not in self.series:
2021 2021 index = self.fullseriesend() + i
2022 2022 self.fullseries[index:index] = [patchname]
2023 2023 self.parseseries()
2024 2024 self.seriesdirty = True
2025 2025 self.ui.warn(_("adding %s to series file\n") % patchname)
2026 2026 self.added.append(patchname)
2027 2027 imported.append(patchname)
2028 2028 patchname = None
2029 2029
2030 2030 self.removeundo(repo)
2031 2031 return imported
2032 2032
2033 2033 def fixkeepchangesopts(ui, opts):
2034 2034 if (not ui.configbool('mq', 'keepchanges') or opts.get('force')
2035 2035 or opts.get('exact')):
2036 2036 return opts
2037 2037 opts = dict(opts)
2038 2038 opts['keep_changes'] = True
2039 2039 return opts
2040 2040
2041 2041 @command("qdelete|qremove|qrm",
2042 2042 [('k', 'keep', None, _('keep patch file')),
2043 2043 ('r', 'rev', [],
2044 2044 _('stop managing a revision (DEPRECATED)'), _('REV'))],
2045 2045 _('hg qdelete [-k] [PATCH]...'))
2046 2046 def delete(ui, repo, *patches, **opts):
2047 2047 """remove patches from queue
2048 2048
2049 2049 The patches must not be applied, and at least one patch is required. Exact
2050 2050 patch identifiers must be given. With -k/--keep, the patch files are
2051 2051 preserved in the patch directory.
2052 2052
2053 2053 To stop managing a patch and move it into permanent history,
2054 2054 use the :hg:`qfinish` command."""
2055 2055 q = repo.mq
2056 2056 q.delete(repo, patches, opts)
2057 2057 q.savedirty()
2058 2058 return 0
2059 2059
2060 2060 @command("qapplied",
2061 2061 [('1', 'last', None, _('show only the preceding applied patch'))
2062 2062 ] + seriesopts,
2063 2063 _('hg qapplied [-1] [-s] [PATCH]'))
2064 2064 def applied(ui, repo, patch=None, **opts):
2065 2065 """print the patches already applied
2066 2066
2067 2067 Returns 0 on success."""
2068 2068
2069 2069 q = repo.mq
2070 2070
2071 2071 if patch:
2072 2072 if patch not in q.series:
2073 2073 raise util.Abort(_("patch %s is not in series file") % patch)
2074 2074 end = q.series.index(patch) + 1
2075 2075 else:
2076 2076 end = q.seriesend(True)
2077 2077
2078 2078 if opts.get('last') and not end:
2079 2079 ui.write(_("no patches applied\n"))
2080 2080 return 1
2081 2081 elif opts.get('last') and end == 1:
2082 2082 ui.write(_("only one patch applied\n"))
2083 2083 return 1
2084 2084 elif opts.get('last'):
2085 2085 start = end - 2
2086 2086 end = 1
2087 2087 else:
2088 2088 start = 0
2089 2089
2090 2090 q.qseries(repo, length=end, start=start, status='A',
2091 2091 summary=opts.get('summary'))
2092 2092
2093 2093
2094 2094 @command("qunapplied",
2095 2095 [('1', 'first', None, _('show only the first patch'))] + seriesopts,
2096 2096 _('hg qunapplied [-1] [-s] [PATCH]'))
2097 2097 def unapplied(ui, repo, patch=None, **opts):
2098 2098 """print the patches not yet applied
2099 2099
2100 2100 Returns 0 on success."""
2101 2101
2102 2102 q = repo.mq
2103 2103 if patch:
2104 2104 if patch not in q.series:
2105 2105 raise util.Abort(_("patch %s is not in series file") % patch)
2106 2106 start = q.series.index(patch) + 1
2107 2107 else:
2108 2108 start = q.seriesend(True)
2109 2109
2110 2110 if start == len(q.series) and opts.get('first'):
2111 2111 ui.write(_("all patches applied\n"))
2112 2112 return 1
2113 2113
2114 2114 length = opts.get('first') and 1 or None
2115 2115 q.qseries(repo, start=start, length=length, status='U',
2116 2116 summary=opts.get('summary'))
2117 2117
2118 2118 @command("qimport",
2119 2119 [('e', 'existing', None, _('import file in patch directory')),
2120 2120 ('n', 'name', '',
2121 2121 _('name of patch file'), _('NAME')),
2122 2122 ('f', 'force', None, _('overwrite existing files')),
2123 2123 ('r', 'rev', [],
2124 2124 _('place existing revisions under mq control'), _('REV')),
2125 2125 ('g', 'git', None, _('use git extended diff format')),
2126 2126 ('P', 'push', None, _('qpush after importing'))],
2127 2127 _('hg qimport [-e] [-n NAME] [-f] [-g] [-P] [-r REV]... [FILE]...'))
2128 2128 def qimport(ui, repo, *filename, **opts):
2129 2129 """import a patch or existing changeset
2130 2130
2131 2131 The patch is inserted into the series after the last applied
2132 2132 patch. If no patches have been applied, qimport prepends the patch
2133 2133 to the series.
2134 2134
2135 2135 The patch will have the same name as its source file unless you
2136 2136 give it a new one with -n/--name.
2137 2137
2138 2138 You can register an existing patch inside the patch directory with
2139 2139 the -e/--existing flag.
2140 2140
2141 2141 With -f/--force, an existing patch of the same name will be
2142 2142 overwritten.
2143 2143
2144 2144 An existing changeset may be placed under mq control with -r/--rev
2145 2145 (e.g. qimport --rev . -n patch will place the current revision
2146 2146 under mq control). With -g/--git, patches imported with --rev will
2147 2147 use the git diff format. See the diffs help topic for information
2148 2148 on why this is important for preserving rename/copy information
2149 2149 and permission changes. Use :hg:`qfinish` to remove changesets
2150 2150 from mq control.
2151 2151
2152 2152 To import a patch from standard input, pass - as the patch file.
2153 2153 When importing from standard input, a patch name must be specified
2154 2154 using the --name flag.
2155 2155
2156 2156 To import an existing patch while renaming it::
2157 2157
2158 2158 hg qimport -e existing-patch -n new-name
2159 2159
2160 2160 Returns 0 if import succeeded.
2161 2161 """
2162 2162 lock = repo.lock() # cause this may move phase
2163 2163 try:
2164 2164 q = repo.mq
2165 2165 try:
2166 2166 imported = q.qimport(
2167 2167 repo, filename, patchname=opts.get('name'),
2168 2168 existing=opts.get('existing'), force=opts.get('force'),
2169 2169 rev=opts.get('rev'), git=opts.get('git'))
2170 2170 finally:
2171 2171 q.savedirty()
2172 2172 finally:
2173 2173 lock.release()
2174 2174
2175 2175 if imported and opts.get('push') and not opts.get('rev'):
2176 2176 return q.push(repo, imported[-1])
2177 2177 return 0
2178 2178
2179 2179 def qinit(ui, repo, create):
2180 2180 """initialize a new queue repository
2181 2181
2182 2182 This command also creates a series file for ordering patches, and
2183 2183 an mq-specific .hgignore file in the queue repository, to exclude
2184 2184 the status and guards files (these contain mostly transient state).
2185 2185
2186 2186 Returns 0 if initialization succeeded."""
2187 2187 q = repo.mq
2188 2188 r = q.init(repo, create)
2189 2189 q.savedirty()
2190 2190 if r:
2191 2191 if not os.path.exists(r.wjoin('.hgignore')):
2192 2192 fp = r.wopener('.hgignore', 'w')
2193 2193 fp.write('^\\.hg\n')
2194 2194 fp.write('^\\.mq\n')
2195 2195 fp.write('syntax: glob\n')
2196 2196 fp.write('status\n')
2197 2197 fp.write('guards\n')
2198 2198 fp.close()
2199 2199 if not os.path.exists(r.wjoin('series')):
2200 2200 r.wopener('series', 'w').close()
2201 2201 r[None].add(['.hgignore', 'series'])
2202 2202 commands.add(ui, r)
2203 2203 return 0
2204 2204
2205 2205 @command("^qinit",
2206 2206 [('c', 'create-repo', None, _('create queue repository'))],
2207 2207 _('hg qinit [-c]'))
2208 2208 def init(ui, repo, **opts):
2209 2209 """init a new queue repository (DEPRECATED)
2210 2210
2211 2211 The queue repository is unversioned by default. If
2212 2212 -c/--create-repo is specified, qinit will create a separate nested
2213 2213 repository for patches (qinit -c may also be run later to convert
2214 2214 an unversioned patch repository into a versioned one). You can use
2215 2215 qcommit to commit changes to this queue repository.
2216 2216
2217 2217 This command is deprecated. Without -c, it's implied by other relevant
2218 2218 commands. With -c, use :hg:`init --mq` instead."""
2219 2219 return qinit(ui, repo, create=opts.get('create_repo'))
2220 2220
2221 2221 @command("qclone",
2222 2222 [('', 'pull', None, _('use pull protocol to copy metadata')),
2223 2223 ('U', 'noupdate', None,
2224 2224 _('do not update the new working directories')),
2225 2225 ('', 'uncompressed', None,
2226 2226 _('use uncompressed transfer (fast over LAN)')),
2227 2227 ('p', 'patches', '',
2228 2228 _('location of source patch repository'), _('REPO')),
2229 2229 ] + commands.remoteopts,
2230 2230 _('hg qclone [OPTION]... SOURCE [DEST]'))
2231 2231 def clone(ui, source, dest=None, **opts):
2232 2232 '''clone main and patch repository at same time
2233 2233
2234 2234 If source is local, destination will have no patches applied. If
2235 2235 source is remote, this command can not check if patches are
2236 2236 applied in source, so cannot guarantee that patches are not
2237 2237 applied in destination. If you clone remote repository, be sure
2238 2238 before that it has no patches applied.
2239 2239
2240 2240 Source patch repository is looked for in <src>/.hg/patches by
2241 2241 default. Use -p <url> to change.
2242 2242
2243 2243 The patch directory must be a nested Mercurial repository, as
2244 2244 would be created by :hg:`init --mq`.
2245 2245
2246 2246 Return 0 on success.
2247 2247 '''
2248 2248 def patchdir(repo):
2249 2249 """compute a patch repo url from a repo object"""
2250 2250 url = repo.url()
2251 2251 if url.endswith('/'):
2252 2252 url = url[:-1]
2253 2253 return url + '/.hg/patches'
2254 2254
2255 2255 # main repo (destination and sources)
2256 2256 if dest is None:
2257 2257 dest = hg.defaultdest(source)
2258 2258 sr = hg.peer(ui, opts, ui.expandpath(source))
2259 2259
2260 2260 # patches repo (source only)
2261 2261 if opts.get('patches'):
2262 2262 patchespath = ui.expandpath(opts.get('patches'))
2263 2263 else:
2264 2264 patchespath = patchdir(sr)
2265 2265 try:
2266 2266 hg.peer(ui, opts, patchespath)
2267 2267 except error.RepoError:
2268 2268 raise util.Abort(_('versioned patch repository not found'
2269 2269 ' (see init --mq)'))
2270 2270 qbase, destrev = None, None
2271 2271 if sr.local():
2272 2272 repo = sr.local()
2273 2273 if repo.mq.applied and repo[qbase].phase() != phases.secret:
2274 2274 qbase = repo.mq.applied[0].node
2275 2275 if not hg.islocal(dest):
2276 2276 heads = set(repo.heads())
2277 2277 destrev = list(heads.difference(repo.heads(qbase)))
2278 2278 destrev.append(repo.changelog.parents(qbase)[0])
2279 2279 elif sr.capable('lookup'):
2280 2280 try:
2281 2281 qbase = sr.lookup('qbase')
2282 2282 except error.RepoError:
2283 2283 pass
2284 2284
2285 2285 ui.note(_('cloning main repository\n'))
2286 2286 sr, dr = hg.clone(ui, opts, sr.url(), dest,
2287 2287 pull=opts.get('pull'),
2288 2288 rev=destrev,
2289 2289 update=False,
2290 2290 stream=opts.get('uncompressed'))
2291 2291
2292 2292 ui.note(_('cloning patch repository\n'))
2293 2293 hg.clone(ui, opts, opts.get('patches') or patchdir(sr), patchdir(dr),
2294 2294 pull=opts.get('pull'), update=not opts.get('noupdate'),
2295 2295 stream=opts.get('uncompressed'))
2296 2296
2297 2297 if dr.local():
2298 2298 repo = dr.local()
2299 2299 if qbase:
2300 2300 ui.note(_('stripping applied patches from destination '
2301 2301 'repository\n'))
2302 2302 strip(ui, repo, [qbase], update=False, backup=None)
2303 2303 if not opts.get('noupdate'):
2304 2304 ui.note(_('updating destination repository\n'))
2305 2305 hg.update(repo, repo.changelog.tip())
2306 2306
2307 2307 @command("qcommit|qci",
2308 2308 commands.table["^commit|ci"][1],
2309 2309 _('hg qcommit [OPTION]... [FILE]...'))
2310 2310 def commit(ui, repo, *pats, **opts):
2311 2311 """commit changes in the queue repository (DEPRECATED)
2312 2312
2313 2313 This command is deprecated; use :hg:`commit --mq` instead."""
2314 2314 q = repo.mq
2315 2315 r = q.qrepo()
2316 2316 if not r:
2317 2317 raise util.Abort('no queue repository')
2318 2318 commands.commit(r.ui, r, *pats, **opts)
2319 2319
2320 2320 @command("qseries",
2321 2321 [('m', 'missing', None, _('print patches not in series')),
2322 2322 ] + seriesopts,
2323 2323 _('hg qseries [-ms]'))
2324 2324 def series(ui, repo, **opts):
2325 2325 """print the entire series file
2326 2326
2327 2327 Returns 0 on success."""
2328 2328 repo.mq.qseries(repo, missing=opts.get('missing'),
2329 2329 summary=opts.get('summary'))
2330 2330 return 0
2331 2331
2332 2332 @command("qtop", seriesopts, _('hg qtop [-s]'))
2333 2333 def top(ui, repo, **opts):
2334 2334 """print the name of the current patch
2335 2335
2336 2336 Returns 0 on success."""
2337 2337 q = repo.mq
2338 2338 t = q.applied and q.seriesend(True) or 0
2339 2339 if t:
2340 2340 q.qseries(repo, start=t - 1, length=1, status='A',
2341 2341 summary=opts.get('summary'))
2342 2342 else:
2343 2343 ui.write(_("no patches applied\n"))
2344 2344 return 1
2345 2345
2346 2346 @command("qnext", seriesopts, _('hg qnext [-s]'))
2347 2347 def next(ui, repo, **opts):
2348 2348 """print the name of the next pushable patch
2349 2349
2350 2350 Returns 0 on success."""
2351 2351 q = repo.mq
2352 2352 end = q.seriesend()
2353 2353 if end == len(q.series):
2354 2354 ui.write(_("all patches applied\n"))
2355 2355 return 1
2356 2356 q.qseries(repo, start=end, length=1, summary=opts.get('summary'))
2357 2357
2358 2358 @command("qprev", seriesopts, _('hg qprev [-s]'))
2359 2359 def prev(ui, repo, **opts):
2360 2360 """print the name of the preceding applied patch
2361 2361
2362 2362 Returns 0 on success."""
2363 2363 q = repo.mq
2364 2364 l = len(q.applied)
2365 2365 if l == 1:
2366 2366 ui.write(_("only one patch applied\n"))
2367 2367 return 1
2368 2368 if not l:
2369 2369 ui.write(_("no patches applied\n"))
2370 2370 return 1
2371 2371 idx = q.series.index(q.applied[-2].name)
2372 2372 q.qseries(repo, start=idx, length=1, status='A',
2373 2373 summary=opts.get('summary'))
2374 2374
2375 2375 def setupheaderopts(ui, opts):
2376 2376 if not opts.get('user') and opts.get('currentuser'):
2377 2377 opts['user'] = ui.username()
2378 2378 if not opts.get('date') and opts.get('currentdate'):
2379 2379 opts['date'] = "%d %d" % util.makedate()
2380 2380
2381 2381 @command("^qnew",
2382 2382 [('e', 'edit', None, _('edit commit message')),
2383 2383 ('f', 'force', None, _('import uncommitted changes (DEPRECATED)')),
2384 2384 ('g', 'git', None, _('use git extended diff format')),
2385 2385 ('U', 'currentuser', None, _('add "From: <current user>" to patch')),
2386 2386 ('u', 'user', '',
2387 2387 _('add "From: <USER>" to patch'), _('USER')),
2388 2388 ('D', 'currentdate', None, _('add "Date: <current date>" to patch')),
2389 2389 ('d', 'date', '',
2390 2390 _('add "Date: <DATE>" to patch'), _('DATE'))
2391 2391 ] + commands.walkopts + commands.commitopts,
2392 2392 _('hg qnew [-e] [-m TEXT] [-l FILE] PATCH [FILE]...'))
2393 2393 def new(ui, repo, patch, *args, **opts):
2394 2394 """create a new patch
2395 2395
2396 2396 qnew creates a new patch on top of the currently-applied patch (if
2397 2397 any). The patch will be initialized with any outstanding changes
2398 2398 in the working directory. You may also use -I/--include,
2399 2399 -X/--exclude, and/or a list of files after the patch name to add
2400 2400 only changes to matching files to the new patch, leaving the rest
2401 2401 as uncommitted modifications.
2402 2402
2403 2403 -u/--user and -d/--date can be used to set the (given) user and
2404 2404 date, respectively. -U/--currentuser and -D/--currentdate set user
2405 2405 to current user and date to current date.
2406 2406
2407 2407 -e/--edit, -m/--message or -l/--logfile set the patch header as
2408 2408 well as the commit message. If none is specified, the header is
2409 2409 empty and the commit message is '[mq]: PATCH'.
2410 2410
2411 2411 Use the -g/--git option to keep the patch in the git extended diff
2412 2412 format. Read the diffs help topic for more information on why this
2413 2413 is important for preserving permission changes and copy/rename
2414 2414 information.
2415 2415
2416 2416 Returns 0 on successful creation of a new patch.
2417 2417 """
2418 2418 msg = cmdutil.logmessage(ui, opts)
2419 2419 def getmsg():
2420 2420 return ui.edit(msg, opts.get('user') or ui.username())
2421 2421 q = repo.mq
2422 2422 opts['msg'] = msg
2423 2423 if opts.get('edit'):
2424 2424 opts['msg'] = getmsg
2425 2425 else:
2426 2426 opts['msg'] = msg
2427 2427 setupheaderopts(ui, opts)
2428 2428 q.new(repo, patch, *args, **opts)
2429 2429 q.savedirty()
2430 2430 return 0
2431 2431
2432 2432 @command("^qrefresh",
2433 2433 [('e', 'edit', None, _('edit commit message')),
2434 2434 ('g', 'git', None, _('use git extended diff format')),
2435 2435 ('s', 'short', None,
2436 2436 _('refresh only files already in the patch and specified files')),
2437 2437 ('U', 'currentuser', None,
2438 2438 _('add/update author field in patch with current user')),
2439 2439 ('u', 'user', '',
2440 2440 _('add/update author field in patch with given user'), _('USER')),
2441 2441 ('D', 'currentdate', None,
2442 2442 _('add/update date field in patch with current date')),
2443 2443 ('d', 'date', '',
2444 2444 _('add/update date field in patch with given date'), _('DATE'))
2445 2445 ] + commands.walkopts + commands.commitopts,
2446 2446 _('hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] [FILE]...'))
2447 2447 def refresh(ui, repo, *pats, **opts):
2448 2448 """update the current patch
2449 2449
2450 2450 If any file patterns are provided, the refreshed patch will
2451 2451 contain only the modifications that match those patterns; the
2452 2452 remaining modifications will remain in the working directory.
2453 2453
2454 2454 If -s/--short is specified, files currently included in the patch
2455 2455 will be refreshed just like matched files and remain in the patch.
2456 2456
2457 2457 If -e/--edit is specified, Mercurial will start your configured editor for
2458 2458 you to enter a message. In case qrefresh fails, you will find a backup of
2459 2459 your message in ``.hg/last-message.txt``.
2460 2460
2461 2461 hg add/remove/copy/rename work as usual, though you might want to
2462 2462 use git-style patches (-g/--git or [diff] git=1) to track copies
2463 2463 and renames. See the diffs help topic for more information on the
2464 2464 git diff format.
2465 2465
2466 2466 Returns 0 on success.
2467 2467 """
2468 2468 q = repo.mq
2469 2469 message = cmdutil.logmessage(ui, opts)
2470 2470 if opts.get('edit'):
2471 2471 if not q.applied:
2472 2472 ui.write(_("no patches applied\n"))
2473 2473 return 1
2474 2474 if message:
2475 2475 raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
2476 2476 patch = q.applied[-1].name
2477 2477 ph = patchheader(q.join(patch), q.plainmode)
2478 2478 message = ui.edit('\n'.join(ph.message), ph.user or ui.username())
2479 2479 # We don't want to lose the patch message if qrefresh fails (issue2062)
2480 2480 repo.savecommitmessage(message)
2481 2481 setupheaderopts(ui, opts)
2482 2482 wlock = repo.wlock()
2483 2483 try:
2484 2484 ret = q.refresh(repo, pats, msg=message, **opts)
2485 2485 q.savedirty()
2486 2486 return ret
2487 2487 finally:
2488 2488 wlock.release()
2489 2489
2490 2490 @command("^qdiff",
2491 2491 commands.diffopts + commands.diffopts2 + commands.walkopts,
2492 2492 _('hg qdiff [OPTION]... [FILE]...'))
2493 2493 def diff(ui, repo, *pats, **opts):
2494 2494 """diff of the current patch and subsequent modifications
2495 2495
2496 2496 Shows a diff which includes the current patch as well as any
2497 2497 changes which have been made in the working directory since the
2498 2498 last refresh (thus showing what the current patch would become
2499 2499 after a qrefresh).
2500 2500
2501 2501 Use :hg:`diff` if you only want to see the changes made since the
2502 2502 last qrefresh, or :hg:`export qtip` if you want to see changes
2503 2503 made by the current patch without including changes made since the
2504 2504 qrefresh.
2505 2505
2506 2506 Returns 0 on success.
2507 2507 """
2508 2508 repo.mq.diff(repo, pats, opts)
2509 2509 return 0
2510 2510
2511 2511 @command('qfold',
2512 2512 [('e', 'edit', None, _('edit patch header')),
2513 2513 ('k', 'keep', None, _('keep folded patch files')),
2514 2514 ] + commands.commitopts,
2515 2515 _('hg qfold [-e] [-k] [-m TEXT] [-l FILE] PATCH...'))
2516 2516 def fold(ui, repo, *files, **opts):
2517 2517 """fold the named patches into the current patch
2518 2518
2519 2519 Patches must not yet be applied. Each patch will be successively
2520 2520 applied to the current patch in the order given. If all the
2521 2521 patches apply successfully, the current patch will be refreshed
2522 2522 with the new cumulative patch, and the folded patches will be
2523 2523 deleted. With -k/--keep, the folded patch files will not be
2524 2524 removed afterwards.
2525 2525
2526 2526 The header for each folded patch will be concatenated with the
2527 2527 current patch header, separated by a line of ``* * *``.
2528 2528
2529 2529 Returns 0 on success."""
2530 2530 q = repo.mq
2531 2531 if not files:
2532 2532 raise util.Abort(_('qfold requires at least one patch name'))
2533 2533 if not q.checktoppatch(repo)[0]:
2534 2534 raise util.Abort(_('no patches applied'))
2535 2535 q.checklocalchanges(repo)
2536 2536
2537 2537 message = cmdutil.logmessage(ui, opts)
2538 2538 if opts.get('edit'):
2539 2539 if message:
2540 2540 raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
2541 2541
2542 2542 parent = q.lookup('qtip')
2543 2543 patches = []
2544 2544 messages = []
2545 2545 for f in files:
2546 2546 p = q.lookup(f)
2547 2547 if p in patches or p == parent:
2548 2548 ui.warn(_('skipping already folded patch %s\n') % p)
2549 2549 if q.isapplied(p):
2550 2550 raise util.Abort(_('qfold cannot fold already applied patch %s')
2551 2551 % p)
2552 2552 patches.append(p)
2553 2553
2554 2554 for p in patches:
2555 2555 if not message:
2556 2556 ph = patchheader(q.join(p), q.plainmode)
2557 2557 if ph.message:
2558 2558 messages.append(ph.message)
2559 2559 pf = q.join(p)
2560 2560 (patchsuccess, files, fuzz) = q.patch(repo, pf)
2561 2561 if not patchsuccess:
2562 2562 raise util.Abort(_('error folding patch %s') % p)
2563 2563
2564 2564 if not message:
2565 2565 ph = patchheader(q.join(parent), q.plainmode)
2566 2566 message, user = ph.message, ph.user
2567 2567 for msg in messages:
2568 2568 message.append('* * *')
2569 2569 message.extend(msg)
2570 2570 message = '\n'.join(message)
2571 2571
2572 2572 if opts.get('edit'):
2573 2573 message = ui.edit(message, user or ui.username())
2574 2574
2575 2575 diffopts = q.patchopts(q.diffopts(), *patches)
2576 2576 wlock = repo.wlock()
2577 2577 try:
2578 2578 q.refresh(repo, msg=message, git=diffopts.git)
2579 2579 q.delete(repo, patches, opts)
2580 2580 q.savedirty()
2581 2581 finally:
2582 2582 wlock.release()
2583 2583
2584 2584 @command("qgoto",
2585 2585 [('', 'keep-changes', None,
2586 2586 _('tolerate non-conflicting local changes')),
2587 2587 ('f', 'force', None, _('overwrite any local changes')),
2588 2588 ('', 'no-backup', None, _('do not save backup copies of files'))],
2589 2589 _('hg qgoto [OPTION]... PATCH'))
2590 2590 def goto(ui, repo, patch, **opts):
2591 2591 '''push or pop patches until named patch is at top of stack
2592 2592
2593 2593 Returns 0 on success.'''
2594 2594 opts = fixkeepchangesopts(ui, opts)
2595 2595 q = repo.mq
2596 2596 patch = q.lookup(patch)
2597 2597 nobackup = opts.get('no_backup')
2598 2598 keepchanges = opts.get('keep_changes')
2599 2599 if q.isapplied(patch):
2600 2600 ret = q.pop(repo, patch, force=opts.get('force'), nobackup=nobackup,
2601 2601 keepchanges=keepchanges)
2602 2602 else:
2603 2603 ret = q.push(repo, patch, force=opts.get('force'), nobackup=nobackup,
2604 2604 keepchanges=keepchanges)
2605 2605 q.savedirty()
2606 2606 return ret
2607 2607
2608 2608 @command("qguard",
2609 2609 [('l', 'list', None, _('list all patches and guards')),
2610 2610 ('n', 'none', None, _('drop all guards'))],
2611 2611 _('hg qguard [-l] [-n] [PATCH] [-- [+GUARD]... [-GUARD]...]'))
2612 2612 def guard(ui, repo, *args, **opts):
2613 2613 '''set or print guards for a patch
2614 2614
2615 2615 Guards control whether a patch can be pushed. A patch with no
2616 2616 guards is always pushed. A patch with a positive guard ("+foo") is
2617 2617 pushed only if the :hg:`qselect` command has activated it. A patch with
2618 2618 a negative guard ("-foo") is never pushed if the :hg:`qselect` command
2619 2619 has activated it.
2620 2620
2621 2621 With no arguments, print the currently active guards.
2622 2622 With arguments, set guards for the named patch.
2623 2623
2624 2624 .. note::
2625
2625 2626 Specifying negative guards now requires '--'.
2626 2627
2627 2628 To set guards on another patch::
2628 2629
2629 2630 hg qguard other.patch -- +2.6.17 -stable
2630 2631
2631 2632 Returns 0 on success.
2632 2633 '''
2633 2634 def status(idx):
2634 2635 guards = q.seriesguards[idx] or ['unguarded']
2635 2636 if q.series[idx] in applied:
2636 2637 state = 'applied'
2637 2638 elif q.pushable(idx)[0]:
2638 2639 state = 'unapplied'
2639 2640 else:
2640 2641 state = 'guarded'
2641 2642 label = 'qguard.patch qguard.%s qseries.%s' % (state, state)
2642 2643 ui.write('%s: ' % ui.label(q.series[idx], label))
2643 2644
2644 2645 for i, guard in enumerate(guards):
2645 2646 if guard.startswith('+'):
2646 2647 ui.write(guard, label='qguard.positive')
2647 2648 elif guard.startswith('-'):
2648 2649 ui.write(guard, label='qguard.negative')
2649 2650 else:
2650 2651 ui.write(guard, label='qguard.unguarded')
2651 2652 if i != len(guards) - 1:
2652 2653 ui.write(' ')
2653 2654 ui.write('\n')
2654 2655 q = repo.mq
2655 2656 applied = set(p.name for p in q.applied)
2656 2657 patch = None
2657 2658 args = list(args)
2658 2659 if opts.get('list'):
2659 2660 if args or opts.get('none'):
2660 2661 raise util.Abort(_('cannot mix -l/--list with options or '
2661 2662 'arguments'))
2662 2663 for i in xrange(len(q.series)):
2663 2664 status(i)
2664 2665 return
2665 2666 if not args or args[0][0:1] in '-+':
2666 2667 if not q.applied:
2667 2668 raise util.Abort(_('no patches applied'))
2668 2669 patch = q.applied[-1].name
2669 2670 if patch is None and args[0][0:1] not in '-+':
2670 2671 patch = args.pop(0)
2671 2672 if patch is None:
2672 2673 raise util.Abort(_('no patch to work with'))
2673 2674 if args or opts.get('none'):
2674 2675 idx = q.findseries(patch)
2675 2676 if idx is None:
2676 2677 raise util.Abort(_('no patch named %s') % patch)
2677 2678 q.setguards(idx, args)
2678 2679 q.savedirty()
2679 2680 else:
2680 2681 status(q.series.index(q.lookup(patch)))
2681 2682
2682 2683 @command("qheader", [], _('hg qheader [PATCH]'))
2683 2684 def header(ui, repo, patch=None):
2684 2685 """print the header of the topmost or specified patch
2685 2686
2686 2687 Returns 0 on success."""
2687 2688 q = repo.mq
2688 2689
2689 2690 if patch:
2690 2691 patch = q.lookup(patch)
2691 2692 else:
2692 2693 if not q.applied:
2693 2694 ui.write(_('no patches applied\n'))
2694 2695 return 1
2695 2696 patch = q.lookup('qtip')
2696 2697 ph = patchheader(q.join(patch), q.plainmode)
2697 2698
2698 2699 ui.write('\n'.join(ph.message) + '\n')
2699 2700
2700 2701 def lastsavename(path):
2701 2702 (directory, base) = os.path.split(path)
2702 2703 names = os.listdir(directory)
2703 2704 namere = re.compile("%s.([0-9]+)" % base)
2704 2705 maxindex = None
2705 2706 maxname = None
2706 2707 for f in names:
2707 2708 m = namere.match(f)
2708 2709 if m:
2709 2710 index = int(m.group(1))
2710 2711 if maxindex is None or index > maxindex:
2711 2712 maxindex = index
2712 2713 maxname = f
2713 2714 if maxname:
2714 2715 return (os.path.join(directory, maxname), maxindex)
2715 2716 return (None, None)
2716 2717
2717 2718 def savename(path):
2718 2719 (last, index) = lastsavename(path)
2719 2720 if last is None:
2720 2721 index = 0
2721 2722 newpath = path + ".%d" % (index + 1)
2722 2723 return newpath
2723 2724
2724 2725 @command("^qpush",
2725 2726 [('', 'keep-changes', None,
2726 2727 _('tolerate non-conflicting local changes')),
2727 2728 ('f', 'force', None, _('apply on top of local changes')),
2728 2729 ('e', 'exact', None,
2729 2730 _('apply the target patch to its recorded parent')),
2730 2731 ('l', 'list', None, _('list patch name in commit text')),
2731 2732 ('a', 'all', None, _('apply all patches')),
2732 2733 ('m', 'merge', None, _('merge from another queue (DEPRECATED)')),
2733 2734 ('n', 'name', '',
2734 2735 _('merge queue name (DEPRECATED)'), _('NAME')),
2735 2736 ('', 'move', None,
2736 2737 _('reorder patch series and apply only the patch')),
2737 2738 ('', 'no-backup', None, _('do not save backup copies of files'))],
2738 2739 _('hg qpush [-f] [-l] [-a] [--move] [PATCH | INDEX]'))
2739 2740 def push(ui, repo, patch=None, **opts):
2740 2741 """push the next patch onto the stack
2741 2742
2742 2743 By default, abort if the working directory contains uncommitted
2743 2744 changes. With --keep-changes, abort only if the uncommitted files
2744 2745 overlap with patched files. With -f/--force, backup and patch over
2745 2746 uncommitted changes.
2746 2747
2747 2748 Return 0 on success.
2748 2749 """
2749 2750 q = repo.mq
2750 2751 mergeq = None
2751 2752
2752 2753 opts = fixkeepchangesopts(ui, opts)
2753 2754 if opts.get('merge'):
2754 2755 if opts.get('name'):
2755 2756 newpath = repo.join(opts.get('name'))
2756 2757 else:
2757 2758 newpath, i = lastsavename(q.path)
2758 2759 if not newpath:
2759 2760 ui.warn(_("no saved queues found, please use -n\n"))
2760 2761 return 1
2761 2762 mergeq = queue(ui, repo.baseui, repo.path, newpath)
2762 2763 ui.warn(_("merging with queue at: %s\n") % mergeq.path)
2763 2764 ret = q.push(repo, patch, force=opts.get('force'), list=opts.get('list'),
2764 2765 mergeq=mergeq, all=opts.get('all'), move=opts.get('move'),
2765 2766 exact=opts.get('exact'), nobackup=opts.get('no_backup'),
2766 2767 keepchanges=opts.get('keep_changes'))
2767 2768 return ret
2768 2769
2769 2770 @command("^qpop",
2770 2771 [('a', 'all', None, _('pop all patches')),
2771 2772 ('n', 'name', '',
2772 2773 _('queue name to pop (DEPRECATED)'), _('NAME')),
2773 2774 ('', 'keep-changes', None,
2774 2775 _('tolerate non-conflicting local changes')),
2775 2776 ('f', 'force', None, _('forget any local changes to patched files')),
2776 2777 ('', 'no-backup', None, _('do not save backup copies of files'))],
2777 2778 _('hg qpop [-a] [-f] [PATCH | INDEX]'))
2778 2779 def pop(ui, repo, patch=None, **opts):
2779 2780 """pop the current patch off the stack
2780 2781
2781 2782 Without argument, pops off the top of the patch stack. If given a
2782 2783 patch name, keeps popping off patches until the named patch is at
2783 2784 the top of the stack.
2784 2785
2785 2786 By default, abort if the working directory contains uncommitted
2786 2787 changes. With --keep-changes, abort only if the uncommitted files
2787 2788 overlap with patched files. With -f/--force, backup and discard
2788 2789 changes made to such files.
2789 2790
2790 2791 Return 0 on success.
2791 2792 """
2792 2793 opts = fixkeepchangesopts(ui, opts)
2793 2794 localupdate = True
2794 2795 if opts.get('name'):
2795 2796 q = queue(ui, repo.baseui, repo.path, repo.join(opts.get('name')))
2796 2797 ui.warn(_('using patch queue: %s\n') % q.path)
2797 2798 localupdate = False
2798 2799 else:
2799 2800 q = repo.mq
2800 2801 ret = q.pop(repo, patch, force=opts.get('force'), update=localupdate,
2801 2802 all=opts.get('all'), nobackup=opts.get('no_backup'),
2802 2803 keepchanges=opts.get('keep_changes'))
2803 2804 q.savedirty()
2804 2805 return ret
2805 2806
2806 2807 @command("qrename|qmv", [], _('hg qrename PATCH1 [PATCH2]'))
2807 2808 def rename(ui, repo, patch, name=None, **opts):
2808 2809 """rename a patch
2809 2810
2810 2811 With one argument, renames the current patch to PATCH1.
2811 2812 With two arguments, renames PATCH1 to PATCH2.
2812 2813
2813 2814 Returns 0 on success."""
2814 2815 q = repo.mq
2815 2816 if not name:
2816 2817 name = patch
2817 2818 patch = None
2818 2819
2819 2820 if patch:
2820 2821 patch = q.lookup(patch)
2821 2822 else:
2822 2823 if not q.applied:
2823 2824 ui.write(_('no patches applied\n'))
2824 2825 return
2825 2826 patch = q.lookup('qtip')
2826 2827 absdest = q.join(name)
2827 2828 if os.path.isdir(absdest):
2828 2829 name = normname(os.path.join(name, os.path.basename(patch)))
2829 2830 absdest = q.join(name)
2830 2831 q.checkpatchname(name)
2831 2832
2832 2833 ui.note(_('renaming %s to %s\n') % (patch, name))
2833 2834 i = q.findseries(patch)
2834 2835 guards = q.guard_re.findall(q.fullseries[i])
2835 2836 q.fullseries[i] = name + ''.join([' #' + g for g in guards])
2836 2837 q.parseseries()
2837 2838 q.seriesdirty = True
2838 2839
2839 2840 info = q.isapplied(patch)
2840 2841 if info:
2841 2842 q.applied[info[0]] = statusentry(info[1], name)
2842 2843 q.applieddirty = True
2843 2844
2844 2845 destdir = os.path.dirname(absdest)
2845 2846 if not os.path.isdir(destdir):
2846 2847 os.makedirs(destdir)
2847 2848 util.rename(q.join(patch), absdest)
2848 2849 r = q.qrepo()
2849 2850 if r and patch in r.dirstate:
2850 2851 wctx = r[None]
2851 2852 wlock = r.wlock()
2852 2853 try:
2853 2854 if r.dirstate[patch] == 'a':
2854 2855 r.dirstate.drop(patch)
2855 2856 r.dirstate.add(name)
2856 2857 else:
2857 2858 wctx.copy(patch, name)
2858 2859 wctx.forget([patch])
2859 2860 finally:
2860 2861 wlock.release()
2861 2862
2862 2863 q.savedirty()
2863 2864
2864 2865 @command("qrestore",
2865 2866 [('d', 'delete', None, _('delete save entry')),
2866 2867 ('u', 'update', None, _('update queue working directory'))],
2867 2868 _('hg qrestore [-d] [-u] REV'))
2868 2869 def restore(ui, repo, rev, **opts):
2869 2870 """restore the queue state saved by a revision (DEPRECATED)
2870 2871
2871 2872 This command is deprecated, use :hg:`rebase` instead."""
2872 2873 rev = repo.lookup(rev)
2873 2874 q = repo.mq
2874 2875 q.restore(repo, rev, delete=opts.get('delete'),
2875 2876 qupdate=opts.get('update'))
2876 2877 q.savedirty()
2877 2878 return 0
2878 2879
2879 2880 @command("qsave",
2880 2881 [('c', 'copy', None, _('copy patch directory')),
2881 2882 ('n', 'name', '',
2882 2883 _('copy directory name'), _('NAME')),
2883 2884 ('e', 'empty', None, _('clear queue status file')),
2884 2885 ('f', 'force', None, _('force copy'))] + commands.commitopts,
2885 2886 _('hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]'))
2886 2887 def save(ui, repo, **opts):
2887 2888 """save current queue state (DEPRECATED)
2888 2889
2889 2890 This command is deprecated, use :hg:`rebase` instead."""
2890 2891 q = repo.mq
2891 2892 message = cmdutil.logmessage(ui, opts)
2892 2893 ret = q.save(repo, msg=message)
2893 2894 if ret:
2894 2895 return ret
2895 2896 q.savedirty() # save to .hg/patches before copying
2896 2897 if opts.get('copy'):
2897 2898 path = q.path
2898 2899 if opts.get('name'):
2899 2900 newpath = os.path.join(q.basepath, opts.get('name'))
2900 2901 if os.path.exists(newpath):
2901 2902 if not os.path.isdir(newpath):
2902 2903 raise util.Abort(_('destination %s exists and is not '
2903 2904 'a directory') % newpath)
2904 2905 if not opts.get('force'):
2905 2906 raise util.Abort(_('destination %s exists, '
2906 2907 'use -f to force') % newpath)
2907 2908 else:
2908 2909 newpath = savename(path)
2909 2910 ui.warn(_("copy %s to %s\n") % (path, newpath))
2910 2911 util.copyfiles(path, newpath)
2911 2912 if opts.get('empty'):
2912 2913 del q.applied[:]
2913 2914 q.applieddirty = True
2914 2915 q.savedirty()
2915 2916 return 0
2916 2917
2917 2918
2918 2919 @command("qselect",
2919 2920 [('n', 'none', None, _('disable all guards')),
2920 2921 ('s', 'series', None, _('list all guards in series file')),
2921 2922 ('', 'pop', None, _('pop to before first guarded applied patch')),
2922 2923 ('', 'reapply', None, _('pop, then reapply patches'))],
2923 2924 _('hg qselect [OPTION]... [GUARD]...'))
2924 2925 def select(ui, repo, *args, **opts):
2925 2926 '''set or print guarded patches to push
2926 2927
2927 2928 Use the :hg:`qguard` command to set or print guards on patch, then use
2928 2929 qselect to tell mq which guards to use. A patch will be pushed if
2929 2930 it has no guards or any positive guards match the currently
2930 2931 selected guard, but will not be pushed if any negative guards
2931 2932 match the current guard. For example::
2932 2933
2933 2934 qguard foo.patch -- -stable (negative guard)
2934 2935 qguard bar.patch +stable (positive guard)
2935 2936 qselect stable
2936 2937
2937 2938 This activates the "stable" guard. mq will skip foo.patch (because
2938 2939 it has a negative match) but push bar.patch (because it has a
2939 2940 positive match).
2940 2941
2941 2942 With no arguments, prints the currently active guards.
2942 2943 With one argument, sets the active guard.
2943 2944
2944 2945 Use -n/--none to deactivate guards (no other arguments needed).
2945 2946 When no guards are active, patches with positive guards are
2946 2947 skipped and patches with negative guards are pushed.
2947 2948
2948 2949 qselect can change the guards on applied patches. It does not pop
2949 2950 guarded patches by default. Use --pop to pop back to the last
2950 2951 applied patch that is not guarded. Use --reapply (which implies
2951 2952 --pop) to push back to the current patch afterwards, but skip
2952 2953 guarded patches.
2953 2954
2954 2955 Use -s/--series to print a list of all guards in the series file
2955 2956 (no other arguments needed). Use -v for more information.
2956 2957
2957 2958 Returns 0 on success.'''
2958 2959
2959 2960 q = repo.mq
2960 2961 guards = q.active()
2961 2962 if args or opts.get('none'):
2962 2963 old_unapplied = q.unapplied(repo)
2963 2964 old_guarded = [i for i in xrange(len(q.applied)) if
2964 2965 not q.pushable(i)[0]]
2965 2966 q.setactive(args)
2966 2967 q.savedirty()
2967 2968 if not args:
2968 2969 ui.status(_('guards deactivated\n'))
2969 2970 if not opts.get('pop') and not opts.get('reapply'):
2970 2971 unapplied = q.unapplied(repo)
2971 2972 guarded = [i for i in xrange(len(q.applied))
2972 2973 if not q.pushable(i)[0]]
2973 2974 if len(unapplied) != len(old_unapplied):
2974 2975 ui.status(_('number of unguarded, unapplied patches has '
2975 2976 'changed from %d to %d\n') %
2976 2977 (len(old_unapplied), len(unapplied)))
2977 2978 if len(guarded) != len(old_guarded):
2978 2979 ui.status(_('number of guarded, applied patches has changed '
2979 2980 'from %d to %d\n') %
2980 2981 (len(old_guarded), len(guarded)))
2981 2982 elif opts.get('series'):
2982 2983 guards = {}
2983 2984 noguards = 0
2984 2985 for gs in q.seriesguards:
2985 2986 if not gs:
2986 2987 noguards += 1
2987 2988 for g in gs:
2988 2989 guards.setdefault(g, 0)
2989 2990 guards[g] += 1
2990 2991 if ui.verbose:
2991 2992 guards['NONE'] = noguards
2992 2993 guards = guards.items()
2993 2994 guards.sort(key=lambda x: x[0][1:])
2994 2995 if guards:
2995 2996 ui.note(_('guards in series file:\n'))
2996 2997 for guard, count in guards:
2997 2998 ui.note('%2d ' % count)
2998 2999 ui.write(guard, '\n')
2999 3000 else:
3000 3001 ui.note(_('no guards in series file\n'))
3001 3002 else:
3002 3003 if guards:
3003 3004 ui.note(_('active guards:\n'))
3004 3005 for g in guards:
3005 3006 ui.write(g, '\n')
3006 3007 else:
3007 3008 ui.write(_('no active guards\n'))
3008 3009 reapply = opts.get('reapply') and q.applied and q.appliedname(-1)
3009 3010 popped = False
3010 3011 if opts.get('pop') or opts.get('reapply'):
3011 3012 for i in xrange(len(q.applied)):
3012 3013 pushable, reason = q.pushable(i)
3013 3014 if not pushable:
3014 3015 ui.status(_('popping guarded patches\n'))
3015 3016 popped = True
3016 3017 if i == 0:
3017 3018 q.pop(repo, all=True)
3018 3019 else:
3019 3020 q.pop(repo, str(i - 1))
3020 3021 break
3021 3022 if popped:
3022 3023 try:
3023 3024 if reapply:
3024 3025 ui.status(_('reapplying unguarded patches\n'))
3025 3026 q.push(repo, reapply)
3026 3027 finally:
3027 3028 q.savedirty()
3028 3029
3029 3030 @command("qfinish",
3030 3031 [('a', 'applied', None, _('finish all applied changesets'))],
3031 3032 _('hg qfinish [-a] [REV]...'))
3032 3033 def finish(ui, repo, *revrange, **opts):
3033 3034 """move applied patches into repository history
3034 3035
3035 3036 Finishes the specified revisions (corresponding to applied
3036 3037 patches) by moving them out of mq control into regular repository
3037 3038 history.
3038 3039
3039 3040 Accepts a revision range or the -a/--applied option. If --applied
3040 3041 is specified, all applied mq revisions are removed from mq
3041 3042 control. Otherwise, the given revisions must be at the base of the
3042 3043 stack of applied patches.
3043 3044
3044 3045 This can be especially useful if your changes have been applied to
3045 3046 an upstream repository, or if you are about to push your changes
3046 3047 to upstream.
3047 3048
3048 3049 Returns 0 on success.
3049 3050 """
3050 3051 if not opts.get('applied') and not revrange:
3051 3052 raise util.Abort(_('no revisions specified'))
3052 3053 elif opts.get('applied'):
3053 3054 revrange = ('qbase::qtip',) + revrange
3054 3055
3055 3056 q = repo.mq
3056 3057 if not q.applied:
3057 3058 ui.status(_('no patches applied\n'))
3058 3059 return 0
3059 3060
3060 3061 revs = scmutil.revrange(repo, revrange)
3061 3062 if repo['.'].rev() in revs and repo[None].files():
3062 3063 ui.warn(_('warning: uncommitted changes in the working directory\n'))
3063 3064 # queue.finish may changes phases but leave the responsibility to lock the
3064 3065 # repo to the caller to avoid deadlock with wlock. This command code is
3065 3066 # responsibility for this locking.
3066 3067 lock = repo.lock()
3067 3068 try:
3068 3069 q.finish(repo, revs)
3069 3070 q.savedirty()
3070 3071 finally:
3071 3072 lock.release()
3072 3073 return 0
3073 3074
3074 3075 @command("qqueue",
3075 3076 [('l', 'list', False, _('list all available queues')),
3076 3077 ('', 'active', False, _('print name of active queue')),
3077 3078 ('c', 'create', False, _('create new queue')),
3078 3079 ('', 'rename', False, _('rename active queue')),
3079 3080 ('', 'delete', False, _('delete reference to queue')),
3080 3081 ('', 'purge', False, _('delete queue, and remove patch dir')),
3081 3082 ],
3082 3083 _('[OPTION] [QUEUE]'))
3083 3084 def qqueue(ui, repo, name=None, **opts):
3084 3085 '''manage multiple patch queues
3085 3086
3086 3087 Supports switching between different patch queues, as well as creating
3087 3088 new patch queues and deleting existing ones.
3088 3089
3089 3090 Omitting a queue name or specifying -l/--list will show you the registered
3090 3091 queues - by default the "normal" patches queue is registered. The currently
3091 3092 active queue will be marked with "(active)". Specifying --active will print
3092 3093 only the name of the active queue.
3093 3094
3094 3095 To create a new queue, use -c/--create. The queue is automatically made
3095 3096 active, except in the case where there are applied patches from the
3096 3097 currently active queue in the repository. Then the queue will only be
3097 3098 created and switching will fail.
3098 3099
3099 3100 To delete an existing queue, use --delete. You cannot delete the currently
3100 3101 active queue.
3101 3102
3102 3103 Returns 0 on success.
3103 3104 '''
3104 3105 q = repo.mq
3105 3106 _defaultqueue = 'patches'
3106 3107 _allqueues = 'patches.queues'
3107 3108 _activequeue = 'patches.queue'
3108 3109
3109 3110 def _getcurrent():
3110 3111 cur = os.path.basename(q.path)
3111 3112 if cur.startswith('patches-'):
3112 3113 cur = cur[8:]
3113 3114 return cur
3114 3115
3115 3116 def _noqueues():
3116 3117 try:
3117 3118 fh = repo.opener(_allqueues, 'r')
3118 3119 fh.close()
3119 3120 except IOError:
3120 3121 return True
3121 3122
3122 3123 return False
3123 3124
3124 3125 def _getqueues():
3125 3126 current = _getcurrent()
3126 3127
3127 3128 try:
3128 3129 fh = repo.opener(_allqueues, 'r')
3129 3130 queues = [queue.strip() for queue in fh if queue.strip()]
3130 3131 fh.close()
3131 3132 if current not in queues:
3132 3133 queues.append(current)
3133 3134 except IOError:
3134 3135 queues = [_defaultqueue]
3135 3136
3136 3137 return sorted(queues)
3137 3138
3138 3139 def _setactive(name):
3139 3140 if q.applied:
3140 3141 raise util.Abort(_('new queue created, but cannot make active '
3141 3142 'as patches are applied'))
3142 3143 _setactivenocheck(name)
3143 3144
3144 3145 def _setactivenocheck(name):
3145 3146 fh = repo.opener(_activequeue, 'w')
3146 3147 if name != 'patches':
3147 3148 fh.write(name)
3148 3149 fh.close()
3149 3150
3150 3151 def _addqueue(name):
3151 3152 fh = repo.opener(_allqueues, 'a')
3152 3153 fh.write('%s\n' % (name,))
3153 3154 fh.close()
3154 3155
3155 3156 def _queuedir(name):
3156 3157 if name == 'patches':
3157 3158 return repo.join('patches')
3158 3159 else:
3159 3160 return repo.join('patches-' + name)
3160 3161
3161 3162 def _validname(name):
3162 3163 for n in name:
3163 3164 if n in ':\\/.':
3164 3165 return False
3165 3166 return True
3166 3167
3167 3168 def _delete(name):
3168 3169 if name not in existing:
3169 3170 raise util.Abort(_('cannot delete queue that does not exist'))
3170 3171
3171 3172 current = _getcurrent()
3172 3173
3173 3174 if name == current:
3174 3175 raise util.Abort(_('cannot delete currently active queue'))
3175 3176
3176 3177 fh = repo.opener('patches.queues.new', 'w')
3177 3178 for queue in existing:
3178 3179 if queue == name:
3179 3180 continue
3180 3181 fh.write('%s\n' % (queue,))
3181 3182 fh.close()
3182 3183 util.rename(repo.join('patches.queues.new'), repo.join(_allqueues))
3183 3184
3184 3185 if not name or opts.get('list') or opts.get('active'):
3185 3186 current = _getcurrent()
3186 3187 if opts.get('active'):
3187 3188 ui.write('%s\n' % (current,))
3188 3189 return
3189 3190 for queue in _getqueues():
3190 3191 ui.write('%s' % (queue,))
3191 3192 if queue == current and not ui.quiet:
3192 3193 ui.write(_(' (active)\n'))
3193 3194 else:
3194 3195 ui.write('\n')
3195 3196 return
3196 3197
3197 3198 if not _validname(name):
3198 3199 raise util.Abort(
3199 3200 _('invalid queue name, may not contain the characters ":\\/."'))
3200 3201
3201 3202 existing = _getqueues()
3202 3203
3203 3204 if opts.get('create'):
3204 3205 if name in existing:
3205 3206 raise util.Abort(_('queue "%s" already exists') % name)
3206 3207 if _noqueues():
3207 3208 _addqueue(_defaultqueue)
3208 3209 _addqueue(name)
3209 3210 _setactive(name)
3210 3211 elif opts.get('rename'):
3211 3212 current = _getcurrent()
3212 3213 if name == current:
3213 3214 raise util.Abort(_('can\'t rename "%s" to its current name') % name)
3214 3215 if name in existing:
3215 3216 raise util.Abort(_('queue "%s" already exists') % name)
3216 3217
3217 3218 olddir = _queuedir(current)
3218 3219 newdir = _queuedir(name)
3219 3220
3220 3221 if os.path.exists(newdir):
3221 3222 raise util.Abort(_('non-queue directory "%s" already exists') %
3222 3223 newdir)
3223 3224
3224 3225 fh = repo.opener('patches.queues.new', 'w')
3225 3226 for queue in existing:
3226 3227 if queue == current:
3227 3228 fh.write('%s\n' % (name,))
3228 3229 if os.path.exists(olddir):
3229 3230 util.rename(olddir, newdir)
3230 3231 else:
3231 3232 fh.write('%s\n' % (queue,))
3232 3233 fh.close()
3233 3234 util.rename(repo.join('patches.queues.new'), repo.join(_allqueues))
3234 3235 _setactivenocheck(name)
3235 3236 elif opts.get('delete'):
3236 3237 _delete(name)
3237 3238 elif opts.get('purge'):
3238 3239 if name in existing:
3239 3240 _delete(name)
3240 3241 qdir = _queuedir(name)
3241 3242 if os.path.exists(qdir):
3242 3243 shutil.rmtree(qdir)
3243 3244 else:
3244 3245 if name not in existing:
3245 3246 raise util.Abort(_('use --create to create a new queue'))
3246 3247 _setactive(name)
3247 3248
3248 3249 def mqphasedefaults(repo, roots):
3249 3250 """callback used to set mq changeset as secret when no phase data exists"""
3250 3251 if repo.mq.applied:
3251 3252 if repo.ui.configbool('mq', 'secret', False):
3252 3253 mqphase = phases.secret
3253 3254 else:
3254 3255 mqphase = phases.draft
3255 3256 qbase = repo[repo.mq.applied[0].node]
3256 3257 roots[mqphase].add(qbase.node())
3257 3258 return roots
3258 3259
3259 3260 def reposetup(ui, repo):
3260 3261 class mqrepo(repo.__class__):
3261 3262 @localrepo.unfilteredpropertycache
3262 3263 def mq(self):
3263 3264 return queue(self.ui, self.baseui, self.path)
3264 3265
3265 3266 def abortifwdirpatched(self, errmsg, force=False):
3266 3267 if self.mq.applied and self.mq.checkapplied and not force:
3267 3268 parents = self.dirstate.parents()
3268 3269 patches = [s.node for s in self.mq.applied]
3269 3270 if parents[0] in patches or parents[1] in patches:
3270 3271 raise util.Abort(errmsg)
3271 3272
3272 3273 def commit(self, text="", user=None, date=None, match=None,
3273 3274 force=False, editor=False, extra={}):
3274 3275 self.abortifwdirpatched(
3275 3276 _('cannot commit over an applied mq patch'),
3276 3277 force)
3277 3278
3278 3279 return super(mqrepo, self).commit(text, user, date, match, force,
3279 3280 editor, extra)
3280 3281
3281 3282 def checkpush(self, force, revs):
3282 3283 if self.mq.applied and self.mq.checkapplied and not force:
3283 3284 outapplied = [e.node for e in self.mq.applied]
3284 3285 if revs:
3285 3286 # Assume applied patches have no non-patch descendants and
3286 3287 # are not on remote already. Filtering any changeset not
3287 3288 # pushed.
3288 3289 heads = set(revs)
3289 3290 for node in reversed(outapplied):
3290 3291 if node in heads:
3291 3292 break
3292 3293 else:
3293 3294 outapplied.pop()
3294 3295 # looking for pushed and shared changeset
3295 3296 for node in outapplied:
3296 3297 if self[node].phase() < phases.secret:
3297 3298 raise util.Abort(_('source has mq patches applied'))
3298 3299 # no non-secret patches pushed
3299 3300 super(mqrepo, self).checkpush(force, revs)
3300 3301
3301 3302 def _findtags(self):
3302 3303 '''augment tags from base class with patch tags'''
3303 3304 result = super(mqrepo, self)._findtags()
3304 3305
3305 3306 q = self.mq
3306 3307 if not q.applied:
3307 3308 return result
3308 3309
3309 3310 mqtags = [(patch.node, patch.name) for patch in q.applied]
3310 3311
3311 3312 try:
3312 3313 # for now ignore filtering business
3313 3314 self.unfiltered().changelog.rev(mqtags[-1][0])
3314 3315 except error.LookupError:
3315 3316 self.ui.warn(_('mq status file refers to unknown node %s\n')
3316 3317 % short(mqtags[-1][0]))
3317 3318 return result
3318 3319
3319 3320 # do not add fake tags for filtered revisions
3320 3321 included = self.changelog.hasnode
3321 3322 mqtags = [mqt for mqt in mqtags if included(mqt[0])]
3322 3323 if not mqtags:
3323 3324 return result
3324 3325
3325 3326 mqtags.append((mqtags[-1][0], 'qtip'))
3326 3327 mqtags.append((mqtags[0][0], 'qbase'))
3327 3328 mqtags.append((self.changelog.parents(mqtags[0][0])[0], 'qparent'))
3328 3329 tags = result[0]
3329 3330 for patch in mqtags:
3330 3331 if patch[1] in tags:
3331 3332 self.ui.warn(_('tag %s overrides mq patch of the same '
3332 3333 'name\n') % patch[1])
3333 3334 else:
3334 3335 tags[patch[1]] = patch[0]
3335 3336
3336 3337 return result
3337 3338
3338 3339 if repo.local():
3339 3340 repo.__class__ = mqrepo
3340 3341
3341 3342 repo._phasedefaults.append(mqphasedefaults)
3342 3343
3343 3344 def mqimport(orig, ui, repo, *args, **kwargs):
3344 3345 if (util.safehasattr(repo, 'abortifwdirpatched')
3345 3346 and not kwargs.get('no_commit', False)):
3346 3347 repo.abortifwdirpatched(_('cannot import over an applied patch'),
3347 3348 kwargs.get('force'))
3348 3349 return orig(ui, repo, *args, **kwargs)
3349 3350
3350 3351 def mqinit(orig, ui, *args, **kwargs):
3351 3352 mq = kwargs.pop('mq', None)
3352 3353
3353 3354 if not mq:
3354 3355 return orig(ui, *args, **kwargs)
3355 3356
3356 3357 if args:
3357 3358 repopath = args[0]
3358 3359 if not hg.islocal(repopath):
3359 3360 raise util.Abort(_('only a local queue repository '
3360 3361 'may be initialized'))
3361 3362 else:
3362 3363 repopath = cmdutil.findrepo(os.getcwd())
3363 3364 if not repopath:
3364 3365 raise util.Abort(_('there is no Mercurial repository here '
3365 3366 '(.hg not found)'))
3366 3367 repo = hg.repository(ui, repopath)
3367 3368 return qinit(ui, repo, True)
3368 3369
3369 3370 def mqcommand(orig, ui, repo, *args, **kwargs):
3370 3371 """Add --mq option to operate on patch repository instead of main"""
3371 3372
3372 3373 # some commands do not like getting unknown options
3373 3374 mq = kwargs.pop('mq', None)
3374 3375
3375 3376 if not mq:
3376 3377 return orig(ui, repo, *args, **kwargs)
3377 3378
3378 3379 q = repo.mq
3379 3380 r = q.qrepo()
3380 3381 if not r:
3381 3382 raise util.Abort(_('no queue repository'))
3382 3383 return orig(r.ui, r, *args, **kwargs)
3383 3384
3384 3385 def summaryhook(ui, repo):
3385 3386 q = repo.mq
3386 3387 m = []
3387 3388 a, u = len(q.applied), len(q.unapplied(repo))
3388 3389 if a:
3389 3390 m.append(ui.label(_("%d applied"), 'qseries.applied') % a)
3390 3391 if u:
3391 3392 m.append(ui.label(_("%d unapplied"), 'qseries.unapplied') % u)
3392 3393 if m:
3393 3394 # i18n: column positioning for "hg summary"
3394 3395 ui.write(_("mq: %s\n") % ', '.join(m))
3395 3396 else:
3396 3397 # i18n: column positioning for "hg summary"
3397 3398 ui.note(_("mq: (empty queue)\n"))
3398 3399
3399 3400 def revsetmq(repo, subset, x):
3400 3401 """``mq()``
3401 3402 Changesets managed by MQ.
3402 3403 """
3403 3404 revset.getargs(x, 0, 0, _("mq takes no arguments"))
3404 3405 applied = set([repo[r.node].rev() for r in repo.mq.applied])
3405 3406 return [r for r in subset if r in applied]
3406 3407
3407 3408 # tell hggettext to extract docstrings from these functions:
3408 3409 i18nfunctions = [revsetmq]
3409 3410
3410 3411 def extsetup(ui):
3411 3412 # Ensure mq wrappers are called first, regardless of extension load order by
3412 3413 # NOT wrapping in uisetup() and instead deferring to init stage two here.
3413 3414 mqopt = [('', 'mq', None, _("operate on patch repository"))]
3414 3415
3415 3416 extensions.wrapcommand(commands.table, 'import', mqimport)
3416 3417 cmdutil.summaryhooks.add('mq', summaryhook)
3417 3418
3418 3419 entry = extensions.wrapcommand(commands.table, 'init', mqinit)
3419 3420 entry[1].extend(mqopt)
3420 3421
3421 3422 nowrap = set(commands.norepo.split(" "))
3422 3423
3423 3424 def dotable(cmdtable):
3424 3425 for cmd in cmdtable.keys():
3425 3426 cmd = cmdutil.parsealiases(cmd)[0]
3426 3427 if cmd in nowrap:
3427 3428 continue
3428 3429 entry = extensions.wrapcommand(cmdtable, cmd, mqcommand)
3429 3430 entry[1].extend(mqopt)
3430 3431
3431 3432 dotable(commands.table)
3432 3433
3433 3434 for extname, extmodule in extensions.extensions():
3434 3435 if extmodule.__file__ != __file__:
3435 3436 dotable(getattr(extmodule, 'cmdtable', {}))
3436 3437
3437 3438 revset.symbols['mq'] = revsetmq
3438 3439
3439 3440 colortable = {'qguard.negative': 'red',
3440 3441 'qguard.positive': 'yellow',
3441 3442 'qguard.unguarded': 'green',
3442 3443 'qseries.applied': 'blue bold underline',
3443 3444 'qseries.guarded': 'black bold',
3444 3445 'qseries.missing': 'red bold',
3445 3446 'qseries.unapplied': 'black bold'}
3446 3447
3447 3448 commands.inferrepo += " qnew qrefresh qdiff qcommit"
@@ -1,74 +1,75 b''
1 1 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 '''share a common history between several working directories'''
7 7
8 8 from mercurial.i18n import _
9 9 from mercurial import hg, commands, util
10 10
11 11 testedwith = 'internal'
12 12
13 13 def share(ui, source, dest=None, noupdate=False):
14 14 """create a new shared repository
15 15
16 16 Initialize a new repository and working directory that shares its
17 17 history with another repository.
18 18
19 19 .. note::
20
20 21 using rollback or extensions that destroy/modify history (mq,
21 22 rebase, etc.) can cause considerable confusion with shared
22 23 clones. In particular, if two shared clones are both updated to
23 24 the same changeset, and one of them destroys that changeset
24 25 with rollback, the other clone will suddenly stop working: all
25 26 operations will fail with "abort: working directory has unknown
26 27 parent". The only known workaround is to use debugsetparents on
27 28 the broken clone to reset it to a changeset that still exists.
28 29 """
29 30
30 31 return hg.share(ui, source, dest, not noupdate)
31 32
32 33 def unshare(ui, repo):
33 34 """convert a shared repository to a normal one
34 35
35 36 Copy the store data to the repo and remove the sharedpath data.
36 37 """
37 38
38 39 if repo.sharedpath == repo.path:
39 40 raise util.Abort(_("this is not a shared repo"))
40 41
41 42 destlock = lock = None
42 43 lock = repo.lock()
43 44 try:
44 45 # we use locks here because if we race with commit, we
45 46 # can end up with extra data in the cloned revlogs that's
46 47 # not pointed to by changesets, thus causing verify to
47 48 # fail
48 49
49 50 destlock = hg.copystore(ui, repo, repo.path)
50 51
51 52 sharefile = repo.join('sharedpath')
52 53 util.rename(sharefile, sharefile + '.old')
53 54
54 55 repo.requirements.discard('sharedpath')
55 56 repo._writerequirements()
56 57 finally:
57 58 destlock and destlock.release()
58 59 lock and lock.release()
59 60
60 61 # update store, spath, sopener and sjoin of repo
61 62 repo.__init__(repo.baseui, repo.root)
62 63
63 64 cmdtable = {
64 65 "share":
65 66 (share,
66 67 [('U', 'noupdate', None, _('do not create a working copy'))],
67 68 _('[-U] SOURCE [DEST]')),
68 69 "unshare":
69 70 (unshare,
70 71 [],
71 72 ''),
72 73 }
73 74
74 75 commands.norepo += " share"
@@ -1,217 +1,219 b''
1 1 """strip changesets and their descendents from history
2 2
3 3 This extension allows you to strip changesets and all their descendants from the
4 4 repository. See the command help for details.
5 5 """
6 6 from mercurial.i18n import _
7 7 from mercurial.node import nullid
8 8 from mercurial.lock import release
9 9 from mercurial import cmdutil, hg, scmutil, util
10 10 from mercurial import repair, bookmarks
11 11
12 12 cmdtable = {}
13 13 command = cmdutil.command(cmdtable)
14 14 testedwith = 'internal'
15 15
16 16 def checksubstate(repo, baserev=None):
17 17 '''return list of subrepos at a different revision than substate.
18 18 Abort if any subrepos have uncommitted changes.'''
19 19 inclsubs = []
20 20 wctx = repo[None]
21 21 if baserev:
22 22 bctx = repo[baserev]
23 23 else:
24 24 bctx = wctx.parents()[0]
25 25 for s in sorted(wctx.substate):
26 26 if wctx.sub(s).dirty(True):
27 27 raise util.Abort(
28 28 _("uncommitted changes in subrepository %s") % s)
29 29 elif s not in bctx.substate or bctx.sub(s).dirty():
30 30 inclsubs.append(s)
31 31 return inclsubs
32 32
33 33 def checklocalchanges(repo, force=False, excsuffix=''):
34 34 cmdutil.checkunfinished(repo)
35 35 m, a, r, d = repo.status()[:4]
36 36 if not force:
37 37 if (m or a or r or d):
38 38 _("local changes found") # i18n tool detection
39 39 raise util.Abort(_("local changes found" + excsuffix))
40 40 if checksubstate(repo):
41 41 _("local changed subrepos found") # i18n tool detection
42 42 raise util.Abort(_("local changed subrepos found" + excsuffix))
43 43 return m, a, r, d
44 44
45 45 def strip(ui, repo, revs, update=True, backup="all", force=None):
46 46 wlock = lock = None
47 47 try:
48 48 wlock = repo.wlock()
49 49 lock = repo.lock()
50 50
51 51 if update:
52 52 checklocalchanges(repo, force=force)
53 53 urev, p2 = repo.changelog.parents(revs[0])
54 if p2 != nullid and p2 in [x.node for x in repo.mq.applied]:
54 if (util.safehasattr(repo, 'mq') and
55 p2 != nullid
56 and p2 in [x.node for x in repo.mq.applied]):
55 57 urev = p2
56 58 hg.clean(repo, urev)
57 59 repo.dirstate.write()
58 60
59 61 repair.strip(ui, repo, revs, backup)
60 62 finally:
61 63 release(lock, wlock)
62 64
63 65
64 66 @command("strip",
65 67 [
66 68 ('r', 'rev', [], _('strip specified revision (optional, '
67 69 'can specify revisions without this '
68 70 'option)'), _('REV')),
69 71 ('f', 'force', None, _('force removal of changesets, discard '
70 72 'uncommitted changes (no backup)')),
71 73 ('b', 'backup', None, _('bundle only changesets with local revision'
72 74 ' number greater than REV which are not'
73 75 ' descendants of REV (DEPRECATED)')),
74 76 ('', 'no-backup', None, _('no backups')),
75 77 ('', 'nobackup', None, _('no backups (DEPRECATED)')),
76 78 ('n', '', None, _('ignored (DEPRECATED)')),
77 79 ('k', 'keep', None, _("do not modify working copy during strip")),
78 80 ('B', 'bookmark', '', _("remove revs only reachable from given"
79 81 " bookmark"))],
80 82 _('hg strip [-k] [-f] [-n] [-B bookmark] [-r] REV...'))
81 83 def stripcmd(ui, repo, *revs, **opts):
82 84 """strip changesets and all their descendants from the repository
83 85
84 86 The strip command removes the specified changesets and all their
85 87 descendants. If the working directory has uncommitted changes, the
86 88 operation is aborted unless the --force flag is supplied, in which
87 89 case changes will be discarded.
88 90
89 91 If a parent of the working directory is stripped, then the working
90 92 directory will automatically be updated to the most recent
91 93 available ancestor of the stripped parent after the operation
92 94 completes.
93 95
94 96 Any stripped changesets are stored in ``.hg/strip-backup`` as a
95 97 bundle (see :hg:`help bundle` and :hg:`help unbundle`). They can
96 98 be restored by running :hg:`unbundle .hg/strip-backup/BUNDLE`,
97 99 where BUNDLE is the bundle file created by the strip. Note that
98 100 the local revision numbers will in general be different after the
99 101 restore.
100 102
101 103 Use the --no-backup option to discard the backup bundle once the
102 104 operation completes.
103 105
104 106 Strip is not a history-rewriting operation and can be used on
105 107 changesets in the public phase. But if the stripped changesets have
106 108 been pushed to a remote repository you will likely pull them again.
107 109
108 110 Return 0 on success.
109 111 """
110 112 backup = 'all'
111 113 if opts.get('backup'):
112 114 backup = 'strip'
113 115 elif opts.get('no_backup') or opts.get('nobackup'):
114 116 backup = 'none'
115 117
116 118 cl = repo.changelog
117 119 revs = list(revs) + opts.get('rev')
118 120 revs = set(scmutil.revrange(repo, revs))
119 121
120 122 if opts.get('bookmark'):
121 123 mark = opts.get('bookmark')
122 124 marks = repo._bookmarks
123 125 if mark not in marks:
124 126 raise util.Abort(_("bookmark '%s' not found") % mark)
125 127
126 128 # If the requested bookmark is not the only one pointing to a
127 129 # a revision we have to only delete the bookmark and not strip
128 130 # anything. revsets cannot detect that case.
129 131 uniquebm = True
130 132 for m, n in marks.iteritems():
131 133 if m != mark and n == repo[mark].node():
132 134 uniquebm = False
133 135 break
134 136 if uniquebm:
135 137 rsrevs = repo.revs("ancestors(bookmark(%s)) - "
136 138 "ancestors(head() and not bookmark(%s)) - "
137 139 "ancestors(bookmark() and not bookmark(%s))",
138 140 mark, mark, mark)
139 141 revs.update(set(rsrevs))
140 142 if not revs:
141 143 del marks[mark]
142 144 marks.write()
143 145 ui.write(_("bookmark '%s' deleted\n") % mark)
144 146
145 147 if not revs:
146 148 raise util.Abort(_('empty revision set'))
147 149
148 150 descendants = set(cl.descendants(revs))
149 151 strippedrevs = revs.union(descendants)
150 152 roots = revs.difference(descendants)
151 153
152 154 update = False
153 155 # if one of the wdir parent is stripped we'll need
154 156 # to update away to an earlier revision
155 157 for p in repo.dirstate.parents():
156 158 if p != nullid and cl.rev(p) in strippedrevs:
157 159 update = True
158 160 break
159 161
160 162 rootnodes = set(cl.node(r) for r in roots)
161 163
162 164 q = getattr(repo, 'mq', None)
163 165 if q is not None and q.applied:
164 166 # refresh queue state if we're about to strip
165 167 # applied patches
166 168 if cl.rev(repo.lookup('qtip')) in strippedrevs:
167 169 q.applieddirty = True
168 170 start = 0
169 171 end = len(q.applied)
170 172 for i, statusentry in enumerate(q.applied):
171 173 if statusentry.node in rootnodes:
172 174 # if one of the stripped roots is an applied
173 175 # patch, only part of the queue is stripped
174 176 start = i
175 177 break
176 178 del q.applied[start:end]
177 179 q.savedirty()
178 180
179 181 revs = sorted(rootnodes)
180 182 if update and opts.get('keep'):
181 183 wlock = repo.wlock()
182 184 try:
183 185 urev, p2 = repo.changelog.parents(revs[0])
184 186 if (util.safehasattr(repo, 'mq') and p2 != nullid
185 187 and p2 in [x.node for x in repo.mq.applied]):
186 188 urev = p2
187 189 uctx = repo[urev]
188 190
189 191 # only reset the dirstate for files that would actually change
190 192 # between the working context and uctx
191 193 descendantrevs = repo.revs("%s::." % uctx.rev())
192 194 changedfiles = []
193 195 for rev in descendantrevs:
194 196 # blindly reset the files, regardless of what actually changed
195 197 changedfiles.extend(repo[rev].files())
196 198
197 199 # reset files that only changed in the dirstate too
198 200 dirstate = repo.dirstate
199 201 dirchanges = [f for f in dirstate if dirstate[f] != 'n']
200 202 changedfiles.extend(dirchanges)
201 203
202 204 repo.dirstate.rebuild(urev, uctx.manifest(), changedfiles)
203 205 repo.dirstate.write()
204 206 update = False
205 207 finally:
206 208 wlock.release()
207 209
208 210 if opts.get('bookmark'):
209 211 if mark == repo._bookmarkcurrent:
210 212 bookmarks.setcurrent(repo, None)
211 213 del marks[mark]
212 214 marks.write()
213 215 ui.write(_("bookmark '%s' deleted\n") % mark)
214 216
215 217 strip(ui, repo, revs, backup=backup, update=update, force=opts.get('force'))
216 218
217 219 return 0
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now