xref: /openbmc/openbmc/poky/meta/lib/oe/patch.py (revision 73bd93f1)
1c342db35SBrad Bishop#
292b42cb3SPatrick Williams# Copyright OpenEmbedded Contributors
392b42cb3SPatrick Williams#
4c342db35SBrad Bishop# SPDX-License-Identifier: GPL-2.0-only
5c342db35SBrad Bishop#
6c342db35SBrad Bishop
78e7b46e2SPatrick Williamsimport os
88e7b46e2SPatrick Williamsimport shlex
98e7b46e2SPatrick Williamsimport subprocess
10eb8dc403SDave Cobbleyimport oe.path
11eb8dc403SDave Cobbleyimport oe.types
12eb8dc403SDave Cobbley
13eb8dc403SDave Cobbleyclass NotFoundError(bb.BBHandledException):
14eb8dc403SDave Cobbley    def __init__(self, path):
15eb8dc403SDave Cobbley        self.path = path
16eb8dc403SDave Cobbley
17eb8dc403SDave Cobbley    def __str__(self):
18eb8dc403SDave Cobbley        return "Error: %s not found." % self.path
19eb8dc403SDave Cobbley
20eb8dc403SDave Cobbleyclass CmdError(bb.BBHandledException):
21eb8dc403SDave Cobbley    def __init__(self, command, exitstatus, output):
22eb8dc403SDave Cobbley        self.command = command
23eb8dc403SDave Cobbley        self.status = exitstatus
24eb8dc403SDave Cobbley        self.output = output
25eb8dc403SDave Cobbley
26eb8dc403SDave Cobbley    def __str__(self):
27eb8dc403SDave Cobbley        return "Command Error: '%s' exited with %d  Output:\n%s" % \
28eb8dc403SDave Cobbley                (self.command, self.status, self.output)
29eb8dc403SDave Cobbley
30eb8dc403SDave Cobbley
31eb8dc403SDave Cobbleydef runcmd(args, dir = None):
32eb8dc403SDave Cobbley    if dir:
33eb8dc403SDave Cobbley        olddir = os.path.abspath(os.curdir)
34eb8dc403SDave Cobbley        if not os.path.exists(dir):
35eb8dc403SDave Cobbley            raise NotFoundError(dir)
36eb8dc403SDave Cobbley        os.chdir(dir)
37eb8dc403SDave Cobbley        # print("cwd: %s -> %s" % (olddir, dir))
38eb8dc403SDave Cobbley
39eb8dc403SDave Cobbley    try:
408e7b46e2SPatrick Williams        args = [ shlex.quote(str(arg)) for arg in args ]
41eb8dc403SDave Cobbley        cmd = " ".join(args)
42eb8dc403SDave Cobbley        # print("cmd: %s" % cmd)
43d1e89497SAndrew Geissler        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
44d1e89497SAndrew Geissler        stdout, stderr = proc.communicate()
45d1e89497SAndrew Geissler        stdout = stdout.decode('utf-8')
46d1e89497SAndrew Geissler        stderr = stderr.decode('utf-8')
47d1e89497SAndrew Geissler        exitstatus = proc.returncode
48eb8dc403SDave Cobbley        if exitstatus != 0:
49d1e89497SAndrew Geissler            raise CmdError(cmd, exitstatus >> 8, "stdout: %s\nstderr: %s" % (stdout, stderr))
50d1e89497SAndrew Geissler        if " fuzz " in stdout and "Hunk " in stdout:
51d89cb5f0SBrad Bishop            # Drop patch fuzz info with header and footer to log file so
52d89cb5f0SBrad Bishop            # insane.bbclass can handle to throw error/warning
53d1e89497SAndrew Geissler            bb.note("--- Patch fuzz start ---\n%s\n--- Patch fuzz end ---" % format(stdout))
54eb8dc403SDave Cobbley
55d1e89497SAndrew Geissler        return stdout
56eb8dc403SDave Cobbley
57eb8dc403SDave Cobbley    finally:
58eb8dc403SDave Cobbley        if dir:
59eb8dc403SDave Cobbley            os.chdir(olddir)
60eb8dc403SDave Cobbley
61595f6308SAndrew Geissler
62eb8dc403SDave Cobbleyclass PatchError(Exception):
63eb8dc403SDave Cobbley    def __init__(self, msg):
64eb8dc403SDave Cobbley        self.msg = msg
65eb8dc403SDave Cobbley
66eb8dc403SDave Cobbley    def __str__(self):
67eb8dc403SDave Cobbley        return "Patch Error: %s" % self.msg
68eb8dc403SDave Cobbley
69eb8dc403SDave Cobbleyclass PatchSet(object):
70eb8dc403SDave Cobbley    defaults = {
71eb8dc403SDave Cobbley        "strippath": 1
72eb8dc403SDave Cobbley    }
73eb8dc403SDave Cobbley
74eb8dc403SDave Cobbley    def __init__(self, dir, d):
75eb8dc403SDave Cobbley        self.dir = dir
76eb8dc403SDave Cobbley        self.d = d
77eb8dc403SDave Cobbley        self.patches = []
78eb8dc403SDave Cobbley        self._current = None
79eb8dc403SDave Cobbley
80eb8dc403SDave Cobbley    def current(self):
81eb8dc403SDave Cobbley        return self._current
82eb8dc403SDave Cobbley
83eb8dc403SDave Cobbley    def Clean(self):
84eb8dc403SDave Cobbley        """
85eb8dc403SDave Cobbley        Clean out the patch set.  Generally includes unapplying all
86eb8dc403SDave Cobbley        patches and wiping out all associated metadata.
87eb8dc403SDave Cobbley        """
88eb8dc403SDave Cobbley        raise NotImplementedError()
89eb8dc403SDave Cobbley
90eb8dc403SDave Cobbley    def Import(self, patch, force):
91eb8dc403SDave Cobbley        if not patch.get("file"):
92eb8dc403SDave Cobbley            if not patch.get("remote"):
93eb8dc403SDave Cobbley                raise PatchError("Patch file must be specified in patch import.")
94eb8dc403SDave Cobbley            else:
95eb8dc403SDave Cobbley                patch["file"] = bb.fetch2.localpath(patch["remote"], self.d)
96eb8dc403SDave Cobbley
97eb8dc403SDave Cobbley        for param in PatchSet.defaults:
98eb8dc403SDave Cobbley            if not patch.get(param):
99eb8dc403SDave Cobbley                patch[param] = PatchSet.defaults[param]
100eb8dc403SDave Cobbley
101eb8dc403SDave Cobbley        if patch.get("remote"):
102eb8dc403SDave Cobbley            patch["file"] = self.d.expand(bb.fetch2.localpath(patch["remote"], self.d))
103eb8dc403SDave Cobbley
104eb8dc403SDave Cobbley        patch["filemd5"] = bb.utils.md5_file(patch["file"])
105eb8dc403SDave Cobbley
106eb8dc403SDave Cobbley    def Push(self, force):
107eb8dc403SDave Cobbley        raise NotImplementedError()
108eb8dc403SDave Cobbley
109eb8dc403SDave Cobbley    def Pop(self, force):
110eb8dc403SDave Cobbley        raise NotImplementedError()
111eb8dc403SDave Cobbley
112eb8dc403SDave Cobbley    def Refresh(self, remote = None, all = None):
113eb8dc403SDave Cobbley        raise NotImplementedError()
114eb8dc403SDave Cobbley
115eb8dc403SDave Cobbley    @staticmethod
116eb8dc403SDave Cobbley    def getPatchedFiles(patchfile, striplevel, srcdir=None):
117eb8dc403SDave Cobbley        """
118eb8dc403SDave Cobbley        Read a patch file and determine which files it will modify.
119eb8dc403SDave Cobbley        Params:
120eb8dc403SDave Cobbley            patchfile: the patch file to read
121eb8dc403SDave Cobbley            striplevel: the strip level at which the patch is going to be applied
122eb8dc403SDave Cobbley            srcdir: optional path to join onto the patched file paths
123eb8dc403SDave Cobbley        Returns:
124eb8dc403SDave Cobbley            A list of tuples of file path and change mode ('A' for add,
125eb8dc403SDave Cobbley            'D' for delete or 'M' for modify)
126eb8dc403SDave Cobbley        """
127eb8dc403SDave Cobbley
128eb8dc403SDave Cobbley        def patchedpath(patchline):
129eb8dc403SDave Cobbley            filepth = patchline.split()[1]
130eb8dc403SDave Cobbley            if filepth.endswith('/dev/null'):
131eb8dc403SDave Cobbley                return '/dev/null'
132eb8dc403SDave Cobbley            filesplit = filepth.split(os.sep)
133eb8dc403SDave Cobbley            if striplevel > len(filesplit):
134eb8dc403SDave Cobbley                bb.error('Patch %s has invalid strip level %d' % (patchfile, striplevel))
135eb8dc403SDave Cobbley                return None
136eb8dc403SDave Cobbley            return os.sep.join(filesplit[striplevel:])
137eb8dc403SDave Cobbley
138eb8dc403SDave Cobbley        for encoding in ['utf-8', 'latin-1']:
139eb8dc403SDave Cobbley            try:
140eb8dc403SDave Cobbley                copiedmode = False
141eb8dc403SDave Cobbley                filelist = []
142eb8dc403SDave Cobbley                with open(patchfile) as f:
143eb8dc403SDave Cobbley                    for line in f:
144eb8dc403SDave Cobbley                        if line.startswith('--- '):
145eb8dc403SDave Cobbley                            patchpth = patchedpath(line)
146eb8dc403SDave Cobbley                            if not patchpth:
147eb8dc403SDave Cobbley                                break
148eb8dc403SDave Cobbley                            if copiedmode:
149eb8dc403SDave Cobbley                                addedfile = patchpth
150eb8dc403SDave Cobbley                            else:
151eb8dc403SDave Cobbley                                removedfile = patchpth
152eb8dc403SDave Cobbley                        elif line.startswith('+++ '):
153eb8dc403SDave Cobbley                            addedfile = patchedpath(line)
154eb8dc403SDave Cobbley                            if not addedfile:
155eb8dc403SDave Cobbley                                break
156eb8dc403SDave Cobbley                        elif line.startswith('*** '):
157eb8dc403SDave Cobbley                            copiedmode = True
158eb8dc403SDave Cobbley                            removedfile = patchedpath(line)
159eb8dc403SDave Cobbley                            if not removedfile:
160eb8dc403SDave Cobbley                                break
161eb8dc403SDave Cobbley                        else:
162eb8dc403SDave Cobbley                            removedfile = None
163eb8dc403SDave Cobbley                            addedfile = None
164eb8dc403SDave Cobbley
165eb8dc403SDave Cobbley                        if addedfile and removedfile:
166eb8dc403SDave Cobbley                            if removedfile == '/dev/null':
167eb8dc403SDave Cobbley                                mode = 'A'
168eb8dc403SDave Cobbley                            elif addedfile == '/dev/null':
169eb8dc403SDave Cobbley                                mode = 'D'
170eb8dc403SDave Cobbley                            else:
171eb8dc403SDave Cobbley                                mode = 'M'
172eb8dc403SDave Cobbley                            if srcdir:
173eb8dc403SDave Cobbley                                fullpath = os.path.abspath(os.path.join(srcdir, addedfile))
174eb8dc403SDave Cobbley                            else:
175eb8dc403SDave Cobbley                                fullpath = addedfile
176eb8dc403SDave Cobbley                            filelist.append((fullpath, mode))
177eb8dc403SDave Cobbley            except UnicodeDecodeError:
178eb8dc403SDave Cobbley                continue
179eb8dc403SDave Cobbley            break
180eb8dc403SDave Cobbley        else:
181eb8dc403SDave Cobbley            raise PatchError('Unable to decode %s' % patchfile)
182eb8dc403SDave Cobbley
183eb8dc403SDave Cobbley        return filelist
184eb8dc403SDave Cobbley
185eb8dc403SDave Cobbley
186eb8dc403SDave Cobbleyclass PatchTree(PatchSet):
187eb8dc403SDave Cobbley    def __init__(self, dir, d):
188eb8dc403SDave Cobbley        PatchSet.__init__(self, dir, d)
189eb8dc403SDave Cobbley        self.patchdir = os.path.join(self.dir, 'patches')
190eb8dc403SDave Cobbley        self.seriespath = os.path.join(self.dir, 'patches', 'series')
191eb8dc403SDave Cobbley        bb.utils.mkdirhier(self.patchdir)
192eb8dc403SDave Cobbley
193eb8dc403SDave Cobbley    def _appendPatchFile(self, patch, strippath):
194eb8dc403SDave Cobbley        with open(self.seriespath, 'a') as f:
195eb8dc403SDave Cobbley            f.write(os.path.basename(patch) + "," + strippath + "\n")
196eb8dc403SDave Cobbley        shellcmd = ["cat", patch, ">" , self.patchdir + "/" + os.path.basename(patch)]
197eb8dc403SDave Cobbley        runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
198eb8dc403SDave Cobbley
199eb8dc403SDave Cobbley    def _removePatch(self, p):
200eb8dc403SDave Cobbley        patch = {}
201eb8dc403SDave Cobbley        patch['file'] = p.split(",")[0]
202eb8dc403SDave Cobbley        patch['strippath'] = p.split(",")[1]
203eb8dc403SDave Cobbley        self._applypatch(patch, False, True)
204eb8dc403SDave Cobbley
205eb8dc403SDave Cobbley    def _removePatchFile(self, all = False):
206eb8dc403SDave Cobbley        if not os.path.exists(self.seriespath):
207eb8dc403SDave Cobbley            return
208eb8dc403SDave Cobbley        with open(self.seriespath, 'r+') as f:
209eb8dc403SDave Cobbley            patches = f.readlines()
210eb8dc403SDave Cobbley        if all:
211eb8dc403SDave Cobbley            for p in reversed(patches):
212eb8dc403SDave Cobbley                self._removePatch(os.path.join(self.patchdir, p.strip()))
213eb8dc403SDave Cobbley            patches = []
214eb8dc403SDave Cobbley        else:
215eb8dc403SDave Cobbley            self._removePatch(os.path.join(self.patchdir, patches[-1].strip()))
216eb8dc403SDave Cobbley            patches.pop()
217eb8dc403SDave Cobbley        with open(self.seriespath, 'w') as f:
218eb8dc403SDave Cobbley            for p in patches:
219eb8dc403SDave Cobbley                f.write(p)
220eb8dc403SDave Cobbley
221eb8dc403SDave Cobbley    def Import(self, patch, force = None):
222eb8dc403SDave Cobbley        """"""
223eb8dc403SDave Cobbley        PatchSet.Import(self, patch, force)
224eb8dc403SDave Cobbley
225eb8dc403SDave Cobbley        if self._current is not None:
226eb8dc403SDave Cobbley            i = self._current + 1
227eb8dc403SDave Cobbley        else:
228eb8dc403SDave Cobbley            i = 0
229eb8dc403SDave Cobbley        self.patches.insert(i, patch)
230eb8dc403SDave Cobbley
231eb8dc403SDave Cobbley    def _applypatch(self, patch, force = False, reverse = False, run = True):
232eb8dc403SDave Cobbley        shellcmd = ["cat", patch['file'], "|", "patch", "--no-backup-if-mismatch", "-p", patch['strippath']]
233eb8dc403SDave Cobbley        if reverse:
234eb8dc403SDave Cobbley            shellcmd.append('-R')
235eb8dc403SDave Cobbley
236eb8dc403SDave Cobbley        if not run:
237eb8dc403SDave Cobbley            return "sh" + "-c" + " ".join(shellcmd)
238eb8dc403SDave Cobbley
239eb8dc403SDave Cobbley        if not force:
240eb8dc403SDave Cobbley            shellcmd.append('--dry-run')
241eb8dc403SDave Cobbley
242eb8dc403SDave Cobbley        try:
243eb8dc403SDave Cobbley            output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
244eb8dc403SDave Cobbley
245eb8dc403SDave Cobbley            if force:
246eb8dc403SDave Cobbley                return
247eb8dc403SDave Cobbley
248eb8dc403SDave Cobbley            shellcmd.pop(len(shellcmd) - 1)
249eb8dc403SDave Cobbley            output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
250eb8dc403SDave Cobbley        except CmdError as err:
251eb8dc403SDave Cobbley            raise bb.BBHandledException("Applying '%s' failed:\n%s" %
252eb8dc403SDave Cobbley                                        (os.path.basename(patch['file']), err.output))
253eb8dc403SDave Cobbley
254eb8dc403SDave Cobbley        if not reverse:
255eb8dc403SDave Cobbley            self._appendPatchFile(patch['file'], patch['strippath'])
256eb8dc403SDave Cobbley
257eb8dc403SDave Cobbley        return output
258eb8dc403SDave Cobbley
259eb8dc403SDave Cobbley    def Push(self, force = False, all = False, run = True):
260eb8dc403SDave Cobbley        bb.note("self._current is %s" % self._current)
261eb8dc403SDave Cobbley        bb.note("patches is %s" % self.patches)
262eb8dc403SDave Cobbley        if all:
263eb8dc403SDave Cobbley            for i in self.patches:
264eb8dc403SDave Cobbley                bb.note("applying patch %s" % i)
265eb8dc403SDave Cobbley                self._applypatch(i, force)
266eb8dc403SDave Cobbley                self._current = i
267eb8dc403SDave Cobbley        else:
268eb8dc403SDave Cobbley            if self._current is not None:
269eb8dc403SDave Cobbley                next = self._current + 1
270eb8dc403SDave Cobbley            else:
271eb8dc403SDave Cobbley                next = 0
272eb8dc403SDave Cobbley
273eb8dc403SDave Cobbley            bb.note("applying patch %s" % self.patches[next])
274eb8dc403SDave Cobbley            ret = self._applypatch(self.patches[next], force)
275eb8dc403SDave Cobbley
276eb8dc403SDave Cobbley            self._current = next
277eb8dc403SDave Cobbley            return ret
278eb8dc403SDave Cobbley
279eb8dc403SDave Cobbley    def Pop(self, force = None, all = None):
280eb8dc403SDave Cobbley        if all:
281eb8dc403SDave Cobbley            self._removePatchFile(True)
282eb8dc403SDave Cobbley            self._current = None
283eb8dc403SDave Cobbley        else:
284eb8dc403SDave Cobbley            self._removePatchFile(False)
285eb8dc403SDave Cobbley
286eb8dc403SDave Cobbley        if self._current == 0:
287eb8dc403SDave Cobbley            self._current = None
288eb8dc403SDave Cobbley
289eb8dc403SDave Cobbley        if self._current is not None:
290eb8dc403SDave Cobbley            self._current = self._current - 1
291eb8dc403SDave Cobbley
292eb8dc403SDave Cobbley    def Clean(self):
293eb8dc403SDave Cobbley        """"""
294eb8dc403SDave Cobbley        self.Pop(all=True)
295eb8dc403SDave Cobbley
296eb8dc403SDave Cobbleyclass GitApplyTree(PatchTree):
297*73bd93f1SPatrick Williams    notes_ref = "refs/notes/devtool"
298*73bd93f1SPatrick Williams    original_patch = 'original patch'
299*73bd93f1SPatrick Williams    ignore_commit = 'ignore'
300eb8dc403SDave Cobbley
301eb8dc403SDave Cobbley    def __init__(self, dir, d):
302eb8dc403SDave Cobbley        PatchTree.__init__(self, dir, d)
303eb8dc403SDave Cobbley        self.commituser = d.getVar('PATCH_GIT_USER_NAME')
304eb8dc403SDave Cobbley        self.commitemail = d.getVar('PATCH_GIT_USER_EMAIL')
305615f2f11SAndrew Geissler        if not self._isInitialized(d):
306595f6308SAndrew Geissler            self._initRepo()
307595f6308SAndrew Geissler
308615f2f11SAndrew Geissler    def _isInitialized(self, d):
309595f6308SAndrew Geissler        cmd = "git rev-parse --show-toplevel"
3107e0e3c0cSAndrew Geissler        try:
3117e0e3c0cSAndrew Geissler            output = runcmd(cmd.split(), self.dir).strip()
3127e0e3c0cSAndrew Geissler        except CmdError as err:
3137e0e3c0cSAndrew Geissler            ## runcmd returned non-zero which most likely means 128
3147e0e3c0cSAndrew Geissler            ## Not a git directory
3157e0e3c0cSAndrew Geissler            return False
316615f2f11SAndrew Geissler        ## Make sure repo is in builddir to not break top-level git repos, or under workdir
317615f2f11SAndrew Geissler        return os.path.samefile(output, self.dir) or oe.path.is_path_parent(d.getVar('WORKDIR'), output)
318595f6308SAndrew Geissler
319595f6308SAndrew Geissler    def _initRepo(self):
320595f6308SAndrew Geissler        runcmd("git init".split(), self.dir)
321595f6308SAndrew Geissler        runcmd("git add .".split(), self.dir)
3227e0e3c0cSAndrew Geissler        runcmd("git commit -a --allow-empty -m bitbake_patching_started".split(), self.dir)
323eb8dc403SDave Cobbley
324eb8dc403SDave Cobbley    @staticmethod
325eb8dc403SDave Cobbley    def extractPatchHeader(patchfile):
326eb8dc403SDave Cobbley        """
327eb8dc403SDave Cobbley        Extract just the header lines from the top of a patch file
328eb8dc403SDave Cobbley        """
329eb8dc403SDave Cobbley        for encoding in ['utf-8', 'latin-1']:
330eb8dc403SDave Cobbley            lines = []
331eb8dc403SDave Cobbley            try:
332eb8dc403SDave Cobbley                with open(patchfile, 'r', encoding=encoding) as f:
333eb8dc403SDave Cobbley                    for line in f:
334eb8dc403SDave Cobbley                        if line.startswith('Index: ') or line.startswith('diff -') or line.startswith('---'):
335eb8dc403SDave Cobbley                            break
336eb8dc403SDave Cobbley                        lines.append(line)
337eb8dc403SDave Cobbley            except UnicodeDecodeError:
338eb8dc403SDave Cobbley                continue
339eb8dc403SDave Cobbley            break
340eb8dc403SDave Cobbley        else:
341eb8dc403SDave Cobbley            raise PatchError('Unable to find a character encoding to decode %s' % patchfile)
342eb8dc403SDave Cobbley        return lines
343eb8dc403SDave Cobbley
344eb8dc403SDave Cobbley    @staticmethod
345eb8dc403SDave Cobbley    def decodeAuthor(line):
346eb8dc403SDave Cobbley        from email.header import decode_header
347eb8dc403SDave Cobbley        authorval = line.split(':', 1)[1].strip().replace('"', '')
348eb8dc403SDave Cobbley        result =  decode_header(authorval)[0][0]
349eb8dc403SDave Cobbley        if hasattr(result, 'decode'):
350eb8dc403SDave Cobbley            result = result.decode('utf-8')
351eb8dc403SDave Cobbley        return result
352eb8dc403SDave Cobbley
353eb8dc403SDave Cobbley    @staticmethod
354eb8dc403SDave Cobbley    def interpretPatchHeader(headerlines):
355eb8dc403SDave Cobbley        import re
35619323693SBrad Bishop        author_re = re.compile(r'[\S ]+ <\S+@\S+\.\S+>')
35719323693SBrad Bishop        from_commit_re = re.compile(r'^From [a-z0-9]{40} .*')
358eb8dc403SDave Cobbley        outlines = []
359eb8dc403SDave Cobbley        author = None
360eb8dc403SDave Cobbley        date = None
361eb8dc403SDave Cobbley        subject = None
362eb8dc403SDave Cobbley        for line in headerlines:
363eb8dc403SDave Cobbley            if line.startswith('Subject: '):
364eb8dc403SDave Cobbley                subject = line.split(':', 1)[1]
365eb8dc403SDave Cobbley                # Remove any [PATCH][oe-core] etc.
366eb8dc403SDave Cobbley                subject = re.sub(r'\[.+?\]\s*', '', subject)
367eb8dc403SDave Cobbley                continue
368eb8dc403SDave Cobbley            elif line.startswith('From: ') or line.startswith('Author: '):
369eb8dc403SDave Cobbley                authorval = GitApplyTree.decodeAuthor(line)
370eb8dc403SDave Cobbley                # git is fussy about author formatting i.e. it must be Name <email@domain>
371eb8dc403SDave Cobbley                if author_re.match(authorval):
372eb8dc403SDave Cobbley                    author = authorval
373eb8dc403SDave Cobbley                    continue
374eb8dc403SDave Cobbley            elif line.startswith('Date: '):
375eb8dc403SDave Cobbley                if date is None:
376eb8dc403SDave Cobbley                    dateval = line.split(':', 1)[1].strip()
377eb8dc403SDave Cobbley                    # Very crude check for date format, since git will blow up if it's not in the right
378eb8dc403SDave Cobbley                    # format. Without e.g. a python-dateutils dependency we can't do a whole lot more
379eb8dc403SDave Cobbley                    if len(dateval) > 12:
380eb8dc403SDave Cobbley                        date = dateval
381eb8dc403SDave Cobbley                continue
382eb8dc403SDave Cobbley            elif not author and line.lower().startswith('signed-off-by: '):
383eb8dc403SDave Cobbley                authorval = GitApplyTree.decodeAuthor(line)
384eb8dc403SDave Cobbley                # git is fussy about author formatting i.e. it must be Name <email@domain>
385eb8dc403SDave Cobbley                if author_re.match(authorval):
386eb8dc403SDave Cobbley                    author = authorval
387eb8dc403SDave Cobbley            elif from_commit_re.match(line):
388eb8dc403SDave Cobbley                # We don't want the From <commit> line - if it's present it will break rebasing
389eb8dc403SDave Cobbley                continue
390eb8dc403SDave Cobbley            outlines.append(line)
391eb8dc403SDave Cobbley
392eb8dc403SDave Cobbley        if not subject:
393eb8dc403SDave Cobbley            firstline = None
394eb8dc403SDave Cobbley            for line in headerlines:
395eb8dc403SDave Cobbley                line = line.strip()
396eb8dc403SDave Cobbley                if firstline:
397eb8dc403SDave Cobbley                    if line:
398eb8dc403SDave Cobbley                        # Second line is not blank, the first line probably isn't usable
399eb8dc403SDave Cobbley                        firstline = None
400eb8dc403SDave Cobbley                    break
401eb8dc403SDave Cobbley                elif line:
402eb8dc403SDave Cobbley                    firstline = line
403eb8dc403SDave Cobbley            if firstline and not firstline.startswith(('#', 'Index:', 'Upstream-Status:')) and len(firstline) < 100:
404eb8dc403SDave Cobbley                subject = firstline
405eb8dc403SDave Cobbley
406eb8dc403SDave Cobbley        return outlines, author, date, subject
407eb8dc403SDave Cobbley
408eb8dc403SDave Cobbley    @staticmethod
409eb8dc403SDave Cobbley    def gitCommandUserOptions(cmd, commituser=None, commitemail=None, d=None):
410eb8dc403SDave Cobbley        if d:
411eb8dc403SDave Cobbley            commituser = d.getVar('PATCH_GIT_USER_NAME')
412eb8dc403SDave Cobbley            commitemail = d.getVar('PATCH_GIT_USER_EMAIL')
413eb8dc403SDave Cobbley        if commituser:
414eb8dc403SDave Cobbley            cmd += ['-c', 'user.name="%s"' % commituser]
415eb8dc403SDave Cobbley        if commitemail:
416eb8dc403SDave Cobbley            cmd += ['-c', 'user.email="%s"' % commitemail]
417eb8dc403SDave Cobbley
418eb8dc403SDave Cobbley    @staticmethod
419eb8dc403SDave Cobbley    def prepareCommit(patchfile, commituser=None, commitemail=None):
420eb8dc403SDave Cobbley        """
421eb8dc403SDave Cobbley        Prepare a git commit command line based on the header from a patch file
422eb8dc403SDave Cobbley        (typically this is useful for patches that cannot be applied with "git am" due to formatting)
423eb8dc403SDave Cobbley        """
424eb8dc403SDave Cobbley        import tempfile
425eb8dc403SDave Cobbley        # Process patch header and extract useful information
426eb8dc403SDave Cobbley        lines = GitApplyTree.extractPatchHeader(patchfile)
427eb8dc403SDave Cobbley        outlines, author, date, subject = GitApplyTree.interpretPatchHeader(lines)
428eb8dc403SDave Cobbley        if not author or not subject or not date:
429eb8dc403SDave Cobbley            try:
430eb8dc403SDave Cobbley                shellcmd = ["git", "log", "--format=email", "--follow", "--diff-filter=A", "--", patchfile]
431eb8dc403SDave Cobbley                out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.dirname(patchfile))
432eb8dc403SDave Cobbley            except CmdError:
433eb8dc403SDave Cobbley                out = None
434eb8dc403SDave Cobbley            if out:
435eb8dc403SDave Cobbley                _, newauthor, newdate, newsubject = GitApplyTree.interpretPatchHeader(out.splitlines())
436eb8dc403SDave Cobbley                if not author:
437eb8dc403SDave Cobbley                    # If we're setting the author then the date should be set as well
438eb8dc403SDave Cobbley                    author = newauthor
439eb8dc403SDave Cobbley                    date = newdate
440eb8dc403SDave Cobbley                elif not date:
441eb8dc403SDave Cobbley                    # If we don't do this we'll get the current date, at least this will be closer
442eb8dc403SDave Cobbley                    date = newdate
443eb8dc403SDave Cobbley                if not subject:
444eb8dc403SDave Cobbley                    subject = newsubject
4454ed12e16SAndrew Geissler        if subject and not (outlines and outlines[0].strip() == subject):
446eb8dc403SDave Cobbley            outlines.insert(0, '%s\n\n' % subject.strip())
447eb8dc403SDave Cobbley
448eb8dc403SDave Cobbley        # Write out commit message to a file
449eb8dc403SDave Cobbley        with tempfile.NamedTemporaryFile('w', delete=False) as tf:
450eb8dc403SDave Cobbley            tmpfile = tf.name
451eb8dc403SDave Cobbley            for line in outlines:
452eb8dc403SDave Cobbley                tf.write(line)
453eb8dc403SDave Cobbley        # Prepare git command
454eb8dc403SDave Cobbley        cmd = ["git"]
455eb8dc403SDave Cobbley        GitApplyTree.gitCommandUserOptions(cmd, commituser, commitemail)
456*73bd93f1SPatrick Williams        cmd += ["commit", "-F", tmpfile, "--no-verify"]
457eb8dc403SDave Cobbley        # git doesn't like plain email addresses as authors
458eb8dc403SDave Cobbley        if author and '<' in author:
459eb8dc403SDave Cobbley            cmd.append('--author="%s"' % author)
460eb8dc403SDave Cobbley        if date:
461eb8dc403SDave Cobbley            cmd.append('--date="%s"' % date)
462eb8dc403SDave Cobbley        return (tmpfile, cmd)
463eb8dc403SDave Cobbley
464eb8dc403SDave Cobbley    @staticmethod
465*73bd93f1SPatrick Williams    def addNote(repo, ref, key, value=None):
466*73bd93f1SPatrick Williams        note = key + (": %s" % value if value else "")
467*73bd93f1SPatrick Williams        notes_ref = GitApplyTree.notes_ref
468*73bd93f1SPatrick Williams        runcmd(["git", "config", "notes.rewriteMode", "ignore"], repo)
469*73bd93f1SPatrick Williams        runcmd(["git", "config", "notes.displayRef", notes_ref, notes_ref], repo)
470*73bd93f1SPatrick Williams        runcmd(["git", "config", "notes.rewriteRef", notes_ref, notes_ref], repo)
471*73bd93f1SPatrick Williams        runcmd(["git", "notes", "--ref", notes_ref, "append", "-m", note, ref], repo)
472*73bd93f1SPatrick Williams
473*73bd93f1SPatrick Williams    @staticmethod
474*73bd93f1SPatrick Williams    def removeNote(repo, ref, key):
475*73bd93f1SPatrick Williams        notes = GitApplyTree.getNotes(repo, ref)
476*73bd93f1SPatrick Williams        notes = {k: v for k, v in notes.items() if k != key and not k.startswith(key + ":")}
477*73bd93f1SPatrick Williams        runcmd(["git", "notes", "--ref", GitApplyTree.notes_ref, "remove", "--ignore-missing", ref], repo)
478*73bd93f1SPatrick Williams        for note, value in notes.items():
479*73bd93f1SPatrick Williams            GitApplyTree.addNote(repo, ref, note, value)
480*73bd93f1SPatrick Williams
481*73bd93f1SPatrick Williams    @staticmethod
482*73bd93f1SPatrick Williams    def getNotes(repo, ref):
483*73bd93f1SPatrick Williams        import re
484*73bd93f1SPatrick Williams
485*73bd93f1SPatrick Williams        note = None
486*73bd93f1SPatrick Williams        try:
487*73bd93f1SPatrick Williams            note = runcmd(["git", "notes", "--ref", GitApplyTree.notes_ref, "show", ref], repo)
488*73bd93f1SPatrick Williams            prefix = ""
489*73bd93f1SPatrick Williams        except CmdError:
490*73bd93f1SPatrick Williams            note = runcmd(['git', 'show', '-s', '--format=%B', ref], repo)
491*73bd93f1SPatrick Williams            prefix = "%% "
492*73bd93f1SPatrick Williams
493*73bd93f1SPatrick Williams        note_re = re.compile(r'^%s(.*?)(?::\s*(.*))?$' % prefix)
494*73bd93f1SPatrick Williams        notes = dict()
495*73bd93f1SPatrick Williams        for line in note.splitlines():
496*73bd93f1SPatrick Williams            m = note_re.match(line)
497*73bd93f1SPatrick Williams            if m:
498*73bd93f1SPatrick Williams                notes[m.group(1)] = m.group(2)
499*73bd93f1SPatrick Williams
500*73bd93f1SPatrick Williams        return notes
501*73bd93f1SPatrick Williams
502*73bd93f1SPatrick Williams    @staticmethod
503*73bd93f1SPatrick Williams    def commitIgnored(subject, dir=None, files=None, d=None):
504*73bd93f1SPatrick Williams        if files:
505*73bd93f1SPatrick Williams            runcmd(['git', 'add'] + files, dir)
506*73bd93f1SPatrick Williams        cmd = ["git"]
507*73bd93f1SPatrick Williams        GitApplyTree.gitCommandUserOptions(cmd, d=d)
508*73bd93f1SPatrick Williams        cmd += ["commit", "-m", subject, "--no-verify"]
509*73bd93f1SPatrick Williams        runcmd(cmd, dir)
510*73bd93f1SPatrick Williams        GitApplyTree.addNote(dir, "HEAD", GitApplyTree.ignore_commit)
511*73bd93f1SPatrick Williams
512*73bd93f1SPatrick Williams    @staticmethod
513da295319SPatrick Williams    def extractPatches(tree, startcommits, outdir, paths=None):
514eb8dc403SDave Cobbley        import tempfile
515eb8dc403SDave Cobbley        import shutil
516eb8dc403SDave Cobbley        tempdir = tempfile.mkdtemp(prefix='oepatch')
517eb8dc403SDave Cobbley        try:
518da295319SPatrick Williams            for name, rev in startcommits.items():
519da295319SPatrick Williams                shellcmd = ["git", "format-patch", "--no-signature", "--no-numbered", rev, "-o", tempdir]
520eb8dc403SDave Cobbley                if paths:
521eb8dc403SDave Cobbley                    shellcmd.append('--')
522eb8dc403SDave Cobbley                    shellcmd.extend(paths)
523da295319SPatrick Williams                out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.join(tree, name))
524eb8dc403SDave Cobbley                if out:
525eb8dc403SDave Cobbley                    for srcfile in out.split():
526*73bd93f1SPatrick Williams                        # This loop, which is used to remove any line that
527*73bd93f1SPatrick Williams                        # starts with "%% original patch", is kept for backwards
528*73bd93f1SPatrick Williams                        # compatibility. If/when that compatibility is dropped,
529*73bd93f1SPatrick Williams                        # it can be replaced with code to just read the first
530*73bd93f1SPatrick Williams                        # line of the patch file to get the SHA-1, and the code
531*73bd93f1SPatrick Williams                        # below that writes the modified patch file can be
532*73bd93f1SPatrick Williams                        # replaced with a simple file move.
533eb8dc403SDave Cobbley                        for encoding in ['utf-8', 'latin-1']:
534eb8dc403SDave Cobbley                            patchlines = []
535eb8dc403SDave Cobbley                            try:
536169d7bccSPatrick Williams                                with open(srcfile, 'r', encoding=encoding, newline='') as f:
537eb8dc403SDave Cobbley                                    for line in f:
538*73bd93f1SPatrick Williams                                        if line.startswith("%% " + GitApplyTree.original_patch):
539eb8dc403SDave Cobbley                                            continue
540eb8dc403SDave Cobbley                                        patchlines.append(line)
541eb8dc403SDave Cobbley                            except UnicodeDecodeError:
542eb8dc403SDave Cobbley                                continue
543eb8dc403SDave Cobbley                            break
544eb8dc403SDave Cobbley                        else:
545eb8dc403SDave Cobbley                            raise PatchError('Unable to find a character encoding to decode %s' % srcfile)
546eb8dc403SDave Cobbley
547*73bd93f1SPatrick Williams                        sha1 = patchlines[0].split()[1]
548*73bd93f1SPatrick Williams                        notes = GitApplyTree.getNotes(os.path.join(tree, name), sha1)
549*73bd93f1SPatrick Williams                        if GitApplyTree.ignore_commit in notes:
550*73bd93f1SPatrick Williams                            continue
551*73bd93f1SPatrick Williams                        outfile = notes.get(GitApplyTree.original_patch, os.path.basename(srcfile))
552*73bd93f1SPatrick Williams
553da295319SPatrick Williams                        bb.utils.mkdirhier(os.path.join(outdir, name))
554da295319SPatrick Williams                        with open(os.path.join(outdir, name, outfile), 'w') as of:
555eb8dc403SDave Cobbley                            for line in patchlines:
556eb8dc403SDave Cobbley                                of.write(line)
557eb8dc403SDave Cobbley        finally:
558eb8dc403SDave Cobbley            shutil.rmtree(tempdir)
559eb8dc403SDave Cobbley
5608e7b46e2SPatrick Williams    def _need_dirty_check(self):
5618e7b46e2SPatrick Williams        fetch = bb.fetch2.Fetch([], self.d)
5628e7b46e2SPatrick Williams        check_dirtyness = False
5638e7b46e2SPatrick Williams        for url in fetch.urls:
5648e7b46e2SPatrick Williams            url_data = fetch.ud[url]
5658e7b46e2SPatrick Williams            parm = url_data.parm
5668e7b46e2SPatrick Williams            # a git url with subpath param will surely be dirty
5678e7b46e2SPatrick Williams            # since the git tree from which we clone will be emptied
5688e7b46e2SPatrick Williams            # from all files that are not in the subpath
5698e7b46e2SPatrick Williams            if url_data.type == 'git' and parm.get('subpath'):
5708e7b46e2SPatrick Williams                check_dirtyness = True
5718e7b46e2SPatrick Williams        return check_dirtyness
5728e7b46e2SPatrick Williams
5738e7b46e2SPatrick Williams    def _commitpatch(self, patch, patchfilevar):
5748e7b46e2SPatrick Williams        output = ""
5758e7b46e2SPatrick Williams        # Add all files
5768e7b46e2SPatrick Williams        shellcmd = ["git", "add", "-f", "-A", "."]
5778e7b46e2SPatrick Williams        output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
5788e7b46e2SPatrick Williams        # Exclude the patches directory
5798e7b46e2SPatrick Williams        shellcmd = ["git", "reset", "HEAD", self.patchdir]
5808e7b46e2SPatrick Williams        output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
5818e7b46e2SPatrick Williams        # Commit the result
5828e7b46e2SPatrick Williams        (tmpfile, shellcmd) = self.prepareCommit(patch['file'], self.commituser, self.commitemail)
5838e7b46e2SPatrick Williams        try:
5848e7b46e2SPatrick Williams            shellcmd.insert(0, patchfilevar)
5858e7b46e2SPatrick Williams            output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
5868e7b46e2SPatrick Williams        finally:
5878e7b46e2SPatrick Williams            os.remove(tmpfile)
5888e7b46e2SPatrick Williams        return output
5898e7b46e2SPatrick Williams
590eb8dc403SDave Cobbley    def _applypatch(self, patch, force = False, reverse = False, run = True):
591eb8dc403SDave Cobbley        import shutil
592eb8dc403SDave Cobbley
593eb8dc403SDave Cobbley        def _applypatchhelper(shellcmd, patch, force = False, reverse = False, run = True):
594eb8dc403SDave Cobbley            if reverse:
595eb8dc403SDave Cobbley                shellcmd.append('-R')
596eb8dc403SDave Cobbley
597eb8dc403SDave Cobbley            shellcmd.append(patch['file'])
598eb8dc403SDave Cobbley
599eb8dc403SDave Cobbley            if not run:
600eb8dc403SDave Cobbley                return "sh" + "-c" + " ".join(shellcmd)
601eb8dc403SDave Cobbley
602eb8dc403SDave Cobbley            return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
603eb8dc403SDave Cobbley
604eb8dc403SDave Cobbley        reporoot = (runcmd("git rev-parse --show-toplevel".split(), self.dir) or '').strip()
605eb8dc403SDave Cobbley        if not reporoot:
606eb8dc403SDave Cobbley            raise Exception("Cannot get repository root for directory %s" % self.dir)
607*73bd93f1SPatrick Williams
608*73bd93f1SPatrick Williams        patch_applied = True
609eb8dc403SDave Cobbley        try:
610eb8dc403SDave Cobbley            patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file'])
6118e7b46e2SPatrick Williams            if self._need_dirty_check():
6128e7b46e2SPatrick Williams                # Check dirtyness of the tree
6138e7b46e2SPatrick Williams                try:
6148e7b46e2SPatrick Williams                    output = runcmd(["git", "--work-tree=%s" % reporoot, "status", "--short"])
6158e7b46e2SPatrick Williams                except CmdError:
6168e7b46e2SPatrick Williams                    pass
6178e7b46e2SPatrick Williams                else:
6188e7b46e2SPatrick Williams                    if output:
619*73bd93f1SPatrick Williams                        # The tree is dirty, no need to try to apply patches with git anymore
6208e7b46e2SPatrick Williams                        # since they fail, fallback directly to patch
6218e7b46e2SPatrick Williams                        output = PatchTree._applypatch(self, patch, force, reverse, run)
6228e7b46e2SPatrick Williams                        output += self._commitpatch(patch, patchfilevar)
6238e7b46e2SPatrick Williams                        return output
624eb8dc403SDave Cobbley            try:
625eb8dc403SDave Cobbley                shellcmd = [patchfilevar, "git", "--work-tree=%s" % reporoot]
626eb8dc403SDave Cobbley                self.gitCommandUserOptions(shellcmd, self.commituser, self.commitemail)
627d1e89497SAndrew Geissler                shellcmd += ["am", "-3", "--keep-cr", "--no-scissors", "-p%s" % patch['strippath']]
628eb8dc403SDave Cobbley                return _applypatchhelper(shellcmd, patch, force, reverse, run)
629eb8dc403SDave Cobbley            except CmdError:
630eb8dc403SDave Cobbley                # Need to abort the git am, or we'll still be within it at the end
631eb8dc403SDave Cobbley                try:
632eb8dc403SDave Cobbley                    shellcmd = ["git", "--work-tree=%s" % reporoot, "am", "--abort"]
633eb8dc403SDave Cobbley                    runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
634eb8dc403SDave Cobbley                except CmdError:
635eb8dc403SDave Cobbley                    pass
636eb8dc403SDave Cobbley                # git am won't always clean up after itself, sadly, so...
637eb8dc403SDave Cobbley                shellcmd = ["git", "--work-tree=%s" % reporoot, "reset", "--hard", "HEAD"]
638eb8dc403SDave Cobbley                runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
639eb8dc403SDave Cobbley                # Also need to take care of any stray untracked files
640eb8dc403SDave Cobbley                shellcmd = ["git", "--work-tree=%s" % reporoot, "clean", "-f"]
641eb8dc403SDave Cobbley                runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
642eb8dc403SDave Cobbley
643eb8dc403SDave Cobbley                # Fall back to git apply
644eb8dc403SDave Cobbley                shellcmd = ["git", "--git-dir=%s" % reporoot, "apply", "-p%s" % patch['strippath']]
645eb8dc403SDave Cobbley                try:
646eb8dc403SDave Cobbley                    output = _applypatchhelper(shellcmd, patch, force, reverse, run)
647eb8dc403SDave Cobbley                except CmdError:
648eb8dc403SDave Cobbley                    # Fall back to patch
649eb8dc403SDave Cobbley                    output = PatchTree._applypatch(self, patch, force, reverse, run)
6508e7b46e2SPatrick Williams                output += self._commitpatch(patch, patchfilevar)
651eb8dc403SDave Cobbley                return output
652*73bd93f1SPatrick Williams        except:
653*73bd93f1SPatrick Williams            patch_applied = False
654*73bd93f1SPatrick Williams            raise
655eb8dc403SDave Cobbley        finally:
656*73bd93f1SPatrick Williams            if patch_applied:
657*73bd93f1SPatrick Williams                GitApplyTree.addNote(self.dir, "HEAD", GitApplyTree.original_patch, os.path.basename(patch['file']))
658eb8dc403SDave Cobbley
659eb8dc403SDave Cobbley
660eb8dc403SDave Cobbleyclass QuiltTree(PatchSet):
661eb8dc403SDave Cobbley    def _runcmd(self, args, run = True):
662eb8dc403SDave Cobbley        quiltrc = self.d.getVar('QUILTRCFILE')
663eb8dc403SDave Cobbley        if not run:
664eb8dc403SDave Cobbley            return ["quilt"] + ["--quiltrc"] + [quiltrc] + args
665eb8dc403SDave Cobbley        runcmd(["quilt"] + ["--quiltrc"] + [quiltrc] + args, self.dir)
666eb8dc403SDave Cobbley
667eb8dc403SDave Cobbley    def _quiltpatchpath(self, file):
668eb8dc403SDave Cobbley        return os.path.join(self.dir, "patches", os.path.basename(file))
669eb8dc403SDave Cobbley
670eb8dc403SDave Cobbley
671eb8dc403SDave Cobbley    def __init__(self, dir, d):
672eb8dc403SDave Cobbley        PatchSet.__init__(self, dir, d)
673eb8dc403SDave Cobbley        self.initialized = False
674eb8dc403SDave Cobbley        p = os.path.join(self.dir, 'patches')
675eb8dc403SDave Cobbley        if not os.path.exists(p):
676eb8dc403SDave Cobbley            os.makedirs(p)
677eb8dc403SDave Cobbley
678eb8dc403SDave Cobbley    def Clean(self):
679eb8dc403SDave Cobbley        try:
68078b72798SAndrew Geissler            # make sure that patches/series file exists before quilt pop to keep quilt-0.67 happy
68178b72798SAndrew Geissler            open(os.path.join(self.dir, "patches","series"), 'a').close()
682eb8dc403SDave Cobbley            self._runcmd(["pop", "-a", "-f"])
683eb8dc403SDave Cobbley            oe.path.remove(os.path.join(self.dir, "patches","series"))
684eb8dc403SDave Cobbley        except Exception:
685eb8dc403SDave Cobbley            pass
686eb8dc403SDave Cobbley        self.initialized = True
687eb8dc403SDave Cobbley
688eb8dc403SDave Cobbley    def InitFromDir(self):
689eb8dc403SDave Cobbley        # read series -> self.patches
690eb8dc403SDave Cobbley        seriespath = os.path.join(self.dir, 'patches', 'series')
691eb8dc403SDave Cobbley        if not os.path.exists(self.dir):
692eb8dc403SDave Cobbley            raise NotFoundError(self.dir)
693eb8dc403SDave Cobbley        if os.path.exists(seriespath):
694eb8dc403SDave Cobbley            with open(seriespath, 'r') as f:
695eb8dc403SDave Cobbley                for line in f.readlines():
696eb8dc403SDave Cobbley                    patch = {}
697eb8dc403SDave Cobbley                    parts = line.strip().split()
698eb8dc403SDave Cobbley                    patch["quiltfile"] = self._quiltpatchpath(parts[0])
699eb8dc403SDave Cobbley                    patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
700eb8dc403SDave Cobbley                    if len(parts) > 1:
701eb8dc403SDave Cobbley                        patch["strippath"] = parts[1][2:]
702eb8dc403SDave Cobbley                    self.patches.append(patch)
703eb8dc403SDave Cobbley
704eb8dc403SDave Cobbley            # determine which patches are applied -> self._current
705eb8dc403SDave Cobbley            try:
706eb8dc403SDave Cobbley                output = runcmd(["quilt", "applied"], self.dir)
707eb8dc403SDave Cobbley            except CmdError:
708eb8dc403SDave Cobbley                import sys
709eb8dc403SDave Cobbley                if sys.exc_value.output.strip() == "No patches applied":
710eb8dc403SDave Cobbley                    return
711eb8dc403SDave Cobbley                else:
712eb8dc403SDave Cobbley                    raise
713eb8dc403SDave Cobbley            output = [val for val in output.split('\n') if not val.startswith('#')]
714eb8dc403SDave Cobbley            for patch in self.patches:
715eb8dc403SDave Cobbley                if os.path.basename(patch["quiltfile"]) == output[-1]:
716eb8dc403SDave Cobbley                    self._current = self.patches.index(patch)
717eb8dc403SDave Cobbley        self.initialized = True
718eb8dc403SDave Cobbley
719eb8dc403SDave Cobbley    def Import(self, patch, force = None):
720eb8dc403SDave Cobbley        if not self.initialized:
721eb8dc403SDave Cobbley            self.InitFromDir()
722eb8dc403SDave Cobbley        PatchSet.Import(self, patch, force)
723eb8dc403SDave Cobbley        oe.path.symlink(patch["file"], self._quiltpatchpath(patch["file"]), force=True)
724eb8dc403SDave Cobbley        with open(os.path.join(self.dir, "patches", "series"), "a") as f:
725eb8dc403SDave Cobbley            f.write(os.path.basename(patch["file"]) + " -p" + patch["strippath"] + "\n")
726eb8dc403SDave Cobbley        patch["quiltfile"] = self._quiltpatchpath(patch["file"])
727eb8dc403SDave Cobbley        patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
728eb8dc403SDave Cobbley
729eb8dc403SDave Cobbley        # TODO: determine if the file being imported:
730eb8dc403SDave Cobbley        #      1) is already imported, and is the same
731eb8dc403SDave Cobbley        #      2) is already imported, but differs
732eb8dc403SDave Cobbley
733eb8dc403SDave Cobbley        self.patches.insert(self._current or 0, patch)
734eb8dc403SDave Cobbley
735eb8dc403SDave Cobbley
736eb8dc403SDave Cobbley    def Push(self, force = False, all = False, run = True):
737eb8dc403SDave Cobbley        # quilt push [-f]
738eb8dc403SDave Cobbley
739eb8dc403SDave Cobbley        args = ["push"]
740eb8dc403SDave Cobbley        if force:
741eb8dc403SDave Cobbley            args.append("-f")
742eb8dc403SDave Cobbley        if all:
743eb8dc403SDave Cobbley            args.append("-a")
744eb8dc403SDave Cobbley        if not run:
745eb8dc403SDave Cobbley            return self._runcmd(args, run)
746eb8dc403SDave Cobbley
747eb8dc403SDave Cobbley        self._runcmd(args)
748eb8dc403SDave Cobbley
749eb8dc403SDave Cobbley        if self._current is not None:
750eb8dc403SDave Cobbley            self._current = self._current + 1
751eb8dc403SDave Cobbley        else:
752eb8dc403SDave Cobbley            self._current = 0
753eb8dc403SDave Cobbley
754eb8dc403SDave Cobbley    def Pop(self, force = None, all = None):
755eb8dc403SDave Cobbley        # quilt pop [-f]
756eb8dc403SDave Cobbley        args = ["pop"]
757eb8dc403SDave Cobbley        if force:
758eb8dc403SDave Cobbley            args.append("-f")
759eb8dc403SDave Cobbley        if all:
760eb8dc403SDave Cobbley            args.append("-a")
761eb8dc403SDave Cobbley
762eb8dc403SDave Cobbley        self._runcmd(args)
763eb8dc403SDave Cobbley
764eb8dc403SDave Cobbley        if self._current == 0:
765eb8dc403SDave Cobbley            self._current = None
766eb8dc403SDave Cobbley
767eb8dc403SDave Cobbley        if self._current is not None:
768eb8dc403SDave Cobbley            self._current = self._current - 1
769eb8dc403SDave Cobbley
770eb8dc403SDave Cobbley    def Refresh(self, **kwargs):
771eb8dc403SDave Cobbley        if kwargs.get("remote"):
772eb8dc403SDave Cobbley            patch = self.patches[kwargs["patch"]]
773eb8dc403SDave Cobbley            if not patch:
774eb8dc403SDave Cobbley                raise PatchError("No patch found at index %s in patchset." % kwargs["patch"])
775eb8dc403SDave Cobbley            (type, host, path, user, pswd, parm) = bb.fetch.decodeurl(patch["remote"])
776eb8dc403SDave Cobbley            if type == "file":
777eb8dc403SDave Cobbley                import shutil
778eb8dc403SDave Cobbley                if not patch.get("file") and patch.get("remote"):
779eb8dc403SDave Cobbley                    patch["file"] = bb.fetch2.localpath(patch["remote"], self.d)
780eb8dc403SDave Cobbley
781eb8dc403SDave Cobbley                shutil.copyfile(patch["quiltfile"], patch["file"])
782eb8dc403SDave Cobbley            else:
783eb8dc403SDave Cobbley                raise PatchError("Unable to do a remote refresh of %s, unsupported remote url scheme %s." % (os.path.basename(patch["quiltfile"]), type))
784eb8dc403SDave Cobbley        else:
785eb8dc403SDave Cobbley            # quilt refresh
786eb8dc403SDave Cobbley            args = ["refresh"]
787eb8dc403SDave Cobbley            if kwargs.get("quiltfile"):
788eb8dc403SDave Cobbley                args.append(os.path.basename(kwargs["quiltfile"]))
789eb8dc403SDave Cobbley            elif kwargs.get("patch"):
790eb8dc403SDave Cobbley                args.append(os.path.basename(self.patches[kwargs["patch"]]["quiltfile"]))
791eb8dc403SDave Cobbley            self._runcmd(args)
792eb8dc403SDave Cobbley
793eb8dc403SDave Cobbleyclass Resolver(object):
794eb8dc403SDave Cobbley    def __init__(self, patchset, terminal):
795eb8dc403SDave Cobbley        raise NotImplementedError()
796eb8dc403SDave Cobbley
797eb8dc403SDave Cobbley    def Resolve(self):
798eb8dc403SDave Cobbley        raise NotImplementedError()
799eb8dc403SDave Cobbley
800eb8dc403SDave Cobbley    def Revert(self):
801eb8dc403SDave Cobbley        raise NotImplementedError()
802eb8dc403SDave Cobbley
803eb8dc403SDave Cobbley    def Finalize(self):
804eb8dc403SDave Cobbley        raise NotImplementedError()
805eb8dc403SDave Cobbley
806eb8dc403SDave Cobbleyclass NOOPResolver(Resolver):
807eb8dc403SDave Cobbley    def __init__(self, patchset, terminal):
808eb8dc403SDave Cobbley        self.patchset = patchset
809eb8dc403SDave Cobbley        self.terminal = terminal
810eb8dc403SDave Cobbley
811eb8dc403SDave Cobbley    def Resolve(self):
812eb8dc403SDave Cobbley        olddir = os.path.abspath(os.curdir)
813eb8dc403SDave Cobbley        os.chdir(self.patchset.dir)
814eb8dc403SDave Cobbley        try:
815eb8dc403SDave Cobbley            self.patchset.Push()
816eb8dc403SDave Cobbley        except Exception:
817eb8dc403SDave Cobbley            import sys
818eb8dc403SDave Cobbley            raise
819ac13d5f3SPatrick Williams        finally:
820ac13d5f3SPatrick Williams            os.chdir(olddir)
821eb8dc403SDave Cobbley
822eb8dc403SDave Cobbley# Patch resolver which relies on the user doing all the work involved in the
823eb8dc403SDave Cobbley# resolution, with the exception of refreshing the remote copy of the patch
824eb8dc403SDave Cobbley# files (the urls).
825eb8dc403SDave Cobbleyclass UserResolver(Resolver):
826eb8dc403SDave Cobbley    def __init__(self, patchset, terminal):
827eb8dc403SDave Cobbley        self.patchset = patchset
828eb8dc403SDave Cobbley        self.terminal = terminal
829eb8dc403SDave Cobbley
830eb8dc403SDave Cobbley    # Force a push in the patchset, then drop to a shell for the user to
831eb8dc403SDave Cobbley    # resolve any rejected hunks
832eb8dc403SDave Cobbley    def Resolve(self):
833eb8dc403SDave Cobbley        olddir = os.path.abspath(os.curdir)
834eb8dc403SDave Cobbley        os.chdir(self.patchset.dir)
835eb8dc403SDave Cobbley        try:
836eb8dc403SDave Cobbley            self.patchset.Push(False)
837eb8dc403SDave Cobbley        except CmdError as v:
838eb8dc403SDave Cobbley            # Patch application failed
839eb8dc403SDave Cobbley            patchcmd = self.patchset.Push(True, False, False)
840eb8dc403SDave Cobbley
841eb8dc403SDave Cobbley            t = self.patchset.d.getVar('T')
842eb8dc403SDave Cobbley            if not t:
843eb8dc403SDave Cobbley                bb.msg.fatal("Build", "T not set")
844eb8dc403SDave Cobbley            bb.utils.mkdirhier(t)
845eb8dc403SDave Cobbley            import random
846eb8dc403SDave Cobbley            rcfile = "%s/bashrc.%s.%s" % (t, str(os.getpid()), random.random())
847eb8dc403SDave Cobbley            with open(rcfile, "w") as f:
848eb8dc403SDave Cobbley                f.write("echo '*** Manual patch resolution mode ***'\n")
849eb8dc403SDave Cobbley                f.write("echo 'Dropping to a shell, so patch rejects can be fixed manually.'\n")
850eb8dc403SDave Cobbley                f.write("echo 'Run \"quilt refresh\" when patch is corrected, press CTRL+D to exit.'\n")
851eb8dc403SDave Cobbley                f.write("echo ''\n")
852eb8dc403SDave Cobbley                f.write(" ".join(patchcmd) + "\n")
853eb8dc403SDave Cobbley            os.chmod(rcfile, 0o775)
854eb8dc403SDave Cobbley
855eb8dc403SDave Cobbley            self.terminal("bash --rcfile " + rcfile, 'Patch Rejects: Please fix patch rejects manually', self.patchset.d)
856eb8dc403SDave Cobbley
857eb8dc403SDave Cobbley            # Construct a new PatchSet after the user's changes, compare the
858eb8dc403SDave Cobbley            # sets, checking patches for modifications, and doing a remote
859eb8dc403SDave Cobbley            # refresh on each.
860eb8dc403SDave Cobbley            oldpatchset = self.patchset
861eb8dc403SDave Cobbley            self.patchset = oldpatchset.__class__(self.patchset.dir, self.patchset.d)
862eb8dc403SDave Cobbley
863eb8dc403SDave Cobbley            for patch in self.patchset.patches:
864eb8dc403SDave Cobbley                oldpatch = None
865eb8dc403SDave Cobbley                for opatch in oldpatchset.patches:
866eb8dc403SDave Cobbley                    if opatch["quiltfile"] == patch["quiltfile"]:
867eb8dc403SDave Cobbley                        oldpatch = opatch
868eb8dc403SDave Cobbley
869eb8dc403SDave Cobbley                if oldpatch:
870eb8dc403SDave Cobbley                    patch["remote"] = oldpatch["remote"]
871eb8dc403SDave Cobbley                    if patch["quiltfile"] == oldpatch["quiltfile"]:
872eb8dc403SDave Cobbley                        if patch["quiltfilemd5"] != oldpatch["quiltfilemd5"]:
873eb8dc403SDave Cobbley                            bb.note("Patch %s has changed, updating remote url %s" % (os.path.basename(patch["quiltfile"]), patch["remote"]))
874eb8dc403SDave Cobbley                            # user change?  remote refresh
875eb8dc403SDave Cobbley                            self.patchset.Refresh(remote=True, patch=self.patchset.patches.index(patch))
876eb8dc403SDave Cobbley                        else:
877eb8dc403SDave Cobbley                            # User did not fix the problem.  Abort.
878eb8dc403SDave Cobbley                            raise PatchError("Patch application failed, and user did not fix and refresh the patch.")
879eb8dc403SDave Cobbley        except Exception:
880eb8dc403SDave Cobbley            raise
881ac13d5f3SPatrick Williams        finally:
882eb8dc403SDave Cobbley            os.chdir(olddir)
883eb8dc403SDave Cobbley
884eb8dc403SDave Cobbley
885eb8dc403SDave Cobbleydef patch_path(url, fetch, workdir, expand=True):
88619323693SBrad Bishop    """Return the local path of a patch, or return nothing if this isn't a patch"""
887eb8dc403SDave Cobbley
888eb8dc403SDave Cobbley    local = fetch.localpath(url)
88919323693SBrad Bishop    if os.path.isdir(local):
89019323693SBrad Bishop        return
891eb8dc403SDave Cobbley    base, ext = os.path.splitext(os.path.basename(local))
892eb8dc403SDave Cobbley    if ext in ('.gz', '.bz2', '.xz', '.Z'):
893eb8dc403SDave Cobbley        if expand:
894eb8dc403SDave Cobbley            local = os.path.join(workdir, base)
895eb8dc403SDave Cobbley        ext = os.path.splitext(base)[1]
896eb8dc403SDave Cobbley
897eb8dc403SDave Cobbley    urldata = fetch.ud[url]
898eb8dc403SDave Cobbley    if "apply" in urldata.parm:
899eb8dc403SDave Cobbley        apply = oe.types.boolean(urldata.parm["apply"])
900eb8dc403SDave Cobbley        if not apply:
901eb8dc403SDave Cobbley            return
902eb8dc403SDave Cobbley    elif ext not in (".diff", ".patch"):
903eb8dc403SDave Cobbley        return
904eb8dc403SDave Cobbley
905eb8dc403SDave Cobbley    return local
906eb8dc403SDave Cobbley
907eb8dc403SDave Cobbleydef src_patches(d, all=False, expand=True):
908eb8dc403SDave Cobbley    workdir = d.getVar('WORKDIR')
909eb8dc403SDave Cobbley    fetch = bb.fetch2.Fetch([], d)
910eb8dc403SDave Cobbley    patches = []
911eb8dc403SDave Cobbley    sources = []
912eb8dc403SDave Cobbley    for url in fetch.urls:
913eb8dc403SDave Cobbley        local = patch_path(url, fetch, workdir, expand)
914eb8dc403SDave Cobbley        if not local:
915eb8dc403SDave Cobbley            if all:
916eb8dc403SDave Cobbley                local = fetch.localpath(url)
917eb8dc403SDave Cobbley                sources.append(local)
918eb8dc403SDave Cobbley            continue
919eb8dc403SDave Cobbley
920eb8dc403SDave Cobbley        urldata = fetch.ud[url]
921eb8dc403SDave Cobbley        parm = urldata.parm
922eb8dc403SDave Cobbley        patchname = parm.get('pname') or os.path.basename(local)
923eb8dc403SDave Cobbley
924eb8dc403SDave Cobbley        apply, reason = should_apply(parm, d)
925eb8dc403SDave Cobbley        if not apply:
926eb8dc403SDave Cobbley            if reason:
927eb8dc403SDave Cobbley                bb.note("Patch %s %s" % (patchname, reason))
928eb8dc403SDave Cobbley            continue
929eb8dc403SDave Cobbley
930eb8dc403SDave Cobbley        patchparm = {'patchname': patchname}
931eb8dc403SDave Cobbley        if "striplevel" in parm:
932eb8dc403SDave Cobbley            striplevel = parm["striplevel"]
933eb8dc403SDave Cobbley        elif "pnum" in parm:
934eb8dc403SDave Cobbley            #bb.msg.warn(None, "Deprecated usage of 'pnum' url parameter in '%s', please use 'striplevel'" % url)
935eb8dc403SDave Cobbley            striplevel = parm["pnum"]
936eb8dc403SDave Cobbley        else:
937eb8dc403SDave Cobbley            striplevel = '1'
938eb8dc403SDave Cobbley        patchparm['striplevel'] = striplevel
939eb8dc403SDave Cobbley
940eb8dc403SDave Cobbley        patchdir = parm.get('patchdir')
941eb8dc403SDave Cobbley        if patchdir:
942eb8dc403SDave Cobbley            patchparm['patchdir'] = patchdir
943eb8dc403SDave Cobbley
944eb8dc403SDave Cobbley        localurl = bb.fetch.encodeurl(('file', '', local, '', '', patchparm))
945eb8dc403SDave Cobbley        patches.append(localurl)
946eb8dc403SDave Cobbley
947eb8dc403SDave Cobbley    if all:
948eb8dc403SDave Cobbley        return sources
949eb8dc403SDave Cobbley
950eb8dc403SDave Cobbley    return patches
951eb8dc403SDave Cobbley
952eb8dc403SDave Cobbley
953eb8dc403SDave Cobbleydef should_apply(parm, d):
954c342db35SBrad Bishop    import bb.utils
955eb8dc403SDave Cobbley    if "mindate" in parm or "maxdate" in parm:
956eb8dc403SDave Cobbley        pn = d.getVar('PN')
957eb8dc403SDave Cobbley        srcdate = d.getVar('SRCDATE_%s' % pn)
958eb8dc403SDave Cobbley        if not srcdate:
959eb8dc403SDave Cobbley            srcdate = d.getVar('SRCDATE')
960eb8dc403SDave Cobbley
961eb8dc403SDave Cobbley        if srcdate == "now":
962eb8dc403SDave Cobbley            srcdate = d.getVar('DATE')
963eb8dc403SDave Cobbley
964eb8dc403SDave Cobbley        if "maxdate" in parm and parm["maxdate"] < srcdate:
965eb8dc403SDave Cobbley            return False, 'is outdated'
966eb8dc403SDave Cobbley
967eb8dc403SDave Cobbley        if "mindate" in parm and parm["mindate"] > srcdate:
968eb8dc403SDave Cobbley            return False, 'is predated'
969eb8dc403SDave Cobbley
970eb8dc403SDave Cobbley
971eb8dc403SDave Cobbley    if "minrev" in parm:
972eb8dc403SDave Cobbley        srcrev = d.getVar('SRCREV')
973eb8dc403SDave Cobbley        if srcrev and srcrev < parm["minrev"]:
974eb8dc403SDave Cobbley            return False, 'applies to later revisions'
975eb8dc403SDave Cobbley
976eb8dc403SDave Cobbley    if "maxrev" in parm:
977eb8dc403SDave Cobbley        srcrev = d.getVar('SRCREV')
978eb8dc403SDave Cobbley        if srcrev and srcrev > parm["maxrev"]:
979eb8dc403SDave Cobbley            return False, 'applies to earlier revisions'
980eb8dc403SDave Cobbley
981eb8dc403SDave Cobbley    if "rev" in parm:
982eb8dc403SDave Cobbley        srcrev = d.getVar('SRCREV')
983eb8dc403SDave Cobbley        if srcrev and parm["rev"] not in srcrev:
984eb8dc403SDave Cobbley            return False, "doesn't apply to revision"
985eb8dc403SDave Cobbley
986eb8dc403SDave Cobbley    if "notrev" in parm:
987eb8dc403SDave Cobbley        srcrev = d.getVar('SRCREV')
988eb8dc403SDave Cobbley        if srcrev and parm["notrev"] in srcrev:
989eb8dc403SDave Cobbley            return False, "doesn't apply to revision"
990eb8dc403SDave Cobbley
991c342db35SBrad Bishop    if "maxver" in parm:
992c342db35SBrad Bishop        pv = d.getVar('PV')
993c342db35SBrad Bishop        if bb.utils.vercmp_string_op(pv, parm["maxver"], ">"):
994c342db35SBrad Bishop            return False, "applies to earlier version"
995c342db35SBrad Bishop
996c342db35SBrad Bishop    if "minver" in parm:
997c342db35SBrad Bishop        pv = d.getVar('PV')
998c342db35SBrad Bishop        if bb.utils.vercmp_string_op(pv, parm["minver"], "<"):
999c342db35SBrad Bishop            return False, "applies to later version"
1000c342db35SBrad Bishop
1001eb8dc403SDave Cobbley    return True, None
1002