# HG changeset patch # User Benoit Boissinot # Date 1224372597 -7200 # Node ID ba897f6dc93469d9e92b2aa90469daac75cdd5c7 # Parent 3250da71a769475e050eac758a7d7eba9f33ab54# Parent 353141d74ca87258361ee8c3e6c970efe8d06b5f merge with crew diff -r 3250da71a769 -r ba897f6dc934 mercurial/patch.py --- a/mercurial/patch.py Sun Oct 19 01:28:35 2008 +0200 +++ b/mercurial/patch.py Sun Oct 19 01:29:57 2008 +0200 @@ -143,23 +143,31 @@ GP_FILTER = 1 << 1 # there's some copy/rename operation GP_BINARY = 1 << 2 # there's a binary patch -def readgitpatch(fp, firstline=None): +class patchmeta: + """Patched file metadata + + 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY + or COPY. 'path' is patched file path. 'oldpath' is set to the + origin file when 'op' is either COPY or RENAME, None otherwise. If + file mode is changed, 'mode' is a tuple (islink, isexec) where + 'islink' is True if the file is a symlink and 'isexec' is True if + the file is executable. Otherwise, 'mode' is None. + """ + def __init__(self, path): + self.path = path + self.oldpath = None + self.mode = None + self.op = 'MODIFY' + self.lineno = 0 + self.binary = False + + def setmode(self, mode): + islink = mode & 020000 + isexec = mode & 0100 + self.mode = (islink, isexec) + +def readgitpatch(lr): """extract git-style metadata about patches from """ - class gitpatch: - "op is one of ADD, DELETE, RENAME, MODIFY or COPY" - def __init__(self, path): - self.path = path - self.oldpath = None - self.mode = None - self.op = 'MODIFY' - self.lineno = 0 - self.binary = False - - def reader(fp, firstline): - if firstline is not None: - yield firstline - for line in fp: - yield line # Filter patch for git information gitre = re.compile('diff --git a/(.*) b/(.*)') @@ -169,7 +177,7 @@ dopatch = 0 lineno = 0 - for line in reader(fp, firstline): + for line in lr: lineno += 1 if line.startswith('diff --git'): m = gitre.match(line) @@ -177,7 +185,7 @@ if gp: gitpatches.append(gp) src, dst = m.group(1, 2) - gp = gitpatch(dst) + gp = patchmeta(dst) gp.lineno = lineno elif gp: if line.startswith('--- '): @@ -201,9 +209,9 @@ gp.op = 'DELETE' elif line.startswith('new file mode '): gp.op = 'ADD' - gp.mode = int(line.rstrip()[-6:], 8) + gp.setmode(int(line.rstrip()[-6:], 8)) elif line.startswith('new mode '): - gp.mode = int(line.rstrip()[-6:], 8) + gp.setmode(int(line.rstrip()[-6:], 8)) elif line.startswith('GIT binary patch'): dopatch |= GP_BINARY gp.binary = True @@ -215,88 +223,6 @@ return (dopatch, gitpatches) -def patch(patchname, ui, strip=1, cwd=None, files={}): - """apply to the working directory. - returns whether patch was applied with fuzz factor.""" - patcher = ui.config('ui', 'patch') - args = [] - try: - if patcher: - return externalpatch(patcher, args, patchname, ui, strip, cwd, - files) - else: - try: - return internalpatch(patchname, ui, strip, cwd, files) - except NoHunks: - patcher = util.find_exe('gpatch') or util.find_exe('patch') - ui.debug(_('no valid hunks found; trying with %r instead\n') % - patcher) - if util.needbinarypatch(): - args.append('--binary') - return externalpatch(patcher, args, patchname, ui, strip, cwd, - files) - except PatchError, err: - s = str(err) - if s: - raise util.Abort(s) - else: - raise util.Abort(_('patch failed to apply')) - -def externalpatch(patcher, args, patchname, ui, strip, cwd, files): - """use to apply to the working directory. - returns whether patch was applied with fuzz factor.""" - - fuzz = False - if cwd: - args.append('-d %s' % util.shellquote(cwd)) - fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip, - util.shellquote(patchname))) - - for line in fp: - line = line.rstrip() - ui.note(line + '\n') - if line.startswith('patching file '): - pf = util.parse_patch_output(line) - printed_file = False - files.setdefault(pf, (None, None)) - elif line.find('with fuzz') >= 0: - fuzz = True - if not printed_file: - ui.warn(pf + '\n') - printed_file = True - ui.warn(line + '\n') - elif line.find('saving rejects to file') >= 0: - ui.warn(line + '\n') - elif line.find('FAILED') >= 0: - if not printed_file: - ui.warn(pf + '\n') - printed_file = True - ui.warn(line + '\n') - code = fp.close() - if code: - raise PatchError(_("patch command failed: %s") % - util.explain_exit(code)[0]) - return fuzz - -def internalpatch(patchobj, ui, strip, cwd, files={}): - """use builtin patch to apply to the working directory. - returns whether patch was applied with fuzz factor.""" - try: - fp = file(patchobj, 'rb') - except TypeError: - fp = patchobj - if cwd: - curdir = os.getcwd() - os.chdir(cwd) - try: - ret = applydiff(ui, fp, files, strip=strip) - finally: - if cwd: - os.chdir(curdir) - if ret < 0: - raise PatchError - return ret > 0 - # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@') contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)') @@ -529,7 +455,7 @@ self.lenb = int(self.lenb) self.starta = int(self.starta) self.startb = int(self.startb) - diffhelpers.addlines(lr.fp, self.hunk, self.lena, self.lenb, self.a, self.b) + diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b) # if we hit eof before finishing out the hunk, the last line will # be zero length. Lets try to fix it up. while len(self.hunk[-1]) == 0: @@ -743,17 +669,17 @@ def new(self): return [self.text] - def extract(self, fp): - line = fp.readline() + def extract(self, lr): + line = lr.readline() self.hunk.append(line) while line and not line.startswith('literal '): - line = fp.readline() + line = lr.readline() self.hunk.append(line) if not line: raise PatchError(_('could not extract binary patch')) size = int(line[8:].rstrip()) dec = [] - line = fp.readline() + line = lr.readline() self.hunk.append(line) while len(line) > 1: l = line[0] @@ -762,7 +688,7 @@ else: l = ord(l) - ord('a') + 27 dec.append(base85.b85decode(line[1:-1])[:l]) - line = fp.readline() + line = lr.readline() self.hunk.append(line) text = zlib.decompress(''.join(dec)) if len(text) != size: @@ -839,7 +765,8 @@ self.buf = [] def push(self, line): - self.buf.append(line) + if line is not None: + self.buf.append(line) def readline(self): if self.buf: @@ -848,6 +775,39 @@ return l return self.fp.readline() + def __iter__(self): + while 1: + l = self.readline() + if not l: + break + yield l + +def scangitpatch(lr, firstline): + """ + Git patches can emit: + - rename a to b + - change b + - copy a to c + - change c + + We cannot apply this sequence as-is, the renamed 'a' could not be + found for it would have been renamed already. And we cannot copy + from 'b' instead because 'b' would have been changed already. So + we scan the git patch for copy and rename commands so we can + perform the copies ahead of time. + """ + pos = 0 + try: + pos = lr.fp.tell() + fp = lr.fp + except IOError: + fp = cStringIO.StringIO(lr.fp.read()) + gitlr = linereader(fp) + gitlr.push(firstline) + (dopatch, gitpatches) = readgitpatch(gitlr) + fp.seek(pos) + return dopatch, gitpatches + def iterhunks(ui, fp, sourcefile=None): """Read a patch and yield the following events: - ("file", afile, bfile, firsthunk): select a new target file. @@ -856,24 +816,6 @@ - ("git", gitchanges): current diff is in git format, gitchanges maps filenames to gitpatch records. Unique event. """ - - def scangitpatch(fp, firstline): - '''git patches can modify a file, then copy that file to - a new file, but expect the source to be the unmodified form. - So we scan the patch looking for that case so we can do - the copies ahead of time.''' - - pos = 0 - try: - pos = fp.tell() - except IOError: - fp = cStringIO.StringIO(fp.read()) - - (dopatch, gitpatches) = readgitpatch(fp, firstline) - fp.seek(pos) - - return fp, dopatch, gitpatches - changed = {} current_hunk = None afile = "" @@ -911,7 +853,7 @@ try: if context == None and x.startswith('***************'): context = True - gpatch = changed.get(bfile[2:], (None, None))[1] + gpatch = changed.get(bfile[2:]) create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD' remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE' current_hunk = hunk(x, hunknum + 1, lr, context, create, remove) @@ -924,12 +866,12 @@ emitfile = False yield 'file', (afile, bfile, current_hunk) elif state == BFILE and x.startswith('GIT binary patch'): - current_hunk = binhunk(changed[bfile[2:]][1]) + current_hunk = binhunk(changed[bfile[2:]]) hunknum += 1 if emitfile: emitfile = False yield 'file', (afile, bfile, current_hunk) - current_hunk.extract(fp) + current_hunk.extract(lr) elif x.startswith('diff --git'): # check for git diff, scanning the whole patch file if needed m = gitre.match(x) @@ -937,14 +879,14 @@ afile, bfile = m.group(1, 2) if not git: git = True - fp, dopatch, gitpatches = scangitpatch(fp, x) + dopatch, gitpatches = scangitpatch(lr, x) yield 'git', gitpatches for gp in gitpatches: - changed[gp.path] = (gp.op, gp) + changed[gp.path] = gp # else error? # copy/rename + modify should modify target, not source - gitop = changed.get(bfile[2:], (None, None))[0] - if gitop in ('COPY', 'DELETE', 'RENAME'): + gp = changed.get(bfile[2:]) + if gp and gp.op in ('COPY', 'DELETE', 'RENAME'): afile = bfile gitworkdone = True newfile = True @@ -988,8 +930,7 @@ if hunknum == 0 and dopatch and not gitworkdone: raise NoHunks -def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False, - rejmerge=None, updatedir=None): +def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False): """reads a patch from fp and tries to apply it. The dict 'changed' is filled in with all of the filenames changed by the patch. Returns 0 for a clean patch, -1 if any rejects were found and 1 if there was @@ -1004,8 +945,6 @@ if not current_file: return 0 current_file.close() - if rejmerge: - rejmerge(current_file) return len(current_file.rej) for state, values in iterhunks(ui, fp, sourcefile): @@ -1015,7 +954,7 @@ current_hunk = values ret = current_file.apply(current_hunk, reverse) if ret >= 0: - changed.setdefault(current_file.fname, (None, None)) + changed.setdefault(current_file.fname, None) if ret > 0: err = 1 elif state == 'file': @@ -1041,14 +980,12 @@ src, dst = [util.canonpath(cwd, cwd, x) for x in [gp.oldpath, gp.path]] copyfile(src, dst) - changed[gp.path] = (gp.op, gp) + changed[gp.path] = gp else: raise util.Abort(_('unsupported parser state: %s') % state) rejects += closefile() - if updatedir and gitpatches: - updatedir(gitpatches) if rejects: return -1 return err @@ -1078,13 +1015,15 @@ if cwd: cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()] for f in patches: - ctype, gp = patches[f] - if ctype == 'RENAME': + gp = patches[f] + if not gp: + continue + if gp.op == 'RENAME': copies.append((gp.oldpath, gp.path)) removes[gp.oldpath] = 1 - elif ctype == 'COPY': + elif gp.op == 'COPY': copies.append((gp.oldpath, gp.path)) - elif ctype == 'DELETE': + elif gp.op == 'DELETE': removes[gp.path] = 1 for src, dst in copies: repo.copy(src, dst) @@ -1092,24 +1031,103 @@ if removes: repo.remove(util.sort(removes), True) for f in patches: - ctype, gp = patches[f] + gp = patches[f] if gp and gp.mode: - flags = '' - if gp.mode & 0100: - flags = 'x' - elif gp.mode & 020000: - flags = 'l' + islink, isexec = gp.mode dst = os.path.join(repo.root, gp.path) # patch won't create empty files - if ctype == 'ADD' and not os.path.exists(dst): + if gp.op == 'ADD' and not os.path.exists(dst): + flags = (isexec and 'x' or '') + (islink and 'l' or '') repo.wwrite(gp.path, '', flags) else: - util.set_flags(dst, 'l' in flags, 'x' in flags) + util.set_flags(dst, islink, isexec) cmdutil.addremove(repo, cfiles) files = patches.keys() files.extend([r for r in removes if r not in files]) return util.sort(files) +def externalpatch(patcher, args, patchname, ui, strip, cwd, files): + """use to apply to the working directory. + returns whether patch was applied with fuzz factor.""" + + fuzz = False + if cwd: + args.append('-d %s' % util.shellquote(cwd)) + fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip, + util.shellquote(patchname))) + + for line in fp: + line = line.rstrip() + ui.note(line + '\n') + if line.startswith('patching file '): + pf = util.parse_patch_output(line) + printed_file = False + files.setdefault(pf, (None, None)) + elif line.find('with fuzz') >= 0: + fuzz = True + if not printed_file: + ui.warn(pf + '\n') + printed_file = True + ui.warn(line + '\n') + elif line.find('saving rejects to file') >= 0: + ui.warn(line + '\n') + elif line.find('FAILED') >= 0: + if not printed_file: + ui.warn(pf + '\n') + printed_file = True + ui.warn(line + '\n') + code = fp.close() + if code: + raise PatchError(_("patch command failed: %s") % + util.explain_exit(code)[0]) + return fuzz + +def internalpatch(patchobj, ui, strip, cwd, files={}): + """use builtin patch to apply to the working directory. + returns whether patch was applied with fuzz factor.""" + try: + fp = file(patchobj, 'rb') + except TypeError: + fp = patchobj + if cwd: + curdir = os.getcwd() + os.chdir(cwd) + try: + ret = applydiff(ui, fp, files, strip=strip) + finally: + if cwd: + os.chdir(curdir) + if ret < 0: + raise PatchError + return ret > 0 + +def patch(patchname, ui, strip=1, cwd=None, files={}): + """apply to the working directory. + returns whether patch was applied with fuzz factor.""" + patcher = ui.config('ui', 'patch') + args = [] + try: + if patcher: + return externalpatch(patcher, args, patchname, ui, strip, cwd, + files) + else: + try: + return internalpatch(patchname, ui, strip, cwd, files) + except NoHunks: + patcher = util.find_exe('gpatch') or util.find_exe('patch') + ui.debug(_('no valid hunks found; trying with %r instead\n') % + patcher) + if util.needbinarypatch(): + args.append('--binary') + return externalpatch(patcher, args, patchname, ui, strip, cwd, + files) + except PatchError, err: + s = str(err) + if s: + raise util.Abort(s) + else: + raise util.Abort(_('patch failed to apply')) + def b85diff(to, tn): '''print base85-encoded binary diff''' def gitindex(text):