This instance will be upgraded to Heptapod 0.21.0rc2 on 2021-04-16 at 14:00 UTC+2 (a few minutes of down time)

patch.py 101 KB
Newer Older
1 2
# patch.py - patch file parsing routines
#
Vadim Gelfer's avatar
Vadim Gelfer committed
3
# Copyright 2006 Brendan Cully <brendan@kublai.com>
4
# Copyright 2007 Chris Mason <chris.mason@oracle.com>
Vadim Gelfer's avatar
Vadim Gelfer committed
5
#
6
# This software may be used and distributed according to the terms of the
Matt Mackall's avatar
Matt Mackall committed
7
# GNU General Public License version 2 or any later version.
8

9
from __future__ import absolute_import, print_function
10

Gregory Szorc's avatar
Gregory Szorc committed
11
import collections
12
import contextlib
Gregory Szorc's avatar
Gregory Szorc committed
13 14 15 16 17 18 19 20 21 22 23 24
import copy
import errno
import os
import re
import shutil
import zlib

from .i18n import _
from .node import (
    hex,
    short,
)
25
from .pycompat import open
Gregory Szorc's avatar
Gregory Szorc committed
26 27
from . import (
    copies,
28
    diffhelper,
29
    diffutil,
Gregory Szorc's avatar
Gregory Szorc committed
30 31
    encoding,
    error,
32
    mail,
Gregory Szorc's avatar
Gregory Szorc committed
33 34
    mdiff,
    pathutil,
35
    pycompat,
Gregory Szorc's avatar
Gregory Szorc committed
36
    scmutil,
37
    similar,
Gregory Szorc's avatar
Gregory Szorc committed
38
    util,
39
    vfs as vfsmod,
Gregory Szorc's avatar
Gregory Szorc committed
40
)
41 42
from .utils import (
    dateutil,
43
    hashutil,
44
    procutil,
45 46
    stringutil,
)
47

48
stringio = util.stringio
49

50 51
gitre = re.compile(br'diff --git a/(.*) b/(.*)')
tabsplitter = re.compile(br'(\t+|[^\t]+)')
52
wordsplitter = re.compile(
53
    br'(\t+| +|[a-zA-Z0-9_\x80-\xff]+|[^ \ta-zA-Z0-9_\x80-\xff])'
54
)
55

56
PatchError = error.PatchError
57 58 59

# public functions

60

61 62
def split(stream):
    '''return an iterator of individual patches from a stream'''
63

64
    def isheader(line, inheader):
65
        if inheader and line.startswith((b' ', b'\t')):
66 67
            # continuation
            return True
68
        if line.startswith((b' ', b'-', b'+')):
69 70
            # diff line - don't check for header pattern in there
            return False
71 72
        l = line.split(b': ', 1)
        return len(l) == 2 and b' ' not in l[0]
73 74

    def chunk(lines):
75
        return stringio(b''.join(lines))
76 77 78 79 80 81 82

    def hgsplit(stream, cur):
        inheader = True

        for line in stream:
            if not line.strip():
                inheader = False
83
            if not inheader and line.startswith(b'# HG changeset patch'):
84 85 86 87 88 89 90 91 92 93 94
                yield chunk(cur)
                cur = []
                inheader = True

            cur.append(line)

        if cur:
            yield chunk(cur)

    def mboxsplit(stream, cur):
        for line in stream:
95
            if line.startswith(b'From '):
96 97 98 99 100 101 102 103 104 105 106 107
                for c in split(chunk(cur[1:])):
                    yield c
                cur = []

            cur.append(line)

        if cur:
            for c in split(chunk(cur[1:])):
                yield c

    def mimesplit(stream, cur):
        def msgfp(m):
108
            fp = stringio()
109
            g = mail.Generator(fp, mangle_from_=False)
110 111 112 113 114 115 116 117
            g.flatten(m)
            fp.seek(0)
            return fp

        for line in stream:
            cur.append(line)
        c = chunk(cur)

118
        m = mail.parse(c)
119 120 121
        if not m.is_multipart():
            yield msgfp(m)
        else:
122
            ok_types = (b'text/plain', b'text/x-diff', b'text/x-patch')
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
            for part in m.walk():
                ct = part.get_content_type()
                if ct not in ok_types:
                    continue
                yield msgfp(part)

    def headersplit(stream, cur):
        inheader = False

        for line in stream:
            if not inheader and isheader(line, inheader):
                yield chunk(cur)
                cur = []
                inheader = True
            if inheader and not isheader(line, inheader):
                inheader = False

            cur.append(line)

        if cur:
            yield chunk(cur)

    def remainder(cur):
        yield chunk(cur)

    class fiter(object):
        def __init__(self, fp):
            self.fp = fp

        def __iter__(self):
            return self

        def next(self):
            l = self.fp.readline()
            if not l:
                raise StopIteration
            return l

161 162
        __next__ = next

163 164 165
    inheader = False
    cur = []

166
    mimeheaders = [b'content-type']
167

168
    if not util.safehasattr(stream, b'next'):
169 170 171 172 173
        # http responses, for example, have readline but not next
        stream = fiter(stream)

    for line in stream:
        cur.append(line)
174
        if line.startswith(b'# HG changeset patch'):
175
            return hgsplit(stream, cur)
176
        elif line.startswith(b'From '):
177 178 179
            return mboxsplit(stream, cur)
        elif isheader(line, inheader):
            inheader = True
180
            if line.split(b':', 1)[0].lower() in mimeheaders:
181 182
                # let email parser handle this
                return mimesplit(stream, cur)
183
        elif line.startswith(b'--- ') and inheader:
184
            # No evil headers seen by diff start, split by hand
185 186 187 188 189 190
            return headersplit(stream, cur)
        # Not enough info, keep reading

    # if we are here, we have a very plain patch
    return remainder(cur)

191

192 193
## Some facility for extensible patch parsing:
# list of pairs ("header to match", "data key")
194
patchheadermap = [
195 196 197
    (b'Date', b'date'),
    (b'Branch', b'branch'),
    (b'Node ID', b'nodeid'),
198 199
]

200

201
@contextlib.contextmanager
202 203 204
def extract(ui, fileobj):
    '''extract patch from data read from fileobj.

Brendan Cully's avatar
Brendan Cully committed
205
    patch can be a normal patch or contained in an email message.
206

207
    return a dictionary. Standard keys are:
208 209 210 211 212 213 214 215
      - filename,
      - message,
      - user,
      - date,
      - branch,
      - node,
      - p1,
      - p2.
216
    Any item can be missing from the dictionary. If filename is missing,
Brendan Cully's avatar
Brendan Cully committed
217
    fileobj did not contain a patch. Caller must unlink filename when done.'''
218

219
    fd, tmpname = pycompat.mkstemp(prefix=b'hg-patch-')
220
    tmpfp = os.fdopen(fd, 'wb')
221 222 223 224 225 226
    try:
        yield _extract(ui, fileobj, tmpname, tmpfp)
    finally:
        tmpfp.close()
        os.unlink(tmpname)

227

228 229
def _extract(ui, fileobj, tmpname, tmpfp):

230 231
    # attempt to detect the start of a patch
    # (this heuristic is borrowed from quilt)
232 233 234 235 236 237 238
    diffre = re.compile(
        br'^(?:Index:[ \t]|diff[ \t]-|RCS file: |'
        br'retrieving revision [0-9]+(\.[0-9]+)*$|'
        br'---[ \t].*?^\+\+\+[ \t]|'
        br'\*\*\*[ \t].*?^---[ \t])',
        re.MULTILINE | re.DOTALL,
    )
239

240
    data = {}
241

242
    msg = mail.parse(fileobj)
243

244 245
    subject = msg['Subject'] and mail.headdecode(msg['Subject'])
    data[b'user'] = msg['From'] and mail.headdecode(msg['From'])
246
    if not subject and not data[b'user']:
247
        # Not an email, restore parsed headers if any
248
        subject = (
249 250
            b'\n'.join(
                b': '.join(map(encoding.strtolocal, h)) for h in msg.items()
251
            )
252
            + b'\n'
253
        )
254 255 256 257

    # should try to parse msg['Date']
    parents = []

258
    nodeid = msg['X-Mercurial-Node']
259 260 261 262
    if nodeid:
        data[b'nodeid'] = nodeid = mail.headdecode(nodeid)
        ui.debug(b'Node ID: %s\n' % nodeid)

263
    if subject:
264 265
        if subject.startswith(b'[PATCH'):
            pend = subject.find(b']')
266
            if pend >= 0:
267
                subject = subject[pend + 1 :].lstrip()
268 269 270 271
        subject = re.sub(br'\n[ \t]+', b' ', subject)
        ui.debug(b'Subject: %s\n' % subject)
    if data[b'user']:
        ui.debug(b'From: %s\n' % data[b'user'])
272
    diffs_seen = 0
273 274
    ok_types = (b'text/plain', b'text/x-diff', b'text/x-patch')
    message = b''
275 276
    for part in msg.walk():
        content_type = pycompat.bytestr(part.get_content_type())
277
        ui.debug(b'Content-Type: %s\n' % content_type)
278 279 280 281 282 283 284 285 286
        if content_type not in ok_types:
            continue
        payload = part.get_payload(decode=True)
        m = diffre.search(payload)
        if m:
            hgpatch = False
            hgpatchheader = False
            ignoretext = False

287
            ui.debug(b'found patch at byte %d\n' % m.start(0))
288 289
            diffs_seen += 1
            cfp = stringio()
290
            for line in payload[: m.start(0)].splitlines():
291 292
                if line.startswith(b'# HG changeset patch') and not hgpatch:
                    ui.debug(b'patch generated by hg export\n')
293 294 295 296 297 298 299
                    hgpatch = True
                    hgpatchheader = True
                    # drop earlier commit message content
                    cfp.seek(0)
                    cfp.truncate()
                    subject = None
                elif hgpatchheader:
300 301 302 303
                    if line.startswith(b'# User '):
                        data[b'user'] = line[7:]
                        ui.debug(b'From: %s\n' % data[b'user'])
                    elif line.startswith(b"# Parent "):
304
                        parents.append(line[9:].lstrip())
305
                    elif line.startswith(b"# "):
306
                        for header, key in patchheadermap:
307
                            prefix = b'# %s ' % header
308
                            if line.startswith(prefix):
309
                                data[key] = line[len(prefix) :]
310
                                ui.debug(b'%s: %s\n' % (header, data[key]))
311 312
                    else:
                        hgpatchheader = False
313
                elif line == b'---':
314 315 316
                    ignoretext = True
                if not hgpatchheader and not ignoretext:
                    cfp.write(line)
317
                    cfp.write(b'\n')
318 319 320
            message = cfp.getvalue()
            if tmpfp:
                tmpfp.write(payload)
321 322 323 324
                if not payload.endswith(b'\n'):
                    tmpfp.write(b'\n')
        elif not diffs_seen and message and content_type == b'text/plain':
            message += b'\n' + payload
325

326
    if subject and not message.startswith(subject):
327 328
        message = b'%s\n%s' % (subject, message)
    data[b'message'] = message
329
    tmpfp.close()
330
    if parents:
331
        data[b'p1'] = parents.pop(0)
332
        if parents:
333
            data[b'p2'] = parents.pop(0)
334

335
    if diffs_seen:
336
        data[b'filename'] = tmpname
337

338
    return data
339

340

Benoit Boissinot's avatar
Benoit Boissinot committed
341
class patchmeta(object):
342 343 344 345
    """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
346 347 348 349
    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.
350
    """
351

352 353 354 355
    def __init__(self, path):
        self.path = path
        self.oldpath = None
        self.mode = None
356
        self.op = b'MODIFY'
357 358
        self.binary = False

359
    def setmode(self, mode):
360 361
        islink = mode & 0o20000
        isexec = mode & 0o100
362 363
        self.mode = (islink, isexec)

364 365 366 367 368 369 370 371
    def copy(self):
        other = patchmeta(self.path)
        other.oldpath = self.oldpath
        other.mode = self.mode
        other.op = self.op
        other.binary = self.binary
        return other

372
    def _ispatchinga(self, afile):
373 374 375
        if afile == b'/dev/null':
            return self.op == b'ADD'
        return afile == b'a/' + (self.oldpath or self.path)
376 377

    def _ispatchingb(self, bfile):
378 379 380
        if bfile == b'/dev/null':
            return self.op == b'DELETE'
        return bfile == b'b/' + self.path
381 382 383 384

    def ispatching(self, afile, bfile):
        return self._ispatchinga(afile) and self._ispatchingb(bfile)

385
    def __repr__(self):
386
        return "<patchmeta %s %r>" % (self.op, self.path)
387

388

389
def readgitpatch(lr):
390
    """extract git-style metadata about patches from <patchname>"""
Thomas Arendsen Hein's avatar
Thomas Arendsen Hein committed
391

392 393 394
    # Filter patch for git information
    gp = None
    gitpatches = []
395
    for line in lr:
396 397
        line = line.rstrip(b' \r\n')
        if line.startswith(b'diff --git a/'):
398 399 400 401
            m = gitre.match(line)
            if m:
                if gp:
                    gitpatches.append(gp)
402
                dst = m.group(2)
403
                gp = patchmeta(dst)
404
        elif gp:
405
            if line.startswith(b'--- '):
406 407 408
                gitpatches.append(gp)
                gp = None
                continue
409 410
            if line.startswith(b'rename from '):
                gp.op = b'RENAME'
411
                gp.oldpath = line[12:]
412
            elif line.startswith(b'rename to '):
413
                gp.path = line[10:]
414 415
            elif line.startswith(b'copy from '):
                gp.op = b'COPY'
416
                gp.oldpath = line[10:]
417
            elif line.startswith(b'copy to '):
418
                gp.path = line[8:]
419 420 421 422
            elif line.startswith(b'deleted file'):
                gp.op = b'DELETE'
            elif line.startswith(b'new file mode '):
                gp.op = b'ADD'
423
                gp.setmode(int(line[-6:], 8))
424
            elif line.startswith(b'new mode '):
425
                gp.setmode(int(line[-6:], 8))
426
            elif line.startswith(b'GIT binary patch'):
427
                gp.binary = True
428 429 430
    if gp:
        gitpatches.append(gp)

431
    return gitpatches
432

433

434
class linereader(object):
435
    # simple class to allow pushing lines back into the input stream
436
    def __init__(self, fp):
437 438 439 440 441 442 443 444 445 446 447 448
        self.fp = fp
        self.buf = []

    def push(self, line):
        if line is not None:
            self.buf.append(line)

    def readline(self):
        if self.buf:
            l = self.buf[0]
            del self.buf[0]
            return l
449
        return self.fp.readline()
450 451

    def __iter__(self):
452
        return iter(self.readline, b'')
453

454

455 456
class abstractbackend(object):
    def __init__(self, ui):
457
        self.ui = ui
458

459 460
    def getfile(self, fname):
        """Return target file data and flags as a (data, (islink,
461
        isexec)) tuple. Data is None if file is missing/deleted.
462 463 464
        """
        raise NotImplementedError

465
    def setfile(self, fname, data, mode, copysource):
466 467
        """Write data to target file fname and set its mode. mode is a
        (islink, isexec) tuple. If data is None, the file content should
468 469
        be left unchanged. If the file is modified after being copied,
        copysource is set to the original file name.
470
        """
471 472 473 474 475 476 477 478 479 480 481 482
        raise NotImplementedError

    def unlink(self, fname):
        """Unlink target file."""
        raise NotImplementedError

    def writerej(self, fname, failed, total, lines):
        """Write rejected lines for fname. total is the number of hunks
        which failed to apply and total the total number of hunks for this
        files.
        """

483 484 485
    def exists(self, fname):
        raise NotImplementedError

486 487 488
    def close(self):
        raise NotImplementedError

489

490
class fsbackend(abstractbackend):
491
    def __init__(self, ui, basedir):
492
        super(fsbackend, self).__init__(ui)
493
        self.opener = vfsmod.vfs(basedir)
494

495
    def getfile(self, fname):
496 497 498
        if self.opener.islink(fname):
            return (self.opener.readlink(fname), (True, False))

499
        isexec = False
500
        try:
501
            isexec = self.opener.lstat(fname).st_mode & 0o100 != 0
502
        except OSError as e:
503 504
            if e.errno != errno.ENOENT:
                raise
505 506
        try:
            return (self.opener.read(fname), (False, isexec))
507
        except IOError as e:
508 509 510
            if e.errno != errno.ENOENT:
                raise
            return None, None
511

512
    def setfile(self, fname, data, mode, copysource):
513 514
        islink, isexec = mode
        if data is None:
515
            self.opener.setflags(fname, islink, isexec)
516
            return
517
        if islink:
518
            self.opener.symlink(data, fname)
519
        else:
520
            self.opener.write(fname, data)
521
            if isexec:
522
                self.opener.setflags(fname, False, True)
523 524

    def unlink(self, fname):
525
        rmdir = self.ui.configbool(b'experimental', b'removeemptydirs')
526
        self.opener.unlinkpath(fname, ignoremissing=True, rmdir=rmdir)
527

528
    def writerej(self, fname, failed, total, lines):
529
        fname = fname + b".rej"
530
        self.ui.warn(
531
            _(b"%d out of %d hunks FAILED -- saving rejects to file %s\n")
532 533
            % (failed, total, fname)
        )
534
        fp = self.opener(fname, b'w')
535 536 537
        fp.writelines(lines)
        fp.close()

538
    def exists(self, fname):
539
        return self.opener.lexists(fname)
540

541

542 543 544 545 546 547 548 549 550
class workingbackend(fsbackend):
    def __init__(self, ui, repo, similarity):
        super(workingbackend, self).__init__(ui, repo.root)
        self.repo = repo
        self.similarity = similarity
        self.removed = set()
        self.changed = set()
        self.copied = []

551
    def _checkknown(self, fname):
552 553
        if self.repo.dirstate[fname] == b'?' and self.exists(fname):
            raise PatchError(_(b'cannot patch %s: file is not tracked') % fname)
554

555
    def setfile(self, fname, data, mode, copysource):
556
        self._checkknown(fname)
557 558 559
        super(workingbackend, self).setfile(fname, data, mode, copysource)
        if copysource is not None:
            self.copied.append((copysource, fname))
560 561 562
        self.changed.add(fname)

    def unlink(self, fname):
563
        self._checkknown(fname)
564 565 566 567 568 569
        super(workingbackend, self).unlink(fname)
        self.removed.add(fname)
        self.changed.add(fname)

    def close(self):
        wctx = self.repo[None]
570
        changed = set(self.changed)
571 572
        for src, dst in self.copied:
            scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
573
        if self.removed:
574
            wctx.forget(sorted(self.removed))
575 576 577 578 579
            for f in self.removed:
                if f not in self.repo.dirstate:
                    # File was deleted and no longer belongs to the
                    # dirstate, it was probably marked added then
                    # deleted, and should not be considered by
580 581 582 583
                    # marktouched().
                    changed.discard(f)
        if changed:
            scmutil.marktouched(self.repo, changed, self.similarity)
584 585
        return sorted(self.changed)

586

587
class filestore(object):
588
    def __init__(self, maxsize=None):
589 590 591
        self.opener = None
        self.files = {}
        self.created = 0
592 593
        self.maxsize = maxsize
        if self.maxsize is None:
594
            self.maxsize = 4 * (2 ** 20)
595 596
        self.size = 0
        self.data = {}
597

598
    def setfile(self, fname, data, mode, copied=None):
599 600 601 602 603
        if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
            self.data[fname] = (data, mode, copied)
            self.size += len(data)
        else:
            if self.opener is None:
604
                root = pycompat.mkdtemp(prefix=b'hg-patch-')
605
                self.opener = vfsmod.vfs(root)
606
            # Avoid filename issues with these simple names
607
            fn = b'%d' % self.created
608 609 610
            self.opener.write(fn, data)
            self.created += 1
            self.files[fname] = (fn, mode, copied)
611 612

    def getfile(self, fname):
613 614 615
        if fname in self.data:
            return self.data[fname]
        if not self.opener or fname not in self.files:
616
            return None, None, None
617 618
        fn, mode, copied = self.files[fname]
        return self.opener.read(fn), mode, copied
619 620 621 622 623

    def close(self):
        if self.opener:
            shutil.rmtree(self.opener.base)

624

Patrick Mezard's avatar
Patrick Mezard committed
625 626 627 628 629 630 631 632 633 634 635 636
class repobackend(abstractbackend):
    def __init__(self, ui, repo, ctx, store):
        super(repobackend, self).__init__(ui)
        self.repo = repo
        self.ctx = ctx
        self.store = store
        self.changed = set()
        self.removed = set()
        self.copied = {}

    def _checkknown(self, fname):
        if fname not in self.ctx:
637
            raise PatchError(_(b'cannot patch %s: file is not tracked') % fname)
Patrick Mezard's avatar
Patrick Mezard committed
638 639 640 641 642

    def getfile(self, fname):
        try:
            fctx = self.ctx[fname]
        except error.LookupError:
643
            return None, None
Patrick Mezard's avatar
Patrick Mezard committed
644
        flags = fctx.flags()
645
        return fctx.data(), (b'l' in flags, b'x' in flags)
Patrick Mezard's avatar
Patrick Mezard committed
646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666

    def setfile(self, fname, data, mode, copysource):
        if copysource:
            self._checkknown(copysource)
        if data is None:
            data = self.ctx[fname].data()
        self.store.setfile(fname, data, mode, copysource)
        self.changed.add(fname)
        if copysource:
            self.copied[fname] = copysource

    def unlink(self, fname):
        self._checkknown(fname)
        self.removed.add(fname)

    def exists(self, fname):
        return fname in self.ctx

    def close(self):
        return self.changed | self.removed

667

668
# @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
669 670
unidesc = re.compile(br'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
contextdesc = re.compile(br'(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
671
eolmodes = [b'strict', b'crlf', b'lf', b'auto']
672

673

674
class patchfile(object):
675
    def __init__(self, ui, gp, backend, store, eolmode=b'strict'):
676
        self.fname = gp.path
677 678 679 680 681 682
        self.eolmode = eolmode
        self.eol = None
        self.backend = backend
        self.ui = ui
        self.lines = []
        self.exists = False
683
        self.missing = True
684 685
        self.mode = gp.mode
        self.copysource = gp.oldpath
686 687
        self.create = gp.op in (b'ADD', b'COPY', b'RENAME')
        self.remove = gp.op == b'DELETE'
688 689 690 691 692 693
        if self.copysource is None:
            data, mode = backend.getfile(self.fname)
        else:
            data, mode = store.getfile(self.copysource)[:2]
        if data is not None:
            self.exists = self.copysource is None or backend.exists(self.fname)
694 695
            self.missing = False
            if data:
696
                self.lines = mdiff.splitnewlines(data)
697 698 699 700
            if self.mode is None:
                self.mode = mode
            if self.lines:
                # Normalize line endings
701 702 703 704 705
                if self.lines[0].endswith(b'\r\n'):
                    self.eol = b'\r\n'
                elif self.lines[0].endswith(b'\n'):
                    self.eol = b'\n'
                if eolmode != b'strict':
706 707
                    nlines = []
                    for l in self.lines:
708 709
                        if l.endswith(b'\r\n'):
                            l = l[:-2] + b'\n'
710 711
                        nlines.append(l)
                    self.lines = nlines
712
        else:
713
            if self.create:
714 715 716 717
                self.missing = False
            if self.mode is None:
                self.mode = (False, False)
        if self.missing:
718
            self.ui.warn(_(b"unable to find '%s' for patching\n") % self.fname)
719 720
            self.ui.warn(
                _(
721 722
                    b"(use '--prefix' to apply patch relative to the "
                    b"current directory)\n"
723 724
                )
            )
725 726 727 728 729 730 731 732 733 734

        self.hash = {}
        self.dirty = 0
        self.offset = 0
        self.skew = 0
        self.rej = []
        self.fileprinted = False
        self.printfile(False)
        self.hunks = 0

735
    def writelines(self, fname, lines, mode):
736
        if self.eolmode == b'auto':
737
            eol = self.eol
738 739
        elif self.eolmode == b'crlf':
            eol = b'\r\n'
740
        else:
741
            eol = b'\n'
742

743
        if self.eolmode != b'strict' and eol and eol != b'\n':
744 745
            rawlines = []
            for l in lines:
746
                if l and l.endswith(b'\n'):
747 748 749 750
                    l = l[:-1] + eol
                rawlines.append(l)
            lines = rawlines

751
        self.backend.setfile(fname, b''.join(lines), mode, self.copysource)
752

753 754 755 756 757
    def printfile(self, warn):
        if self.fileprinted:
            return
        if warn or self.ui.verbose:
            self.fileprinted = True
758
        s = _(b"patching file %s\n") % self.fname
759 760 761 762 763 764 765 766 767
        if warn:
            self.ui.warn(s)
        else:
            self.ui.note(s)

    def findlines(self, l, linenum):
        # looks through the hash and finds candidate lines.  The
        # result is a list of line numbers sorted based on distance
        # from linenum
768

Benoit Boissinot's avatar
Benoit Boissinot committed
769
        cand = self.hash.get(l, [])
770 771
        if len(cand) > 1:
            # resort our list of potentials forward then back.
772
            cand.sort(key=lambda x: abs(x - linenum))
773 774 775 776 777 778 779 780 781
        return cand

    def write_rej(self):
        # our rejects are a little different from patch(1).  This always
        # creates rejects in the same form as the original patch.  A file
        # header is inserted so that you can run the reject through patch again
        # without having to type the filename.
        if not self.rej:
            return
782
        base = os.path.basename(self.fname)
783
        lines = [b"--- %s\n+++ %s\n" % (base, base)]
784 785 786
        for x in self.rej:
            for l in x.hunk:
                lines.append(l)
787
                if l[-1:] != b'\n':
788
                    lines.append(b'\n' + diffhelper.MISSING_NEWLINE_MARKER)
789
        self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
790

791
    def apply(self, h):
792
        if not h.complete():
793
            raise PatchError(
794
                _(b"bad hunk #%d %s (%d %d %d %d)")
795 796
                % (h.number, h.desc, len(h.a), h.lena, len(h.b), h.lenb)
            )
797 798 799

        self.hunks += 1

800 801 802 803
        if self.missing:
            self.rej.append(h)
            return -1

804
        if self.exists and self.create:
805
            if self.copysource: