##// END OF EJS Templates
patch: use temporary files to handle intermediate copies...
Patrick Mezard -
r14452:ee574cfd default
parent child Browse files
Show More
@@ -1,691 +1,692 b''
1 1 # keyword.py - $Keyword$ expansion for Mercurial
2 2 #
3 3 # Copyright 2007-2010 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 DSCM
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 The more specific you are in your filename patterns the less you
57 57 lose speed in huge repositories.
58 58
59 59 For [keywordmaps] template mapping and expansion demonstration and
60 60 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
61 61 available templates and filters.
62 62
63 63 Three additional date template filters are provided:
64 64
65 65 :``utcdate``: "2006/09/18 15:13:13"
66 66 :``svnutcdate``: "2006-09-18 15:13:13Z"
67 67 :``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
68 68
69 69 The default template mappings (view with :hg:`kwdemo -d`) can be
70 70 replaced with customized keywords and templates. Again, run
71 71 :hg:`kwdemo` to control the results of your configuration changes.
72 72
73 73 Before changing/disabling active keywords, you must run :hg:`kwshrink`
74 74 to avoid storing expanded keywords in the change history.
75 75
76 76 To force expansion after enabling it, or a configuration change, run
77 77 :hg:`kwexpand`.
78 78
79 79 Expansions spanning more than one line and incremental expansions,
80 80 like CVS' $Log$, are not supported. A keyword template map "Log =
81 81 {desc}" expands to the first line of the changeset description.
82 82 '''
83 83
84 84 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
85 85 from mercurial import localrepo, match, patch, templatefilters, templater, util
86 86 from mercurial import scmutil
87 87 from mercurial.hgweb import webcommands
88 88 from mercurial.i18n import _
89 89 import os, re, shutil, tempfile
90 90
91 91 commands.optionalrepo += ' kwdemo'
92 92
93 93 cmdtable = {}
94 94 command = cmdutil.command(cmdtable)
95 95
96 96 # hg commands that do not act on keywords
97 97 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
98 98 ' outgoing push tip verify convert email glog')
99 99
100 100 # hg commands that trigger expansion only when writing to working dir,
101 101 # not when reading filelog, and unexpand when reading from working dir
102 102 restricted = 'merge kwexpand kwshrink record qrecord resolve transplant'
103 103
104 104 # names of extensions using dorecord
105 105 recordextensions = 'record'
106 106
107 107 colortable = {
108 108 'kwfiles.enabled': 'green bold',
109 109 'kwfiles.deleted': 'cyan bold underline',
110 110 'kwfiles.enabledunknown': 'green',
111 111 'kwfiles.ignored': 'bold',
112 112 'kwfiles.ignoredunknown': 'none'
113 113 }
114 114
115 115 # date like in cvs' $Date
116 116 def utcdate(text):
117 117 ''':utcdate: Date. Returns a UTC-date in this format: "2009/08/18 11:00:13".
118 118 '''
119 119 return util.datestr((text[0], 0), '%Y/%m/%d %H:%M:%S')
120 120 # date like in svn's $Date
121 121 def svnisodate(text):
122 122 ''':svnisodate: Date. Returns a date in this format: "2009-08-18 13:00:13
123 123 +0200 (Tue, 18 Aug 2009)".
124 124 '''
125 125 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
126 126 # date like in svn's $Id
127 127 def svnutcdate(text):
128 128 ''':svnutcdate: Date. Returns a UTC-date in this format: "2009-08-18
129 129 11:00:13Z".
130 130 '''
131 131 return util.datestr((text[0], 0), '%Y-%m-%d %H:%M:%SZ')
132 132
133 133 templatefilters.filters.update({'utcdate': utcdate,
134 134 'svnisodate': svnisodate,
135 135 'svnutcdate': svnutcdate})
136 136
137 137 # make keyword tools accessible
138 138 kwtools = {'templater': None, 'hgcmd': ''}
139 139
140 140 def _defaultkwmaps(ui):
141 141 '''Returns default keywordmaps according to keywordset configuration.'''
142 142 templates = {
143 143 'Revision': '{node|short}',
144 144 'Author': '{author|user}',
145 145 }
146 146 kwsets = ({
147 147 'Date': '{date|utcdate}',
148 148 'RCSfile': '{file|basename},v',
149 149 'RCSFile': '{file|basename},v', # kept for backwards compatibility
150 150 # with hg-keyword
151 151 'Source': '{root}/{file},v',
152 152 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
153 153 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
154 154 }, {
155 155 'Date': '{date|svnisodate}',
156 156 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
157 157 'LastChangedRevision': '{node|short}',
158 158 'LastChangedBy': '{author|user}',
159 159 'LastChangedDate': '{date|svnisodate}',
160 160 })
161 161 templates.update(kwsets[ui.configbool('keywordset', 'svn')])
162 162 return templates
163 163
164 164 def _shrinktext(text, subfunc):
165 165 '''Helper for keyword expansion removal in text.
166 166 Depending on subfunc also returns number of substitutions.'''
167 167 return subfunc(r'$\1$', text)
168 168
169 169 def _preselect(wstatus, changed):
170 170 '''Retrieves modfied and added files from a working directory state
171 171 and returns the subset of each contained in given changed files
172 172 retrieved from a change context.'''
173 173 modified, added = wstatus[:2]
174 174 modified = [f for f in modified if f in changed]
175 175 added = [f for f in added if f in changed]
176 176 return modified, added
177 177
178 178
179 179 class kwtemplater(object):
180 180 '''
181 181 Sets up keyword templates, corresponding keyword regex, and
182 182 provides keyword substitution functions.
183 183 '''
184 184
185 185 def __init__(self, ui, repo, inc, exc):
186 186 self.ui = ui
187 187 self.repo = repo
188 188 self.match = match.match(repo.root, '', [], inc, exc)
189 189 self.restrict = kwtools['hgcmd'] in restricted.split()
190 190 self.record = False
191 191
192 192 kwmaps = self.ui.configitems('keywordmaps')
193 193 if kwmaps: # override default templates
194 194 self.templates = dict((k, templater.parsestring(v, False))
195 195 for k, v in kwmaps)
196 196 else:
197 197 self.templates = _defaultkwmaps(self.ui)
198 198
199 199 @util.propertycache
200 200 def escape(self):
201 201 '''Returns bar-separated and escaped keywords.'''
202 202 return '|'.join(map(re.escape, self.templates.keys()))
203 203
204 204 @util.propertycache
205 205 def rekw(self):
206 206 '''Returns regex for unexpanded keywords.'''
207 207 return re.compile(r'\$(%s)\$' % self.escape)
208 208
209 209 @util.propertycache
210 210 def rekwexp(self):
211 211 '''Returns regex for expanded keywords.'''
212 212 return re.compile(r'\$(%s): [^$\n\r]*? \$' % self.escape)
213 213
214 214 def substitute(self, data, path, ctx, subfunc):
215 215 '''Replaces keywords in data with expanded template.'''
216 216 def kwsub(mobj):
217 217 kw = mobj.group(1)
218 218 ct = cmdutil.changeset_templater(self.ui, self.repo,
219 219 False, None, '', False)
220 220 ct.use_template(self.templates[kw])
221 221 self.ui.pushbuffer()
222 222 ct.show(ctx, root=self.repo.root, file=path)
223 223 ekw = templatefilters.firstline(self.ui.popbuffer())
224 224 return '$%s: %s $' % (kw, ekw)
225 225 return subfunc(kwsub, data)
226 226
227 227 def linkctx(self, path, fileid):
228 228 '''Similar to filelog.linkrev, but returns a changectx.'''
229 229 return self.repo.filectx(path, fileid=fileid).changectx()
230 230
231 231 def expand(self, path, node, data):
232 232 '''Returns data with keywords expanded.'''
233 233 if not self.restrict and self.match(path) and not util.binary(data):
234 234 ctx = self.linkctx(path, node)
235 235 return self.substitute(data, path, ctx, self.rekw.sub)
236 236 return data
237 237
238 238 def iskwfile(self, cand, ctx):
239 239 '''Returns subset of candidates which are configured for keyword
240 240 expansion are not symbolic links.'''
241 241 return [f for f in cand if self.match(f) and not 'l' in ctx.flags(f)]
242 242
243 243 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
244 244 '''Overwrites selected files expanding/shrinking keywords.'''
245 245 if self.restrict or lookup or self.record: # exclude kw_copy
246 246 candidates = self.iskwfile(candidates, ctx)
247 247 if not candidates:
248 248 return
249 249 kwcmd = self.restrict and lookup # kwexpand/kwshrink
250 250 if self.restrict or expand and lookup:
251 251 mf = ctx.manifest()
252 252 lctx = ctx
253 253 re_kw = (self.restrict or rekw) and self.rekw or self.rekwexp
254 254 msg = (expand and _('overwriting %s expanding keywords\n')
255 255 or _('overwriting %s shrinking keywords\n'))
256 256 for f in candidates:
257 257 if self.restrict:
258 258 data = self.repo.file(f).read(mf[f])
259 259 else:
260 260 data = self.repo.wread(f)
261 261 if util.binary(data):
262 262 continue
263 263 if expand:
264 264 if lookup:
265 265 lctx = self.linkctx(f, mf[f])
266 266 data, found = self.substitute(data, f, lctx, re_kw.subn)
267 267 elif self.restrict:
268 268 found = re_kw.search(data)
269 269 else:
270 270 data, found = _shrinktext(data, re_kw.subn)
271 271 if found:
272 272 self.ui.note(msg % f)
273 273 self.repo.wwrite(f, data, ctx.flags(f))
274 274 if kwcmd:
275 275 self.repo.dirstate.normal(f)
276 276 elif self.record:
277 277 self.repo.dirstate.normallookup(f)
278 278
279 279 def shrink(self, fname, text):
280 280 '''Returns text with all keyword substitutions removed.'''
281 281 if self.match(fname) and not util.binary(text):
282 282 return _shrinktext(text, self.rekwexp.sub)
283 283 return text
284 284
285 285 def shrinklines(self, fname, lines):
286 286 '''Returns lines with keyword substitutions removed.'''
287 287 if self.match(fname):
288 288 text = ''.join(lines)
289 289 if not util.binary(text):
290 290 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
291 291 return lines
292 292
293 293 def wread(self, fname, data):
294 294 '''If in restricted mode returns data read from wdir with
295 295 keyword substitutions removed.'''
296 296 return self.restrict and self.shrink(fname, data) or data
297 297
298 298 class kwfilelog(filelog.filelog):
299 299 '''
300 300 Subclass of filelog to hook into its read, add, cmp methods.
301 301 Keywords are "stored" unexpanded, and processed on reading.
302 302 '''
303 303 def __init__(self, opener, kwt, path):
304 304 super(kwfilelog, self).__init__(opener, path)
305 305 self.kwt = kwt
306 306 self.path = path
307 307
308 308 def read(self, node):
309 309 '''Expands keywords when reading filelog.'''
310 310 data = super(kwfilelog, self).read(node)
311 311 if self.renamed(node):
312 312 return data
313 313 return self.kwt.expand(self.path, node, data)
314 314
315 315 def add(self, text, meta, tr, link, p1=None, p2=None):
316 316 '''Removes keyword substitutions when adding to filelog.'''
317 317 text = self.kwt.shrink(self.path, text)
318 318 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
319 319
320 320 def cmp(self, node, text):
321 321 '''Removes keyword substitutions for comparison.'''
322 322 text = self.kwt.shrink(self.path, text)
323 323 return super(kwfilelog, self).cmp(node, text)
324 324
325 325 def _status(ui, repo, kwt, *pats, **opts):
326 326 '''Bails out if [keyword] configuration is not active.
327 327 Returns status of working directory.'''
328 328 if kwt:
329 329 return repo.status(match=scmutil.match(repo, pats, opts), clean=True,
330 330 unknown=opts.get('unknown') or opts.get('all'))
331 331 if ui.configitems('keyword'):
332 332 raise util.Abort(_('[keyword] patterns cannot match'))
333 333 raise util.Abort(_('no [keyword] patterns configured'))
334 334
335 335 def _kwfwrite(ui, repo, expand, *pats, **opts):
336 336 '''Selects files and passes them to kwtemplater.overwrite.'''
337 337 wctx = repo[None]
338 338 if len(wctx.parents()) > 1:
339 339 raise util.Abort(_('outstanding uncommitted merge'))
340 340 kwt = kwtools['templater']
341 341 wlock = repo.wlock()
342 342 try:
343 343 status = _status(ui, repo, kwt, *pats, **opts)
344 344 modified, added, removed, deleted, unknown, ignored, clean = status
345 345 if modified or added or removed or deleted:
346 346 raise util.Abort(_('outstanding uncommitted changes'))
347 347 kwt.overwrite(wctx, clean, True, expand)
348 348 finally:
349 349 wlock.release()
350 350
351 351 @command('kwdemo',
352 352 [('d', 'default', None, _('show default keyword template maps')),
353 353 ('f', 'rcfile', '',
354 354 _('read maps from rcfile'), _('FILE'))],
355 355 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'))
356 356 def demo(ui, repo, *args, **opts):
357 357 '''print [keywordmaps] configuration and an expansion example
358 358
359 359 Show current, custom, or default keyword template maps and their
360 360 expansions.
361 361
362 362 Extend the current configuration by specifying maps as arguments
363 363 and using -f/--rcfile to source an external hgrc file.
364 364
365 365 Use -d/--default to disable current configuration.
366 366
367 367 See :hg:`help templates` for information on templates and filters.
368 368 '''
369 369 def demoitems(section, items):
370 370 ui.write('[%s]\n' % section)
371 371 for k, v in sorted(items):
372 372 ui.write('%s = %s\n' % (k, v))
373 373
374 374 fn = 'demo.txt'
375 375 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
376 376 ui.note(_('creating temporary repository at %s\n') % tmpdir)
377 377 repo = localrepo.localrepository(ui, tmpdir, True)
378 378 ui.setconfig('keyword', fn, '')
379 379 svn = ui.configbool('keywordset', 'svn')
380 380 # explicitly set keywordset for demo output
381 381 ui.setconfig('keywordset', 'svn', svn)
382 382
383 383 uikwmaps = ui.configitems('keywordmaps')
384 384 if args or opts.get('rcfile'):
385 385 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
386 386 if uikwmaps:
387 387 ui.status(_('\textending current template maps\n'))
388 388 if opts.get('default') or not uikwmaps:
389 389 if svn:
390 390 ui.status(_('\toverriding default svn keywordset\n'))
391 391 else:
392 392 ui.status(_('\toverriding default cvs keywordset\n'))
393 393 if opts.get('rcfile'):
394 394 ui.readconfig(opts.get('rcfile'))
395 395 if args:
396 396 # simulate hgrc parsing
397 397 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
398 398 fp = repo.opener('hgrc', 'w')
399 399 fp.writelines(rcmaps)
400 400 fp.close()
401 401 ui.readconfig(repo.join('hgrc'))
402 402 kwmaps = dict(ui.configitems('keywordmaps'))
403 403 elif opts.get('default'):
404 404 if svn:
405 405 ui.status(_('\n\tconfiguration using default svn keywordset\n'))
406 406 else:
407 407 ui.status(_('\n\tconfiguration using default cvs keywordset\n'))
408 408 kwmaps = _defaultkwmaps(ui)
409 409 if uikwmaps:
410 410 ui.status(_('\tdisabling current template maps\n'))
411 411 for k, v in kwmaps.iteritems():
412 412 ui.setconfig('keywordmaps', k, v)
413 413 else:
414 414 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
415 415 kwmaps = dict(uikwmaps) or _defaultkwmaps(ui)
416 416
417 417 uisetup(ui)
418 418 reposetup(ui, repo)
419 419 ui.write('[extensions]\nkeyword =\n')
420 420 demoitems('keyword', ui.configitems('keyword'))
421 421 demoitems('keywordset', ui.configitems('keywordset'))
422 422 demoitems('keywordmaps', kwmaps.iteritems())
423 423 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
424 424 repo.wopener.write(fn, keywords)
425 425 repo[None].add([fn])
426 426 ui.note(_('\nkeywords written to %s:\n') % fn)
427 427 ui.note(keywords)
428 428 repo.dirstate.setbranch('demobranch')
429 429 for name, cmd in ui.configitems('hooks'):
430 430 if name.split('.', 1)[0].find('commit') > -1:
431 431 repo.ui.setconfig('hooks', name, '')
432 432 msg = _('hg keyword configuration and expansion example')
433 433 ui.note("hg ci -m '%s'\n" % msg)
434 434 repo.commit(text=msg)
435 435 ui.status(_('\n\tkeywords expanded\n'))
436 436 ui.write(repo.wread(fn))
437 437 shutil.rmtree(tmpdir, ignore_errors=True)
438 438
439 439 @command('kwexpand', commands.walkopts, _('hg kwexpand [OPTION]... [FILE]...'))
440 440 def expand(ui, repo, *pats, **opts):
441 441 '''expand keywords in the working directory
442 442
443 443 Run after (re)enabling keyword expansion.
444 444
445 445 kwexpand refuses to run if given files contain local changes.
446 446 '''
447 447 # 3rd argument sets expansion to True
448 448 _kwfwrite(ui, repo, True, *pats, **opts)
449 449
450 450 @command('kwfiles',
451 451 [('A', 'all', None, _('show keyword status flags of all files')),
452 452 ('i', 'ignore', None, _('show files excluded from expansion')),
453 453 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
454 454 ] + commands.walkopts,
455 455 _('hg kwfiles [OPTION]... [FILE]...'))
456 456 def files(ui, repo, *pats, **opts):
457 457 '''show files configured for keyword expansion
458 458
459 459 List which files in the working directory are matched by the
460 460 [keyword] configuration patterns.
461 461
462 462 Useful to prevent inadvertent keyword expansion and to speed up
463 463 execution by including only files that are actual candidates for
464 464 expansion.
465 465
466 466 See :hg:`help keyword` on how to construct patterns both for
467 467 inclusion and exclusion of files.
468 468
469 469 With -A/--all and -v/--verbose the codes used to show the status
470 470 of files are::
471 471
472 472 K = keyword expansion candidate
473 473 k = keyword expansion candidate (not tracked)
474 474 I = ignored
475 475 i = ignored (not tracked)
476 476 '''
477 477 kwt = kwtools['templater']
478 478 status = _status(ui, repo, kwt, *pats, **opts)
479 479 cwd = pats and repo.getcwd() or ''
480 480 modified, added, removed, deleted, unknown, ignored, clean = status
481 481 files = []
482 482 if not opts.get('unknown') or opts.get('all'):
483 483 files = sorted(modified + added + clean)
484 484 wctx = repo[None]
485 485 kwfiles = kwt.iskwfile(files, wctx)
486 486 kwdeleted = kwt.iskwfile(deleted, wctx)
487 487 kwunknown = kwt.iskwfile(unknown, wctx)
488 488 if not opts.get('ignore') or opts.get('all'):
489 489 showfiles = kwfiles, kwdeleted, kwunknown
490 490 else:
491 491 showfiles = [], [], []
492 492 if opts.get('all') or opts.get('ignore'):
493 493 showfiles += ([f for f in files if f not in kwfiles],
494 494 [f for f in unknown if f not in kwunknown])
495 495 kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split()
496 496 kwstates = zip('K!kIi', showfiles, kwlabels)
497 497 for char, filenames, kwstate in kwstates:
498 498 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
499 499 for f in filenames:
500 500 ui.write(fmt % repo.pathto(f, cwd), label='kwfiles.' + kwstate)
501 501
502 502 @command('kwshrink', commands.walkopts, _('hg kwshrink [OPTION]... [FILE]...'))
503 503 def shrink(ui, repo, *pats, **opts):
504 504 '''revert expanded keywords in the working directory
505 505
506 506 Must be run before changing/disabling active keywords.
507 507
508 508 kwshrink refuses to run if given files contain local changes.
509 509 '''
510 510 # 3rd argument sets expansion to False
511 511 _kwfwrite(ui, repo, False, *pats, **opts)
512 512
513 513
514 514 def uisetup(ui):
515 515 ''' Monkeypatches dispatch._parse to retrieve user command.'''
516 516
517 517 def kwdispatch_parse(orig, ui, args):
518 518 '''Monkeypatch dispatch._parse to obtain running hg command.'''
519 519 cmd, func, args, options, cmdoptions = orig(ui, args)
520 520 kwtools['hgcmd'] = cmd
521 521 return cmd, func, args, options, cmdoptions
522 522
523 523 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
524 524
525 525 def reposetup(ui, repo):
526 526 '''Sets up repo as kwrepo for keyword substitution.
527 527 Overrides file method to return kwfilelog instead of filelog
528 528 if file matches user configuration.
529 529 Wraps commit to overwrite configured files with updated
530 530 keyword substitutions.
531 531 Monkeypatches patch and webcommands.'''
532 532
533 533 try:
534 534 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
535 535 or '.hg' in util.splitpath(repo.root)
536 536 or repo._url.startswith('bundle:')):
537 537 return
538 538 except AttributeError:
539 539 pass
540 540
541 541 inc, exc = [], ['.hg*']
542 542 for pat, opt in ui.configitems('keyword'):
543 543 if opt != 'ignore':
544 544 inc.append(pat)
545 545 else:
546 546 exc.append(pat)
547 547 if not inc:
548 548 return
549 549
550 550 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
551 551
552 552 class kwrepo(repo.__class__):
553 553 def file(self, f):
554 554 if f[0] == '/':
555 555 f = f[1:]
556 556 return kwfilelog(self.sopener, kwt, f)
557 557
558 558 def wread(self, filename):
559 559 data = super(kwrepo, self).wread(filename)
560 560 return kwt.wread(filename, data)
561 561
562 562 def commit(self, *args, **opts):
563 563 # use custom commitctx for user commands
564 564 # other extensions can still wrap repo.commitctx directly
565 565 self.commitctx = self.kwcommitctx
566 566 try:
567 567 return super(kwrepo, self).commit(*args, **opts)
568 568 finally:
569 569 del self.commitctx
570 570
571 571 def kwcommitctx(self, ctx, error=False):
572 572 n = super(kwrepo, self).commitctx(ctx, error)
573 573 # no lock needed, only called from repo.commit() which already locks
574 574 if not kwt.record:
575 575 restrict = kwt.restrict
576 576 kwt.restrict = True
577 577 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
578 578 False, True)
579 579 kwt.restrict = restrict
580 580 return n
581 581
582 582 def rollback(self, dryrun=False):
583 583 wlock = self.wlock()
584 584 try:
585 585 if not dryrun:
586 586 changed = self['.'].files()
587 587 ret = super(kwrepo, self).rollback(dryrun)
588 588 if not dryrun:
589 589 ctx = self['.']
590 590 modified, added = _preselect(self[None].status(), changed)
591 591 kwt.overwrite(ctx, modified, True, True)
592 592 kwt.overwrite(ctx, added, True, False)
593 593 return ret
594 594 finally:
595 595 wlock.release()
596 596
597 597 # monkeypatches
598 def kwpatchfile_init(orig, self, ui, fname, backend, mode, create, remove,
599 missing=False, eolmode=None):
598 def kwpatchfile_init(orig, self, ui, fname, backend, store, mode, create,
599 remove, eolmode=None, copysource=None):
600 600 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
601 601 rejects or conflicts due to expanded keywords in working dir.'''
602 orig(self, ui, fname, backend, mode, create, remove, missing, eolmode)
602 orig(self, ui, fname, backend, store, mode, create, remove,
603 eolmode, copysource)
603 604 # shrink keywords read from working dir
604 605 self.lines = kwt.shrinklines(self.fname, self.lines)
605 606
606 607 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
607 608 opts=None, prefix=''):
608 609 '''Monkeypatch patch.diff to avoid expansion.'''
609 610 kwt.restrict = True
610 611 return orig(repo, node1, node2, match, changes, opts, prefix)
611 612
612 613 def kwweb_skip(orig, web, req, tmpl):
613 614 '''Wraps webcommands.x turning off keyword expansion.'''
614 615 kwt.match = util.never
615 616 return orig(web, req, tmpl)
616 617
617 618 def kw_copy(orig, ui, repo, pats, opts, rename=False):
618 619 '''Wraps cmdutil.copy so that copy/rename destinations do not
619 620 contain expanded keywords.
620 621 Note that the source of a regular file destination may also be a
621 622 symlink:
622 623 hg cp sym x -> x is symlink
623 624 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
624 625 For the latter we have to follow the symlink to find out whether its
625 626 target is configured for expansion and we therefore must unexpand the
626 627 keywords in the destination.'''
627 628 orig(ui, repo, pats, opts, rename)
628 629 if opts.get('dry_run'):
629 630 return
630 631 wctx = repo[None]
631 632 cwd = repo.getcwd()
632 633
633 634 def haskwsource(dest):
634 635 '''Returns true if dest is a regular file and configured for
635 636 expansion or a symlink which points to a file configured for
636 637 expansion. '''
637 638 source = repo.dirstate.copied(dest)
638 639 if 'l' in wctx.flags(source):
639 640 source = scmutil.canonpath(repo.root, cwd,
640 641 os.path.realpath(source))
641 642 return kwt.match(source)
642 643
643 644 candidates = [f for f in repo.dirstate.copies() if
644 645 not 'l' in wctx.flags(f) and haskwsource(f)]
645 646 kwt.overwrite(wctx, candidates, False, False)
646 647
647 648 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
648 649 '''Wraps record.dorecord expanding keywords after recording.'''
649 650 wlock = repo.wlock()
650 651 try:
651 652 # record returns 0 even when nothing has changed
652 653 # therefore compare nodes before and after
653 654 kwt.record = True
654 655 ctx = repo['.']
655 656 wstatus = repo[None].status()
656 657 ret = orig(ui, repo, commitfunc, *pats, **opts)
657 658 recctx = repo['.']
658 659 if ctx != recctx:
659 660 modified, added = _preselect(wstatus, recctx.files())
660 661 kwt.restrict = False
661 662 kwt.overwrite(recctx, modified, False, True)
662 663 kwt.overwrite(recctx, added, False, True, True)
663 664 kwt.restrict = True
664 665 return ret
665 666 finally:
666 667 wlock.release()
667 668
668 669 def kwfilectx_cmp(orig, self, fctx):
669 670 # keyword affects data size, comparing wdir and filelog size does
670 671 # not make sense
671 672 if (fctx._filerev is None and
672 673 (self._repo._encodefilterpats or
673 674 kwt.match(fctx.path()) and not 'l' in fctx.flags()) or
674 675 self.size() == fctx.size()):
675 676 return self._filelog.cmp(self._filenode, fctx.data())
676 677 return True
677 678
678 679 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
679 680 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
680 681 extensions.wrapfunction(patch, 'diff', kw_diff)
681 682 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
682 683 for c in 'annotate changeset rev filediff diff'.split():
683 684 extensions.wrapfunction(webcommands, c, kwweb_skip)
684 685 for name in recordextensions.split():
685 686 try:
686 687 record = extensions.find(name)
687 688 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
688 689 except KeyError:
689 690 pass
690 691
691 692 repo.__class__ = kwrepo
@@ -1,1751 +1,1780 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 import cStringIO, email.Parser, os, errno, re
10 import tempfile, zlib
10 import tempfile, zlib, shutil
11 11
12 12 from i18n import _
13 13 from node import hex, nullid, short
14 14 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding
15 15
16 16 gitre = re.compile('diff --git a/(.*) b/(.*)')
17 17
18 18 class PatchError(Exception):
19 19 pass
20 20
21 21
22 22 # public functions
23 23
24 24 def split(stream):
25 25 '''return an iterator of individual patches from a stream'''
26 26 def isheader(line, inheader):
27 27 if inheader and line[0] in (' ', '\t'):
28 28 # continuation
29 29 return True
30 30 if line[0] in (' ', '-', '+'):
31 31 # diff line - don't check for header pattern in there
32 32 return False
33 33 l = line.split(': ', 1)
34 34 return len(l) == 2 and ' ' not in l[0]
35 35
36 36 def chunk(lines):
37 37 return cStringIO.StringIO(''.join(lines))
38 38
39 39 def hgsplit(stream, cur):
40 40 inheader = True
41 41
42 42 for line in stream:
43 43 if not line.strip():
44 44 inheader = False
45 45 if not inheader and line.startswith('# HG changeset patch'):
46 46 yield chunk(cur)
47 47 cur = []
48 48 inheader = True
49 49
50 50 cur.append(line)
51 51
52 52 if cur:
53 53 yield chunk(cur)
54 54
55 55 def mboxsplit(stream, cur):
56 56 for line in stream:
57 57 if line.startswith('From '):
58 58 for c in split(chunk(cur[1:])):
59 59 yield c
60 60 cur = []
61 61
62 62 cur.append(line)
63 63
64 64 if cur:
65 65 for c in split(chunk(cur[1:])):
66 66 yield c
67 67
68 68 def mimesplit(stream, cur):
69 69 def msgfp(m):
70 70 fp = cStringIO.StringIO()
71 71 g = email.Generator.Generator(fp, mangle_from_=False)
72 72 g.flatten(m)
73 73 fp.seek(0)
74 74 return fp
75 75
76 76 for line in stream:
77 77 cur.append(line)
78 78 c = chunk(cur)
79 79
80 80 m = email.Parser.Parser().parse(c)
81 81 if not m.is_multipart():
82 82 yield msgfp(m)
83 83 else:
84 84 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
85 85 for part in m.walk():
86 86 ct = part.get_content_type()
87 87 if ct not in ok_types:
88 88 continue
89 89 yield msgfp(part)
90 90
91 91 def headersplit(stream, cur):
92 92 inheader = False
93 93
94 94 for line in stream:
95 95 if not inheader and isheader(line, inheader):
96 96 yield chunk(cur)
97 97 cur = []
98 98 inheader = True
99 99 if inheader and not isheader(line, inheader):
100 100 inheader = False
101 101
102 102 cur.append(line)
103 103
104 104 if cur:
105 105 yield chunk(cur)
106 106
107 107 def remainder(cur):
108 108 yield chunk(cur)
109 109
110 110 class fiter(object):
111 111 def __init__(self, fp):
112 112 self.fp = fp
113 113
114 114 def __iter__(self):
115 115 return self
116 116
117 117 def next(self):
118 118 l = self.fp.readline()
119 119 if not l:
120 120 raise StopIteration
121 121 return l
122 122
123 123 inheader = False
124 124 cur = []
125 125
126 126 mimeheaders = ['content-type']
127 127
128 128 if not hasattr(stream, 'next'):
129 129 # http responses, for example, have readline but not next
130 130 stream = fiter(stream)
131 131
132 132 for line in stream:
133 133 cur.append(line)
134 134 if line.startswith('# HG changeset patch'):
135 135 return hgsplit(stream, cur)
136 136 elif line.startswith('From '):
137 137 return mboxsplit(stream, cur)
138 138 elif isheader(line, inheader):
139 139 inheader = True
140 140 if line.split(':', 1)[0].lower() in mimeheaders:
141 141 # let email parser handle this
142 142 return mimesplit(stream, cur)
143 143 elif line.startswith('--- ') and inheader:
144 144 # No evil headers seen by diff start, split by hand
145 145 return headersplit(stream, cur)
146 146 # Not enough info, keep reading
147 147
148 148 # if we are here, we have a very plain patch
149 149 return remainder(cur)
150 150
151 151 def extract(ui, fileobj):
152 152 '''extract patch from data read from fileobj.
153 153
154 154 patch can be a normal patch or contained in an email message.
155 155
156 156 return tuple (filename, message, user, date, branch, node, p1, p2).
157 157 Any item in the returned tuple can be None. If filename is None,
158 158 fileobj did not contain a patch. Caller must unlink filename when done.'''
159 159
160 160 # attempt to detect the start of a patch
161 161 # (this heuristic is borrowed from quilt)
162 162 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
163 163 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
164 164 r'---[ \t].*?^\+\+\+[ \t]|'
165 165 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
166 166
167 167 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
168 168 tmpfp = os.fdopen(fd, 'w')
169 169 try:
170 170 msg = email.Parser.Parser().parse(fileobj)
171 171
172 172 subject = msg['Subject']
173 173 user = msg['From']
174 174 if not subject and not user:
175 175 # Not an email, restore parsed headers if any
176 176 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
177 177
178 178 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
179 179 # should try to parse msg['Date']
180 180 date = None
181 181 nodeid = None
182 182 branch = None
183 183 parents = []
184 184
185 185 if subject:
186 186 if subject.startswith('[PATCH'):
187 187 pend = subject.find(']')
188 188 if pend >= 0:
189 189 subject = subject[pend + 1:].lstrip()
190 190 subject = subject.replace('\n\t', ' ')
191 191 ui.debug('Subject: %s\n' % subject)
192 192 if user:
193 193 ui.debug('From: %s\n' % user)
194 194 diffs_seen = 0
195 195 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
196 196 message = ''
197 197 for part in msg.walk():
198 198 content_type = part.get_content_type()
199 199 ui.debug('Content-Type: %s\n' % content_type)
200 200 if content_type not in ok_types:
201 201 continue
202 202 payload = part.get_payload(decode=True)
203 203 m = diffre.search(payload)
204 204 if m:
205 205 hgpatch = False
206 206 hgpatchheader = False
207 207 ignoretext = False
208 208
209 209 ui.debug('found patch at byte %d\n' % m.start(0))
210 210 diffs_seen += 1
211 211 cfp = cStringIO.StringIO()
212 212 for line in payload[:m.start(0)].splitlines():
213 213 if line.startswith('# HG changeset patch') and not hgpatch:
214 214 ui.debug('patch generated by hg export\n')
215 215 hgpatch = True
216 216 hgpatchheader = True
217 217 # drop earlier commit message content
218 218 cfp.seek(0)
219 219 cfp.truncate()
220 220 subject = None
221 221 elif hgpatchheader:
222 222 if line.startswith('# User '):
223 223 user = line[7:]
224 224 ui.debug('From: %s\n' % user)
225 225 elif line.startswith("# Date "):
226 226 date = line[7:]
227 227 elif line.startswith("# Branch "):
228 228 branch = line[9:]
229 229 elif line.startswith("# Node ID "):
230 230 nodeid = line[10:]
231 231 elif line.startswith("# Parent "):
232 232 parents.append(line[10:])
233 233 elif not line.startswith("# "):
234 234 hgpatchheader = False
235 235 elif line == '---' and gitsendmail:
236 236 ignoretext = True
237 237 if not hgpatchheader and not ignoretext:
238 238 cfp.write(line)
239 239 cfp.write('\n')
240 240 message = cfp.getvalue()
241 241 if tmpfp:
242 242 tmpfp.write(payload)
243 243 if not payload.endswith('\n'):
244 244 tmpfp.write('\n')
245 245 elif not diffs_seen and message and content_type == 'text/plain':
246 246 message += '\n' + payload
247 247 except:
248 248 tmpfp.close()
249 249 os.unlink(tmpname)
250 250 raise
251 251
252 252 if subject and not message.startswith(subject):
253 253 message = '%s\n%s' % (subject, message)
254 254 tmpfp.close()
255 255 if not diffs_seen:
256 256 os.unlink(tmpname)
257 257 return None, message, user, date, branch, None, None, None
258 258 p1 = parents and parents.pop(0) or None
259 259 p2 = parents and parents.pop(0) or None
260 260 return tmpname, message, user, date, branch, nodeid, p1, p2
261 261
262 262 class patchmeta(object):
263 263 """Patched file metadata
264 264
265 265 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
266 266 or COPY. 'path' is patched file path. 'oldpath' is set to the
267 267 origin file when 'op' is either COPY or RENAME, None otherwise. If
268 268 file mode is changed, 'mode' is a tuple (islink, isexec) where
269 269 'islink' is True if the file is a symlink and 'isexec' is True if
270 270 the file is executable. Otherwise, 'mode' is None.
271 271 """
272 272 def __init__(self, path):
273 273 self.path = path
274 274 self.oldpath = None
275 275 self.mode = None
276 276 self.op = 'MODIFY'
277 277 self.binary = False
278 278
279 279 def setmode(self, mode):
280 280 islink = mode & 020000
281 281 isexec = mode & 0100
282 282 self.mode = (islink, isexec)
283 283
284 284 def __repr__(self):
285 285 return "<patchmeta %s %r>" % (self.op, self.path)
286 286
287 287 def readgitpatch(lr):
288 288 """extract git-style metadata about patches from <patchname>"""
289 289
290 290 # Filter patch for git information
291 291 gp = None
292 292 gitpatches = []
293 293 for line in lr:
294 294 line = line.rstrip(' \r\n')
295 295 if line.startswith('diff --git'):
296 296 m = gitre.match(line)
297 297 if m:
298 298 if gp:
299 299 gitpatches.append(gp)
300 300 dst = m.group(2)
301 301 gp = patchmeta(dst)
302 302 elif gp:
303 303 if line.startswith('--- '):
304 304 gitpatches.append(gp)
305 305 gp = None
306 306 continue
307 307 if line.startswith('rename from '):
308 308 gp.op = 'RENAME'
309 309 gp.oldpath = line[12:]
310 310 elif line.startswith('rename to '):
311 311 gp.path = line[10:]
312 312 elif line.startswith('copy from '):
313 313 gp.op = 'COPY'
314 314 gp.oldpath = line[10:]
315 315 elif line.startswith('copy to '):
316 316 gp.path = line[8:]
317 317 elif line.startswith('deleted file'):
318 318 gp.op = 'DELETE'
319 319 elif line.startswith('new file mode '):
320 320 gp.op = 'ADD'
321 321 gp.setmode(int(line[-6:], 8))
322 322 elif line.startswith('new mode '):
323 323 gp.setmode(int(line[-6:], 8))
324 324 elif line.startswith('GIT binary patch'):
325 325 gp.binary = True
326 326 if gp:
327 327 gitpatches.append(gp)
328 328
329 329 return gitpatches
330 330
331 331 class linereader(object):
332 332 # simple class to allow pushing lines back into the input stream
333 333 def __init__(self, fp):
334 334 self.fp = fp
335 335 self.buf = []
336 336
337 337 def push(self, line):
338 338 if line is not None:
339 339 self.buf.append(line)
340 340
341 341 def readline(self):
342 342 if self.buf:
343 343 l = self.buf[0]
344 344 del self.buf[0]
345 345 return l
346 346 return self.fp.readline()
347 347
348 348 def __iter__(self):
349 349 while 1:
350 350 l = self.readline()
351 351 if not l:
352 352 break
353 353 yield l
354 354
355 355 class abstractbackend(object):
356 356 def __init__(self, ui):
357 357 self.ui = ui
358 358
359 359 def getfile(self, fname):
360 360 """Return target file data and flags as a (data, (islink,
361 361 isexec)) tuple.
362 362 """
363 363 raise NotImplementedError
364 364
365 def setfile(self, fname, data, mode):
365 def setfile(self, fname, data, mode, copysource):
366 366 """Write data to target file fname and set its mode. mode is a
367 367 (islink, isexec) tuple. If data is None, the file content should
368 be left unchanged.
368 be left unchanged. If the file is modified after being copied,
369 copysource is set to the original file name.
369 370 """
370 371 raise NotImplementedError
371 372
372 373 def unlink(self, fname):
373 374 """Unlink target file."""
374 375 raise NotImplementedError
375 376
376 377 def writerej(self, fname, failed, total, lines):
377 378 """Write rejected lines for fname. total is the number of hunks
378 379 which failed to apply and total the total number of hunks for this
379 380 files.
380 381 """
381 382 pass
382 383
383 def copy(self, src, dst):
384 """Copy src file into dst file. Create intermediate directories if
385 necessary. Files are specified relatively to the patching base
386 directory.
387 """
388 raise NotImplementedError
389
390 384 def exists(self, fname):
391 385 raise NotImplementedError
392 386
393 387 class fsbackend(abstractbackend):
394 388 def __init__(self, ui, basedir):
395 389 super(fsbackend, self).__init__(ui)
396 390 self.opener = scmutil.opener(basedir)
397 391
398 392 def _join(self, f):
399 393 return os.path.join(self.opener.base, f)
400 394
401 395 def getfile(self, fname):
402 396 path = self._join(fname)
403 397 if os.path.islink(path):
404 398 return (os.readlink(path), (True, False))
405 399 isexec, islink = False, False
406 400 try:
407 401 isexec = os.lstat(path).st_mode & 0100 != 0
408 402 islink = os.path.islink(path)
409 403 except OSError, e:
410 404 if e.errno != errno.ENOENT:
411 405 raise
412 406 return (self.opener.read(fname), (islink, isexec))
413 407
414 def setfile(self, fname, data, mode):
408 def setfile(self, fname, data, mode, copysource):
415 409 islink, isexec = mode
416 410 if data is None:
417 411 util.setflags(self._join(fname), islink, isexec)
418 412 return
419 413 if islink:
420 414 self.opener.symlink(data, fname)
421 415 else:
422 416 self.opener.write(fname, data)
423 417 if isexec:
424 418 util.setflags(self._join(fname), False, True)
425 419
426 420 def unlink(self, fname):
427 421 try:
428 422 util.unlinkpath(self._join(fname))
429 423 except OSError, inst:
430 424 if inst.errno != errno.ENOENT:
431 425 raise
432 426
433 427 def writerej(self, fname, failed, total, lines):
434 428 fname = fname + ".rej"
435 429 self.ui.warn(
436 430 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
437 431 (failed, total, fname))
438 432 fp = self.opener(fname, 'w')
439 433 fp.writelines(lines)
440 434 fp.close()
441 435
442 def copy(self, src, dst):
443 basedir = self.opener.base
444 abssrc, absdst = [scmutil.canonpath(basedir, basedir, x)
445 for x in [src, dst]]
446 if os.path.lexists(absdst):
447 raise util.Abort(_("cannot create %s: destination already exists")
448 % dst)
449 dstdir = os.path.dirname(absdst)
450 if dstdir and not os.path.isdir(dstdir):
451 try:
452 os.makedirs(dstdir)
453 except IOError:
454 raise util.Abort(
455 _("cannot create %s: unable to create destination directory")
456 % dst)
457 util.copyfile(abssrc, absdst)
458
459 436 def exists(self, fname):
460 437 return os.path.lexists(self._join(fname))
461 438
462 439 class workingbackend(fsbackend):
463 440 def __init__(self, ui, repo, similarity):
464 441 super(workingbackend, self).__init__(ui, repo.root)
465 442 self.repo = repo
466 443 self.similarity = similarity
467 444 self.removed = set()
468 445 self.changed = set()
469 446 self.copied = []
470 447
471 def setfile(self, fname, data, mode):
472 super(workingbackend, self).setfile(fname, data, mode)
448 def setfile(self, fname, data, mode, copysource):
449 super(workingbackend, self).setfile(fname, data, mode, copysource)
450 if copysource is not None:
451 self.copied.append((copysource, fname))
473 452 self.changed.add(fname)
474 453
475 454 def unlink(self, fname):
476 455 super(workingbackend, self).unlink(fname)
477 456 self.removed.add(fname)
478 457 self.changed.add(fname)
479 458
480 def copy(self, src, dst):
481 super(workingbackend, self).copy(src, dst)
482 self.copied.append((src, dst))
483 self.changed.add(dst)
484
485 459 def close(self):
486 460 wctx = self.repo[None]
487 461 addremoved = set(self.changed)
488 462 for src, dst in self.copied:
489 463 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
490 464 addremoved.discard(src)
491 465 if (not self.similarity) and self.removed:
492 466 wctx.forget(sorted(self.removed))
493 467 if addremoved:
494 468 cwd = self.repo.getcwd()
495 469 if cwd:
496 470 addremoved = [util.pathto(self.repo.root, cwd, f)
497 471 for f in addremoved]
498 472 scmutil.addremove(self.repo, addremoved, similarity=self.similarity)
499 473 return sorted(self.changed)
500 474
475 class filestore(object):
476 def __init__(self):
477 self.opener = None
478 self.files = {}
479 self.created = 0
480
481 def setfile(self, fname, data, mode):
482 if self.opener is None:
483 root = tempfile.mkdtemp(prefix='hg-patch-')
484 self.opener = scmutil.opener(root)
485 # Avoid filename issues with these simple names
486 fn = str(self.created)
487 self.opener.write(fn, data)
488 self.created += 1
489 self.files[fname] = (fn, mode)
490
491 def getfile(self, fname):
492 if fname not in self.files:
493 raise IOError()
494 fn, mode = self.files[fname]
495 return self.opener.read(fn), mode
496
497 def close(self):
498 if self.opener:
499 shutil.rmtree(self.opener.base)
500
501 501 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
502 502 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
503 503 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
504 504 eolmodes = ['strict', 'crlf', 'lf', 'auto']
505 505
506 506 class patchfile(object):
507 def __init__(self, ui, fname, backend, mode, create, remove, missing=False,
508 eolmode='strict'):
507 def __init__(self, ui, fname, backend, store, mode, create, remove,
508 eolmode='strict', copysource=None):
509 509 self.fname = fname
510 510 self.eolmode = eolmode
511 511 self.eol = None
512 512 self.backend = backend
513 513 self.ui = ui
514 514 self.lines = []
515 515 self.exists = False
516 self.missing = missing
516 self.missing = True
517 517 self.mode = mode
518 self.copysource = copysource
518 519 self.create = create
519 520 self.remove = remove
520 if not missing:
521 try:
522 data, mode = self.backend.getfile(fname)
523 if data:
524 self.lines = data.splitlines(True)
525 if self.mode is None:
526 self.mode = mode
527 if self.lines:
528 # Normalize line endings
529 if self.lines[0].endswith('\r\n'):
530 self.eol = '\r\n'
531 elif self.lines[0].endswith('\n'):
532 self.eol = '\n'
533 if eolmode != 'strict':
534 nlines = []
535 for l in self.lines:
536 if l.endswith('\r\n'):
537 l = l[:-2] + '\n'
538 nlines.append(l)
539 self.lines = nlines
521 try:
522 if copysource is None:
523 data, mode = backend.getfile(fname)
540 524 self.exists = True
541 except IOError:
542 if self.mode is None:
543 self.mode = (False, False)
544 else:
545 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
525 else:
526 data, mode = store.getfile(copysource)
527 self.exists = backend.exists(fname)
528 self.missing = False
529 if data:
530 self.lines = data.splitlines(True)
531 if self.mode is None:
532 self.mode = mode
533 if self.lines:
534 # Normalize line endings
535 if self.lines[0].endswith('\r\n'):
536 self.eol = '\r\n'
537 elif self.lines[0].endswith('\n'):
538 self.eol = '\n'
539 if eolmode != 'strict':
540 nlines = []
541 for l in self.lines:
542 if l.endswith('\r\n'):
543 l = l[:-2] + '\n'
544 nlines.append(l)
545 self.lines = nlines
546 except IOError:
547 if create:
548 self.missing = False
549 if self.mode is None:
550 self.mode = (False, False)
551 if self.missing:
552 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
546 553
547 554 self.hash = {}
548 555 self.dirty = 0
549 556 self.offset = 0
550 557 self.skew = 0
551 558 self.rej = []
552 559 self.fileprinted = False
553 560 self.printfile(False)
554 561 self.hunks = 0
555 562
556 563 def writelines(self, fname, lines, mode):
557 564 if self.eolmode == 'auto':
558 565 eol = self.eol
559 566 elif self.eolmode == 'crlf':
560 567 eol = '\r\n'
561 568 else:
562 569 eol = '\n'
563 570
564 571 if self.eolmode != 'strict' and eol and eol != '\n':
565 572 rawlines = []
566 573 for l in lines:
567 574 if l and l[-1] == '\n':
568 575 l = l[:-1] + eol
569 576 rawlines.append(l)
570 577 lines = rawlines
571 578
572 self.backend.setfile(fname, ''.join(lines), mode)
579 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
573 580
574 581 def printfile(self, warn):
575 582 if self.fileprinted:
576 583 return
577 584 if warn or self.ui.verbose:
578 585 self.fileprinted = True
579 586 s = _("patching file %s\n") % self.fname
580 587 if warn:
581 588 self.ui.warn(s)
582 589 else:
583 590 self.ui.note(s)
584 591
585 592
586 593 def findlines(self, l, linenum):
587 594 # looks through the hash and finds candidate lines. The
588 595 # result is a list of line numbers sorted based on distance
589 596 # from linenum
590 597
591 598 cand = self.hash.get(l, [])
592 599 if len(cand) > 1:
593 600 # resort our list of potentials forward then back.
594 601 cand.sort(key=lambda x: abs(x - linenum))
595 602 return cand
596 603
597 604 def write_rej(self):
598 605 # our rejects are a little different from patch(1). This always
599 606 # creates rejects in the same form as the original patch. A file
600 607 # header is inserted so that you can run the reject through patch again
601 608 # without having to type the filename.
602 609 if not self.rej:
603 610 return
604 611 base = os.path.basename(self.fname)
605 612 lines = ["--- %s\n+++ %s\n" % (base, base)]
606 613 for x in self.rej:
607 614 for l in x.hunk:
608 615 lines.append(l)
609 616 if l[-1] != '\n':
610 617 lines.append("\n\ No newline at end of file\n")
611 618 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
612 619
613 620 def apply(self, h):
614 621 if not h.complete():
615 622 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
616 623 (h.number, h.desc, len(h.a), h.lena, len(h.b),
617 624 h.lenb))
618 625
619 626 self.hunks += 1
620 627
621 628 if self.missing:
622 629 self.rej.append(h)
623 630 return -1
624 631
625 632 if self.exists and self.create:
626 self.ui.warn(_("file %s already exists\n") % self.fname)
633 if self.copysource:
634 self.ui.warn(_("cannot create %s: destination already "
635 "exists\n" % self.fname))
636 else:
637 self.ui.warn(_("file %s already exists\n") % self.fname)
627 638 self.rej.append(h)
628 639 return -1
629 640
630 641 if isinstance(h, binhunk):
631 642 if self.remove:
632 643 self.backend.unlink(self.fname)
633 644 else:
634 645 self.lines[:] = h.new()
635 646 self.offset += len(h.new())
636 647 self.dirty = True
637 648 return 0
638 649
639 650 horig = h
640 651 if (self.eolmode in ('crlf', 'lf')
641 652 or self.eolmode == 'auto' and self.eol):
642 653 # If new eols are going to be normalized, then normalize
643 654 # hunk data before patching. Otherwise, preserve input
644 655 # line-endings.
645 656 h = h.getnormalized()
646 657
647 658 # fast case first, no offsets, no fuzz
648 659 old = h.old()
649 660 # patch starts counting at 1 unless we are adding the file
650 661 if h.starta == 0:
651 662 start = 0
652 663 else:
653 664 start = h.starta + self.offset - 1
654 665 orig_start = start
655 666 # if there's skew we want to emit the "(offset %d lines)" even
656 667 # when the hunk cleanly applies at start + skew, so skip the
657 668 # fast case code
658 669 if self.skew == 0 and diffhelpers.testhunk(old, self.lines, start) == 0:
659 670 if self.remove:
660 671 self.backend.unlink(self.fname)
661 672 else:
662 673 self.lines[start : start + h.lena] = h.new()
663 674 self.offset += h.lenb - h.lena
664 675 self.dirty = True
665 676 return 0
666 677
667 678 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
668 679 self.hash = {}
669 680 for x, s in enumerate(self.lines):
670 681 self.hash.setdefault(s, []).append(x)
671 682 if h.hunk[-1][0] != ' ':
672 683 # if the hunk tried to put something at the bottom of the file
673 684 # override the start line and use eof here
674 685 search_start = len(self.lines)
675 686 else:
676 687 search_start = orig_start + self.skew
677 688
678 689 for fuzzlen in xrange(3):
679 690 for toponly in [True, False]:
680 691 old = h.old(fuzzlen, toponly)
681 692
682 693 cand = self.findlines(old[0][1:], search_start)
683 694 for l in cand:
684 695 if diffhelpers.testhunk(old, self.lines, l) == 0:
685 696 newlines = h.new(fuzzlen, toponly)
686 697 self.lines[l : l + len(old)] = newlines
687 698 self.offset += len(newlines) - len(old)
688 699 self.skew = l - orig_start
689 700 self.dirty = True
690 701 offset = l - orig_start - fuzzlen
691 702 if fuzzlen:
692 703 msg = _("Hunk #%d succeeded at %d "
693 704 "with fuzz %d "
694 705 "(offset %d lines).\n")
695 706 self.printfile(True)
696 707 self.ui.warn(msg %
697 708 (h.number, l + 1, fuzzlen, offset))
698 709 else:
699 710 msg = _("Hunk #%d succeeded at %d "
700 711 "(offset %d lines).\n")
701 712 self.ui.note(msg % (h.number, l + 1, offset))
702 713 return fuzzlen
703 714 self.printfile(True)
704 715 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
705 716 self.rej.append(horig)
706 717 return -1
707 718
708 719 def close(self):
709 720 if self.dirty:
710 721 self.writelines(self.fname, self.lines, self.mode)
711 722 self.write_rej()
712 723 return len(self.rej)
713 724
714 725 class hunk(object):
715 726 def __init__(self, desc, num, lr, context):
716 727 self.number = num
717 728 self.desc = desc
718 729 self.hunk = [desc]
719 730 self.a = []
720 731 self.b = []
721 732 self.starta = self.lena = None
722 733 self.startb = self.lenb = None
723 734 if lr is not None:
724 735 if context:
725 736 self.read_context_hunk(lr)
726 737 else:
727 738 self.read_unified_hunk(lr)
728 739
729 740 def getnormalized(self):
730 741 """Return a copy with line endings normalized to LF."""
731 742
732 743 def normalize(lines):
733 744 nlines = []
734 745 for line in lines:
735 746 if line.endswith('\r\n'):
736 747 line = line[:-2] + '\n'
737 748 nlines.append(line)
738 749 return nlines
739 750
740 751 # Dummy object, it is rebuilt manually
741 752 nh = hunk(self.desc, self.number, None, None)
742 753 nh.number = self.number
743 754 nh.desc = self.desc
744 755 nh.hunk = self.hunk
745 756 nh.a = normalize(self.a)
746 757 nh.b = normalize(self.b)
747 758 nh.starta = self.starta
748 759 nh.startb = self.startb
749 760 nh.lena = self.lena
750 761 nh.lenb = self.lenb
751 762 return nh
752 763
753 764 def read_unified_hunk(self, lr):
754 765 m = unidesc.match(self.desc)
755 766 if not m:
756 767 raise PatchError(_("bad hunk #%d") % self.number)
757 768 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
758 769 if self.lena is None:
759 770 self.lena = 1
760 771 else:
761 772 self.lena = int(self.lena)
762 773 if self.lenb is None:
763 774 self.lenb = 1
764 775 else:
765 776 self.lenb = int(self.lenb)
766 777 self.starta = int(self.starta)
767 778 self.startb = int(self.startb)
768 779 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
769 780 # if we hit eof before finishing out the hunk, the last line will
770 781 # be zero length. Lets try to fix it up.
771 782 while len(self.hunk[-1]) == 0:
772 783 del self.hunk[-1]
773 784 del self.a[-1]
774 785 del self.b[-1]
775 786 self.lena -= 1
776 787 self.lenb -= 1
777 788 self._fixnewline(lr)
778 789
779 790 def read_context_hunk(self, lr):
780 791 self.desc = lr.readline()
781 792 m = contextdesc.match(self.desc)
782 793 if not m:
783 794 raise PatchError(_("bad hunk #%d") % self.number)
784 795 foo, self.starta, foo2, aend, foo3 = m.groups()
785 796 self.starta = int(self.starta)
786 797 if aend is None:
787 798 aend = self.starta
788 799 self.lena = int(aend) - self.starta
789 800 if self.starta:
790 801 self.lena += 1
791 802 for x in xrange(self.lena):
792 803 l = lr.readline()
793 804 if l.startswith('---'):
794 805 # lines addition, old block is empty
795 806 lr.push(l)
796 807 break
797 808 s = l[2:]
798 809 if l.startswith('- ') or l.startswith('! '):
799 810 u = '-' + s
800 811 elif l.startswith(' '):
801 812 u = ' ' + s
802 813 else:
803 814 raise PatchError(_("bad hunk #%d old text line %d") %
804 815 (self.number, x))
805 816 self.a.append(u)
806 817 self.hunk.append(u)
807 818
808 819 l = lr.readline()
809 820 if l.startswith('\ '):
810 821 s = self.a[-1][:-1]
811 822 self.a[-1] = s
812 823 self.hunk[-1] = s
813 824 l = lr.readline()
814 825 m = contextdesc.match(l)
815 826 if not m:
816 827 raise PatchError(_("bad hunk #%d") % self.number)
817 828 foo, self.startb, foo2, bend, foo3 = m.groups()
818 829 self.startb = int(self.startb)
819 830 if bend is None:
820 831 bend = self.startb
821 832 self.lenb = int(bend) - self.startb
822 833 if self.startb:
823 834 self.lenb += 1
824 835 hunki = 1
825 836 for x in xrange(self.lenb):
826 837 l = lr.readline()
827 838 if l.startswith('\ '):
828 839 # XXX: the only way to hit this is with an invalid line range.
829 840 # The no-eol marker is not counted in the line range, but I
830 841 # guess there are diff(1) out there which behave differently.
831 842 s = self.b[-1][:-1]
832 843 self.b[-1] = s
833 844 self.hunk[hunki - 1] = s
834 845 continue
835 846 if not l:
836 847 # line deletions, new block is empty and we hit EOF
837 848 lr.push(l)
838 849 break
839 850 s = l[2:]
840 851 if l.startswith('+ ') or l.startswith('! '):
841 852 u = '+' + s
842 853 elif l.startswith(' '):
843 854 u = ' ' + s
844 855 elif len(self.b) == 0:
845 856 # line deletions, new block is empty
846 857 lr.push(l)
847 858 break
848 859 else:
849 860 raise PatchError(_("bad hunk #%d old text line %d") %
850 861 (self.number, x))
851 862 self.b.append(s)
852 863 while True:
853 864 if hunki >= len(self.hunk):
854 865 h = ""
855 866 else:
856 867 h = self.hunk[hunki]
857 868 hunki += 1
858 869 if h == u:
859 870 break
860 871 elif h.startswith('-'):
861 872 continue
862 873 else:
863 874 self.hunk.insert(hunki - 1, u)
864 875 break
865 876
866 877 if not self.a:
867 878 # this happens when lines were only added to the hunk
868 879 for x in self.hunk:
869 880 if x.startswith('-') or x.startswith(' '):
870 881 self.a.append(x)
871 882 if not self.b:
872 883 # this happens when lines were only deleted from the hunk
873 884 for x in self.hunk:
874 885 if x.startswith('+') or x.startswith(' '):
875 886 self.b.append(x[1:])
876 887 # @@ -start,len +start,len @@
877 888 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
878 889 self.startb, self.lenb)
879 890 self.hunk[0] = self.desc
880 891 self._fixnewline(lr)
881 892
882 893 def _fixnewline(self, lr):
883 894 l = lr.readline()
884 895 if l.startswith('\ '):
885 896 diffhelpers.fix_newline(self.hunk, self.a, self.b)
886 897 else:
887 898 lr.push(l)
888 899
889 900 def complete(self):
890 901 return len(self.a) == self.lena and len(self.b) == self.lenb
891 902
892 903 def fuzzit(self, l, fuzz, toponly):
893 904 # this removes context lines from the top and bottom of list 'l'. It
894 905 # checks the hunk to make sure only context lines are removed, and then
895 906 # returns a new shortened list of lines.
896 907 fuzz = min(fuzz, len(l)-1)
897 908 if fuzz:
898 909 top = 0
899 910 bot = 0
900 911 hlen = len(self.hunk)
901 912 for x in xrange(hlen - 1):
902 913 # the hunk starts with the @@ line, so use x+1
903 914 if self.hunk[x + 1][0] == ' ':
904 915 top += 1
905 916 else:
906 917 break
907 918 if not toponly:
908 919 for x in xrange(hlen - 1):
909 920 if self.hunk[hlen - bot - 1][0] == ' ':
910 921 bot += 1
911 922 else:
912 923 break
913 924
914 925 # top and bot now count context in the hunk
915 926 # adjust them if either one is short
916 927 context = max(top, bot, 3)
917 928 if bot < context:
918 929 bot = max(0, fuzz - (context - bot))
919 930 else:
920 931 bot = min(fuzz, bot)
921 932 if top < context:
922 933 top = max(0, fuzz - (context - top))
923 934 else:
924 935 top = min(fuzz, top)
925 936
926 937 return l[top:len(l)-bot]
927 938 return l
928 939
929 940 def old(self, fuzz=0, toponly=False):
930 941 return self.fuzzit(self.a, fuzz, toponly)
931 942
932 943 def new(self, fuzz=0, toponly=False):
933 944 return self.fuzzit(self.b, fuzz, toponly)
934 945
935 946 class binhunk:
936 947 'A binary patch file. Only understands literals so far.'
937 948 def __init__(self, lr):
938 949 self.text = None
939 950 self.hunk = ['GIT binary patch\n']
940 951 self._read(lr)
941 952
942 953 def complete(self):
943 954 return self.text is not None
944 955
945 956 def new(self):
946 957 return [self.text]
947 958
948 959 def _read(self, lr):
949 960 line = lr.readline()
950 961 self.hunk.append(line)
951 962 while line and not line.startswith('literal '):
952 963 line = lr.readline()
953 964 self.hunk.append(line)
954 965 if not line:
955 966 raise PatchError(_('could not extract binary patch'))
956 967 size = int(line[8:].rstrip())
957 968 dec = []
958 969 line = lr.readline()
959 970 self.hunk.append(line)
960 971 while len(line) > 1:
961 972 l = line[0]
962 973 if l <= 'Z' and l >= 'A':
963 974 l = ord(l) - ord('A') + 1
964 975 else:
965 976 l = ord(l) - ord('a') + 27
966 977 dec.append(base85.b85decode(line[1:-1])[:l])
967 978 line = lr.readline()
968 979 self.hunk.append(line)
969 980 text = zlib.decompress(''.join(dec))
970 981 if len(text) != size:
971 982 raise PatchError(_('binary patch is %d bytes, not %d') %
972 983 len(text), size)
973 984 self.text = text
974 985
975 986 def parsefilename(str):
976 987 # --- filename \t|space stuff
977 988 s = str[4:].rstrip('\r\n')
978 989 i = s.find('\t')
979 990 if i < 0:
980 991 i = s.find(' ')
981 992 if i < 0:
982 993 return s
983 994 return s[:i]
984 995
985 996 def pathstrip(path, strip):
986 997 pathlen = len(path)
987 998 i = 0
988 999 if strip == 0:
989 1000 return '', path.rstrip()
990 1001 count = strip
991 1002 while count > 0:
992 1003 i = path.find('/', i)
993 1004 if i == -1:
994 1005 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
995 1006 (count, strip, path))
996 1007 i += 1
997 1008 # consume '//' in the path
998 1009 while i < pathlen - 1 and path[i] == '/':
999 1010 i += 1
1000 1011 count -= 1
1001 1012 return path[:i].lstrip(), path[i:].rstrip()
1002 1013
1003 1014 def selectfile(backend, afile_orig, bfile_orig, hunk, strip, gp):
1004 1015 if gp:
1005 1016 # Git patches do not play games. Excluding copies from the
1006 1017 # following heuristic avoids a lot of confusion
1007 1018 fname = pathstrip(gp.path, strip - 1)[1]
1008 create = gp.op == 'ADD'
1019 create = gp.op in ('ADD', 'COPY', 'RENAME')
1009 1020 remove = gp.op == 'DELETE'
1010 1021 missing = not create and not backend.exists(fname)
1011 return fname, missing, create, remove
1022 return fname, create, remove
1012 1023 nulla = afile_orig == "/dev/null"
1013 1024 nullb = bfile_orig == "/dev/null"
1014 1025 create = nulla and hunk.starta == 0 and hunk.lena == 0
1015 1026 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1016 1027 abase, afile = pathstrip(afile_orig, strip)
1017 1028 gooda = not nulla and backend.exists(afile)
1018 1029 bbase, bfile = pathstrip(bfile_orig, strip)
1019 1030 if afile == bfile:
1020 1031 goodb = gooda
1021 1032 else:
1022 1033 goodb = not nullb and backend.exists(bfile)
1023 1034 missing = not goodb and not gooda and not create
1024 1035
1025 1036 # some diff programs apparently produce patches where the afile is
1026 1037 # not /dev/null, but afile starts with bfile
1027 1038 abasedir = afile[:afile.rfind('/') + 1]
1028 1039 bbasedir = bfile[:bfile.rfind('/') + 1]
1029 1040 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1030 1041 and hunk.starta == 0 and hunk.lena == 0):
1031 1042 create = True
1032 1043 missing = False
1033 1044
1034 1045 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1035 1046 # diff is between a file and its backup. In this case, the original
1036 1047 # file should be patched (see original mpatch code).
1037 1048 isbackup = (abase == bbase and bfile.startswith(afile))
1038 1049 fname = None
1039 1050 if not missing:
1040 1051 if gooda and goodb:
1041 1052 fname = isbackup and afile or bfile
1042 1053 elif gooda:
1043 1054 fname = afile
1044 1055
1045 1056 if not fname:
1046 1057 if not nullb:
1047 1058 fname = isbackup and afile or bfile
1048 1059 elif not nulla:
1049 1060 fname = afile
1050 1061 else:
1051 1062 raise PatchError(_("undefined source and destination files"))
1052 1063
1053 return fname, missing, create, remove
1064 return fname, create, remove
1054 1065
1055 1066 def scangitpatch(lr, firstline):
1056 1067 """
1057 1068 Git patches can emit:
1058 1069 - rename a to b
1059 1070 - change b
1060 1071 - copy a to c
1061 1072 - change c
1062 1073
1063 1074 We cannot apply this sequence as-is, the renamed 'a' could not be
1064 1075 found for it would have been renamed already. And we cannot copy
1065 1076 from 'b' instead because 'b' would have been changed already. So
1066 1077 we scan the git patch for copy and rename commands so we can
1067 1078 perform the copies ahead of time.
1068 1079 """
1069 1080 pos = 0
1070 1081 try:
1071 1082 pos = lr.fp.tell()
1072 1083 fp = lr.fp
1073 1084 except IOError:
1074 1085 fp = cStringIO.StringIO(lr.fp.read())
1075 1086 gitlr = linereader(fp)
1076 1087 gitlr.push(firstline)
1077 1088 gitpatches = readgitpatch(gitlr)
1078 1089 fp.seek(pos)
1079 1090 return gitpatches
1080 1091
1081 1092 def iterhunks(fp):
1082 1093 """Read a patch and yield the following events:
1083 1094 - ("file", afile, bfile, firsthunk): select a new target file.
1084 1095 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1085 1096 "file" event.
1086 1097 - ("git", gitchanges): current diff is in git format, gitchanges
1087 1098 maps filenames to gitpatch records. Unique event.
1088 1099 """
1089 1100 afile = ""
1090 1101 bfile = ""
1091 1102 state = None
1092 1103 hunknum = 0
1093 1104 emitfile = newfile = False
1094 1105 gitpatches = None
1095 1106
1096 1107 # our states
1097 1108 BFILE = 1
1098 1109 context = None
1099 1110 lr = linereader(fp)
1100 1111
1101 1112 while True:
1102 1113 x = lr.readline()
1103 1114 if not x:
1104 1115 break
1105 1116 if state == BFILE and (
1106 1117 (not context and x[0] == '@')
1107 1118 or (context is not False and x.startswith('***************'))
1108 1119 or x.startswith('GIT binary patch')):
1109 1120 gp = None
1110 1121 if gitpatches and gitpatches[-1][0] == bfile:
1111 1122 gp = gitpatches.pop()[1]
1112 1123 if x.startswith('GIT binary patch'):
1113 1124 h = binhunk(lr)
1114 1125 else:
1115 1126 if context is None and x.startswith('***************'):
1116 1127 context = True
1117 1128 h = hunk(x, hunknum + 1, lr, context)
1118 1129 hunknum += 1
1119 1130 if emitfile:
1120 1131 emitfile = False
1121 1132 yield 'file', (afile, bfile, h, gp)
1122 1133 yield 'hunk', h
1123 1134 elif x.startswith('diff --git'):
1124 1135 m = gitre.match(x)
1125 1136 if not m:
1126 1137 continue
1127 1138 if gitpatches is None:
1128 1139 # scan whole input for git metadata
1129 1140 gitpatches = [('b/' + gp.path, gp) for gp
1130 1141 in scangitpatch(lr, x)]
1131 1142 yield 'git', [g[1] for g in gitpatches
1132 1143 if g[1].op in ('COPY', 'RENAME')]
1133 1144 gitpatches.reverse()
1134 1145 afile = 'a/' + m.group(1)
1135 1146 bfile = 'b/' + m.group(2)
1136 1147 while bfile != gitpatches[-1][0]:
1137 1148 gp = gitpatches.pop()[1]
1138 1149 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp)
1139 1150 gp = gitpatches[-1][1]
1140 1151 # copy/rename + modify should modify target, not source
1141 1152 if gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD') or gp.mode:
1142 1153 afile = bfile
1143 1154 newfile = True
1144 1155 elif x.startswith('---'):
1145 1156 # check for a unified diff
1146 1157 l2 = lr.readline()
1147 1158 if not l2.startswith('+++'):
1148 1159 lr.push(l2)
1149 1160 continue
1150 1161 newfile = True
1151 1162 context = False
1152 1163 afile = parsefilename(x)
1153 1164 bfile = parsefilename(l2)
1154 1165 elif x.startswith('***'):
1155 1166 # check for a context diff
1156 1167 l2 = lr.readline()
1157 1168 if not l2.startswith('---'):
1158 1169 lr.push(l2)
1159 1170 continue
1160 1171 l3 = lr.readline()
1161 1172 lr.push(l3)
1162 1173 if not l3.startswith("***************"):
1163 1174 lr.push(l2)
1164 1175 continue
1165 1176 newfile = True
1166 1177 context = True
1167 1178 afile = parsefilename(x)
1168 1179 bfile = parsefilename(l2)
1169 1180
1170 1181 if newfile:
1171 1182 newfile = False
1172 1183 emitfile = True
1173 1184 state = BFILE
1174 1185 hunknum = 0
1175 1186
1176 1187 while gitpatches:
1177 1188 gp = gitpatches.pop()[1]
1178 1189 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp)
1179 1190
1180 def applydiff(ui, fp, changed, backend, strip=1, eolmode='strict'):
1191 def applydiff(ui, fp, changed, backend, store, strip=1, eolmode='strict'):
1181 1192 """Reads a patch from fp and tries to apply it.
1182 1193
1183 1194 The dict 'changed' is filled in with all of the filenames changed
1184 1195 by the patch. Returns 0 for a clean patch, -1 if any rejects were
1185 1196 found and 1 if there was any fuzz.
1186 1197
1187 1198 If 'eolmode' is 'strict', the patch content and patched file are
1188 1199 read in binary mode. Otherwise, line endings are ignored when
1189 1200 patching then normalized according to 'eolmode'.
1190 1201 """
1191 return _applydiff(ui, fp, patchfile, backend, changed, strip=strip,
1202 return _applydiff(ui, fp, patchfile, backend, store, changed, strip=strip,
1192 1203 eolmode=eolmode)
1193 1204
1194 def _applydiff(ui, fp, patcher, backend, changed, strip=1, eolmode='strict'):
1205 def _applydiff(ui, fp, patcher, backend, store, changed, strip=1,
1206 eolmode='strict'):
1195 1207
1196 1208 def pstrip(p):
1197 1209 return pathstrip(p, strip - 1)[1]
1198 1210
1199 1211 rejects = 0
1200 1212 err = 0
1201 1213 current_file = None
1202 1214
1203 1215 for state, values in iterhunks(fp):
1204 1216 if state == 'hunk':
1205 1217 if not current_file:
1206 1218 continue
1207 1219 ret = current_file.apply(values)
1208 1220 if ret >= 0:
1209 1221 changed.setdefault(current_file.fname, None)
1210 1222 if ret > 0:
1211 1223 err = 1
1212 1224 elif state == 'file':
1213 1225 if current_file:
1214 1226 rejects += current_file.close()
1215 1227 current_file = None
1216 1228 afile, bfile, first_hunk, gp = values
1229 copysource = None
1217 1230 if gp:
1218 1231 path = pstrip(gp.path)
1232 if gp.oldpath:
1233 copysource = pstrip(gp.oldpath)
1219 1234 changed[path] = gp
1220 1235 if gp.op == 'DELETE':
1221 1236 backend.unlink(path)
1222 1237 continue
1223 1238 if gp.op == 'RENAME':
1224 backend.unlink(pstrip(gp.oldpath))
1225 if gp.mode and not first_hunk:
1226 data = None
1227 if gp.op == 'ADD':
1228 # Added files without content have no hunk and
1229 # must be created
1230 data = ''
1231 backend.setfile(path, data, gp.mode)
1239 backend.unlink(copysource)
1240 if not first_hunk:
1241 data, mode = None, None
1242 if gp.op in ('RENAME', 'COPY'):
1243 data, mode = store.getfile(copysource)
1244 if gp.mode:
1245 mode = gp.mode
1246 if gp.op == 'ADD':
1247 # Added files without content have no hunk and
1248 # must be created
1249 data = ''
1250 if data or mode:
1251 if (gp.op in ('ADD', 'RENAME', 'COPY')
1252 and backend.exists(path)):
1253 raise PatchError(_("cannot create %s: destination "
1254 "already exists") % path)
1255 backend.setfile(path, data, mode, copysource)
1232 1256 if not first_hunk:
1233 1257 continue
1234 1258 try:
1235 1259 mode = gp and gp.mode or None
1236 current_file, missing, create, remove = selectfile(
1260 current_file, create, remove = selectfile(
1237 1261 backend, afile, bfile, first_hunk, strip, gp)
1238 current_file = patcher(ui, current_file, backend, mode,
1239 create, remove, missing=missing,
1240 eolmode=eolmode)
1262 current_file = patcher(ui, current_file, backend, store, mode,
1263 create, remove, eolmode=eolmode,
1264 copysource=copysource)
1241 1265 except PatchError, inst:
1242 1266 ui.warn(str(inst) + '\n')
1243 1267 current_file = None
1244 1268 rejects += 1
1245 1269 continue
1246 1270 elif state == 'git':
1247 1271 for gp in values:
1248 backend.copy(pstrip(gp.oldpath), pstrip(gp.path))
1272 path = pstrip(gp.oldpath)
1273 data, mode = backend.getfile(path)
1274 store.setfile(path, data, mode)
1249 1275 else:
1250 1276 raise util.Abort(_('unsupported parser state: %s') % state)
1251 1277
1252 1278 if current_file:
1253 1279 rejects += current_file.close()
1254 1280
1255 1281 if rejects:
1256 1282 return -1
1257 1283 return err
1258 1284
1259 1285 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1260 1286 similarity):
1261 1287 """use <patcher> to apply <patchname> to the working directory.
1262 1288 returns whether patch was applied with fuzz factor."""
1263 1289
1264 1290 fuzz = False
1265 1291 args = []
1266 1292 cwd = repo.root
1267 1293 if cwd:
1268 1294 args.append('-d %s' % util.shellquote(cwd))
1269 1295 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1270 1296 util.shellquote(patchname)))
1271 1297 try:
1272 1298 for line in fp:
1273 1299 line = line.rstrip()
1274 1300 ui.note(line + '\n')
1275 1301 if line.startswith('patching file '):
1276 1302 pf = util.parsepatchoutput(line)
1277 1303 printed_file = False
1278 1304 files.setdefault(pf, None)
1279 1305 elif line.find('with fuzz') >= 0:
1280 1306 fuzz = True
1281 1307 if not printed_file:
1282 1308 ui.warn(pf + '\n')
1283 1309 printed_file = True
1284 1310 ui.warn(line + '\n')
1285 1311 elif line.find('saving rejects to file') >= 0:
1286 1312 ui.warn(line + '\n')
1287 1313 elif line.find('FAILED') >= 0:
1288 1314 if not printed_file:
1289 1315 ui.warn(pf + '\n')
1290 1316 printed_file = True
1291 1317 ui.warn(line + '\n')
1292 1318 finally:
1293 1319 if files:
1294 1320 cfiles = list(files)
1295 1321 cwd = repo.getcwd()
1296 1322 if cwd:
1297 1323 cfiles = [util.pathto(repo.root, cwd, f)
1298 1324 for f in cfile]
1299 1325 scmutil.addremove(repo, cfiles, similarity=similarity)
1300 1326 code = fp.close()
1301 1327 if code:
1302 1328 raise PatchError(_("patch command failed: %s") %
1303 1329 util.explainexit(code)[0])
1304 1330 return fuzz
1305 1331
1306 1332 def internalpatch(ui, repo, patchobj, strip, files=None, eolmode='strict',
1307 1333 similarity=0):
1308 1334 """use builtin patch to apply <patchobj> to the working directory.
1309 1335 returns whether patch was applied with fuzz factor."""
1310 1336
1311 1337 if files is None:
1312 1338 files = {}
1313 1339 if eolmode is None:
1314 1340 eolmode = ui.config('patch', 'eol', 'strict')
1315 1341 if eolmode.lower() not in eolmodes:
1316 1342 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1317 1343 eolmode = eolmode.lower()
1318 1344
1345 store = filestore()
1319 1346 backend = workingbackend(ui, repo, similarity)
1320 1347 try:
1321 1348 fp = open(patchobj, 'rb')
1322 1349 except TypeError:
1323 1350 fp = patchobj
1324 1351 try:
1325 ret = applydiff(ui, fp, files, backend, strip=strip, eolmode=eolmode)
1352 ret = applydiff(ui, fp, files, backend, store, strip=strip,
1353 eolmode=eolmode)
1326 1354 finally:
1327 1355 if fp != patchobj:
1328 1356 fp.close()
1329 1357 files.update(dict.fromkeys(backend.close()))
1358 store.close()
1330 1359 if ret < 0:
1331 1360 raise PatchError(_('patch failed to apply'))
1332 1361 return ret > 0
1333 1362
1334 1363 def patch(ui, repo, patchname, strip=1, files=None, eolmode='strict',
1335 1364 similarity=0):
1336 1365 """Apply <patchname> to the working directory.
1337 1366
1338 1367 'eolmode' specifies how end of lines should be handled. It can be:
1339 1368 - 'strict': inputs are read in binary mode, EOLs are preserved
1340 1369 - 'crlf': EOLs are ignored when patching and reset to CRLF
1341 1370 - 'lf': EOLs are ignored when patching and reset to LF
1342 1371 - None: get it from user settings, default to 'strict'
1343 1372 'eolmode' is ignored when using an external patcher program.
1344 1373
1345 1374 Returns whether patch was applied with fuzz factor.
1346 1375 """
1347 1376 patcher = ui.config('ui', 'patch')
1348 1377 if files is None:
1349 1378 files = {}
1350 1379 try:
1351 1380 if patcher:
1352 1381 return _externalpatch(ui, repo, patcher, patchname, strip,
1353 1382 files, similarity)
1354 1383 return internalpatch(ui, repo, patchname, strip, files, eolmode,
1355 1384 similarity)
1356 1385 except PatchError, err:
1357 1386 raise util.Abort(str(err))
1358 1387
1359 1388 def changedfiles(ui, repo, patchpath, strip=1):
1360 1389 backend = fsbackend(ui, repo.root)
1361 1390 fp = open(patchpath, 'rb')
1362 1391 try:
1363 1392 changed = set()
1364 1393 for state, values in iterhunks(fp):
1365 1394 if state == 'file':
1366 1395 afile, bfile, first_hunk, gp = values
1367 1396 if gp:
1368 1397 changed.add(pathstrip(gp.path, strip - 1)[1])
1369 1398 if gp.op == 'RENAME':
1370 1399 changed.add(pathstrip(gp.oldpath, strip - 1)[1])
1371 1400 if not first_hunk:
1372 1401 continue
1373 current_file, missing, create, remove = selectfile(
1402 current_file, create, remove = selectfile(
1374 1403 backend, afile, bfile, first_hunk, strip, gp)
1375 1404 changed.add(current_file)
1376 1405 elif state not in ('hunk', 'git'):
1377 1406 raise util.Abort(_('unsupported parser state: %s') % state)
1378 1407 return changed
1379 1408 finally:
1380 1409 fp.close()
1381 1410
1382 1411 def b85diff(to, tn):
1383 1412 '''print base85-encoded binary diff'''
1384 1413 def gitindex(text):
1385 1414 if not text:
1386 1415 return hex(nullid)
1387 1416 l = len(text)
1388 1417 s = util.sha1('blob %d\0' % l)
1389 1418 s.update(text)
1390 1419 return s.hexdigest()
1391 1420
1392 1421 def fmtline(line):
1393 1422 l = len(line)
1394 1423 if l <= 26:
1395 1424 l = chr(ord('A') + l - 1)
1396 1425 else:
1397 1426 l = chr(l - 26 + ord('a') - 1)
1398 1427 return '%c%s\n' % (l, base85.b85encode(line, True))
1399 1428
1400 1429 def chunk(text, csize=52):
1401 1430 l = len(text)
1402 1431 i = 0
1403 1432 while i < l:
1404 1433 yield text[i:i + csize]
1405 1434 i += csize
1406 1435
1407 1436 tohash = gitindex(to)
1408 1437 tnhash = gitindex(tn)
1409 1438 if tohash == tnhash:
1410 1439 return ""
1411 1440
1412 1441 # TODO: deltas
1413 1442 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1414 1443 (tohash, tnhash, len(tn))]
1415 1444 for l in chunk(zlib.compress(tn)):
1416 1445 ret.append(fmtline(l))
1417 1446 ret.append('\n')
1418 1447 return ''.join(ret)
1419 1448
1420 1449 class GitDiffRequired(Exception):
1421 1450 pass
1422 1451
1423 1452 def diffopts(ui, opts=None, untrusted=False):
1424 1453 def get(key, name=None, getter=ui.configbool):
1425 1454 return ((opts and opts.get(key)) or
1426 1455 getter('diff', name or key, None, untrusted=untrusted))
1427 1456 return mdiff.diffopts(
1428 1457 text=opts and opts.get('text'),
1429 1458 git=get('git'),
1430 1459 nodates=get('nodates'),
1431 1460 showfunc=get('show_function', 'showfunc'),
1432 1461 ignorews=get('ignore_all_space', 'ignorews'),
1433 1462 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1434 1463 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1435 1464 context=get('unified', getter=ui.config))
1436 1465
1437 1466 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
1438 1467 losedatafn=None, prefix=''):
1439 1468 '''yields diff of changes to files between two nodes, or node and
1440 1469 working directory.
1441 1470
1442 1471 if node1 is None, use first dirstate parent instead.
1443 1472 if node2 is None, compare node1 with working directory.
1444 1473
1445 1474 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
1446 1475 every time some change cannot be represented with the current
1447 1476 patch format. Return False to upgrade to git patch format, True to
1448 1477 accept the loss or raise an exception to abort the diff. It is
1449 1478 called with the name of current file being diffed as 'fn'. If set
1450 1479 to None, patches will always be upgraded to git format when
1451 1480 necessary.
1452 1481
1453 1482 prefix is a filename prefix that is prepended to all filenames on
1454 1483 display (used for subrepos).
1455 1484 '''
1456 1485
1457 1486 if opts is None:
1458 1487 opts = mdiff.defaultopts
1459 1488
1460 1489 if not node1 and not node2:
1461 1490 node1 = repo.dirstate.p1()
1462 1491
1463 1492 def lrugetfilectx():
1464 1493 cache = {}
1465 1494 order = []
1466 1495 def getfilectx(f, ctx):
1467 1496 fctx = ctx.filectx(f, filelog=cache.get(f))
1468 1497 if f not in cache:
1469 1498 if len(cache) > 20:
1470 1499 del cache[order.pop(0)]
1471 1500 cache[f] = fctx.filelog()
1472 1501 else:
1473 1502 order.remove(f)
1474 1503 order.append(f)
1475 1504 return fctx
1476 1505 return getfilectx
1477 1506 getfilectx = lrugetfilectx()
1478 1507
1479 1508 ctx1 = repo[node1]
1480 1509 ctx2 = repo[node2]
1481 1510
1482 1511 if not changes:
1483 1512 changes = repo.status(ctx1, ctx2, match=match)
1484 1513 modified, added, removed = changes[:3]
1485 1514
1486 1515 if not modified and not added and not removed:
1487 1516 return []
1488 1517
1489 1518 revs = None
1490 1519 if not repo.ui.quiet:
1491 1520 hexfunc = repo.ui.debugflag and hex or short
1492 1521 revs = [hexfunc(node) for node in [node1, node2] if node]
1493 1522
1494 1523 copy = {}
1495 1524 if opts.git or opts.upgrade:
1496 1525 copy = copies.copies(repo, ctx1, ctx2, repo[nullid])[0]
1497 1526
1498 1527 difffn = lambda opts, losedata: trydiff(repo, revs, ctx1, ctx2,
1499 1528 modified, added, removed, copy, getfilectx, opts, losedata, prefix)
1500 1529 if opts.upgrade and not opts.git:
1501 1530 try:
1502 1531 def losedata(fn):
1503 1532 if not losedatafn or not losedatafn(fn=fn):
1504 1533 raise GitDiffRequired()
1505 1534 # Buffer the whole output until we are sure it can be generated
1506 1535 return list(difffn(opts.copy(git=False), losedata))
1507 1536 except GitDiffRequired:
1508 1537 return difffn(opts.copy(git=True), None)
1509 1538 else:
1510 1539 return difffn(opts, None)
1511 1540
1512 1541 def difflabel(func, *args, **kw):
1513 1542 '''yields 2-tuples of (output, label) based on the output of func()'''
1514 1543 prefixes = [('diff', 'diff.diffline'),
1515 1544 ('copy', 'diff.extended'),
1516 1545 ('rename', 'diff.extended'),
1517 1546 ('old', 'diff.extended'),
1518 1547 ('new', 'diff.extended'),
1519 1548 ('deleted', 'diff.extended'),
1520 1549 ('---', 'diff.file_a'),
1521 1550 ('+++', 'diff.file_b'),
1522 1551 ('@@', 'diff.hunk'),
1523 1552 ('-', 'diff.deleted'),
1524 1553 ('+', 'diff.inserted')]
1525 1554
1526 1555 for chunk in func(*args, **kw):
1527 1556 lines = chunk.split('\n')
1528 1557 for i, line in enumerate(lines):
1529 1558 if i != 0:
1530 1559 yield ('\n', '')
1531 1560 stripline = line
1532 1561 if line and line[0] in '+-':
1533 1562 # highlight trailing whitespace, but only in changed lines
1534 1563 stripline = line.rstrip()
1535 1564 for prefix, label in prefixes:
1536 1565 if stripline.startswith(prefix):
1537 1566 yield (stripline, label)
1538 1567 break
1539 1568 else:
1540 1569 yield (line, '')
1541 1570 if line != stripline:
1542 1571 yield (line[len(stripline):], 'diff.trailingwhitespace')
1543 1572
1544 1573 def diffui(*args, **kw):
1545 1574 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
1546 1575 return difflabel(diff, *args, **kw)
1547 1576
1548 1577
1549 1578 def _addmodehdr(header, omode, nmode):
1550 1579 if omode != nmode:
1551 1580 header.append('old mode %s\n' % omode)
1552 1581 header.append('new mode %s\n' % nmode)
1553 1582
1554 1583 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
1555 1584 copy, getfilectx, opts, losedatafn, prefix):
1556 1585
1557 1586 def join(f):
1558 1587 return os.path.join(prefix, f)
1559 1588
1560 1589 date1 = util.datestr(ctx1.date())
1561 1590 man1 = ctx1.manifest()
1562 1591
1563 1592 gone = set()
1564 1593 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1565 1594
1566 1595 copyto = dict([(v, k) for k, v in copy.items()])
1567 1596
1568 1597 if opts.git:
1569 1598 revs = None
1570 1599
1571 1600 for f in sorted(modified + added + removed):
1572 1601 to = None
1573 1602 tn = None
1574 1603 dodiff = True
1575 1604 header = []
1576 1605 if f in man1:
1577 1606 to = getfilectx(f, ctx1).data()
1578 1607 if f not in removed:
1579 1608 tn = getfilectx(f, ctx2).data()
1580 1609 a, b = f, f
1581 1610 if opts.git or losedatafn:
1582 1611 if f in added:
1583 1612 mode = gitmode[ctx2.flags(f)]
1584 1613 if f in copy or f in copyto:
1585 1614 if opts.git:
1586 1615 if f in copy:
1587 1616 a = copy[f]
1588 1617 else:
1589 1618 a = copyto[f]
1590 1619 omode = gitmode[man1.flags(a)]
1591 1620 _addmodehdr(header, omode, mode)
1592 1621 if a in removed and a not in gone:
1593 1622 op = 'rename'
1594 1623 gone.add(a)
1595 1624 else:
1596 1625 op = 'copy'
1597 1626 header.append('%s from %s\n' % (op, join(a)))
1598 1627 header.append('%s to %s\n' % (op, join(f)))
1599 1628 to = getfilectx(a, ctx1).data()
1600 1629 else:
1601 1630 losedatafn(f)
1602 1631 else:
1603 1632 if opts.git:
1604 1633 header.append('new file mode %s\n' % mode)
1605 1634 elif ctx2.flags(f):
1606 1635 losedatafn(f)
1607 1636 # In theory, if tn was copied or renamed we should check
1608 1637 # if the source is binary too but the copy record already
1609 1638 # forces git mode.
1610 1639 if util.binary(tn):
1611 1640 if opts.git:
1612 1641 dodiff = 'binary'
1613 1642 else:
1614 1643 losedatafn(f)
1615 1644 if not opts.git and not tn:
1616 1645 # regular diffs cannot represent new empty file
1617 1646 losedatafn(f)
1618 1647 elif f in removed:
1619 1648 if opts.git:
1620 1649 # have we already reported a copy above?
1621 1650 if ((f in copy and copy[f] in added
1622 1651 and copyto[copy[f]] == f) or
1623 1652 (f in copyto and copyto[f] in added
1624 1653 and copy[copyto[f]] == f)):
1625 1654 dodiff = False
1626 1655 else:
1627 1656 header.append('deleted file mode %s\n' %
1628 1657 gitmode[man1.flags(f)])
1629 1658 elif not to or util.binary(to):
1630 1659 # regular diffs cannot represent empty file deletion
1631 1660 losedatafn(f)
1632 1661 else:
1633 1662 oflag = man1.flags(f)
1634 1663 nflag = ctx2.flags(f)
1635 1664 binary = util.binary(to) or util.binary(tn)
1636 1665 if opts.git:
1637 1666 _addmodehdr(header, gitmode[oflag], gitmode[nflag])
1638 1667 if binary:
1639 1668 dodiff = 'binary'
1640 1669 elif binary or nflag != oflag:
1641 1670 losedatafn(f)
1642 1671 if opts.git:
1643 1672 header.insert(0, mdiff.diffline(revs, join(a), join(b), opts))
1644 1673
1645 1674 if dodiff:
1646 1675 if dodiff == 'binary':
1647 1676 text = b85diff(to, tn)
1648 1677 else:
1649 1678 text = mdiff.unidiff(to, date1,
1650 1679 # ctx2 date may be dynamic
1651 1680 tn, util.datestr(ctx2.date()),
1652 1681 join(a), join(b), revs, opts=opts)
1653 1682 if header and (text or len(header) > 1):
1654 1683 yield ''.join(header)
1655 1684 if text:
1656 1685 yield text
1657 1686
1658 1687 def diffstatsum(stats):
1659 1688 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
1660 1689 for f, a, r, b in stats:
1661 1690 maxfile = max(maxfile, encoding.colwidth(f))
1662 1691 maxtotal = max(maxtotal, a + r)
1663 1692 addtotal += a
1664 1693 removetotal += r
1665 1694 binary = binary or b
1666 1695
1667 1696 return maxfile, maxtotal, addtotal, removetotal, binary
1668 1697
1669 1698 def diffstatdata(lines):
1670 1699 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
1671 1700
1672 1701 results = []
1673 1702 filename, adds, removes = None, 0, 0
1674 1703
1675 1704 def addresult():
1676 1705 if filename:
1677 1706 isbinary = adds == 0 and removes == 0
1678 1707 results.append((filename, adds, removes, isbinary))
1679 1708
1680 1709 for line in lines:
1681 1710 if line.startswith('diff'):
1682 1711 addresult()
1683 1712 # set numbers to 0 anyway when starting new file
1684 1713 adds, removes = 0, 0
1685 1714 if line.startswith('diff --git'):
1686 1715 filename = gitre.search(line).group(1)
1687 1716 elif line.startswith('diff -r'):
1688 1717 # format: "diff -r ... -r ... filename"
1689 1718 filename = diffre.search(line).group(1)
1690 1719 elif line.startswith('+') and not line.startswith('+++'):
1691 1720 adds += 1
1692 1721 elif line.startswith('-') and not line.startswith('---'):
1693 1722 removes += 1
1694 1723 addresult()
1695 1724 return results
1696 1725
1697 1726 def diffstat(lines, width=80, git=False):
1698 1727 output = []
1699 1728 stats = diffstatdata(lines)
1700 1729 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
1701 1730
1702 1731 countwidth = len(str(maxtotal))
1703 1732 if hasbinary and countwidth < 3:
1704 1733 countwidth = 3
1705 1734 graphwidth = width - countwidth - maxname - 6
1706 1735 if graphwidth < 10:
1707 1736 graphwidth = 10
1708 1737
1709 1738 def scale(i):
1710 1739 if maxtotal <= graphwidth:
1711 1740 return i
1712 1741 # If diffstat runs out of room it doesn't print anything,
1713 1742 # which isn't very useful, so always print at least one + or -
1714 1743 # if there were at least some changes.
1715 1744 return max(i * graphwidth // maxtotal, int(bool(i)))
1716 1745
1717 1746 for filename, adds, removes, isbinary in stats:
1718 1747 if git and isbinary:
1719 1748 count = 'Bin'
1720 1749 else:
1721 1750 count = adds + removes
1722 1751 pluses = '+' * scale(adds)
1723 1752 minuses = '-' * scale(removes)
1724 1753 output.append(' %s%s | %*s %s%s\n' %
1725 1754 (filename, ' ' * (maxname - encoding.colwidth(filename)),
1726 1755 countwidth, count, pluses, minuses))
1727 1756
1728 1757 if stats:
1729 1758 output.append(_(' %d files changed, %d insertions(+), %d deletions(-)\n')
1730 1759 % (len(stats), totaladds, totalremoves))
1731 1760
1732 1761 return ''.join(output)
1733 1762
1734 1763 def diffstatui(*args, **kw):
1735 1764 '''like diffstat(), but yields 2-tuples of (output, label) for
1736 1765 ui.write()
1737 1766 '''
1738 1767
1739 1768 for line in diffstat(*args, **kw).splitlines():
1740 1769 if line and line[-1] in '+-':
1741 1770 name, graph = line.rsplit(' ', 1)
1742 1771 yield (name + ' ', '')
1743 1772 m = re.search(r'\++', graph)
1744 1773 if m:
1745 1774 yield (m.group(0), 'diffstat.inserted')
1746 1775 m = re.search(r'-+', graph)
1747 1776 if m:
1748 1777 yield (m.group(0), 'diffstat.deleted')
1749 1778 else:
1750 1779 yield (line, '')
1751 1780 yield ('\n', '')
@@ -1,443 +1,445 b''
1 1
2 2 $ hg init
3 3
4 4 New file:
5 5
6 6 $ hg import -d "1000000 0" -mnew - <<EOF
7 7 > diff --git a/new b/new
8 8 > new file mode 100644
9 9 > index 0000000..7898192
10 10 > --- /dev/null
11 11 > +++ b/new
12 12 > @@ -0,0 +1 @@
13 13 > +a
14 14 > EOF
15 15 applying patch from stdin
16 16
17 17 $ hg tip -q
18 18 0:ae3ee40d2079
19 19
20 20 New empty file:
21 21
22 22 $ hg import -d "1000000 0" -mempty - <<EOF
23 23 > diff --git a/empty b/empty
24 24 > new file mode 100644
25 25 > EOF
26 26 applying patch from stdin
27 27
28 28 $ hg tip -q
29 29 1:ab199dc869b5
30 30
31 31 $ hg locate empty
32 32 empty
33 33
34 34 chmod +x:
35 35
36 36 $ hg import -d "1000000 0" -msetx - <<EOF
37 37 > diff --git a/new b/new
38 38 > old mode 100644
39 39 > new mode 100755
40 40 > EOF
41 41 applying patch from stdin
42 42
43 43 $ hg tip -q
44 44 2:3a34410f282e
45 45
46 46 $ test -x new
47 47
48 48 Copy:
49 49
50 50 $ hg import -d "1000000 0" -mcopy - <<EOF
51 51 > diff --git a/new b/copy
52 52 > old mode 100755
53 53 > new mode 100644
54 54 > similarity index 100%
55 55 > copy from new
56 56 > copy to copy
57 57 > diff --git a/new b/copyx
58 58 > similarity index 100%
59 59 > copy from new
60 60 > copy to copyx
61 61 > EOF
62 62 applying patch from stdin
63 63
64 64 $ hg tip -q
65 65 3:37bacb7ca14d
66 66
67 67 $ if "$TESTDIR/hghave" -q execbit; then
68 68 > test -f copy -a ! -x copy || echo bad
69 69 > test -x copyx || echo bad
70 70 > else
71 71 > test -f copy || echo bad
72 72 > fi
73 73
74 74 $ cat copy
75 75 a
76 76
77 77 $ hg cat copy
78 78 a
79 79
80 80 Rename:
81 81
82 82 $ hg import -d "1000000 0" -mrename - <<EOF
83 83 > diff --git a/copy b/rename
84 84 > similarity index 100%
85 85 > rename from copy
86 86 > rename to rename
87 87 > EOF
88 88 applying patch from stdin
89 89
90 90 $ hg tip -q
91 91 4:47b81a94361d
92 92
93 93 $ hg locate
94 94 copyx
95 95 empty
96 96 new
97 97 rename
98 98
99 99 Delete:
100 100
101 101 $ hg import -d "1000000 0" -mdelete - <<EOF
102 102 > diff --git a/copyx b/copyx
103 103 > deleted file mode 100755
104 104 > index 7898192..0000000
105 105 > --- a/copyx
106 106 > +++ /dev/null
107 107 > @@ -1 +0,0 @@
108 108 > -a
109 109 > EOF
110 110 applying patch from stdin
111 111
112 112 $ hg tip -q
113 113 5:d9b001d98336
114 114
115 115 $ hg locate
116 116 empty
117 117 new
118 118 rename
119 119
120 120 $ test -f copyx
121 121 [1]
122 122
123 123 Regular diff:
124 124
125 125 $ hg import -d "1000000 0" -mregular - <<EOF
126 126 > diff --git a/rename b/rename
127 127 > index 7898192..72e1fe3 100644
128 128 > --- a/rename
129 129 > +++ b/rename
130 130 > @@ -1 +1,5 @@
131 131 > a
132 132 > +a
133 133 > +a
134 134 > +a
135 135 > +a
136 136 > EOF
137 137 applying patch from stdin
138 138
139 139 $ hg tip -q
140 140 6:ebe901e7576b
141 141
142 142 Copy and modify:
143 143
144 144 $ hg import -d "1000000 0" -mcopymod - <<EOF
145 145 > diff --git a/rename b/copy2
146 146 > similarity index 80%
147 147 > copy from rename
148 148 > copy to copy2
149 149 > index 72e1fe3..b53c148 100644
150 150 > --- a/rename
151 151 > +++ b/copy2
152 152 > @@ -1,5 +1,5 @@
153 153 > a
154 154 > a
155 155 > -a
156 156 > +b
157 157 > a
158 158 > a
159 159 > EOF
160 160 applying patch from stdin
161 161
162 162 $ hg tip -q
163 163 7:18f368958ecd
164 164
165 165 $ hg cat copy2
166 166 a
167 167 a
168 168 b
169 169 a
170 170 a
171 171
172 172 Rename and modify:
173 173
174 174 $ hg import -d "1000000 0" -mrenamemod - <<EOF
175 175 > diff --git a/copy2 b/rename2
176 176 > similarity index 80%
177 177 > rename from copy2
178 178 > rename to rename2
179 179 > index b53c148..8f81e29 100644
180 180 > --- a/copy2
181 181 > +++ b/rename2
182 182 > @@ -1,5 +1,5 @@
183 183 > a
184 184 > a
185 185 > b
186 186 > -a
187 187 > +c
188 188 > a
189 189 > EOF
190 190 applying patch from stdin
191 191
192 192 $ hg tip -q
193 193 8:c32b0d7e6f44
194 194
195 195 $ hg locate copy2
196 196 [1]
197 197 $ hg cat rename2
198 198 a
199 199 a
200 200 b
201 201 c
202 202 a
203 203
204 204 One file renamed multiple times:
205 205
206 206 $ hg import -d "1000000 0" -mmultirenames - <<EOF
207 207 > diff --git a/rename2 b/rename3
208 208 > rename from rename2
209 209 > rename to rename3
210 210 > diff --git a/rename2 b/rename3-2
211 211 > rename from rename2
212 212 > rename to rename3-2
213 213 > EOF
214 214 applying patch from stdin
215 215
216 216 $ hg tip -q
217 217 9:034a6bf95330
218 218
219 219 $ hg log -vr. --template '{rev} {files} / {file_copies}\n'
220 220 9 rename2 rename3 rename3-2 / rename3 (rename2)rename3-2 (rename2)
221 221
222 222 $ hg locate rename2 rename3 rename3-2
223 223 rename3
224 224 rename3-2
225 225
226 226 $ hg cat rename3
227 227 a
228 228 a
229 229 b
230 230 c
231 231 a
232 232
233 233 $ hg cat rename3-2
234 234 a
235 235 a
236 236 b
237 237 c
238 238 a
239 239
240 240 $ echo foo > foo
241 241 $ hg add foo
242 242 $ hg ci -m 'add foo'
243 243
244 244 Binary files and regular patch hunks:
245 245
246 246 $ hg import -d "1000000 0" -m binaryregular - <<EOF
247 247 > diff --git a/binary b/binary
248 248 > new file mode 100644
249 249 > index 0000000000000000000000000000000000000000..593f4708db84ac8fd0f5cc47c634f38c013fe9e4
250 250 > GIT binary patch
251 251 > literal 4
252 252 > Lc\${NkU|;|M00aO5
253 253 >
254 254 > diff --git a/foo b/foo2
255 255 > rename from foo
256 256 > rename to foo2
257 257 > EOF
258 258 applying patch from stdin
259 259
260 260 $ hg tip -q
261 261 11:c39bce63e786
262 262
263 263 $ cat foo2
264 264 foo
265 265
266 266 $ hg manifest --debug | grep binary
267 267 045c85ba38952325e126c70962cc0f9d9077bc67 644 binary
268 268
269 269 Multiple binary files:
270 270
271 271 $ hg import -d "1000000 0" -m multibinary - <<EOF
272 272 > diff --git a/mbinary1 b/mbinary1
273 273 > new file mode 100644
274 274 > index 0000000000000000000000000000000000000000..593f4708db84ac8fd0f5cc47c634f38c013fe9e4
275 275 > GIT binary patch
276 276 > literal 4
277 277 > Lc\${NkU|;|M00aO5
278 278 >
279 279 > diff --git a/mbinary2 b/mbinary2
280 280 > new file mode 100644
281 281 > index 0000000000000000000000000000000000000000..112363ac1917b417ffbd7f376ca786a1e5fa7490
282 282 > GIT binary patch
283 283 > literal 5
284 284 > Mc\${NkU|\`?^000jF3jhEB
285 285 >
286 286 > EOF
287 287 applying patch from stdin
288 288
289 289 $ hg tip -q
290 290 12:30b530085242
291 291
292 292 $ hg manifest --debug | grep mbinary
293 293 045c85ba38952325e126c70962cc0f9d9077bc67 644 mbinary1
294 294 a874b471193996e7cb034bb301cac7bdaf3e3f46 644 mbinary2
295 295
296 296 Filenames with spaces:
297 297
298 298 $ hg import -d "1000000 0" -m spaces - <<EOF
299 299 > diff --git a/foo bar b/foo bar
300 300 > new file mode 100644
301 301 > index 0000000..257cc56
302 302 > --- /dev/null
303 303 > +++ b/foo bar
304 304 > @@ -0,0 +1 @@
305 305 > +foo
306 306 > EOF
307 307 applying patch from stdin
308 308
309 309 $ hg tip -q
310 310 13:04750ef42fb3
311 311
312 312 $ cat "foo bar"
313 313 foo
314 314
315 315 Copy then modify the original file:
316 316
317 317 $ hg import -d "1000000 0" -m copy-mod-orig - <<EOF
318 318 > diff --git a/foo2 b/foo2
319 319 > index 257cc56..fe08ec6 100644
320 320 > --- a/foo2
321 321 > +++ b/foo2
322 322 > @@ -1 +1,2 @@
323 323 > foo
324 324 > +new line
325 325 > diff --git a/foo2 b/foo3
326 326 > similarity index 100%
327 327 > copy from foo2
328 328 > copy to foo3
329 329 > EOF
330 330 applying patch from stdin
331 331
332 332 $ hg tip -q
333 333 14:c4cd9cdeaa74
334 334
335 335 $ cat foo3
336 336 foo
337 337
338 338 Move text file and patch as binary
339 339
340 340 $ echo a > text2
341 341 $ hg ci -Am0
342 342 adding text2
343 343 $ hg import -d "1000000 0" -m rename-as-binary - <<"EOF"
344 344 > diff --git a/text2 b/binary2
345 345 > rename from text2
346 346 > rename to binary2
347 347 > index 78981922613b2afb6025042ff6bd878ac1994e85..10efcb362e9f3b3420fcfbfc0e37f3dc16e29757
348 348 > GIT binary patch
349 349 > literal 5
350 350 > Mc$`b*O5$Pw00T?_*Z=?k
351 351 >
352 352 > EOF
353 353 applying patch from stdin
354 354
355 355 $ cat binary2
356 356 a
357 357 b
358 358 \x00 (no-eol) (esc)
359 359
360 360 $ hg st --copies --change .
361 361 A binary2
362 362 text2
363 363 R text2
364 364 $ cd ..
365 365
366 366 Consecutive import with renames (issue2459)
367 367
368 368 $ hg init issue2459
369 369 $ cd issue2459
370 370 $ hg import --no-commit --force - <<EOF
371 371 > diff --git a/a b/a
372 372 > new file mode 100644
373 373 > EOF
374 374 applying patch from stdin
375 375 $ hg import --no-commit --force - <<EOF
376 376 > diff --git a/a b/b
377 377 > rename from a
378 378 > rename to b
379 379 > EOF
380 380 applying patch from stdin
381 381 a has not been committed yet, so no copy data will be stored for b.
382 382 $ hg debugstate
383 383 a 0 -1 unset b
384 384 $ hg ci -m done
385 385 $ cd ..
386 386
387 387 Renames and strip
388 388
389 389 $ hg init renameandstrip
390 390 $ cd renameandstrip
391 391 $ echo a > a
392 392 $ hg ci -Am adda
393 393 adding a
394 394 $ hg import --no-commit -p2 - <<EOF
395 395 > diff --git a/foo/a b/foo/b
396 396 > rename from foo/a
397 397 > rename to foo/b
398 398 > EOF
399 399 applying patch from stdin
400 400 $ hg st --copies
401 401 A b
402 402 a
403 403 R a
404 404 $ cd ..
405 405
406 406 Pure copy with existing destination
407 407
408 408 $ hg init copytoexisting
409 409 $ cd copytoexisting
410 410 $ echo a > a
411 411 $ echo b > b
412 412 $ hg ci -Am add
413 413 adding a
414 414 adding b
415 415 $ hg import --no-commit - <<EOF
416 416 > diff --git a/a b/b
417 417 > copy from a
418 418 > copy to b
419 419 > EOF
420 420 applying patch from stdin
421 421 abort: cannot create b: destination already exists
422 422 [255]
423 423 $ cat b
424 424 b
425 425
426 426 Copy and changes with existing destination
427 427
428 428 $ hg import --no-commit - <<EOF
429 429 > diff --git a/a b/b
430 430 > copy from a
431 431 > copy to b
432 432 > --- a/a
433 433 > +++ b/b
434 434 > @@ -1,1 +1,2 @@
435 435 > a
436 436 > +b
437 437 > EOF
438 438 applying patch from stdin
439 abort: cannot create b: destination already exists
439 cannot create b: destination already exists
440 1 out of 1 hunks FAILED -- saving rejects to file b.rej
441 abort: patch failed to apply
440 442 [255]
441 443 $ cat b
442 444 b
443 445 $ cd ..
@@ -1,930 +1,930 b''
1 1 $ hg init a
2 2 $ mkdir a/d1
3 3 $ mkdir a/d1/d2
4 4 $ echo line 1 > a/a
5 5 $ echo line 1 > a/d1/d2/a
6 6 $ hg --cwd a ci -Ama
7 7 adding a
8 8 adding d1/d2/a
9 9
10 10 $ echo line 2 >> a/a
11 11 $ hg --cwd a ci -u someone -d '1 0' -m'second change'
12 12
13 13
14 14 generate patches for the test
15 15
16 16 $ hg --cwd a export tip > exported-tip.patch
17 17 $ hg --cwd a diff -r0:1 > diffed-tip.patch
18 18
19 19
20 20 import exported patch
21 21
22 22 $ hg clone -r0 a b
23 23 adding changesets
24 24 adding manifests
25 25 adding file changes
26 26 added 1 changesets with 2 changes to 2 files
27 27 updating to branch default
28 28 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
29 29 $ hg --cwd b import ../exported-tip.patch
30 30 applying ../exported-tip.patch
31 31
32 32 message and committer should be same
33 33
34 34 $ hg --cwd b tip
35 35 changeset: 1:1d4bd90af0e4
36 36 tag: tip
37 37 user: someone
38 38 date: Thu Jan 01 00:00:01 1970 +0000
39 39 summary: second change
40 40
41 41 $ rm -r b
42 42
43 43
44 44 import exported patch with external patcher
45 45
46 46 $ cat > dummypatch.py <<EOF
47 47 > print 'patching file a'
48 48 > file('a', 'wb').write('line2\n')
49 49 > EOF
50 50 $ chmod +x dummypatch.py
51 51 $ hg clone -r0 a b
52 52 adding changesets
53 53 adding manifests
54 54 adding file changes
55 55 added 1 changesets with 2 changes to 2 files
56 56 updating to branch default
57 57 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
58 58 $ hg --config ui.patch='python ../dummypatch.py' --cwd b import ../exported-tip.patch
59 59 applying ../exported-tip.patch
60 60 $ cat b/a
61 61 line2
62 62 $ rm -r b
63 63
64 64
65 65 import of plain diff should fail without message
66 66
67 67 $ hg clone -r0 a b
68 68 adding changesets
69 69 adding manifests
70 70 adding file changes
71 71 added 1 changesets with 2 changes to 2 files
72 72 updating to branch default
73 73 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
74 74 $ hg --cwd b import ../diffed-tip.patch
75 75 applying ../diffed-tip.patch
76 76 abort: empty commit message
77 77 [255]
78 78 $ rm -r b
79 79
80 80
81 81 import of plain diff should be ok with message
82 82
83 83 $ hg clone -r0 a b
84 84 adding changesets
85 85 adding manifests
86 86 adding file changes
87 87 added 1 changesets with 2 changes to 2 files
88 88 updating to branch default
89 89 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
90 90 $ hg --cwd b import -mpatch ../diffed-tip.patch
91 91 applying ../diffed-tip.patch
92 92 $ rm -r b
93 93
94 94
95 95 import of plain diff with specific date and user
96 96
97 97 $ hg clone -r0 a b
98 98 adding changesets
99 99 adding manifests
100 100 adding file changes
101 101 added 1 changesets with 2 changes to 2 files
102 102 updating to branch default
103 103 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
104 104 $ hg --cwd b import -mpatch -d '1 0' -u 'user@nowhere.net' ../diffed-tip.patch
105 105 applying ../diffed-tip.patch
106 106 $ hg -R b tip -pv
107 107 changeset: 1:ca68f19f3a40
108 108 tag: tip
109 109 user: user@nowhere.net
110 110 date: Thu Jan 01 00:00:01 1970 +0000
111 111 files: a
112 112 description:
113 113 patch
114 114
115 115
116 116 diff -r 80971e65b431 -r ca68f19f3a40 a
117 117 --- a/a Thu Jan 01 00:00:00 1970 +0000
118 118 +++ b/a Thu Jan 01 00:00:01 1970 +0000
119 119 @@ -1,1 +1,2 @@
120 120 line 1
121 121 +line 2
122 122
123 123 $ rm -r b
124 124
125 125
126 126 import of plain diff should be ok with --no-commit
127 127
128 128 $ hg clone -r0 a b
129 129 adding changesets
130 130 adding manifests
131 131 adding file changes
132 132 added 1 changesets with 2 changes to 2 files
133 133 updating to branch default
134 134 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
135 135 $ hg --cwd b import --no-commit ../diffed-tip.patch
136 136 applying ../diffed-tip.patch
137 137 $ hg --cwd b diff --nodates
138 138 diff -r 80971e65b431 a
139 139 --- a/a
140 140 +++ b/a
141 141 @@ -1,1 +1,2 @@
142 142 line 1
143 143 +line 2
144 144 $ rm -r b
145 145
146 146
147 147 import of malformed plain diff should fail
148 148
149 149 $ hg clone -r0 a b
150 150 adding changesets
151 151 adding manifests
152 152 adding file changes
153 153 added 1 changesets with 2 changes to 2 files
154 154 updating to branch default
155 155 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
156 156 $ sed 's/1,1/foo/' < diffed-tip.patch > broken.patch
157 157 $ hg --cwd b import -mpatch ../broken.patch
158 158 applying ../broken.patch
159 159 abort: bad hunk #1
160 160 [255]
161 161 $ rm -r b
162 162
163 163
164 164 hg -R repo import
165 165 put the clone in a subdir - having a directory named "a"
166 166 used to hide a bug.
167 167
168 168 $ mkdir dir
169 169 $ hg clone -r0 a dir/b
170 170 adding changesets
171 171 adding manifests
172 172 adding file changes
173 173 added 1 changesets with 2 changes to 2 files
174 174 updating to branch default
175 175 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
176 176 $ cd dir
177 177 $ hg -R b import ../exported-tip.patch
178 178 applying ../exported-tip.patch
179 179 $ cd ..
180 180 $ rm -r dir
181 181
182 182
183 183 import from stdin
184 184
185 185 $ hg clone -r0 a b
186 186 adding changesets
187 187 adding manifests
188 188 adding file changes
189 189 added 1 changesets with 2 changes to 2 files
190 190 updating to branch default
191 191 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
192 192 $ hg --cwd b import - < exported-tip.patch
193 193 applying patch from stdin
194 194 $ rm -r b
195 195
196 196
197 197 import two patches in one stream
198 198
199 199 $ hg init b
200 200 $ hg --cwd a export 0:tip | hg --cwd b import -
201 201 applying patch from stdin
202 202 applied 80971e65b431
203 203 $ hg --cwd a id
204 204 1d4bd90af0e4 tip
205 205 $ hg --cwd b id
206 206 1d4bd90af0e4 tip
207 207 $ rm -r b
208 208
209 209
210 210 override commit message
211 211
212 212 $ hg clone -r0 a b
213 213 adding changesets
214 214 adding manifests
215 215 adding file changes
216 216 added 1 changesets with 2 changes to 2 files
217 217 updating to branch default
218 218 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
219 219 $ hg --cwd b import -m 'override' - < exported-tip.patch
220 220 applying patch from stdin
221 221 $ hg --cwd b tip | grep override
222 222 summary: override
223 223 $ rm -r b
224 224
225 225 $ cat > mkmsg.py <<EOF
226 226 > import email.Message, sys
227 227 > msg = email.Message.Message()
228 228 > patch = open(sys.argv[1], 'rb').read()
229 229 > msg.set_payload('email commit message\n' + patch)
230 230 > msg['Subject'] = 'email patch'
231 231 > msg['From'] = 'email patcher'
232 232 > sys.stdout.write(msg.as_string())
233 233 > EOF
234 234
235 235
236 236 plain diff in email, subject, message body
237 237
238 238 $ hg clone -r0 a b
239 239 adding changesets
240 240 adding manifests
241 241 adding file changes
242 242 added 1 changesets with 2 changes to 2 files
243 243 updating to branch default
244 244 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
245 245 $ python mkmsg.py diffed-tip.patch > msg.patch
246 246 $ hg --cwd b import ../msg.patch
247 247 applying ../msg.patch
248 248 $ hg --cwd b tip | grep email
249 249 user: email patcher
250 250 summary: email patch
251 251 $ rm -r b
252 252
253 253
254 254 plain diff in email, no subject, message body
255 255
256 256 $ hg clone -r0 a b
257 257 adding changesets
258 258 adding manifests
259 259 adding file changes
260 260 added 1 changesets with 2 changes to 2 files
261 261 updating to branch default
262 262 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
263 263 $ grep -v '^Subject:' msg.patch | hg --cwd b import -
264 264 applying patch from stdin
265 265 $ rm -r b
266 266
267 267
268 268 plain diff in email, subject, no message body
269 269
270 270 $ hg clone -r0 a b
271 271 adding changesets
272 272 adding manifests
273 273 adding file changes
274 274 added 1 changesets with 2 changes to 2 files
275 275 updating to branch default
276 276 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
277 277 $ grep -v '^email ' msg.patch | hg --cwd b import -
278 278 applying patch from stdin
279 279 $ rm -r b
280 280
281 281
282 282 plain diff in email, no subject, no message body, should fail
283 283
284 284 $ hg clone -r0 a b
285 285 adding changesets
286 286 adding manifests
287 287 adding file changes
288 288 added 1 changesets with 2 changes to 2 files
289 289 updating to branch default
290 290 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
291 291 $ egrep -v '^(Subject|email)' msg.patch | hg --cwd b import -
292 292 applying patch from stdin
293 293 abort: empty commit message
294 294 [255]
295 295 $ rm -r b
296 296
297 297
298 298 hg export in email, should use patch header
299 299
300 300 $ hg clone -r0 a b
301 301 adding changesets
302 302 adding manifests
303 303 adding file changes
304 304 added 1 changesets with 2 changes to 2 files
305 305 updating to branch default
306 306 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
307 307 $ python mkmsg.py exported-tip.patch | hg --cwd b import -
308 308 applying patch from stdin
309 309 $ hg --cwd b tip | grep second
310 310 summary: second change
311 311 $ rm -r b
312 312
313 313
314 314 subject: duplicate detection, removal of [PATCH]
315 315 The '---' tests the gitsendmail handling without proper mail headers
316 316
317 317 $ cat > mkmsg2.py <<EOF
318 318 > import email.Message, sys
319 319 > msg = email.Message.Message()
320 320 > patch = open(sys.argv[1], 'rb').read()
321 321 > msg.set_payload('email patch\n\nnext line\n---\n' + patch)
322 322 > msg['Subject'] = '[PATCH] email patch'
323 323 > msg['From'] = 'email patcher'
324 324 > sys.stdout.write(msg.as_string())
325 325 > EOF
326 326
327 327
328 328 plain diff in email, [PATCH] subject, message body with subject
329 329
330 330 $ hg clone -r0 a b
331 331 adding changesets
332 332 adding manifests
333 333 adding file changes
334 334 added 1 changesets with 2 changes to 2 files
335 335 updating to branch default
336 336 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
337 337 $ python mkmsg2.py diffed-tip.patch | hg --cwd b import -
338 338 applying patch from stdin
339 339 $ hg --cwd b tip --template '{desc}\n'
340 340 email patch
341 341
342 342 next line
343 343 ---
344 344 $ rm -r b
345 345
346 346
347 347 Issue963: Parent of working dir incorrect after import of multiple
348 348 patches and rollback
349 349
350 350 We weren't backing up the correct dirstate file when importing many
351 351 patches: import patch1 patch2; rollback
352 352
353 353 $ echo line 3 >> a/a
354 354 $ hg --cwd a ci -m'third change'
355 355 $ hg --cwd a export -o '../patch%R' 1 2
356 356 $ hg clone -qr0 a b
357 357 $ hg --cwd b parents --template 'parent: {rev}\n'
358 358 parent: 0
359 359 $ hg --cwd b import ../patch1 ../patch2
360 360 applying ../patch1
361 361 applying ../patch2
362 362 applied 1d4bd90af0e4
363 363 $ hg --cwd b rollback
364 364 repository tip rolled back to revision 1 (undo commit)
365 365 working directory now based on revision 1
366 366 $ hg --cwd b parents --template 'parent: {rev}\n'
367 367 parent: 1
368 368 $ rm -r b
369 369
370 370
371 371 importing a patch in a subdirectory failed at the commit stage
372 372
373 373 $ echo line 2 >> a/d1/d2/a
374 374 $ hg --cwd a ci -u someoneelse -d '1 0' -m'subdir change'
375 375
376 376 hg import in a subdirectory
377 377
378 378 $ hg clone -r0 a b
379 379 adding changesets
380 380 adding manifests
381 381 adding file changes
382 382 added 1 changesets with 2 changes to 2 files
383 383 updating to branch default
384 384 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
385 385 $ hg --cwd a export tip > tmp
386 386 $ sed -e 's/d1\/d2\///' < tmp > subdir-tip.patch
387 387 $ dir=`pwd`
388 388 $ cd b/d1/d2 2>&1 > /dev/null
389 389 $ hg import ../../../subdir-tip.patch
390 390 applying ../../../subdir-tip.patch
391 391 $ cd "$dir"
392 392
393 393 message should be 'subdir change'
394 394 committer should be 'someoneelse'
395 395
396 396 $ hg --cwd b tip
397 397 changeset: 1:3577f5aea227
398 398 tag: tip
399 399 user: someoneelse
400 400 date: Thu Jan 01 00:00:01 1970 +0000
401 401 summary: subdir change
402 402
403 403
404 404 should be empty
405 405
406 406 $ hg --cwd b status
407 407
408 408
409 409 Test fuzziness (ambiguous patch location, fuzz=2)
410 410
411 411 $ hg init fuzzy
412 412 $ cd fuzzy
413 413 $ echo line1 > a
414 414 $ echo line0 >> a
415 415 $ echo line3 >> a
416 416 $ hg ci -Am adda
417 417 adding a
418 418 $ echo line1 > a
419 419 $ echo line2 >> a
420 420 $ echo line0 >> a
421 421 $ echo line3 >> a
422 422 $ hg ci -m change a
423 423 $ hg export tip > fuzzy-tip.patch
424 424 $ hg up -C 0
425 425 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
426 426 $ echo line1 > a
427 427 $ echo line0 >> a
428 428 $ echo line1 >> a
429 429 $ echo line0 >> a
430 430 $ hg ci -m brancha
431 431 created new head
432 432 $ hg import --no-commit -v fuzzy-tip.patch
433 433 applying fuzzy-tip.patch
434 434 patching file a
435 435 Hunk #1 succeeded at 1 with fuzz 2 (offset -2 lines).
436 436 $ hg revert -a
437 437 reverting a
438 438
439 439
440 440 import with --no-commit should have written .hg/last-message.txt
441 441
442 442 $ cat .hg/last-message.txt
443 443 change (no-eol)
444 444
445 445
446 446 test fuzziness with eol=auto
447 447
448 448 $ hg --config patch.eol=auto import --no-commit -v fuzzy-tip.patch
449 449 applying fuzzy-tip.patch
450 450 patching file a
451 451 Hunk #1 succeeded at 1 with fuzz 2 (offset -2 lines).
452 452 $ cd ..
453 453
454 454
455 455 Test hunk touching empty files (issue906)
456 456
457 457 $ hg init empty
458 458 $ cd empty
459 459 $ touch a
460 460 $ touch b1
461 461 $ touch c1
462 462 $ echo d > d
463 463 $ hg ci -Am init
464 464 adding a
465 465 adding b1
466 466 adding c1
467 467 adding d
468 468 $ echo a > a
469 469 $ echo b > b1
470 470 $ hg mv b1 b2
471 471 $ echo c > c1
472 472 $ hg copy c1 c2
473 473 $ rm d
474 474 $ touch d
475 475 $ hg diff --git
476 476 diff --git a/a b/a
477 477 --- a/a
478 478 +++ b/a
479 479 @@ -0,0 +1,1 @@
480 480 +a
481 481 diff --git a/b1 b/b2
482 482 rename from b1
483 483 rename to b2
484 484 --- a/b1
485 485 +++ b/b2
486 486 @@ -0,0 +1,1 @@
487 487 +b
488 488 diff --git a/c1 b/c1
489 489 --- a/c1
490 490 +++ b/c1
491 491 @@ -0,0 +1,1 @@
492 492 +c
493 493 diff --git a/c1 b/c2
494 494 copy from c1
495 495 copy to c2
496 496 --- a/c1
497 497 +++ b/c2
498 498 @@ -0,0 +1,1 @@
499 499 +c
500 500 diff --git a/d b/d
501 501 --- a/d
502 502 +++ b/d
503 503 @@ -1,1 +0,0 @@
504 504 -d
505 505 $ hg ci -m empty
506 506 $ hg export --git tip > empty.diff
507 507 $ hg up -C 0
508 508 4 files updated, 0 files merged, 2 files removed, 0 files unresolved
509 509 $ hg import empty.diff
510 510 applying empty.diff
511 511 $ for name in a b1 b2 c1 c2 d; do
512 512 > echo % $name file
513 513 > test -f $name && cat $name
514 514 > done
515 515 % a file
516 516 a
517 517 % b1 file
518 518 % b2 file
519 519 b
520 520 % c1 file
521 521 c
522 522 % c2 file
523 523 c
524 524 % d file
525 525 $ cd ..
526 526
527 527
528 528 Test importing a patch ending with a binary file removal
529 529
530 530 $ hg init binaryremoval
531 531 $ cd binaryremoval
532 532 $ echo a > a
533 533 $ python -c "file('b', 'wb').write('a\x00b')"
534 534 $ hg ci -Am addall
535 535 adding a
536 536 adding b
537 537 $ hg rm a
538 538 $ hg rm b
539 539 $ hg st
540 540 R a
541 541 R b
542 542 $ hg ci -m remove
543 543 $ hg export --git . > remove.diff
544 544 $ cat remove.diff | grep git
545 545 diff --git a/a b/a
546 546 diff --git a/b b/b
547 547 $ hg up -C 0
548 548 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
549 549 $ hg import remove.diff
550 550 applying remove.diff
551 551 $ hg manifest
552 552 $ cd ..
553 553
554 554
555 555 Issue927: test update+rename with common name
556 556
557 557 $ hg init t
558 558 $ cd t
559 559 $ touch a
560 560 $ hg ci -Am t
561 561 adding a
562 562 $ echo a > a
563 563
564 564 Here, bfile.startswith(afile)
565 565
566 566 $ hg copy a a2
567 567 $ hg ci -m copya
568 568 $ hg export --git tip > copy.diff
569 569 $ hg up -C 0
570 570 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
571 571 $ hg import copy.diff
572 572 applying copy.diff
573 573
574 574 a should contain an 'a'
575 575
576 576 $ cat a
577 577 a
578 578
579 579 and a2 should have duplicated it
580 580
581 581 $ cat a2
582 582 a
583 583 $ cd ..
584 584
585 585
586 586 test -p0
587 587
588 588 $ hg init p0
589 589 $ cd p0
590 590 $ echo a > a
591 591 $ hg ci -Am t
592 592 adding a
593 593 $ hg import -p0 - << EOF
594 594 > foobar
595 595 > --- a Sat Apr 12 22:43:58 2008 -0400
596 596 > +++ a Sat Apr 12 22:44:05 2008 -0400
597 597 > @@ -1,1 +1,1 @@
598 598 > -a
599 599 > +bb
600 600 > EOF
601 601 applying patch from stdin
602 602 $ hg status
603 603 $ cat a
604 604 bb
605 605 $ cd ..
606 606
607 607
608 608 test paths outside repo root
609 609
610 610 $ mkdir outside
611 611 $ touch outside/foo
612 612 $ hg init inside
613 613 $ cd inside
614 614 $ hg import - <<EOF
615 615 > diff --git a/a b/b
616 616 > rename from ../outside/foo
617 617 > rename to bar
618 618 > EOF
619 619 applying patch from stdin
620 abort: ../outside/foo not under root
620 abort: path contains illegal component: ../outside/foo
621 621 [255]
622 622 $ cd ..
623 623
624 624
625 625 test import with similarity and git and strip (issue295 et al.)
626 626
627 627 $ hg init sim
628 628 $ cd sim
629 629 $ echo 'this is a test' > a
630 630 $ hg ci -Ama
631 631 adding a
632 632 $ cat > ../rename.diff <<EOF
633 633 > diff --git a/foo/a b/foo/a
634 634 > deleted file mode 100644
635 635 > --- a/foo/a
636 636 > +++ /dev/null
637 637 > @@ -1,1 +0,0 @@
638 638 > -this is a test
639 639 > diff --git a/foo/b b/foo/b
640 640 > new file mode 100644
641 641 > --- /dev/null
642 642 > +++ b/foo/b
643 643 > @@ -0,0 +1,2 @@
644 644 > +this is a test
645 645 > +foo
646 646 > EOF
647 647 $ hg import --no-commit -v -s 1 ../rename.diff -p2
648 648 applying ../rename.diff
649 649 patching file a
650 650 patching file b
651 651 removing a
652 652 adding b
653 653 recording removal of a as rename to b (88% similar)
654 654 $ hg st -C
655 655 A b
656 656 a
657 657 R a
658 658 $ hg revert -a
659 659 undeleting a
660 660 forgetting b
661 661 $ rm b
662 662 $ hg import --no-commit -v -s 100 ../rename.diff -p2
663 663 applying ../rename.diff
664 664 patching file a
665 665 patching file b
666 666 removing a
667 667 adding b
668 668 $ hg st -C
669 669 A b
670 670 R a
671 671 $ cd ..
672 672
673 673
674 674 Issue1495: add empty file from the end of patch
675 675
676 676 $ hg init addemptyend
677 677 $ cd addemptyend
678 678 $ touch a
679 679 $ hg addremove
680 680 adding a
681 681 $ hg ci -m "commit"
682 682 $ cat > a.patch <<EOF
683 683 > diff --git a/a b/a
684 684 > --- a/a
685 685 > +++ b/a
686 686 > @@ -0,0 +1,1 @@
687 687 > +a
688 688 > diff --git a/b b/b
689 689 > new file mode 100644
690 690 > EOF
691 691 $ hg import --no-commit a.patch
692 692 applying a.patch
693 693 $ cd ..
694 694
695 695
696 696 create file when source is not /dev/null
697 697
698 698 $ cat > create.patch <<EOF
699 699 > diff -Naur proj-orig/foo proj-new/foo
700 700 > --- proj-orig/foo 1969-12-31 16:00:00.000000000 -0800
701 701 > +++ proj-new/foo 2009-07-17 16:50:45.801368000 -0700
702 702 > @@ -0,0 +1,1 @@
703 703 > +a
704 704 > EOF
705 705
706 706 some people have patches like the following too
707 707
708 708 $ cat > create2.patch <<EOF
709 709 > diff -Naur proj-orig/foo proj-new/foo
710 710 > --- proj-orig/foo.orig 1969-12-31 16:00:00.000000000 -0800
711 711 > +++ proj-new/foo 2009-07-17 16:50:45.801368000 -0700
712 712 > @@ -0,0 +1,1 @@
713 713 > +a
714 714 > EOF
715 715 $ hg init oddcreate
716 716 $ cd oddcreate
717 717 $ hg import --no-commit ../create.patch
718 718 applying ../create.patch
719 719 $ cat foo
720 720 a
721 721 $ rm foo
722 722 $ hg revert foo
723 723 $ hg import --no-commit ../create2.patch
724 724 applying ../create2.patch
725 725 $ cat foo
726 726 a
727 727
728 728
729 729 Issue1859: first line mistaken for email headers
730 730
731 731 $ hg init emailconfusion
732 732 $ cd emailconfusion
733 733 $ cat > a.patch <<EOF
734 734 > module: summary
735 735 >
736 736 > description
737 737 >
738 738 >
739 739 > diff -r 000000000000 -r 9b4c1e343b55 test.txt
740 740 > --- /dev/null
741 741 > +++ b/a
742 742 > @@ -0,0 +1,1 @@
743 743 > +a
744 744 > EOF
745 745 $ hg import -d '0 0' a.patch
746 746 applying a.patch
747 747 $ hg parents -v
748 748 changeset: 0:5a681217c0ad
749 749 tag: tip
750 750 user: test
751 751 date: Thu Jan 01 00:00:00 1970 +0000
752 752 files: a
753 753 description:
754 754 module: summary
755 755
756 756 description
757 757
758 758
759 759 $ cd ..
760 760
761 761
762 762 --- in commit message
763 763
764 764 $ hg init commitconfusion
765 765 $ cd commitconfusion
766 766 $ cat > a.patch <<EOF
767 767 > module: summary
768 768 >
769 769 > --- description
770 770 >
771 771 > diff --git a/a b/a
772 772 > new file mode 100644
773 773 > --- /dev/null
774 774 > +++ b/a
775 775 > @@ -0,0 +1,1 @@
776 776 > +a
777 777 > EOF
778 778 > hg import -d '0 0' a.patch
779 779 > hg parents -v
780 780 > cd ..
781 781 >
782 782 > echo '% tricky header splitting'
783 783 > cat > trickyheaders.patch <<EOF
784 784 > From: User A <user@a>
785 785 > Subject: [PATCH] from: tricky!
786 786 >
787 787 > # HG changeset patch
788 788 > # User User B
789 789 > # Date 1266264441 18000
790 790 > # Branch stable
791 791 > # Node ID f2be6a1170ac83bf31cb4ae0bad00d7678115bc0
792 792 > # Parent 0000000000000000000000000000000000000000
793 793 > from: tricky!
794 794 >
795 795 > That is not a header.
796 796 >
797 797 > diff -r 000000000000 -r f2be6a1170ac foo
798 798 > --- /dev/null
799 799 > +++ b/foo
800 800 > @@ -0,0 +1,1 @@
801 801 > +foo
802 802 > EOF
803 803 applying a.patch
804 804 changeset: 0:f34d9187897d
805 805 tag: tip
806 806 user: test
807 807 date: Thu Jan 01 00:00:00 1970 +0000
808 808 files: a
809 809 description:
810 810 module: summary
811 811
812 812
813 813 % tricky header splitting
814 814
815 815 $ hg init trickyheaders
816 816 $ cd trickyheaders
817 817 $ hg import -d '0 0' ../trickyheaders.patch
818 818 applying ../trickyheaders.patch
819 819 $ hg export --git tip
820 820 # HG changeset patch
821 821 # User User B
822 822 # Date 0 0
823 823 # Node ID eb56ab91903632294ac504838508cb370c0901d2
824 824 # Parent 0000000000000000000000000000000000000000
825 825 from: tricky!
826 826
827 827 That is not a header.
828 828
829 829 diff --git a/foo b/foo
830 830 new file mode 100644
831 831 --- /dev/null
832 832 +++ b/foo
833 833 @@ -0,0 +1,1 @@
834 834 +foo
835 835 $ cd ..
836 836
837 837
838 838 Issue2102: hg export and hg import speak different languages
839 839
840 840 $ hg init issue2102
841 841 $ cd issue2102
842 842 $ mkdir -p src/cmd/gc
843 843 $ touch src/cmd/gc/mksys.bash
844 844 $ hg ci -Am init
845 845 adding src/cmd/gc/mksys.bash
846 846 $ hg import - <<EOF
847 847 > # HG changeset patch
848 848 > # User Rob Pike
849 849 > # Date 1216685449 25200
850 850 > # Node ID 03aa2b206f499ad6eb50e6e207b9e710d6409c98
851 851 > # Parent 93d10138ad8df586827ca90b4ddb5033e21a3a84
852 852 > help management of empty pkg and lib directories in perforce
853 853 >
854 854 > R=gri
855 855 > DELTA=4 (4 added, 0 deleted, 0 changed)
856 856 > OCL=13328
857 857 > CL=13328
858 858 >
859 859 > diff --git a/lib/place-holder b/lib/place-holder
860 860 > new file mode 100644
861 861 > --- /dev/null
862 862 > +++ b/lib/place-holder
863 863 > @@ -0,0 +1,2 @@
864 864 > +perforce does not maintain empty directories.
865 865 > +this file helps.
866 866 > diff --git a/pkg/place-holder b/pkg/place-holder
867 867 > new file mode 100644
868 868 > --- /dev/null
869 869 > +++ b/pkg/place-holder
870 870 > @@ -0,0 +1,2 @@
871 871 > +perforce does not maintain empty directories.
872 872 > +this file helps.
873 873 > diff --git a/src/cmd/gc/mksys.bash b/src/cmd/gc/mksys.bash
874 874 > old mode 100644
875 875 > new mode 100755
876 876 > EOF
877 877 applying patch from stdin
878 878 $ hg sum
879 879 parent: 1:d59915696727 tip
880 880 help management of empty pkg and lib directories in perforce
881 881 branch: default
882 882 commit: (clean)
883 883 update: (current)
884 884 $ hg diff --git -c tip
885 885 diff --git a/lib/place-holder b/lib/place-holder
886 886 new file mode 100644
887 887 --- /dev/null
888 888 +++ b/lib/place-holder
889 889 @@ -0,0 +1,2 @@
890 890 +perforce does not maintain empty directories.
891 891 +this file helps.
892 892 diff --git a/pkg/place-holder b/pkg/place-holder
893 893 new file mode 100644
894 894 --- /dev/null
895 895 +++ b/pkg/place-holder
896 896 @@ -0,0 +1,2 @@
897 897 +perforce does not maintain empty directories.
898 898 +this file helps.
899 899 diff --git a/src/cmd/gc/mksys.bash b/src/cmd/gc/mksys.bash
900 900 old mode 100644
901 901 new mode 100755
902 902 $ cd ..
903 903
904 904
905 905 diff lines looking like headers
906 906
907 907 $ hg init difflineslikeheaders
908 908 $ cd difflineslikeheaders
909 909 $ echo a >a
910 910 $ echo b >b
911 911 $ echo c >c
912 912 $ hg ci -Am1
913 913 adding a
914 914 adding b
915 915 adding c
916 916
917 917 $ echo "key: value" >>a
918 918 $ echo "key: value" >>b
919 919 $ echo "foo" >>c
920 920 $ hg ci -m2
921 921
922 922 $ hg up -C 0
923 923 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
924 924 $ hg diff --git -c1 >want
925 925 $ hg diff -c1 | hg import --no-commit -
926 926 applying patch from stdin
927 927 $ hg diff --git >have
928 928 $ diff want have
929 929 $ cd ..
930 930
@@ -1,524 +1,525 b''
1 1 $ echo "[extensions]" >> $HGRCPATH
2 2 $ echo "mq=" >> $HGRCPATH
3 3 $ echo "[diff]" >> $HGRCPATH
4 4 $ echo "nodates=1" >> $HGRCPATH
5 5
6 6 $ hg init a
7 7 $ cd a
8 8
9 9 $ mkdir 1 2
10 10 $ echo 'base' > 1/base
11 11 $ echo 'base' > 2/base
12 12 $ hg ci -Ambase
13 13 adding 1/base
14 14 adding 2/base
15 15
16 16 $ hg qnew -mmqbase mqbase
17 17
18 18 $ echo 'patched' > 1/base
19 19 $ echo 'patched' > 2/base
20 20 $ hg qrefresh
21 21
22 22 $ hg qdiff
23 23 diff -r e7af5904b465 1/base
24 24 --- a/1/base
25 25 +++ b/1/base
26 26 @@ -1,1 +1,1 @@
27 27 -base
28 28 +patched
29 29 diff -r e7af5904b465 2/base
30 30 --- a/2/base
31 31 +++ b/2/base
32 32 @@ -1,1 +1,1 @@
33 33 -base
34 34 +patched
35 35
36 36 $ hg qdiff .
37 37 diff -r e7af5904b465 1/base
38 38 --- a/1/base
39 39 +++ b/1/base
40 40 @@ -1,1 +1,1 @@
41 41 -base
42 42 +patched
43 43 diff -r e7af5904b465 2/base
44 44 --- a/2/base
45 45 +++ b/2/base
46 46 @@ -1,1 +1,1 @@
47 47 -base
48 48 +patched
49 49
50 50 $ cat .hg/patches/mqbase
51 51 # HG changeset patch
52 52 # Parent e7af5904b465cd1f4f3cf6b26fe14e8db6f63eaa
53 53 mqbase
54 54
55 55 diff -r e7af5904b465 1/base
56 56 --- a/1/base
57 57 +++ b/1/base
58 58 @@ -1,1 +1,1 @@
59 59 -base
60 60 +patched
61 61 diff -r e7af5904b465 2/base
62 62 --- a/2/base
63 63 +++ b/2/base
64 64 @@ -1,1 +1,1 @@
65 65 -base
66 66 +patched
67 67
68 68 $ echo 'patched again' > base
69 69 $ hg qrefresh 1
70 70
71 71 $ hg qdiff
72 72 diff -r e7af5904b465 1/base
73 73 --- a/1/base
74 74 +++ b/1/base
75 75 @@ -1,1 +1,1 @@
76 76 -base
77 77 +patched
78 78 diff -r e7af5904b465 2/base
79 79 --- a/2/base
80 80 +++ b/2/base
81 81 @@ -1,1 +1,1 @@
82 82 -base
83 83 +patched
84 84
85 85 $ hg qdiff .
86 86 diff -r e7af5904b465 1/base
87 87 --- a/1/base
88 88 +++ b/1/base
89 89 @@ -1,1 +1,1 @@
90 90 -base
91 91 +patched
92 92 diff -r e7af5904b465 2/base
93 93 --- a/2/base
94 94 +++ b/2/base
95 95 @@ -1,1 +1,1 @@
96 96 -base
97 97 +patched
98 98
99 99 $ cat .hg/patches/mqbase
100 100 # HG changeset patch
101 101 # Parent e7af5904b465cd1f4f3cf6b26fe14e8db6f63eaa
102 102 mqbase
103 103
104 104 diff -r e7af5904b465 1/base
105 105 --- a/1/base
106 106 +++ b/1/base
107 107 @@ -1,1 +1,1 @@
108 108 -base
109 109 +patched
110 110
111 111 qrefresh . in subdir:
112 112
113 113 $ ( cd 1 ; hg qrefresh . )
114 114
115 115 $ hg qdiff
116 116 diff -r e7af5904b465 1/base
117 117 --- a/1/base
118 118 +++ b/1/base
119 119 @@ -1,1 +1,1 @@
120 120 -base
121 121 +patched
122 122 diff -r e7af5904b465 2/base
123 123 --- a/2/base
124 124 +++ b/2/base
125 125 @@ -1,1 +1,1 @@
126 126 -base
127 127 +patched
128 128
129 129 $ hg qdiff .
130 130 diff -r e7af5904b465 1/base
131 131 --- a/1/base
132 132 +++ b/1/base
133 133 @@ -1,1 +1,1 @@
134 134 -base
135 135 +patched
136 136 diff -r e7af5904b465 2/base
137 137 --- a/2/base
138 138 +++ b/2/base
139 139 @@ -1,1 +1,1 @@
140 140 -base
141 141 +patched
142 142
143 143 $ cat .hg/patches/mqbase
144 144 # HG changeset patch
145 145 # Parent e7af5904b465cd1f4f3cf6b26fe14e8db6f63eaa
146 146 mqbase
147 147
148 148 diff -r e7af5904b465 1/base
149 149 --- a/1/base
150 150 +++ b/1/base
151 151 @@ -1,1 +1,1 @@
152 152 -base
153 153 +patched
154 154
155 155 qrefresh in hg-root again:
156 156
157 157 $ hg qrefresh
158 158
159 159 $ hg qdiff
160 160 diff -r e7af5904b465 1/base
161 161 --- a/1/base
162 162 +++ b/1/base
163 163 @@ -1,1 +1,1 @@
164 164 -base
165 165 +patched
166 166 diff -r e7af5904b465 2/base
167 167 --- a/2/base
168 168 +++ b/2/base
169 169 @@ -1,1 +1,1 @@
170 170 -base
171 171 +patched
172 172
173 173 $ hg qdiff .
174 174 diff -r e7af5904b465 1/base
175 175 --- a/1/base
176 176 +++ b/1/base
177 177 @@ -1,1 +1,1 @@
178 178 -base
179 179 +patched
180 180 diff -r e7af5904b465 2/base
181 181 --- a/2/base
182 182 +++ b/2/base
183 183 @@ -1,1 +1,1 @@
184 184 -base
185 185 +patched
186 186
187 187 $ cat .hg/patches/mqbase
188 188 # HG changeset patch
189 189 # Parent e7af5904b465cd1f4f3cf6b26fe14e8db6f63eaa
190 190 mqbase
191 191
192 192 diff -r e7af5904b465 1/base
193 193 --- a/1/base
194 194 +++ b/1/base
195 195 @@ -1,1 +1,1 @@
196 196 -base
197 197 +patched
198 198 diff -r e7af5904b465 2/base
199 199 --- a/2/base
200 200 +++ b/2/base
201 201 @@ -1,1 +1,1 @@
202 202 -base
203 203 +patched
204 204
205 205
206 206 qrefresh --short tests:
207 207
208 208 $ echo 'orphan' > orphanchild
209 209 $ hg add orphanchild
210 210 $ hg qrefresh nonexistingfilename # clear patch
211 211 $ hg qrefresh --short 1/base
212 212 $ hg qrefresh --short 2/base
213 213
214 214 $ hg qdiff
215 215 diff -r e7af5904b465 1/base
216 216 --- a/1/base
217 217 +++ b/1/base
218 218 @@ -1,1 +1,1 @@
219 219 -base
220 220 +patched
221 221 diff -r e7af5904b465 2/base
222 222 --- a/2/base
223 223 +++ b/2/base
224 224 @@ -1,1 +1,1 @@
225 225 -base
226 226 +patched
227 227 diff -r e7af5904b465 orphanchild
228 228 --- /dev/null
229 229 +++ b/orphanchild
230 230 @@ -0,0 +1,1 @@
231 231 +orphan
232 232
233 233 $ cat .hg/patches/mqbase
234 234 # HG changeset patch
235 235 # Parent e7af5904b465cd1f4f3cf6b26fe14e8db6f63eaa
236 236 mqbase
237 237
238 238 diff -r e7af5904b465 1/base
239 239 --- a/1/base
240 240 +++ b/1/base
241 241 @@ -1,1 +1,1 @@
242 242 -base
243 243 +patched
244 244 diff -r e7af5904b465 2/base
245 245 --- a/2/base
246 246 +++ b/2/base
247 247 @@ -1,1 +1,1 @@
248 248 -base
249 249 +patched
250 250
251 251 $ hg st
252 252 A orphanchild
253 253 ? base
254 254
255 255 diff shows what is not in patch:
256 256
257 257 $ hg diff
258 258 diff -r ???????????? orphanchild (glob)
259 259 --- /dev/null
260 260 +++ b/orphanchild
261 261 @@ -0,0 +1,1 @@
262 262 +orphan
263 263
264 264 Before starting exclusive tests:
265 265
266 266 $ cat .hg/patches/mqbase
267 267 # HG changeset patch
268 268 # Parent e7af5904b465cd1f4f3cf6b26fe14e8db6f63eaa
269 269 mqbase
270 270
271 271 diff -r e7af5904b465 1/base
272 272 --- a/1/base
273 273 +++ b/1/base
274 274 @@ -1,1 +1,1 @@
275 275 -base
276 276 +patched
277 277 diff -r e7af5904b465 2/base
278 278 --- a/2/base
279 279 +++ b/2/base
280 280 @@ -1,1 +1,1 @@
281 281 -base
282 282 +patched
283 283
284 284 Exclude 2/base:
285 285
286 286 $ hg qref -s -X 2/base
287 287
288 288 $ cat .hg/patches/mqbase
289 289 # HG changeset patch
290 290 # Parent e7af5904b465cd1f4f3cf6b26fe14e8db6f63eaa
291 291 mqbase
292 292
293 293 diff -r e7af5904b465 1/base
294 294 --- a/1/base
295 295 +++ b/1/base
296 296 @@ -1,1 +1,1 @@
297 297 -base
298 298 +patched
299 299
300 300 status shows 2/base as dirty:
301 301
302 302 $ hg status
303 303 M 2/base
304 304 A orphanchild
305 305 ? base
306 306
307 307 Remove 1/base and add 2/base again but not orphanchild:
308 308
309 309 $ hg qref -s -X orphanchild -X 1/base 2/base orphanchild
310 310
311 311 $ cat .hg/patches/mqbase
312 312 # HG changeset patch
313 313 # Parent e7af5904b465cd1f4f3cf6b26fe14e8db6f63eaa
314 314 mqbase
315 315
316 316 diff -r e7af5904b465 2/base
317 317 --- a/2/base
318 318 +++ b/2/base
319 319 @@ -1,1 +1,1 @@
320 320 -base
321 321 +patched
322 322
323 323 Add 1/base with include filter - and thus remove 2/base from patch:
324 324
325 325 $ hg qref -s -I 1/ o* */*
326 326
327 327 $ cat .hg/patches/mqbase
328 328 # HG changeset patch
329 329 # Parent e7af5904b465cd1f4f3cf6b26fe14e8db6f63eaa
330 330 mqbase
331 331
332 332 diff -r e7af5904b465 1/base
333 333 --- a/1/base
334 334 +++ b/1/base
335 335 @@ -1,1 +1,1 @@
336 336 -base
337 337 +patched
338 338
339 339 $ cd ..
340 340
341 341
342 342 Test qrefresh --git losing copy metadata:
343 343
344 344 $ hg init repo
345 345 $ cd repo
346 346
347 347 $ echo "[diff]" >> .hg/hgrc
348 348 $ echo "git=True" >> .hg/hgrc
349 349 $ echo a > a
350 350
351 351 $ hg ci -Am adda
352 352 adding a
353 353 $ hg copy a ab
354 354 $ echo b >> ab
355 355 $ hg copy a ac
356 356 $ echo c >> ac
357 357
358 358 Capture changes:
359 359
360 360 $ hg qnew -f p1
361 361
362 362 $ hg qdiff
363 363 diff --git a/a b/ab
364 364 copy from a
365 365 copy to ab
366 366 --- a/a
367 367 +++ b/ab
368 368 @@ -1,1 +1,2 @@
369 369 a
370 370 +b
371 371 diff --git a/a b/ac
372 372 copy from a
373 373 copy to ac
374 374 --- a/a
375 375 +++ b/ac
376 376 @@ -1,1 +1,2 @@
377 377 a
378 378 +c
379 379
380 380 Refresh and check changes again:
381 381
382 382 $ hg qrefresh
383 383
384 384 $ hg qdiff
385 385 diff --git a/a b/ab
386 386 copy from a
387 387 copy to ab
388 388 --- a/a
389 389 +++ b/ab
390 390 @@ -1,1 +1,2 @@
391 391 a
392 392 +b
393 393 diff --git a/a b/ac
394 394 copy from a
395 395 copy to ac
396 396 --- a/a
397 397 +++ b/ac
398 398 @@ -1,1 +1,2 @@
399 399 a
400 400 +c
401 401
402 402 $ cd ..
403 403
404 404
405 405 Issue1441: qrefresh confused after hg rename:
406 406
407 407 $ hg init repo-1441
408 408 $ cd repo-1441
409 409 $ echo a > a
410 410 $ hg add a
411 411 $ hg qnew -f p
412 412 $ hg mv a b
413 413 $ hg qrefresh
414 414
415 415 $ hg qdiff
416 416 diff -r 000000000000 b
417 417 --- /dev/null
418 418 +++ b/b
419 419 @@ -0,0 +1,1 @@
420 420 +a
421 421
422 422 $ cd ..
423 423
424 424
425 425 Issue2025: qrefresh does not honor filtering options when tip !=
426 426 qtip:
427 427
428 428 $ hg init repo-2025
429 429 $ cd repo-2025
430 430 $ echo a > a
431 431 $ echo b > b
432 432 $ hg ci -qAm addab
433 433 $ echo a >> a
434 434 $ echo b >> b
435 435 $ hg qnew -f patch
436 436 $ hg up -qC 0
437 437 $ echo c > c
438 438 $ hg ci -qAm addc
439 439 $ hg up -qC 1
440 440
441 441 refresh with tip != qtip:
442 442
443 443 $ hg --config diff.nodates=1 qrefresh -I b
444 444
445 445 $ hg st
446 446 M a
447 447
448 448 $ cat b
449 449 b
450 450 b
451 451
452 452 $ cat .hg/patches/patch
453 453 # HG changeset patch
454 454 # Parent 1a60229be7ac3e4a7f647508e99b87bef1f03593
455 455
456 456 diff -r 1a60229be7ac b
457 457 --- a/b
458 458 +++ b/b
459 459 @@ -1,1 +1,2 @@
460 460 b
461 461 +b
462 462
463 463 $ cd ..
464 464
465 465
466 466 Issue1441 with git patches:
467 467
468 468 $ hg init repo-1441-git
469 469 $ cd repo-1441-git
470 470
471 471 $ echo "[diff]" >> .hg/hgrc
472 472 $ echo "git=True" >> .hg/hgrc
473 473
474 474 $ echo a > a
475 475 $ hg add a
476 476 $ hg qnew -f p
477 477 $ hg mv a b
478 478 $ hg qrefresh
479 479
480 480 $ hg qdiff --nodates
481 481 diff --git a/b b/b
482 482 new file mode 100644
483 483 --- /dev/null
484 484 +++ b/b
485 485 @@ -0,0 +1,1 @@
486 486 +a
487 487
488 488 $ cd ..
489 489
490 490 Refresh with bad usernames. Mercurial used to abort on bad usernames,
491 491 but only after writing the bad name into the patch.
492 492
493 493 $ hg init bad-usernames
494 494 $ cd bad-usernames
495 495 $ touch a
496 496 $ hg add a
497 497 $ hg qnew a
498 498 $ hg qrefresh -u 'foo
499 499 > bar'
500 500 transaction abort!
501 501 rollback completed
502 502 refresh interrupted while patch was popped! (revert --all, qpush to recover)
503 503 abort: username 'foo\nbar' contains a newline!
504 504 [255]
505 $ rm a
505 506 $ cat .hg/patches/a
506 507 # HG changeset patch
507 508 # Parent 0000000000000000000000000000000000000000
508 509 diff --git a/a b/a
509 510 new file mode 100644
510 511 $ hg qpush
511 512 applying a
512 513 now at: a
513 514 $ hg qrefresh -u ' '
514 515 transaction abort!
515 516 rollback completed
516 517 refresh interrupted while patch was popped! (revert --all, qpush to recover)
517 518 abort: empty username!
518 519 [255]
519 520 $ cat .hg/patches/a
520 521 # HG changeset patch
521 522 # Parent 0000000000000000000000000000000000000000
522 523 diff --git a/a b/a
523 524 new file mode 100644
524 525 $ cd ..
@@ -1,121 +1,123 b''
1 1 $ "$TESTDIR/hghave" symlink || exit 80
2 2
3 3 $ echo "[extensions]" >> $HGRCPATH
4 4 $ echo "mq=" >> $HGRCPATH
5 5
6 6 $ hg init
7 7 $ hg qinit
8 8 $ hg qnew base.patch
9 9 $ echo aaa > a
10 10 $ echo bbb > b
11 11 $ echo ccc > c
12 12 $ hg add a b c
13 13 $ hg qrefresh
14 14 $ $TESTDIR/readlink.py a
15 15 a -> a not a symlink
16 16
17 17
18 18 test replacing a file with a symlink
19 19
20 20 $ hg qnew symlink.patch
21 21 $ rm a
22 22 $ ln -s b a
23 23 $ hg qrefresh --git
24 24 $ $TESTDIR/readlink.py a
25 25 a -> b
26 26
27 27 $ hg qpop
28 28 popping symlink.patch
29 29 now at: base.patch
30 30 $ hg qpush
31 31 applying symlink.patch
32 32 now at: symlink.patch
33 33 $ $TESTDIR/readlink.py a
34 34 a -> b
35 35
36 36
37 37 test updating a symlink
38 38
39 39 $ rm a
40 40 $ ln -s c a
41 41 $ hg qnew --git -f updatelink
42 42 $ $TESTDIR/readlink.py a
43 43 a -> c
44 44 $ hg qpop
45 45 popping updatelink
46 46 now at: symlink.patch
47 47 $ hg qpush --debug
48 48 applying updatelink
49 49 patching file a
50 50 a
51 51 now at: updatelink
52 52 $ $TESTDIR/readlink.py a
53 53 a -> c
54 54 $ hg st
55 55
56 56
57 57 test replacing a symlink with a file
58 58
59 59 $ ln -s c s
60 60 $ hg add s
61 61 $ hg qnew --git -f addlink
62 62 $ rm s
63 63 $ echo sss > s
64 64 $ hg qnew --git -f replacelinkwithfile
65 65 $ hg qpop
66 66 popping replacelinkwithfile
67 67 now at: addlink
68 68 $ hg qpush
69 69 applying replacelinkwithfile
70 70 now at: replacelinkwithfile
71 71 $ cat s
72 72 sss
73 73 $ hg st
74 74
75 75
76 76 test symlink removal
77 77
78 78 $ hg qnew removesl.patch
79 79 $ hg rm a
80 80 $ hg qrefresh --git
81 81 $ hg qpop
82 82 popping removesl.patch
83 83 now at: replacelinkwithfile
84 84 $ hg qpush
85 85 applying removesl.patch
86 86 now at: removesl.patch
87 87 $ hg st -c
88 88 C b
89 89 C c
90 90 C s
91 91
92 92 replace broken symlink with another broken symlink
93 93
94 94 $ ln -s linka linka
95 95 $ hg add linka
96 96 $ hg qnew link
97 97 $ hg mv linka linkb
98 98 $ rm linkb
99 99 $ ln -s linkb linkb
100 100 $ hg qnew movelink
101 101 $ hg qpop
102 102 popping movelink
103 103 now at: link
104 104 $ hg qpush
105 105 applying movelink
106 106 now at: movelink
107 107 $ $TESTDIR/readlink.py linkb
108 108 linkb -> linkb
109 109
110 110 check patch does not overwrite untracked symlinks
111 111
112 112 $ hg qpop
113 113 popping movelink
114 114 now at: link
115 115 $ ln -s linkbb linkb
116 116 $ hg qpush
117 117 applying movelink
118 cannot create linkb: destination already exists
119 1 out of 1 hunks FAILED -- saving rejects to file linkb.rej
118 120 patch failed, unable to continue (try -v)
119 121 patch failed, rejects left in working dir
120 122 errors during apply, please fix and refresh movelink
121 123 [2]
General Comments 0
You need to be logged in to leave comments. Login now