Mercurial > traipse_dev
comparison upmana/mercurial/patch.py @ 121:496dbf12a6cb alpha
Traipse Alpha 'OpenRPG' {091030-00}
Traipse is a distribution of OpenRPG that is designed to be easy to setup and go.
Traipse also makes it easy for developers to work on code without fear of
sacrifice. 'Ornery-Orc' continues the trend of 'Grumpy' and adds fixes to the
code. 'Ornery-Orc's main goal is to offer more advanced features and enhance the
productivity of the user.
Update Summary (Cleaning up for Beta):
Adds Bookmarks (Alpha) with cool Smiley Star and Plus Symbol images!
Changes made to the map for increased portability. SnowDog has changes planned in
Core, though.
Added an initial push to the BCG. Not much to see, just shows off how it is
re-writing Main code.
Fix to remote admin commands
Minor fix to texted based server, works in /System/ folder
Some Core changes to gametree to correctly disply Pretty Print, thanks David!
Fix to Splitter Nodes not being created.
Added images to Plugin Control panel for Autostart feature
Fix to massive amounts of images loading; from Core
fix to gsclient so with_statement imports
Added 'boot' command to remote admin
Prep work in Pass tool for remote admin rankings and different passwords, ei,
Server, Admin, Moderator, etc.
Remote Admin Commands more organized, more prep work.
Added Confirmation window for sent nodes.
Minor changes to allow for portability to an OpenSUSE linux OS (hopefully without
breaking)
{091028}
Made changes to gametree to start working with Element Tree, mostly from Core
Minor changes to Map to start working with Element Tree, from Core
Preliminary changes to map efficiency, from FlexiRPG
Miniatures Layer pop up box allows users to turn off Mini labels, from FlexiRPG
Changes to main.py to start working with Element Tree
{091029}
Changes made to server to start working with Element Tree.
Changes made to Meta Server Lib. Prepping test work for a multi meta network
page.
Minor bug fixed with mini to gametree
Zoom Mouse plugin added.
{091030}
Getting ready for Beta. Server needs debugging so Alpha remains bugged.
Plugin UI code cleaned. Auto start works with a graphic, pop-up asks to enable or
disable plugin.
Update Manager now has a partially working Status Bar. Status Bar captures
terminal text, so Merc out put is visible. Manifest.xml file, will be renamed, is
now much cleaner.
Debug Console has a clear button and a Report Bug button. Prep work for a Term2Win
class in Debug Console.
Known: Current Alpha fails in Windows.
author | sirebral |
---|---|
date | Fri, 30 Oct 2009 22:21:40 -0500 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
120:d86e762a994f | 121:496dbf12a6cb |
---|---|
1 # patch.py - patch file parsing routines | |
2 # | |
3 # Copyright 2006 Brendan Cully <brendan@kublai.com> | |
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com> | |
5 # | |
6 # This software may be used and distributed according to the terms of the | |
7 # GNU General Public License version 2, incorporated herein by reference. | |
8 | |
9 from i18n import _ | |
10 from node import hex, nullid, short | |
11 import base85, cmdutil, mdiff, util, diffhelpers, copies | |
12 import cStringIO, email.Parser, os, re, math | |
13 import sys, tempfile, zlib | |
14 | |
15 gitre = re.compile('diff --git a/(.*) b/(.*)') | |
16 | |
17 class PatchError(Exception): | |
18 pass | |
19 | |
20 class NoHunks(PatchError): | |
21 pass | |
22 | |
23 # helper functions | |
24 | |
25 def copyfile(src, dst, basedir): | |
26 abssrc, absdst = [util.canonpath(basedir, basedir, x) for x in [src, dst]] | |
27 if os.path.exists(absdst): | |
28 raise util.Abort(_("cannot create %s: destination already exists") % | |
29 dst) | |
30 | |
31 dstdir = os.path.dirname(absdst) | |
32 if dstdir and not os.path.isdir(dstdir): | |
33 try: | |
34 os.makedirs(dstdir) | |
35 except IOError: | |
36 raise util.Abort( | |
37 _("cannot create %s: unable to create destination directory") | |
38 % dst) | |
39 | |
40 util.copyfile(abssrc, absdst) | |
41 | |
42 # public functions | |
43 | |
44 def extract(ui, fileobj): | |
45 '''extract patch from data read from fileobj. | |
46 | |
47 patch can be a normal patch or contained in an email message. | |
48 | |
49 return tuple (filename, message, user, date, node, p1, p2). | |
50 Any item in the returned tuple can be None. If filename is None, | |
51 fileobj did not contain a patch. Caller must unlink filename when done.''' | |
52 | |
53 # attempt to detect the start of a patch | |
54 # (this heuristic is borrowed from quilt) | |
55 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' | |
56 r'retrieving revision [0-9]+(\.[0-9]+)*$|' | |
57 r'(---|\*\*\*)[ \t])', re.MULTILINE) | |
58 | |
59 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-') | |
60 tmpfp = os.fdopen(fd, 'w') | |
61 try: | |
62 msg = email.Parser.Parser().parse(fileobj) | |
63 | |
64 subject = msg['Subject'] | |
65 user = msg['From'] | |
66 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '') | |
67 # should try to parse msg['Date'] | |
68 date = None | |
69 nodeid = None | |
70 branch = None | |
71 parents = [] | |
72 | |
73 if subject: | |
74 if subject.startswith('[PATCH'): | |
75 pend = subject.find(']') | |
76 if pend >= 0: | |
77 subject = subject[pend+1:].lstrip() | |
78 subject = subject.replace('\n\t', ' ') | |
79 ui.debug('Subject: %s\n' % subject) | |
80 if user: | |
81 ui.debug('From: %s\n' % user) | |
82 diffs_seen = 0 | |
83 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch') | |
84 message = '' | |
85 for part in msg.walk(): | |
86 content_type = part.get_content_type() | |
87 ui.debug('Content-Type: %s\n' % content_type) | |
88 if content_type not in ok_types: | |
89 continue | |
90 payload = part.get_payload(decode=True) | |
91 m = diffre.search(payload) | |
92 if m: | |
93 hgpatch = False | |
94 ignoretext = False | |
95 | |
96 ui.debug(_('found patch at byte %d\n') % m.start(0)) | |
97 diffs_seen += 1 | |
98 cfp = cStringIO.StringIO() | |
99 for line in payload[:m.start(0)].splitlines(): | |
100 if line.startswith('# HG changeset patch'): | |
101 ui.debug(_('patch generated by hg export\n')) | |
102 hgpatch = True | |
103 # drop earlier commit message content | |
104 cfp.seek(0) | |
105 cfp.truncate() | |
106 subject = None | |
107 elif hgpatch: | |
108 if line.startswith('# User '): | |
109 user = line[7:] | |
110 ui.debug('From: %s\n' % user) | |
111 elif line.startswith("# Date "): | |
112 date = line[7:] | |
113 elif line.startswith("# Branch "): | |
114 branch = line[9:] | |
115 elif line.startswith("# Node ID "): | |
116 nodeid = line[10:] | |
117 elif line.startswith("# Parent "): | |
118 parents.append(line[10:]) | |
119 elif line == '---' and gitsendmail: | |
120 ignoretext = True | |
121 if not line.startswith('# ') and not ignoretext: | |
122 cfp.write(line) | |
123 cfp.write('\n') | |
124 message = cfp.getvalue() | |
125 if tmpfp: | |
126 tmpfp.write(payload) | |
127 if not payload.endswith('\n'): | |
128 tmpfp.write('\n') | |
129 elif not diffs_seen and message and content_type == 'text/plain': | |
130 message += '\n' + payload | |
131 except: | |
132 tmpfp.close() | |
133 os.unlink(tmpname) | |
134 raise | |
135 | |
136 if subject and not message.startswith(subject): | |
137 message = '%s\n%s' % (subject, message) | |
138 tmpfp.close() | |
139 if not diffs_seen: | |
140 os.unlink(tmpname) | |
141 return None, message, user, date, branch, None, None, None | |
142 p1 = parents and parents.pop(0) or None | |
143 p2 = parents and parents.pop(0) or None | |
144 return tmpname, message, user, date, branch, nodeid, p1, p2 | |
145 | |
146 GP_PATCH = 1 << 0 # we have to run patch | |
147 GP_FILTER = 1 << 1 # there's some copy/rename operation | |
148 GP_BINARY = 1 << 2 # there's a binary patch | |
149 | |
150 class patchmeta(object): | |
151 """Patched file metadata | |
152 | |
153 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY | |
154 or COPY. 'path' is patched file path. 'oldpath' is set to the | |
155 origin file when 'op' is either COPY or RENAME, None otherwise. If | |
156 file mode is changed, 'mode' is a tuple (islink, isexec) where | |
157 'islink' is True if the file is a symlink and 'isexec' is True if | |
158 the file is executable. Otherwise, 'mode' is None. | |
159 """ | |
160 def __init__(self, path): | |
161 self.path = path | |
162 self.oldpath = None | |
163 self.mode = None | |
164 self.op = 'MODIFY' | |
165 self.lineno = 0 | |
166 self.binary = False | |
167 | |
168 def setmode(self, mode): | |
169 islink = mode & 020000 | |
170 isexec = mode & 0100 | |
171 self.mode = (islink, isexec) | |
172 | |
173 def readgitpatch(lr): | |
174 """extract git-style metadata about patches from <patchname>""" | |
175 | |
176 # Filter patch for git information | |
177 gp = None | |
178 gitpatches = [] | |
179 # Can have a git patch with only metadata, causing patch to complain | |
180 dopatch = 0 | |
181 | |
182 lineno = 0 | |
183 for line in lr: | |
184 lineno += 1 | |
185 if line.startswith('diff --git'): | |
186 m = gitre.match(line) | |
187 if m: | |
188 if gp: | |
189 gitpatches.append(gp) | |
190 src, dst = m.group(1, 2) | |
191 gp = patchmeta(dst) | |
192 gp.lineno = lineno | |
193 elif gp: | |
194 if line.startswith('--- '): | |
195 if gp.op in ('COPY', 'RENAME'): | |
196 dopatch |= GP_FILTER | |
197 gitpatches.append(gp) | |
198 gp = None | |
199 dopatch |= GP_PATCH | |
200 continue | |
201 if line.startswith('rename from '): | |
202 gp.op = 'RENAME' | |
203 gp.oldpath = line[12:].rstrip() | |
204 elif line.startswith('rename to '): | |
205 gp.path = line[10:].rstrip() | |
206 elif line.startswith('copy from '): | |
207 gp.op = 'COPY' | |
208 gp.oldpath = line[10:].rstrip() | |
209 elif line.startswith('copy to '): | |
210 gp.path = line[8:].rstrip() | |
211 elif line.startswith('deleted file'): | |
212 gp.op = 'DELETE' | |
213 # is the deleted file a symlink? | |
214 gp.setmode(int(line.rstrip()[-6:], 8)) | |
215 elif line.startswith('new file mode '): | |
216 gp.op = 'ADD' | |
217 gp.setmode(int(line.rstrip()[-6:], 8)) | |
218 elif line.startswith('new mode '): | |
219 gp.setmode(int(line.rstrip()[-6:], 8)) | |
220 elif line.startswith('GIT binary patch'): | |
221 dopatch |= GP_BINARY | |
222 gp.binary = True | |
223 if gp: | |
224 gitpatches.append(gp) | |
225 | |
226 if not gitpatches: | |
227 dopatch = GP_PATCH | |
228 | |
229 return (dopatch, gitpatches) | |
230 | |
231 class linereader(object): | |
232 # simple class to allow pushing lines back into the input stream | |
233 def __init__(self, fp, textmode=False): | |
234 self.fp = fp | |
235 self.buf = [] | |
236 self.textmode = textmode | |
237 | |
238 def push(self, line): | |
239 if line is not None: | |
240 self.buf.append(line) | |
241 | |
242 def readline(self): | |
243 if self.buf: | |
244 l = self.buf[0] | |
245 del self.buf[0] | |
246 return l | |
247 l = self.fp.readline() | |
248 if self.textmode and l.endswith('\r\n'): | |
249 l = l[:-2] + '\n' | |
250 return l | |
251 | |
252 def __iter__(self): | |
253 while 1: | |
254 l = self.readline() | |
255 if not l: | |
256 break | |
257 yield l | |
258 | |
259 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1 | |
260 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@') | |
261 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)') | |
262 | |
263 class patchfile(object): | |
264 def __init__(self, ui, fname, opener, missing=False, eol=None): | |
265 self.fname = fname | |
266 self.eol = eol | |
267 self.opener = opener | |
268 self.ui = ui | |
269 self.lines = [] | |
270 self.exists = False | |
271 self.missing = missing | |
272 if not missing: | |
273 try: | |
274 self.lines = self.readlines(fname) | |
275 self.exists = True | |
276 except IOError: | |
277 pass | |
278 else: | |
279 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname) | |
280 | |
281 self.hash = {} | |
282 self.dirty = 0 | |
283 self.offset = 0 | |
284 self.rej = [] | |
285 self.fileprinted = False | |
286 self.printfile(False) | |
287 self.hunks = 0 | |
288 | |
289 def readlines(self, fname): | |
290 fp = self.opener(fname, 'r') | |
291 try: | |
292 return list(linereader(fp, self.eol is not None)) | |
293 finally: | |
294 fp.close() | |
295 | |
296 def writelines(self, fname, lines): | |
297 fp = self.opener(fname, 'w') | |
298 try: | |
299 if self.eol and self.eol != '\n': | |
300 for l in lines: | |
301 if l and l[-1] == '\n': | |
302 l = l[:-1] + self.eol | |
303 fp.write(l) | |
304 else: | |
305 fp.writelines(lines) | |
306 finally: | |
307 fp.close() | |
308 | |
309 def unlink(self, fname): | |
310 os.unlink(fname) | |
311 | |
312 def printfile(self, warn): | |
313 if self.fileprinted: | |
314 return | |
315 if warn or self.ui.verbose: | |
316 self.fileprinted = True | |
317 s = _("patching file %s\n") % self.fname | |
318 if warn: | |
319 self.ui.warn(s) | |
320 else: | |
321 self.ui.note(s) | |
322 | |
323 | |
324 def findlines(self, l, linenum): | |
325 # looks through the hash and finds candidate lines. The | |
326 # result is a list of line numbers sorted based on distance | |
327 # from linenum | |
328 def sorter(a, b): | |
329 vala = abs(a - linenum) | |
330 valb = abs(b - linenum) | |
331 return cmp(vala, valb) | |
332 | |
333 try: | |
334 cand = self.hash[l] | |
335 except: | |
336 return [] | |
337 | |
338 if len(cand) > 1: | |
339 # resort our list of potentials forward then back. | |
340 cand.sort(sorter) | |
341 return cand | |
342 | |
343 def hashlines(self): | |
344 self.hash = {} | |
345 for x, s in enumerate(self.lines): | |
346 self.hash.setdefault(s, []).append(x) | |
347 | |
348 def write_rej(self): | |
349 # our rejects are a little different from patch(1). This always | |
350 # creates rejects in the same form as the original patch. A file | |
351 # header is inserted so that you can run the reject through patch again | |
352 # without having to type the filename. | |
353 | |
354 if not self.rej: | |
355 return | |
356 | |
357 fname = self.fname + ".rej" | |
358 self.ui.warn( | |
359 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") % | |
360 (len(self.rej), self.hunks, fname)) | |
361 | |
362 def rejlines(): | |
363 base = os.path.basename(self.fname) | |
364 yield "--- %s\n+++ %s\n" % (base, base) | |
365 for x in self.rej: | |
366 for l in x.hunk: | |
367 yield l | |
368 if l[-1] != '\n': | |
369 yield "\n\ No newline at end of file\n" | |
370 | |
371 self.writelines(fname, rejlines()) | |
372 | |
373 def write(self, dest=None): | |
374 if not self.dirty: | |
375 return | |
376 if not dest: | |
377 dest = self.fname | |
378 self.writelines(dest, self.lines) | |
379 | |
380 def close(self): | |
381 self.write() | |
382 self.write_rej() | |
383 | |
384 def apply(self, h, reverse): | |
385 if not h.complete(): | |
386 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") % | |
387 (h.number, h.desc, len(h.a), h.lena, len(h.b), | |
388 h.lenb)) | |
389 | |
390 self.hunks += 1 | |
391 if reverse: | |
392 h.reverse() | |
393 | |
394 if self.missing: | |
395 self.rej.append(h) | |
396 return -1 | |
397 | |
398 if self.exists and h.createfile(): | |
399 self.ui.warn(_("file %s already exists\n") % self.fname) | |
400 self.rej.append(h) | |
401 return -1 | |
402 | |
403 if isinstance(h, githunk): | |
404 if h.rmfile(): | |
405 self.unlink(self.fname) | |
406 else: | |
407 self.lines[:] = h.new() | |
408 self.offset += len(h.new()) | |
409 self.dirty = 1 | |
410 return 0 | |
411 | |
412 # fast case first, no offsets, no fuzz | |
413 old = h.old() | |
414 # patch starts counting at 1 unless we are adding the file | |
415 if h.starta == 0: | |
416 start = 0 | |
417 else: | |
418 start = h.starta + self.offset - 1 | |
419 orig_start = start | |
420 if diffhelpers.testhunk(old, self.lines, start) == 0: | |
421 if h.rmfile(): | |
422 self.unlink(self.fname) | |
423 else: | |
424 self.lines[start : start + h.lena] = h.new() | |
425 self.offset += h.lenb - h.lena | |
426 self.dirty = 1 | |
427 return 0 | |
428 | |
429 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it | |
430 self.hashlines() | |
431 if h.hunk[-1][0] != ' ': | |
432 # if the hunk tried to put something at the bottom of the file | |
433 # override the start line and use eof here | |
434 search_start = len(self.lines) | |
435 else: | |
436 search_start = orig_start | |
437 | |
438 for fuzzlen in xrange(3): | |
439 for toponly in [ True, False ]: | |
440 old = h.old(fuzzlen, toponly) | |
441 | |
442 cand = self.findlines(old[0][1:], search_start) | |
443 for l in cand: | |
444 if diffhelpers.testhunk(old, self.lines, l) == 0: | |
445 newlines = h.new(fuzzlen, toponly) | |
446 self.lines[l : l + len(old)] = newlines | |
447 self.offset += len(newlines) - len(old) | |
448 self.dirty = 1 | |
449 if fuzzlen: | |
450 fuzzstr = "with fuzz %d " % fuzzlen | |
451 f = self.ui.warn | |
452 self.printfile(True) | |
453 else: | |
454 fuzzstr = "" | |
455 f = self.ui.note | |
456 offset = l - orig_start - fuzzlen | |
457 if offset == 1: | |
458 msg = _("Hunk #%d succeeded at %d %s" | |
459 "(offset %d line).\n") | |
460 else: | |
461 msg = _("Hunk #%d succeeded at %d %s" | |
462 "(offset %d lines).\n") | |
463 f(msg % (h.number, l+1, fuzzstr, offset)) | |
464 return fuzzlen | |
465 self.printfile(True) | |
466 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start)) | |
467 self.rej.append(h) | |
468 return -1 | |
469 | |
470 class hunk(object): | |
471 def __init__(self, desc, num, lr, context, create=False, remove=False): | |
472 self.number = num | |
473 self.desc = desc | |
474 self.hunk = [ desc ] | |
475 self.a = [] | |
476 self.b = [] | |
477 if context: | |
478 self.read_context_hunk(lr) | |
479 else: | |
480 self.read_unified_hunk(lr) | |
481 self.create = create | |
482 self.remove = remove and not create | |
483 | |
484 def read_unified_hunk(self, lr): | |
485 m = unidesc.match(self.desc) | |
486 if not m: | |
487 raise PatchError(_("bad hunk #%d") % self.number) | |
488 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups() | |
489 if self.lena is None: | |
490 self.lena = 1 | |
491 else: | |
492 self.lena = int(self.lena) | |
493 if self.lenb is None: | |
494 self.lenb = 1 | |
495 else: | |
496 self.lenb = int(self.lenb) | |
497 self.starta = int(self.starta) | |
498 self.startb = int(self.startb) | |
499 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b) | |
500 # if we hit eof before finishing out the hunk, the last line will | |
501 # be zero length. Lets try to fix it up. | |
502 while len(self.hunk[-1]) == 0: | |
503 del self.hunk[-1] | |
504 del self.a[-1] | |
505 del self.b[-1] | |
506 self.lena -= 1 | |
507 self.lenb -= 1 | |
508 | |
509 def read_context_hunk(self, lr): | |
510 self.desc = lr.readline() | |
511 m = contextdesc.match(self.desc) | |
512 if not m: | |
513 raise PatchError(_("bad hunk #%d") % self.number) | |
514 foo, self.starta, foo2, aend, foo3 = m.groups() | |
515 self.starta = int(self.starta) | |
516 if aend is None: | |
517 aend = self.starta | |
518 self.lena = int(aend) - self.starta | |
519 if self.starta: | |
520 self.lena += 1 | |
521 for x in xrange(self.lena): | |
522 l = lr.readline() | |
523 if l.startswith('---'): | |
524 lr.push(l) | |
525 break | |
526 s = l[2:] | |
527 if l.startswith('- ') or l.startswith('! '): | |
528 u = '-' + s | |
529 elif l.startswith(' '): | |
530 u = ' ' + s | |
531 else: | |
532 raise PatchError(_("bad hunk #%d old text line %d") % | |
533 (self.number, x)) | |
534 self.a.append(u) | |
535 self.hunk.append(u) | |
536 | |
537 l = lr.readline() | |
538 if l.startswith('\ '): | |
539 s = self.a[-1][:-1] | |
540 self.a[-1] = s | |
541 self.hunk[-1] = s | |
542 l = lr.readline() | |
543 m = contextdesc.match(l) | |
544 if not m: | |
545 raise PatchError(_("bad hunk #%d") % self.number) | |
546 foo, self.startb, foo2, bend, foo3 = m.groups() | |
547 self.startb = int(self.startb) | |
548 if bend is None: | |
549 bend = self.startb | |
550 self.lenb = int(bend) - self.startb | |
551 if self.startb: | |
552 self.lenb += 1 | |
553 hunki = 1 | |
554 for x in xrange(self.lenb): | |
555 l = lr.readline() | |
556 if l.startswith('\ '): | |
557 s = self.b[-1][:-1] | |
558 self.b[-1] = s | |
559 self.hunk[hunki-1] = s | |
560 continue | |
561 if not l: | |
562 lr.push(l) | |
563 break | |
564 s = l[2:] | |
565 if l.startswith('+ ') or l.startswith('! '): | |
566 u = '+' + s | |
567 elif l.startswith(' '): | |
568 u = ' ' + s | |
569 elif len(self.b) == 0: | |
570 # this can happen when the hunk does not add any lines | |
571 lr.push(l) | |
572 break | |
573 else: | |
574 raise PatchError(_("bad hunk #%d old text line %d") % | |
575 (self.number, x)) | |
576 self.b.append(s) | |
577 while True: | |
578 if hunki >= len(self.hunk): | |
579 h = "" | |
580 else: | |
581 h = self.hunk[hunki] | |
582 hunki += 1 | |
583 if h == u: | |
584 break | |
585 elif h.startswith('-'): | |
586 continue | |
587 else: | |
588 self.hunk.insert(hunki-1, u) | |
589 break | |
590 | |
591 if not self.a: | |
592 # this happens when lines were only added to the hunk | |
593 for x in self.hunk: | |
594 if x.startswith('-') or x.startswith(' '): | |
595 self.a.append(x) | |
596 if not self.b: | |
597 # this happens when lines were only deleted from the hunk | |
598 for x in self.hunk: | |
599 if x.startswith('+') or x.startswith(' '): | |
600 self.b.append(x[1:]) | |
601 # @@ -start,len +start,len @@ | |
602 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena, | |
603 self.startb, self.lenb) | |
604 self.hunk[0] = self.desc | |
605 | |
606 def reverse(self): | |
607 self.create, self.remove = self.remove, self.create | |
608 origlena = self.lena | |
609 origstarta = self.starta | |
610 self.lena = self.lenb | |
611 self.starta = self.startb | |
612 self.lenb = origlena | |
613 self.startb = origstarta | |
614 self.a = [] | |
615 self.b = [] | |
616 # self.hunk[0] is the @@ description | |
617 for x in xrange(1, len(self.hunk)): | |
618 o = self.hunk[x] | |
619 if o.startswith('-'): | |
620 n = '+' + o[1:] | |
621 self.b.append(o[1:]) | |
622 elif o.startswith('+'): | |
623 n = '-' + o[1:] | |
624 self.a.append(n) | |
625 else: | |
626 n = o | |
627 self.b.append(o[1:]) | |
628 self.a.append(o) | |
629 self.hunk[x] = o | |
630 | |
631 def fix_newline(self): | |
632 diffhelpers.fix_newline(self.hunk, self.a, self.b) | |
633 | |
634 def complete(self): | |
635 return len(self.a) == self.lena and len(self.b) == self.lenb | |
636 | |
637 def createfile(self): | |
638 return self.starta == 0 and self.lena == 0 and self.create | |
639 | |
640 def rmfile(self): | |
641 return self.startb == 0 and self.lenb == 0 and self.remove | |
642 | |
643 def fuzzit(self, l, fuzz, toponly): | |
644 # this removes context lines from the top and bottom of list 'l'. It | |
645 # checks the hunk to make sure only context lines are removed, and then | |
646 # returns a new shortened list of lines. | |
647 fuzz = min(fuzz, len(l)-1) | |
648 if fuzz: | |
649 top = 0 | |
650 bot = 0 | |
651 hlen = len(self.hunk) | |
652 for x in xrange(hlen-1): | |
653 # the hunk starts with the @@ line, so use x+1 | |
654 if self.hunk[x+1][0] == ' ': | |
655 top += 1 | |
656 else: | |
657 break | |
658 if not toponly: | |
659 for x in xrange(hlen-1): | |
660 if self.hunk[hlen-bot-1][0] == ' ': | |
661 bot += 1 | |
662 else: | |
663 break | |
664 | |
665 # top and bot now count context in the hunk | |
666 # adjust them if either one is short | |
667 context = max(top, bot, 3) | |
668 if bot < context: | |
669 bot = max(0, fuzz - (context - bot)) | |
670 else: | |
671 bot = min(fuzz, bot) | |
672 if top < context: | |
673 top = max(0, fuzz - (context - top)) | |
674 else: | |
675 top = min(fuzz, top) | |
676 | |
677 return l[top:len(l)-bot] | |
678 return l | |
679 | |
680 def old(self, fuzz=0, toponly=False): | |
681 return self.fuzzit(self.a, fuzz, toponly) | |
682 | |
683 def newctrl(self): | |
684 res = [] | |
685 for x in self.hunk: | |
686 c = x[0] | |
687 if c == ' ' or c == '+': | |
688 res.append(x) | |
689 return res | |
690 | |
691 def new(self, fuzz=0, toponly=False): | |
692 return self.fuzzit(self.b, fuzz, toponly) | |
693 | |
694 class githunk(object): | |
695 """A git hunk""" | |
696 def __init__(self, gitpatch): | |
697 self.gitpatch = gitpatch | |
698 self.text = None | |
699 self.hunk = [] | |
700 | |
701 def createfile(self): | |
702 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY') | |
703 | |
704 def rmfile(self): | |
705 return self.gitpatch.op == 'DELETE' | |
706 | |
707 def complete(self): | |
708 return self.text is not None | |
709 | |
710 def new(self): | |
711 return [self.text] | |
712 | |
713 class binhunk(githunk): | |
714 'A binary patch file. Only understands literals so far.' | |
715 def __init__(self, gitpatch): | |
716 super(binhunk, self).__init__(gitpatch) | |
717 self.hunk = ['GIT binary patch\n'] | |
718 | |
719 def extract(self, lr): | |
720 line = lr.readline() | |
721 self.hunk.append(line) | |
722 while line and not line.startswith('literal '): | |
723 line = lr.readline() | |
724 self.hunk.append(line) | |
725 if not line: | |
726 raise PatchError(_('could not extract binary patch')) | |
727 size = int(line[8:].rstrip()) | |
728 dec = [] | |
729 line = lr.readline() | |
730 self.hunk.append(line) | |
731 while len(line) > 1: | |
732 l = line[0] | |
733 if l <= 'Z' and l >= 'A': | |
734 l = ord(l) - ord('A') + 1 | |
735 else: | |
736 l = ord(l) - ord('a') + 27 | |
737 dec.append(base85.b85decode(line[1:-1])[:l]) | |
738 line = lr.readline() | |
739 self.hunk.append(line) | |
740 text = zlib.decompress(''.join(dec)) | |
741 if len(text) != size: | |
742 raise PatchError(_('binary patch is %d bytes, not %d') % | |
743 len(text), size) | |
744 self.text = text | |
745 | |
746 class symlinkhunk(githunk): | |
747 """A git symlink hunk""" | |
748 def __init__(self, gitpatch, hunk): | |
749 super(symlinkhunk, self).__init__(gitpatch) | |
750 self.hunk = hunk | |
751 | |
752 def complete(self): | |
753 return True | |
754 | |
755 def fix_newline(self): | |
756 return | |
757 | |
758 def parsefilename(str): | |
759 # --- filename \t|space stuff | |
760 s = str[4:].rstrip('\r\n') | |
761 i = s.find('\t') | |
762 if i < 0: | |
763 i = s.find(' ') | |
764 if i < 0: | |
765 return s | |
766 return s[:i] | |
767 | |
768 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse): | |
769 def pathstrip(path, count=1): | |
770 pathlen = len(path) | |
771 i = 0 | |
772 if count == 0: | |
773 return '', path.rstrip() | |
774 while count > 0: | |
775 i = path.find('/', i) | |
776 if i == -1: | |
777 raise PatchError(_("unable to strip away %d dirs from %s") % | |
778 (count, path)) | |
779 i += 1 | |
780 # consume '//' in the path | |
781 while i < pathlen - 1 and path[i] == '/': | |
782 i += 1 | |
783 count -= 1 | |
784 return path[:i].lstrip(), path[i:].rstrip() | |
785 | |
786 nulla = afile_orig == "/dev/null" | |
787 nullb = bfile_orig == "/dev/null" | |
788 abase, afile = pathstrip(afile_orig, strip) | |
789 gooda = not nulla and util.lexists(afile) | |
790 bbase, bfile = pathstrip(bfile_orig, strip) | |
791 if afile == bfile: | |
792 goodb = gooda | |
793 else: | |
794 goodb = not nullb and os.path.exists(bfile) | |
795 createfunc = hunk.createfile | |
796 if reverse: | |
797 createfunc = hunk.rmfile | |
798 missing = not goodb and not gooda and not createfunc() | |
799 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the | |
800 # diff is between a file and its backup. In this case, the original | |
801 # file should be patched (see original mpatch code). | |
802 isbackup = (abase == bbase and bfile.startswith(afile)) | |
803 fname = None | |
804 if not missing: | |
805 if gooda and goodb: | |
806 fname = isbackup and afile or bfile | |
807 elif gooda: | |
808 fname = afile | |
809 | |
810 if not fname: | |
811 if not nullb: | |
812 fname = isbackup and afile or bfile | |
813 elif not nulla: | |
814 fname = afile | |
815 else: | |
816 raise PatchError(_("undefined source and destination files")) | |
817 | |
818 return fname, missing | |
819 | |
820 def scangitpatch(lr, firstline): | |
821 """ | |
822 Git patches can emit: | |
823 - rename a to b | |
824 - change b | |
825 - copy a to c | |
826 - change c | |
827 | |
828 We cannot apply this sequence as-is, the renamed 'a' could not be | |
829 found for it would have been renamed already. And we cannot copy | |
830 from 'b' instead because 'b' would have been changed already. So | |
831 we scan the git patch for copy and rename commands so we can | |
832 perform the copies ahead of time. | |
833 """ | |
834 pos = 0 | |
835 try: | |
836 pos = lr.fp.tell() | |
837 fp = lr.fp | |
838 except IOError: | |
839 fp = cStringIO.StringIO(lr.fp.read()) | |
840 gitlr = linereader(fp, lr.textmode) | |
841 gitlr.push(firstline) | |
842 (dopatch, gitpatches) = readgitpatch(gitlr) | |
843 fp.seek(pos) | |
844 return dopatch, gitpatches | |
845 | |
846 def iterhunks(ui, fp, sourcefile=None, textmode=False): | |
847 """Read a patch and yield the following events: | |
848 - ("file", afile, bfile, firsthunk): select a new target file. | |
849 - ("hunk", hunk): a new hunk is ready to be applied, follows a | |
850 "file" event. | |
851 - ("git", gitchanges): current diff is in git format, gitchanges | |
852 maps filenames to gitpatch records. Unique event. | |
853 | |
854 If textmode is True, input line-endings are normalized to LF. | |
855 """ | |
856 changed = {} | |
857 current_hunk = None | |
858 afile = "" | |
859 bfile = "" | |
860 state = None | |
861 hunknum = 0 | |
862 emitfile = False | |
863 git = False | |
864 | |
865 # our states | |
866 BFILE = 1 | |
867 context = None | |
868 lr = linereader(fp, textmode) | |
869 dopatch = True | |
870 # gitworkdone is True if a git operation (copy, rename, ...) was | |
871 # performed already for the current file. Useful when the file | |
872 # section may have no hunk. | |
873 gitworkdone = False | |
874 | |
875 while True: | |
876 newfile = False | |
877 x = lr.readline() | |
878 if not x: | |
879 break | |
880 if current_hunk: | |
881 if x.startswith('\ '): | |
882 current_hunk.fix_newline() | |
883 yield 'hunk', current_hunk | |
884 current_hunk = None | |
885 gitworkdone = False | |
886 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or | |
887 ((context is not False) and x.startswith('***************')))): | |
888 try: | |
889 if context is None and x.startswith('***************'): | |
890 context = True | |
891 gpatch = changed.get(bfile) | |
892 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD' | |
893 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE' | |
894 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove) | |
895 if remove: | |
896 gpatch = changed.get(afile[2:]) | |
897 if gpatch and gpatch.mode[0]: | |
898 current_hunk = symlinkhunk(gpatch, current_hunk) | |
899 except PatchError, err: | |
900 ui.debug(err) | |
901 current_hunk = None | |
902 continue | |
903 hunknum += 1 | |
904 if emitfile: | |
905 emitfile = False | |
906 yield 'file', (afile, bfile, current_hunk) | |
907 elif state == BFILE and x.startswith('GIT binary patch'): | |
908 current_hunk = binhunk(changed[bfile]) | |
909 hunknum += 1 | |
910 if emitfile: | |
911 emitfile = False | |
912 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk) | |
913 current_hunk.extract(lr) | |
914 elif x.startswith('diff --git'): | |
915 # check for git diff, scanning the whole patch file if needed | |
916 m = gitre.match(x) | |
917 if m: | |
918 afile, bfile = m.group(1, 2) | |
919 if not git: | |
920 git = True | |
921 dopatch, gitpatches = scangitpatch(lr, x) | |
922 yield 'git', gitpatches | |
923 for gp in gitpatches: | |
924 changed[gp.path] = gp | |
925 # else error? | |
926 # copy/rename + modify should modify target, not source | |
927 gp = changed.get(bfile) | |
928 if gp and gp.op in ('COPY', 'DELETE', 'RENAME', 'ADD'): | |
929 afile = bfile | |
930 gitworkdone = True | |
931 newfile = True | |
932 elif x.startswith('---'): | |
933 # check for a unified diff | |
934 l2 = lr.readline() | |
935 if not l2.startswith('+++'): | |
936 lr.push(l2) | |
937 continue | |
938 newfile = True | |
939 context = False | |
940 afile = parsefilename(x) | |
941 bfile = parsefilename(l2) | |
942 elif x.startswith('***'): | |
943 # check for a context diff | |
944 l2 = lr.readline() | |
945 if not l2.startswith('---'): | |
946 lr.push(l2) | |
947 continue | |
948 l3 = lr.readline() | |
949 lr.push(l3) | |
950 if not l3.startswith("***************"): | |
951 lr.push(l2) | |
952 continue | |
953 newfile = True | |
954 context = True | |
955 afile = parsefilename(x) | |
956 bfile = parsefilename(l2) | |
957 | |
958 if newfile: | |
959 emitfile = True | |
960 state = BFILE | |
961 hunknum = 0 | |
962 if current_hunk: | |
963 if current_hunk.complete(): | |
964 yield 'hunk', current_hunk | |
965 else: | |
966 raise PatchError(_("malformed patch %s %s") % (afile, | |
967 current_hunk.desc)) | |
968 | |
969 if hunknum == 0 and dopatch and not gitworkdone: | |
970 raise NoHunks | |
971 | |
972 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False, | |
973 eol=None): | |
974 """ | |
975 Reads a patch from fp and tries to apply it. | |
976 | |
977 The dict 'changed' is filled in with all of the filenames changed | |
978 by the patch. Returns 0 for a clean patch, -1 if any rejects were | |
979 found and 1 if there was any fuzz. | |
980 | |
981 If 'eol' is None, the patch content and patched file are read in | |
982 binary mode. Otherwise, line endings are ignored when patching then | |
983 normalized to 'eol' (usually '\n' or \r\n'). | |
984 """ | |
985 rejects = 0 | |
986 err = 0 | |
987 current_file = None | |
988 gitpatches = None | |
989 opener = util.opener(os.getcwd()) | |
990 textmode = eol is not None | |
991 | |
992 def closefile(): | |
993 if not current_file: | |
994 return 0 | |
995 current_file.close() | |
996 return len(current_file.rej) | |
997 | |
998 for state, values in iterhunks(ui, fp, sourcefile, textmode): | |
999 if state == 'hunk': | |
1000 if not current_file: | |
1001 continue | |
1002 current_hunk = values | |
1003 ret = current_file.apply(current_hunk, reverse) | |
1004 if ret >= 0: | |
1005 changed.setdefault(current_file.fname, None) | |
1006 if ret > 0: | |
1007 err = 1 | |
1008 elif state == 'file': | |
1009 rejects += closefile() | |
1010 afile, bfile, first_hunk = values | |
1011 try: | |
1012 if sourcefile: | |
1013 current_file = patchfile(ui, sourcefile, opener, eol=eol) | |
1014 else: | |
1015 current_file, missing = selectfile(afile, bfile, first_hunk, | |
1016 strip, reverse) | |
1017 current_file = patchfile(ui, current_file, opener, missing, eol) | |
1018 except PatchError, err: | |
1019 ui.warn(str(err) + '\n') | |
1020 current_file, current_hunk = None, None | |
1021 rejects += 1 | |
1022 continue | |
1023 elif state == 'git': | |
1024 gitpatches = values | |
1025 cwd = os.getcwd() | |
1026 for gp in gitpatches: | |
1027 if gp.op in ('COPY', 'RENAME'): | |
1028 copyfile(gp.oldpath, gp.path, cwd) | |
1029 changed[gp.path] = gp | |
1030 else: | |
1031 raise util.Abort(_('unsupported parser state: %s') % state) | |
1032 | |
1033 rejects += closefile() | |
1034 | |
1035 if rejects: | |
1036 return -1 | |
1037 return err | |
1038 | |
1039 def diffopts(ui, opts={}, untrusted=False): | |
1040 def get(key, name=None, getter=ui.configbool): | |
1041 return (opts.get(key) or | |
1042 getter('diff', name or key, None, untrusted=untrusted)) | |
1043 return mdiff.diffopts( | |
1044 text=opts.get('text'), | |
1045 git=get('git'), | |
1046 nodates=get('nodates'), | |
1047 showfunc=get('show_function', 'showfunc'), | |
1048 ignorews=get('ignore_all_space', 'ignorews'), | |
1049 ignorewsamount=get('ignore_space_change', 'ignorewsamount'), | |
1050 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'), | |
1051 context=get('unified', getter=ui.config)) | |
1052 | |
1053 def updatedir(ui, repo, patches, similarity=0): | |
1054 '''Update dirstate after patch application according to metadata''' | |
1055 if not patches: | |
1056 return | |
1057 copies = [] | |
1058 removes = set() | |
1059 cfiles = patches.keys() | |
1060 cwd = repo.getcwd() | |
1061 if cwd: | |
1062 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()] | |
1063 for f in patches: | |
1064 gp = patches[f] | |
1065 if not gp: | |
1066 continue | |
1067 if gp.op == 'RENAME': | |
1068 copies.append((gp.oldpath, gp.path)) | |
1069 removes.add(gp.oldpath) | |
1070 elif gp.op == 'COPY': | |
1071 copies.append((gp.oldpath, gp.path)) | |
1072 elif gp.op == 'DELETE': | |
1073 removes.add(gp.path) | |
1074 for src, dst in copies: | |
1075 repo.copy(src, dst) | |
1076 if (not similarity) and removes: | |
1077 repo.remove(sorted(removes), True) | |
1078 for f in patches: | |
1079 gp = patches[f] | |
1080 if gp and gp.mode: | |
1081 islink, isexec = gp.mode | |
1082 dst = repo.wjoin(gp.path) | |
1083 # patch won't create empty files | |
1084 if gp.op == 'ADD' and not os.path.exists(dst): | |
1085 flags = (isexec and 'x' or '') + (islink and 'l' or '') | |
1086 repo.wwrite(gp.path, '', flags) | |
1087 elif gp.op != 'DELETE': | |
1088 util.set_flags(dst, islink, isexec) | |
1089 cmdutil.addremove(repo, cfiles, similarity=similarity) | |
1090 files = patches.keys() | |
1091 files.extend([r for r in removes if r not in files]) | |
1092 return sorted(files) | |
1093 | |
1094 def externalpatch(patcher, args, patchname, ui, strip, cwd, files): | |
1095 """use <patcher> to apply <patchname> to the working directory. | |
1096 returns whether patch was applied with fuzz factor.""" | |
1097 | |
1098 fuzz = False | |
1099 if cwd: | |
1100 args.append('-d %s' % util.shellquote(cwd)) | |
1101 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip, | |
1102 util.shellquote(patchname))) | |
1103 | |
1104 for line in fp: | |
1105 line = line.rstrip() | |
1106 ui.note(line + '\n') | |
1107 if line.startswith('patching file '): | |
1108 pf = util.parse_patch_output(line) | |
1109 printed_file = False | |
1110 files.setdefault(pf, None) | |
1111 elif line.find('with fuzz') >= 0: | |
1112 fuzz = True | |
1113 if not printed_file: | |
1114 ui.warn(pf + '\n') | |
1115 printed_file = True | |
1116 ui.warn(line + '\n') | |
1117 elif line.find('saving rejects to file') >= 0: | |
1118 ui.warn(line + '\n') | |
1119 elif line.find('FAILED') >= 0: | |
1120 if not printed_file: | |
1121 ui.warn(pf + '\n') | |
1122 printed_file = True | |
1123 ui.warn(line + '\n') | |
1124 code = fp.close() | |
1125 if code: | |
1126 raise PatchError(_("patch command failed: %s") % | |
1127 util.explain_exit(code)[0]) | |
1128 return fuzz | |
1129 | |
1130 def internalpatch(patchobj, ui, strip, cwd, files={}, eolmode='strict'): | |
1131 """use builtin patch to apply <patchobj> to the working directory. | |
1132 returns whether patch was applied with fuzz factor.""" | |
1133 | |
1134 if eolmode is None: | |
1135 eolmode = ui.config('patch', 'eol', 'strict') | |
1136 try: | |
1137 eol = {'strict': None, 'crlf': '\r\n', 'lf': '\n'}[eolmode.lower()] | |
1138 except KeyError: | |
1139 raise util.Abort(_('Unsupported line endings type: %s') % eolmode) | |
1140 | |
1141 try: | |
1142 fp = file(patchobj, 'rb') | |
1143 except TypeError: | |
1144 fp = patchobj | |
1145 if cwd: | |
1146 curdir = os.getcwd() | |
1147 os.chdir(cwd) | |
1148 try: | |
1149 ret = applydiff(ui, fp, files, strip=strip, eol=eol) | |
1150 finally: | |
1151 if cwd: | |
1152 os.chdir(curdir) | |
1153 if ret < 0: | |
1154 raise PatchError | |
1155 return ret > 0 | |
1156 | |
1157 def patch(patchname, ui, strip=1, cwd=None, files={}, eolmode='strict'): | |
1158 """Apply <patchname> to the working directory. | |
1159 | |
1160 'eolmode' specifies how end of lines should be handled. It can be: | |
1161 - 'strict': inputs are read in binary mode, EOLs are preserved | |
1162 - 'crlf': EOLs are ignored when patching and reset to CRLF | |
1163 - 'lf': EOLs are ignored when patching and reset to LF | |
1164 - None: get it from user settings, default to 'strict' | |
1165 'eolmode' is ignored when using an external patcher program. | |
1166 | |
1167 Returns whether patch was applied with fuzz factor. | |
1168 """ | |
1169 patcher = ui.config('ui', 'patch') | |
1170 args = [] | |
1171 try: | |
1172 if patcher: | |
1173 return externalpatch(patcher, args, patchname, ui, strip, cwd, | |
1174 files) | |
1175 else: | |
1176 try: | |
1177 return internalpatch(patchname, ui, strip, cwd, files, eolmode) | |
1178 except NoHunks: | |
1179 patcher = util.find_exe('gpatch') or util.find_exe('patch') or 'patch' | |
1180 ui.debug(_('no valid hunks found; trying with %r instead\n') % | |
1181 patcher) | |
1182 if util.needbinarypatch(): | |
1183 args.append('--binary') | |
1184 return externalpatch(patcher, args, patchname, ui, strip, cwd, | |
1185 files) | |
1186 except PatchError, err: | |
1187 s = str(err) | |
1188 if s: | |
1189 raise util.Abort(s) | |
1190 else: | |
1191 raise util.Abort(_('patch failed to apply')) | |
1192 | |
1193 def b85diff(to, tn): | |
1194 '''print base85-encoded binary diff''' | |
1195 def gitindex(text): | |
1196 if not text: | |
1197 return '0' * 40 | |
1198 l = len(text) | |
1199 s = util.sha1('blob %d\0' % l) | |
1200 s.update(text) | |
1201 return s.hexdigest() | |
1202 | |
1203 def fmtline(line): | |
1204 l = len(line) | |
1205 if l <= 26: | |
1206 l = chr(ord('A') + l - 1) | |
1207 else: | |
1208 l = chr(l - 26 + ord('a') - 1) | |
1209 return '%c%s\n' % (l, base85.b85encode(line, True)) | |
1210 | |
1211 def chunk(text, csize=52): | |
1212 l = len(text) | |
1213 i = 0 | |
1214 while i < l: | |
1215 yield text[i:i+csize] | |
1216 i += csize | |
1217 | |
1218 tohash = gitindex(to) | |
1219 tnhash = gitindex(tn) | |
1220 if tohash == tnhash: | |
1221 return "" | |
1222 | |
1223 # TODO: deltas | |
1224 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' % | |
1225 (tohash, tnhash, len(tn))] | |
1226 for l in chunk(zlib.compress(tn)): | |
1227 ret.append(fmtline(l)) | |
1228 ret.append('\n') | |
1229 return ''.join(ret) | |
1230 | |
1231 def _addmodehdr(header, omode, nmode): | |
1232 if omode != nmode: | |
1233 header.append('old mode %s\n' % omode) | |
1234 header.append('new mode %s\n' % nmode) | |
1235 | |
1236 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None): | |
1237 '''yields diff of changes to files between two nodes, or node and | |
1238 working directory. | |
1239 | |
1240 if node1 is None, use first dirstate parent instead. | |
1241 if node2 is None, compare node1 with working directory.''' | |
1242 | |
1243 if opts is None: | |
1244 opts = mdiff.defaultopts | |
1245 | |
1246 if not node1: | |
1247 node1 = repo.dirstate.parents()[0] | |
1248 | |
1249 def lrugetfilectx(): | |
1250 cache = {} | |
1251 order = [] | |
1252 def getfilectx(f, ctx): | |
1253 fctx = ctx.filectx(f, filelog=cache.get(f)) | |
1254 if f not in cache: | |
1255 if len(cache) > 20: | |
1256 del cache[order.pop(0)] | |
1257 cache[f] = fctx._filelog | |
1258 else: | |
1259 order.remove(f) | |
1260 order.append(f) | |
1261 return fctx | |
1262 return getfilectx | |
1263 getfilectx = lrugetfilectx() | |
1264 | |
1265 ctx1 = repo[node1] | |
1266 ctx2 = repo[node2] | |
1267 | |
1268 if not changes: | |
1269 changes = repo.status(ctx1, ctx2, match=match) | |
1270 modified, added, removed = changes[:3] | |
1271 | |
1272 if not modified and not added and not removed: | |
1273 return | |
1274 | |
1275 date1 = util.datestr(ctx1.date()) | |
1276 man1 = ctx1.manifest() | |
1277 | |
1278 if repo.ui.quiet: | |
1279 r = None | |
1280 else: | |
1281 hexfunc = repo.ui.debugflag and hex or short | |
1282 r = [hexfunc(node) for node in [node1, node2] if node] | |
1283 | |
1284 if opts.git: | |
1285 copy, diverge = copies.copies(repo, ctx1, ctx2, repo[nullid]) | |
1286 copy = copy.copy() | |
1287 for k, v in copy.items(): | |
1288 copy[v] = k | |
1289 | |
1290 gone = set() | |
1291 gitmode = {'l': '120000', 'x': '100755', '': '100644'} | |
1292 | |
1293 for f in sorted(modified + added + removed): | |
1294 to = None | |
1295 tn = None | |
1296 dodiff = True | |
1297 header = [] | |
1298 if f in man1: | |
1299 to = getfilectx(f, ctx1).data() | |
1300 if f not in removed: | |
1301 tn = getfilectx(f, ctx2).data() | |
1302 a, b = f, f | |
1303 if opts.git: | |
1304 if f in added: | |
1305 mode = gitmode[ctx2.flags(f)] | |
1306 if f in copy: | |
1307 a = copy[f] | |
1308 omode = gitmode[man1.flags(a)] | |
1309 _addmodehdr(header, omode, mode) | |
1310 if a in removed and a not in gone: | |
1311 op = 'rename' | |
1312 gone.add(a) | |
1313 else: | |
1314 op = 'copy' | |
1315 header.append('%s from %s\n' % (op, a)) | |
1316 header.append('%s to %s\n' % (op, f)) | |
1317 to = getfilectx(a, ctx1).data() | |
1318 else: | |
1319 header.append('new file mode %s\n' % mode) | |
1320 if util.binary(tn): | |
1321 dodiff = 'binary' | |
1322 elif f in removed: | |
1323 # have we already reported a copy above? | |
1324 if f in copy and copy[f] in added and copy[copy[f]] == f: | |
1325 dodiff = False | |
1326 else: | |
1327 header.append('deleted file mode %s\n' % | |
1328 gitmode[man1.flags(f)]) | |
1329 else: | |
1330 omode = gitmode[man1.flags(f)] | |
1331 nmode = gitmode[ctx2.flags(f)] | |
1332 _addmodehdr(header, omode, nmode) | |
1333 if util.binary(to) or util.binary(tn): | |
1334 dodiff = 'binary' | |
1335 r = None | |
1336 header.insert(0, mdiff.diffline(r, a, b, opts)) | |
1337 if dodiff: | |
1338 if dodiff == 'binary': | |
1339 text = b85diff(to, tn) | |
1340 else: | |
1341 text = mdiff.unidiff(to, date1, | |
1342 # ctx2 date may be dynamic | |
1343 tn, util.datestr(ctx2.date()), | |
1344 a, b, r, opts=opts) | |
1345 if header and (text or len(header) > 1): | |
1346 yield ''.join(header) | |
1347 if text: | |
1348 yield text | |
1349 | |
1350 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False, | |
1351 opts=None): | |
1352 '''export changesets as hg patches.''' | |
1353 | |
1354 total = len(revs) | |
1355 revwidth = max([len(str(rev)) for rev in revs]) | |
1356 | |
1357 def single(rev, seqno, fp): | |
1358 ctx = repo[rev] | |
1359 node = ctx.node() | |
1360 parents = [p.node() for p in ctx.parents() if p] | |
1361 branch = ctx.branch() | |
1362 if switch_parent: | |
1363 parents.reverse() | |
1364 prev = (parents and parents[0]) or nullid | |
1365 | |
1366 if not fp: | |
1367 fp = cmdutil.make_file(repo, template, node, total=total, | |
1368 seqno=seqno, revwidth=revwidth, | |
1369 mode='ab') | |
1370 if fp != sys.stdout and hasattr(fp, 'name'): | |
1371 repo.ui.note("%s\n" % fp.name) | |
1372 | |
1373 fp.write("# HG changeset patch\n") | |
1374 fp.write("# User %s\n" % ctx.user()) | |
1375 fp.write("# Date %d %d\n" % ctx.date()) | |
1376 if branch and (branch != 'default'): | |
1377 fp.write("# Branch %s\n" % branch) | |
1378 fp.write("# Node ID %s\n" % hex(node)) | |
1379 fp.write("# Parent %s\n" % hex(prev)) | |
1380 if len(parents) > 1: | |
1381 fp.write("# Parent %s\n" % hex(parents[1])) | |
1382 fp.write(ctx.description().rstrip()) | |
1383 fp.write("\n\n") | |
1384 | |
1385 for chunk in diff(repo, prev, node, opts=opts): | |
1386 fp.write(chunk) | |
1387 | |
1388 for seqno, rev in enumerate(revs): | |
1389 single(rev, seqno+1, fp) | |
1390 | |
1391 def diffstatdata(lines): | |
1392 filename, adds, removes = None, 0, 0 | |
1393 for line in lines: | |
1394 if line.startswith('diff'): | |
1395 if filename: | |
1396 yield (filename, adds, removes) | |
1397 # set numbers to 0 anyway when starting new file | |
1398 adds, removes = 0, 0 | |
1399 if line.startswith('diff --git'): | |
1400 filename = gitre.search(line).group(1) | |
1401 else: | |
1402 # format: "diff -r ... -r ... filename" | |
1403 filename = line.split(None, 5)[-1] | |
1404 elif line.startswith('+') and not line.startswith('+++'): | |
1405 adds += 1 | |
1406 elif line.startswith('-') and not line.startswith('---'): | |
1407 removes += 1 | |
1408 if filename: | |
1409 yield (filename, adds, removes) | |
1410 | |
1411 def diffstat(lines, width=80): | |
1412 output = [] | |
1413 stats = list(diffstatdata(lines)) | |
1414 | |
1415 maxtotal, maxname = 0, 0 | |
1416 totaladds, totalremoves = 0, 0 | |
1417 for filename, adds, removes in stats: | |
1418 totaladds += adds | |
1419 totalremoves += removes | |
1420 maxname = max(maxname, len(filename)) | |
1421 maxtotal = max(maxtotal, adds+removes) | |
1422 | |
1423 countwidth = len(str(maxtotal)) | |
1424 graphwidth = width - countwidth - maxname | |
1425 if graphwidth < 10: | |
1426 graphwidth = 10 | |
1427 | |
1428 factor = max(int(math.ceil(float(maxtotal) / graphwidth)), 1) | |
1429 | |
1430 for filename, adds, removes in stats: | |
1431 # If diffstat runs out of room it doesn't print anything, which | |
1432 # isn't very useful, so always print at least one + or - if there | |
1433 # were at least some changes | |
1434 pluses = '+' * max(adds/factor, int(bool(adds))) | |
1435 minuses = '-' * max(removes/factor, int(bool(removes))) | |
1436 output.append(' %-*s | %*.d %s%s\n' % (maxname, filename, countwidth, | |
1437 adds+removes, pluses, minuses)) | |
1438 | |
1439 if stats: | |
1440 output.append(' %d files changed, %d insertions(+), %d deletions(-)\n' | |
1441 % (len(stats), totaladds, totalremoves)) | |
1442 | |
1443 return ''.join(output) |