xref: /openbmc/openbmc/poky/meta/lib/oe/patch.py (revision da295319)
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):
297eb8dc403SDave Cobbley    patch_line_prefix = '%% original patch'
298eb8dc403SDave Cobbley    ignore_commit_prefix = '%% ignore'
299eb8dc403SDave Cobbley
300eb8dc403SDave Cobbley    def __init__(self, dir, d):
301eb8dc403SDave Cobbley        PatchTree.__init__(self, dir, d)
302eb8dc403SDave Cobbley        self.commituser = d.getVar('PATCH_GIT_USER_NAME')
303eb8dc403SDave Cobbley        self.commitemail = d.getVar('PATCH_GIT_USER_EMAIL')
304615f2f11SAndrew Geissler        if not self._isInitialized(d):
305595f6308SAndrew Geissler            self._initRepo()
306595f6308SAndrew Geissler
307615f2f11SAndrew Geissler    def _isInitialized(self, d):
308595f6308SAndrew Geissler        cmd = "git rev-parse --show-toplevel"
3097e0e3c0cSAndrew Geissler        try:
3107e0e3c0cSAndrew Geissler            output = runcmd(cmd.split(), self.dir).strip()
3117e0e3c0cSAndrew Geissler        except CmdError as err:
3127e0e3c0cSAndrew Geissler            ## runcmd returned non-zero which most likely means 128
3137e0e3c0cSAndrew Geissler            ## Not a git directory
3147e0e3c0cSAndrew Geissler            return False
315615f2f11SAndrew Geissler        ## Make sure repo is in builddir to not break top-level git repos, or under workdir
316615f2f11SAndrew Geissler        return os.path.samefile(output, self.dir) or oe.path.is_path_parent(d.getVar('WORKDIR'), output)
317595f6308SAndrew Geissler
318595f6308SAndrew Geissler    def _initRepo(self):
319595f6308SAndrew Geissler        runcmd("git init".split(), self.dir)
320595f6308SAndrew Geissler        runcmd("git add .".split(), self.dir)
3217e0e3c0cSAndrew Geissler        runcmd("git commit -a --allow-empty -m bitbake_patching_started".split(), self.dir)
322eb8dc403SDave Cobbley
323eb8dc403SDave Cobbley    @staticmethod
324eb8dc403SDave Cobbley    def extractPatchHeader(patchfile):
325eb8dc403SDave Cobbley        """
326eb8dc403SDave Cobbley        Extract just the header lines from the top of a patch file
327eb8dc403SDave Cobbley        """
328eb8dc403SDave Cobbley        for encoding in ['utf-8', 'latin-1']:
329eb8dc403SDave Cobbley            lines = []
330eb8dc403SDave Cobbley            try:
331eb8dc403SDave Cobbley                with open(patchfile, 'r', encoding=encoding) as f:
332eb8dc403SDave Cobbley                    for line in f:
333eb8dc403SDave Cobbley                        if line.startswith('Index: ') or line.startswith('diff -') or line.startswith('---'):
334eb8dc403SDave Cobbley                            break
335eb8dc403SDave Cobbley                        lines.append(line)
336eb8dc403SDave Cobbley            except UnicodeDecodeError:
337eb8dc403SDave Cobbley                continue
338eb8dc403SDave Cobbley            break
339eb8dc403SDave Cobbley        else:
340eb8dc403SDave Cobbley            raise PatchError('Unable to find a character encoding to decode %s' % patchfile)
341eb8dc403SDave Cobbley        return lines
342eb8dc403SDave Cobbley
343eb8dc403SDave Cobbley    @staticmethod
344eb8dc403SDave Cobbley    def decodeAuthor(line):
345eb8dc403SDave Cobbley        from email.header import decode_header
346eb8dc403SDave Cobbley        authorval = line.split(':', 1)[1].strip().replace('"', '')
347eb8dc403SDave Cobbley        result =  decode_header(authorval)[0][0]
348eb8dc403SDave Cobbley        if hasattr(result, 'decode'):
349eb8dc403SDave Cobbley            result = result.decode('utf-8')
350eb8dc403SDave Cobbley        return result
351eb8dc403SDave Cobbley
352eb8dc403SDave Cobbley    @staticmethod
353eb8dc403SDave Cobbley    def interpretPatchHeader(headerlines):
354eb8dc403SDave Cobbley        import re
35519323693SBrad Bishop        author_re = re.compile(r'[\S ]+ <\S+@\S+\.\S+>')
35619323693SBrad Bishop        from_commit_re = re.compile(r'^From [a-z0-9]{40} .*')
357eb8dc403SDave Cobbley        outlines = []
358eb8dc403SDave Cobbley        author = None
359eb8dc403SDave Cobbley        date = None
360eb8dc403SDave Cobbley        subject = None
361eb8dc403SDave Cobbley        for line in headerlines:
362eb8dc403SDave Cobbley            if line.startswith('Subject: '):
363eb8dc403SDave Cobbley                subject = line.split(':', 1)[1]
364eb8dc403SDave Cobbley                # Remove any [PATCH][oe-core] etc.
365eb8dc403SDave Cobbley                subject = re.sub(r'\[.+?\]\s*', '', subject)
366eb8dc403SDave Cobbley                continue
367eb8dc403SDave Cobbley            elif line.startswith('From: ') or line.startswith('Author: '):
368eb8dc403SDave Cobbley                authorval = GitApplyTree.decodeAuthor(line)
369eb8dc403SDave Cobbley                # git is fussy about author formatting i.e. it must be Name <email@domain>
370eb8dc403SDave Cobbley                if author_re.match(authorval):
371eb8dc403SDave Cobbley                    author = authorval
372eb8dc403SDave Cobbley                    continue
373eb8dc403SDave Cobbley            elif line.startswith('Date: '):
374eb8dc403SDave Cobbley                if date is None:
375eb8dc403SDave Cobbley                    dateval = line.split(':', 1)[1].strip()
376eb8dc403SDave Cobbley                    # Very crude check for date format, since git will blow up if it's not in the right
377eb8dc403SDave Cobbley                    # format. Without e.g. a python-dateutils dependency we can't do a whole lot more
378eb8dc403SDave Cobbley                    if len(dateval) > 12:
379eb8dc403SDave Cobbley                        date = dateval
380eb8dc403SDave Cobbley                continue
381eb8dc403SDave Cobbley            elif not author and line.lower().startswith('signed-off-by: '):
382eb8dc403SDave Cobbley                authorval = GitApplyTree.decodeAuthor(line)
383eb8dc403SDave Cobbley                # git is fussy about author formatting i.e. it must be Name <email@domain>
384eb8dc403SDave Cobbley                if author_re.match(authorval):
385eb8dc403SDave Cobbley                    author = authorval
386eb8dc403SDave Cobbley            elif from_commit_re.match(line):
387eb8dc403SDave Cobbley                # We don't want the From <commit> line - if it's present it will break rebasing
388eb8dc403SDave Cobbley                continue
389eb8dc403SDave Cobbley            outlines.append(line)
390eb8dc403SDave Cobbley
391eb8dc403SDave Cobbley        if not subject:
392eb8dc403SDave Cobbley            firstline = None
393eb8dc403SDave Cobbley            for line in headerlines:
394eb8dc403SDave Cobbley                line = line.strip()
395eb8dc403SDave Cobbley                if firstline:
396eb8dc403SDave Cobbley                    if line:
397eb8dc403SDave Cobbley                        # Second line is not blank, the first line probably isn't usable
398eb8dc403SDave Cobbley                        firstline = None
399eb8dc403SDave Cobbley                    break
400eb8dc403SDave Cobbley                elif line:
401eb8dc403SDave Cobbley                    firstline = line
402eb8dc403SDave Cobbley            if firstline and not firstline.startswith(('#', 'Index:', 'Upstream-Status:')) and len(firstline) < 100:
403eb8dc403SDave Cobbley                subject = firstline
404eb8dc403SDave Cobbley
405eb8dc403SDave Cobbley        return outlines, author, date, subject
406eb8dc403SDave Cobbley
407eb8dc403SDave Cobbley    @staticmethod
408eb8dc403SDave Cobbley    def gitCommandUserOptions(cmd, commituser=None, commitemail=None, d=None):
409eb8dc403SDave Cobbley        if d:
410eb8dc403SDave Cobbley            commituser = d.getVar('PATCH_GIT_USER_NAME')
411eb8dc403SDave Cobbley            commitemail = d.getVar('PATCH_GIT_USER_EMAIL')
412eb8dc403SDave Cobbley        if commituser:
413eb8dc403SDave Cobbley            cmd += ['-c', 'user.name="%s"' % commituser]
414eb8dc403SDave Cobbley        if commitemail:
415eb8dc403SDave Cobbley            cmd += ['-c', 'user.email="%s"' % commitemail]
416eb8dc403SDave Cobbley
417eb8dc403SDave Cobbley    @staticmethod
418eb8dc403SDave Cobbley    def prepareCommit(patchfile, commituser=None, commitemail=None):
419eb8dc403SDave Cobbley        """
420eb8dc403SDave Cobbley        Prepare a git commit command line based on the header from a patch file
421eb8dc403SDave Cobbley        (typically this is useful for patches that cannot be applied with "git am" due to formatting)
422eb8dc403SDave Cobbley        """
423eb8dc403SDave Cobbley        import tempfile
424eb8dc403SDave Cobbley        # Process patch header and extract useful information
425eb8dc403SDave Cobbley        lines = GitApplyTree.extractPatchHeader(patchfile)
426eb8dc403SDave Cobbley        outlines, author, date, subject = GitApplyTree.interpretPatchHeader(lines)
427eb8dc403SDave Cobbley        if not author or not subject or not date:
428eb8dc403SDave Cobbley            try:
429eb8dc403SDave Cobbley                shellcmd = ["git", "log", "--format=email", "--follow", "--diff-filter=A", "--", patchfile]
430eb8dc403SDave Cobbley                out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.dirname(patchfile))
431eb8dc403SDave Cobbley            except CmdError:
432eb8dc403SDave Cobbley                out = None
433eb8dc403SDave Cobbley            if out:
434eb8dc403SDave Cobbley                _, newauthor, newdate, newsubject = GitApplyTree.interpretPatchHeader(out.splitlines())
435eb8dc403SDave Cobbley                if not author:
436eb8dc403SDave Cobbley                    # If we're setting the author then the date should be set as well
437eb8dc403SDave Cobbley                    author = newauthor
438eb8dc403SDave Cobbley                    date = newdate
439eb8dc403SDave Cobbley                elif not date:
440eb8dc403SDave Cobbley                    # If we don't do this we'll get the current date, at least this will be closer
441eb8dc403SDave Cobbley                    date = newdate
442eb8dc403SDave Cobbley                if not subject:
443eb8dc403SDave Cobbley                    subject = newsubject
4444ed12e16SAndrew Geissler        if subject and not (outlines and outlines[0].strip() == subject):
445eb8dc403SDave Cobbley            outlines.insert(0, '%s\n\n' % subject.strip())
446eb8dc403SDave Cobbley
447eb8dc403SDave Cobbley        # Write out commit message to a file
448eb8dc403SDave Cobbley        with tempfile.NamedTemporaryFile('w', delete=False) as tf:
449eb8dc403SDave Cobbley            tmpfile = tf.name
450eb8dc403SDave Cobbley            for line in outlines:
451eb8dc403SDave Cobbley                tf.write(line)
452eb8dc403SDave Cobbley        # Prepare git command
453eb8dc403SDave Cobbley        cmd = ["git"]
454eb8dc403SDave Cobbley        GitApplyTree.gitCommandUserOptions(cmd, commituser, commitemail)
455eb8dc403SDave Cobbley        cmd += ["commit", "-F", tmpfile]
456eb8dc403SDave Cobbley        # git doesn't like plain email addresses as authors
457eb8dc403SDave Cobbley        if author and '<' in author:
458eb8dc403SDave Cobbley            cmd.append('--author="%s"' % author)
459eb8dc403SDave Cobbley        if date:
460eb8dc403SDave Cobbley            cmd.append('--date="%s"' % date)
461eb8dc403SDave Cobbley        return (tmpfile, cmd)
462eb8dc403SDave Cobbley
463eb8dc403SDave Cobbley    @staticmethod
464*da295319SPatrick Williams    def extractPatches(tree, startcommits, outdir, paths=None):
465eb8dc403SDave Cobbley        import tempfile
466eb8dc403SDave Cobbley        import shutil
467eb8dc403SDave Cobbley        tempdir = tempfile.mkdtemp(prefix='oepatch')
468eb8dc403SDave Cobbley        try:
469*da295319SPatrick Williams            for name, rev in startcommits.items():
470*da295319SPatrick Williams                shellcmd = ["git", "format-patch", "--no-signature", "--no-numbered", rev, "-o", tempdir]
471eb8dc403SDave Cobbley                if paths:
472eb8dc403SDave Cobbley                    shellcmd.append('--')
473eb8dc403SDave Cobbley                    shellcmd.extend(paths)
474*da295319SPatrick Williams                out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.join(tree, name))
475eb8dc403SDave Cobbley                if out:
476eb8dc403SDave Cobbley                    for srcfile in out.split():
477eb8dc403SDave Cobbley                        for encoding in ['utf-8', 'latin-1']:
478eb8dc403SDave Cobbley                            patchlines = []
479eb8dc403SDave Cobbley                            outfile = None
480eb8dc403SDave Cobbley                            try:
481eb8dc403SDave Cobbley                                with open(srcfile, 'r', encoding=encoding) as f:
482eb8dc403SDave Cobbley                                    for line in f:
4834ed12e16SAndrew Geissler                                        if line.startswith(GitApplyTree.patch_line_prefix):
484eb8dc403SDave Cobbley                                            outfile = line.split()[-1].strip()
485eb8dc403SDave Cobbley                                            continue
4864ed12e16SAndrew Geissler                                        if line.startswith(GitApplyTree.ignore_commit_prefix):
487eb8dc403SDave Cobbley                                            continue
488eb8dc403SDave Cobbley                                        patchlines.append(line)
489eb8dc403SDave Cobbley                            except UnicodeDecodeError:
490eb8dc403SDave Cobbley                                continue
491eb8dc403SDave Cobbley                            break
492eb8dc403SDave Cobbley                        else:
493eb8dc403SDave Cobbley                            raise PatchError('Unable to find a character encoding to decode %s' % srcfile)
494eb8dc403SDave Cobbley
495eb8dc403SDave Cobbley                        if not outfile:
496eb8dc403SDave Cobbley                            outfile = os.path.basename(srcfile)
497*da295319SPatrick Williams                        bb.utils.mkdirhier(os.path.join(outdir, name))
498*da295319SPatrick Williams                        with open(os.path.join(outdir, name, outfile), 'w') as of:
499eb8dc403SDave Cobbley                            for line in patchlines:
500eb8dc403SDave Cobbley                                of.write(line)
501eb8dc403SDave Cobbley        finally:
502eb8dc403SDave Cobbley            shutil.rmtree(tempdir)
503eb8dc403SDave Cobbley
5048e7b46e2SPatrick Williams    def _need_dirty_check(self):
5058e7b46e2SPatrick Williams        fetch = bb.fetch2.Fetch([], self.d)
5068e7b46e2SPatrick Williams        check_dirtyness = False
5078e7b46e2SPatrick Williams        for url in fetch.urls:
5088e7b46e2SPatrick Williams            url_data = fetch.ud[url]
5098e7b46e2SPatrick Williams            parm = url_data.parm
5108e7b46e2SPatrick Williams            # a git url with subpath param will surely be dirty
5118e7b46e2SPatrick Williams            # since the git tree from which we clone will be emptied
5128e7b46e2SPatrick Williams            # from all files that are not in the subpath
5138e7b46e2SPatrick Williams            if url_data.type == 'git' and parm.get('subpath'):
5148e7b46e2SPatrick Williams                check_dirtyness = True
5158e7b46e2SPatrick Williams        return check_dirtyness
5168e7b46e2SPatrick Williams
5178e7b46e2SPatrick Williams    def _commitpatch(self, patch, patchfilevar):
5188e7b46e2SPatrick Williams        output = ""
5198e7b46e2SPatrick Williams        # Add all files
5208e7b46e2SPatrick Williams        shellcmd = ["git", "add", "-f", "-A", "."]
5218e7b46e2SPatrick Williams        output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
5228e7b46e2SPatrick Williams        # Exclude the patches directory
5238e7b46e2SPatrick Williams        shellcmd = ["git", "reset", "HEAD", self.patchdir]
5248e7b46e2SPatrick Williams        output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
5258e7b46e2SPatrick Williams        # Commit the result
5268e7b46e2SPatrick Williams        (tmpfile, shellcmd) = self.prepareCommit(patch['file'], self.commituser, self.commitemail)
5278e7b46e2SPatrick Williams        try:
5288e7b46e2SPatrick Williams            shellcmd.insert(0, patchfilevar)
5298e7b46e2SPatrick Williams            output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
5308e7b46e2SPatrick Williams        finally:
5318e7b46e2SPatrick Williams            os.remove(tmpfile)
5328e7b46e2SPatrick Williams        return output
5338e7b46e2SPatrick Williams
534eb8dc403SDave Cobbley    def _applypatch(self, patch, force = False, reverse = False, run = True):
535eb8dc403SDave Cobbley        import shutil
536eb8dc403SDave Cobbley
537eb8dc403SDave Cobbley        def _applypatchhelper(shellcmd, patch, force = False, reverse = False, run = True):
538eb8dc403SDave Cobbley            if reverse:
539eb8dc403SDave Cobbley                shellcmd.append('-R')
540eb8dc403SDave Cobbley
541eb8dc403SDave Cobbley            shellcmd.append(patch['file'])
542eb8dc403SDave Cobbley
543eb8dc403SDave Cobbley            if not run:
544eb8dc403SDave Cobbley                return "sh" + "-c" + " ".join(shellcmd)
545eb8dc403SDave Cobbley
546eb8dc403SDave Cobbley            return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
547eb8dc403SDave Cobbley
548eb8dc403SDave Cobbley        # Add hooks which add a pointer to the original patch file name in the commit message
549eb8dc403SDave Cobbley        reporoot = (runcmd("git rev-parse --show-toplevel".split(), self.dir) or '').strip()
550eb8dc403SDave Cobbley        if not reporoot:
551eb8dc403SDave Cobbley            raise Exception("Cannot get repository root for directory %s" % self.dir)
5525082cc7fSAndrew Geissler        gitdir = (runcmd("git rev-parse --absolute-git-dir".split(), self.dir) or '').strip()
5535082cc7fSAndrew Geissler        if not gitdir:
5545082cc7fSAndrew Geissler            raise Exception("Cannot get gitdir for directory %s" % self.dir)
5555082cc7fSAndrew Geissler        hooks_dir = os.path.join(gitdir, 'hooks')
556eb8dc403SDave Cobbley        hooks_dir_backup = hooks_dir + '.devtool-orig'
557eb8dc403SDave Cobbley        if os.path.lexists(hooks_dir_backup):
558eb8dc403SDave Cobbley            raise Exception("Git hooks backup directory already exists: %s" % hooks_dir_backup)
559eb8dc403SDave Cobbley        if os.path.lexists(hooks_dir):
560eb8dc403SDave Cobbley            shutil.move(hooks_dir, hooks_dir_backup)
561eb8dc403SDave Cobbley        os.mkdir(hooks_dir)
562eb8dc403SDave Cobbley        commithook = os.path.join(hooks_dir, 'commit-msg')
563eb8dc403SDave Cobbley        applyhook = os.path.join(hooks_dir, 'applypatch-msg')
564eb8dc403SDave Cobbley        with open(commithook, 'w') as f:
565eb8dc403SDave Cobbley            # NOTE: the formatting here is significant; if you change it you'll also need to
566eb8dc403SDave Cobbley            # change other places which read it back
5674ed12e16SAndrew Geissler            f.write('echo "\n%s: $PATCHFILE" >> $1' % GitApplyTree.patch_line_prefix)
568eb8dc403SDave Cobbley        os.chmod(commithook, 0o755)
569eb8dc403SDave Cobbley        shutil.copy2(commithook, applyhook)
570eb8dc403SDave Cobbley        try:
571eb8dc403SDave Cobbley            patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file'])
5728e7b46e2SPatrick Williams            if self._need_dirty_check():
5738e7b46e2SPatrick Williams                # Check dirtyness of the tree
5748e7b46e2SPatrick Williams                try:
5758e7b46e2SPatrick Williams                    output = runcmd(["git", "--work-tree=%s" % reporoot, "status", "--short"])
5768e7b46e2SPatrick Williams                except CmdError:
5778e7b46e2SPatrick Williams                    pass
5788e7b46e2SPatrick Williams                else:
5798e7b46e2SPatrick Williams                    if output:
5808e7b46e2SPatrick Williams                        # The tree is dirty, not need to try to apply patches with git anymore
5818e7b46e2SPatrick Williams                        # since they fail, fallback directly to patch
5828e7b46e2SPatrick Williams                        output = PatchTree._applypatch(self, patch, force, reverse, run)
5838e7b46e2SPatrick Williams                        output += self._commitpatch(patch, patchfilevar)
5848e7b46e2SPatrick Williams                        return output
585eb8dc403SDave Cobbley            try:
586eb8dc403SDave Cobbley                shellcmd = [patchfilevar, "git", "--work-tree=%s" % reporoot]
587eb8dc403SDave Cobbley                self.gitCommandUserOptions(shellcmd, self.commituser, self.commitemail)
588d1e89497SAndrew Geissler                shellcmd += ["am", "-3", "--keep-cr", "--no-scissors", "-p%s" % patch['strippath']]
589eb8dc403SDave Cobbley                return _applypatchhelper(shellcmd, patch, force, reverse, run)
590eb8dc403SDave Cobbley            except CmdError:
591eb8dc403SDave Cobbley                # Need to abort the git am, or we'll still be within it at the end
592eb8dc403SDave Cobbley                try:
593eb8dc403SDave Cobbley                    shellcmd = ["git", "--work-tree=%s" % reporoot, "am", "--abort"]
594eb8dc403SDave Cobbley                    runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
595eb8dc403SDave Cobbley                except CmdError:
596eb8dc403SDave Cobbley                    pass
597eb8dc403SDave Cobbley                # git am won't always clean up after itself, sadly, so...
598eb8dc403SDave Cobbley                shellcmd = ["git", "--work-tree=%s" % reporoot, "reset", "--hard", "HEAD"]
599eb8dc403SDave Cobbley                runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
600eb8dc403SDave Cobbley                # Also need to take care of any stray untracked files
601eb8dc403SDave Cobbley                shellcmd = ["git", "--work-tree=%s" % reporoot, "clean", "-f"]
602eb8dc403SDave Cobbley                runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
603eb8dc403SDave Cobbley
604eb8dc403SDave Cobbley                # Fall back to git apply
605eb8dc403SDave Cobbley                shellcmd = ["git", "--git-dir=%s" % reporoot, "apply", "-p%s" % patch['strippath']]
606eb8dc403SDave Cobbley                try:
607eb8dc403SDave Cobbley                    output = _applypatchhelper(shellcmd, patch, force, reverse, run)
608eb8dc403SDave Cobbley                except CmdError:
609eb8dc403SDave Cobbley                    # Fall back to patch
610eb8dc403SDave Cobbley                    output = PatchTree._applypatch(self, patch, force, reverse, run)
6118e7b46e2SPatrick Williams                output += self._commitpatch(patch, patchfilevar)
612eb8dc403SDave Cobbley                return output
613eb8dc403SDave Cobbley        finally:
614eb8dc403SDave Cobbley            shutil.rmtree(hooks_dir)
615eb8dc403SDave Cobbley            if os.path.lexists(hooks_dir_backup):
616eb8dc403SDave Cobbley                shutil.move(hooks_dir_backup, hooks_dir)
617eb8dc403SDave Cobbley
618eb8dc403SDave Cobbley
619eb8dc403SDave Cobbleyclass QuiltTree(PatchSet):
620eb8dc403SDave Cobbley    def _runcmd(self, args, run = True):
621eb8dc403SDave Cobbley        quiltrc = self.d.getVar('QUILTRCFILE')
622eb8dc403SDave Cobbley        if not run:
623eb8dc403SDave Cobbley            return ["quilt"] + ["--quiltrc"] + [quiltrc] + args
624eb8dc403SDave Cobbley        runcmd(["quilt"] + ["--quiltrc"] + [quiltrc] + args, self.dir)
625eb8dc403SDave Cobbley
626eb8dc403SDave Cobbley    def _quiltpatchpath(self, file):
627eb8dc403SDave Cobbley        return os.path.join(self.dir, "patches", os.path.basename(file))
628eb8dc403SDave Cobbley
629eb8dc403SDave Cobbley
630eb8dc403SDave Cobbley    def __init__(self, dir, d):
631eb8dc403SDave Cobbley        PatchSet.__init__(self, dir, d)
632eb8dc403SDave Cobbley        self.initialized = False
633eb8dc403SDave Cobbley        p = os.path.join(self.dir, 'patches')
634eb8dc403SDave Cobbley        if not os.path.exists(p):
635eb8dc403SDave Cobbley            os.makedirs(p)
636eb8dc403SDave Cobbley
637eb8dc403SDave Cobbley    def Clean(self):
638eb8dc403SDave Cobbley        try:
63978b72798SAndrew Geissler            # make sure that patches/series file exists before quilt pop to keep quilt-0.67 happy
64078b72798SAndrew Geissler            open(os.path.join(self.dir, "patches","series"), 'a').close()
641eb8dc403SDave Cobbley            self._runcmd(["pop", "-a", "-f"])
642eb8dc403SDave Cobbley            oe.path.remove(os.path.join(self.dir, "patches","series"))
643eb8dc403SDave Cobbley        except Exception:
644eb8dc403SDave Cobbley            pass
645eb8dc403SDave Cobbley        self.initialized = True
646eb8dc403SDave Cobbley
647eb8dc403SDave Cobbley    def InitFromDir(self):
648eb8dc403SDave Cobbley        # read series -> self.patches
649eb8dc403SDave Cobbley        seriespath = os.path.join(self.dir, 'patches', 'series')
650eb8dc403SDave Cobbley        if not os.path.exists(self.dir):
651eb8dc403SDave Cobbley            raise NotFoundError(self.dir)
652eb8dc403SDave Cobbley        if os.path.exists(seriespath):
653eb8dc403SDave Cobbley            with open(seriespath, 'r') as f:
654eb8dc403SDave Cobbley                for line in f.readlines():
655eb8dc403SDave Cobbley                    patch = {}
656eb8dc403SDave Cobbley                    parts = line.strip().split()
657eb8dc403SDave Cobbley                    patch["quiltfile"] = self._quiltpatchpath(parts[0])
658eb8dc403SDave Cobbley                    patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
659eb8dc403SDave Cobbley                    if len(parts) > 1:
660eb8dc403SDave Cobbley                        patch["strippath"] = parts[1][2:]
661eb8dc403SDave Cobbley                    self.patches.append(patch)
662eb8dc403SDave Cobbley
663eb8dc403SDave Cobbley            # determine which patches are applied -> self._current
664eb8dc403SDave Cobbley            try:
665eb8dc403SDave Cobbley                output = runcmd(["quilt", "applied"], self.dir)
666eb8dc403SDave Cobbley            except CmdError:
667eb8dc403SDave Cobbley                import sys
668eb8dc403SDave Cobbley                if sys.exc_value.output.strip() == "No patches applied":
669eb8dc403SDave Cobbley                    return
670eb8dc403SDave Cobbley                else:
671eb8dc403SDave Cobbley                    raise
672eb8dc403SDave Cobbley            output = [val for val in output.split('\n') if not val.startswith('#')]
673eb8dc403SDave Cobbley            for patch in self.patches:
674eb8dc403SDave Cobbley                if os.path.basename(patch["quiltfile"]) == output[-1]:
675eb8dc403SDave Cobbley                    self._current = self.patches.index(patch)
676eb8dc403SDave Cobbley        self.initialized = True
677eb8dc403SDave Cobbley
678eb8dc403SDave Cobbley    def Import(self, patch, force = None):
679eb8dc403SDave Cobbley        if not self.initialized:
680eb8dc403SDave Cobbley            self.InitFromDir()
681eb8dc403SDave Cobbley        PatchSet.Import(self, patch, force)
682eb8dc403SDave Cobbley        oe.path.symlink(patch["file"], self._quiltpatchpath(patch["file"]), force=True)
683eb8dc403SDave Cobbley        with open(os.path.join(self.dir, "patches", "series"), "a") as f:
684eb8dc403SDave Cobbley            f.write(os.path.basename(patch["file"]) + " -p" + patch["strippath"] + "\n")
685eb8dc403SDave Cobbley        patch["quiltfile"] = self._quiltpatchpath(patch["file"])
686eb8dc403SDave Cobbley        patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
687eb8dc403SDave Cobbley
688eb8dc403SDave Cobbley        # TODO: determine if the file being imported:
689eb8dc403SDave Cobbley        #      1) is already imported, and is the same
690eb8dc403SDave Cobbley        #      2) is already imported, but differs
691eb8dc403SDave Cobbley
692eb8dc403SDave Cobbley        self.patches.insert(self._current or 0, patch)
693eb8dc403SDave Cobbley
694eb8dc403SDave Cobbley
695eb8dc403SDave Cobbley    def Push(self, force = False, all = False, run = True):
696eb8dc403SDave Cobbley        # quilt push [-f]
697eb8dc403SDave Cobbley
698eb8dc403SDave Cobbley        args = ["push"]
699eb8dc403SDave Cobbley        if force:
700eb8dc403SDave Cobbley            args.append("-f")
701eb8dc403SDave Cobbley        if all:
702eb8dc403SDave Cobbley            args.append("-a")
703eb8dc403SDave Cobbley        if not run:
704eb8dc403SDave Cobbley            return self._runcmd(args, run)
705eb8dc403SDave Cobbley
706eb8dc403SDave Cobbley        self._runcmd(args)
707eb8dc403SDave Cobbley
708eb8dc403SDave Cobbley        if self._current is not None:
709eb8dc403SDave Cobbley            self._current = self._current + 1
710eb8dc403SDave Cobbley        else:
711eb8dc403SDave Cobbley            self._current = 0
712eb8dc403SDave Cobbley
713eb8dc403SDave Cobbley    def Pop(self, force = None, all = None):
714eb8dc403SDave Cobbley        # quilt pop [-f]
715eb8dc403SDave Cobbley        args = ["pop"]
716eb8dc403SDave Cobbley        if force:
717eb8dc403SDave Cobbley            args.append("-f")
718eb8dc403SDave Cobbley        if all:
719eb8dc403SDave Cobbley            args.append("-a")
720eb8dc403SDave Cobbley
721eb8dc403SDave Cobbley        self._runcmd(args)
722eb8dc403SDave Cobbley
723eb8dc403SDave Cobbley        if self._current == 0:
724eb8dc403SDave Cobbley            self._current = None
725eb8dc403SDave Cobbley
726eb8dc403SDave Cobbley        if self._current is not None:
727eb8dc403SDave Cobbley            self._current = self._current - 1
728eb8dc403SDave Cobbley
729eb8dc403SDave Cobbley    def Refresh(self, **kwargs):
730eb8dc403SDave Cobbley        if kwargs.get("remote"):
731eb8dc403SDave Cobbley            patch = self.patches[kwargs["patch"]]
732eb8dc403SDave Cobbley            if not patch:
733eb8dc403SDave Cobbley                raise PatchError("No patch found at index %s in patchset." % kwargs["patch"])
734eb8dc403SDave Cobbley            (type, host, path, user, pswd, parm) = bb.fetch.decodeurl(patch["remote"])
735eb8dc403SDave Cobbley            if type == "file":
736eb8dc403SDave Cobbley                import shutil
737eb8dc403SDave Cobbley                if not patch.get("file") and patch.get("remote"):
738eb8dc403SDave Cobbley                    patch["file"] = bb.fetch2.localpath(patch["remote"], self.d)
739eb8dc403SDave Cobbley
740eb8dc403SDave Cobbley                shutil.copyfile(patch["quiltfile"], patch["file"])
741eb8dc403SDave Cobbley            else:
742eb8dc403SDave Cobbley                raise PatchError("Unable to do a remote refresh of %s, unsupported remote url scheme %s." % (os.path.basename(patch["quiltfile"]), type))
743eb8dc403SDave Cobbley        else:
744eb8dc403SDave Cobbley            # quilt refresh
745eb8dc403SDave Cobbley            args = ["refresh"]
746eb8dc403SDave Cobbley            if kwargs.get("quiltfile"):
747eb8dc403SDave Cobbley                args.append(os.path.basename(kwargs["quiltfile"]))
748eb8dc403SDave Cobbley            elif kwargs.get("patch"):
749eb8dc403SDave Cobbley                args.append(os.path.basename(self.patches[kwargs["patch"]]["quiltfile"]))
750eb8dc403SDave Cobbley            self._runcmd(args)
751eb8dc403SDave Cobbley
752eb8dc403SDave Cobbleyclass Resolver(object):
753eb8dc403SDave Cobbley    def __init__(self, patchset, terminal):
754eb8dc403SDave Cobbley        raise NotImplementedError()
755eb8dc403SDave Cobbley
756eb8dc403SDave Cobbley    def Resolve(self):
757eb8dc403SDave Cobbley        raise NotImplementedError()
758eb8dc403SDave Cobbley
759eb8dc403SDave Cobbley    def Revert(self):
760eb8dc403SDave Cobbley        raise NotImplementedError()
761eb8dc403SDave Cobbley
762eb8dc403SDave Cobbley    def Finalize(self):
763eb8dc403SDave Cobbley        raise NotImplementedError()
764eb8dc403SDave Cobbley
765eb8dc403SDave Cobbleyclass NOOPResolver(Resolver):
766eb8dc403SDave Cobbley    def __init__(self, patchset, terminal):
767eb8dc403SDave Cobbley        self.patchset = patchset
768eb8dc403SDave Cobbley        self.terminal = terminal
769eb8dc403SDave Cobbley
770eb8dc403SDave Cobbley    def Resolve(self):
771eb8dc403SDave Cobbley        olddir = os.path.abspath(os.curdir)
772eb8dc403SDave Cobbley        os.chdir(self.patchset.dir)
773eb8dc403SDave Cobbley        try:
774eb8dc403SDave Cobbley            self.patchset.Push()
775eb8dc403SDave Cobbley        except Exception:
776eb8dc403SDave Cobbley            import sys
777eb8dc403SDave Cobbley            raise
778ac13d5f3SPatrick Williams        finally:
779ac13d5f3SPatrick Williams            os.chdir(olddir)
780eb8dc403SDave Cobbley
781eb8dc403SDave Cobbley# Patch resolver which relies on the user doing all the work involved in the
782eb8dc403SDave Cobbley# resolution, with the exception of refreshing the remote copy of the patch
783eb8dc403SDave Cobbley# files (the urls).
784eb8dc403SDave Cobbleyclass UserResolver(Resolver):
785eb8dc403SDave Cobbley    def __init__(self, patchset, terminal):
786eb8dc403SDave Cobbley        self.patchset = patchset
787eb8dc403SDave Cobbley        self.terminal = terminal
788eb8dc403SDave Cobbley
789eb8dc403SDave Cobbley    # Force a push in the patchset, then drop to a shell for the user to
790eb8dc403SDave Cobbley    # resolve any rejected hunks
791eb8dc403SDave Cobbley    def Resolve(self):
792eb8dc403SDave Cobbley        olddir = os.path.abspath(os.curdir)
793eb8dc403SDave Cobbley        os.chdir(self.patchset.dir)
794eb8dc403SDave Cobbley        try:
795eb8dc403SDave Cobbley            self.patchset.Push(False)
796eb8dc403SDave Cobbley        except CmdError as v:
797eb8dc403SDave Cobbley            # Patch application failed
798eb8dc403SDave Cobbley            patchcmd = self.patchset.Push(True, False, False)
799eb8dc403SDave Cobbley
800eb8dc403SDave Cobbley            t = self.patchset.d.getVar('T')
801eb8dc403SDave Cobbley            if not t:
802eb8dc403SDave Cobbley                bb.msg.fatal("Build", "T not set")
803eb8dc403SDave Cobbley            bb.utils.mkdirhier(t)
804eb8dc403SDave Cobbley            import random
805eb8dc403SDave Cobbley            rcfile = "%s/bashrc.%s.%s" % (t, str(os.getpid()), random.random())
806eb8dc403SDave Cobbley            with open(rcfile, "w") as f:
807eb8dc403SDave Cobbley                f.write("echo '*** Manual patch resolution mode ***'\n")
808eb8dc403SDave Cobbley                f.write("echo 'Dropping to a shell, so patch rejects can be fixed manually.'\n")
809eb8dc403SDave Cobbley                f.write("echo 'Run \"quilt refresh\" when patch is corrected, press CTRL+D to exit.'\n")
810eb8dc403SDave Cobbley                f.write("echo ''\n")
811eb8dc403SDave Cobbley                f.write(" ".join(patchcmd) + "\n")
812eb8dc403SDave Cobbley            os.chmod(rcfile, 0o775)
813eb8dc403SDave Cobbley
814eb8dc403SDave Cobbley            self.terminal("bash --rcfile " + rcfile, 'Patch Rejects: Please fix patch rejects manually', self.patchset.d)
815eb8dc403SDave Cobbley
816eb8dc403SDave Cobbley            # Construct a new PatchSet after the user's changes, compare the
817eb8dc403SDave Cobbley            # sets, checking patches for modifications, and doing a remote
818eb8dc403SDave Cobbley            # refresh on each.
819eb8dc403SDave Cobbley            oldpatchset = self.patchset
820eb8dc403SDave Cobbley            self.patchset = oldpatchset.__class__(self.patchset.dir, self.patchset.d)
821eb8dc403SDave Cobbley
822eb8dc403SDave Cobbley            for patch in self.patchset.patches:
823eb8dc403SDave Cobbley                oldpatch = None
824eb8dc403SDave Cobbley                for opatch in oldpatchset.patches:
825eb8dc403SDave Cobbley                    if opatch["quiltfile"] == patch["quiltfile"]:
826eb8dc403SDave Cobbley                        oldpatch = opatch
827eb8dc403SDave Cobbley
828eb8dc403SDave Cobbley                if oldpatch:
829eb8dc403SDave Cobbley                    patch["remote"] = oldpatch["remote"]
830eb8dc403SDave Cobbley                    if patch["quiltfile"] == oldpatch["quiltfile"]:
831eb8dc403SDave Cobbley                        if patch["quiltfilemd5"] != oldpatch["quiltfilemd5"]:
832eb8dc403SDave Cobbley                            bb.note("Patch %s has changed, updating remote url %s" % (os.path.basename(patch["quiltfile"]), patch["remote"]))
833eb8dc403SDave Cobbley                            # user change?  remote refresh
834eb8dc403SDave Cobbley                            self.patchset.Refresh(remote=True, patch=self.patchset.patches.index(patch))
835eb8dc403SDave Cobbley                        else:
836eb8dc403SDave Cobbley                            # User did not fix the problem.  Abort.
837eb8dc403SDave Cobbley                            raise PatchError("Patch application failed, and user did not fix and refresh the patch.")
838eb8dc403SDave Cobbley        except Exception:
839eb8dc403SDave Cobbley            raise
840ac13d5f3SPatrick Williams        finally:
841eb8dc403SDave Cobbley            os.chdir(olddir)
842eb8dc403SDave Cobbley
843eb8dc403SDave Cobbley
844eb8dc403SDave Cobbleydef patch_path(url, fetch, workdir, expand=True):
84519323693SBrad Bishop    """Return the local path of a patch, or return nothing if this isn't a patch"""
846eb8dc403SDave Cobbley
847eb8dc403SDave Cobbley    local = fetch.localpath(url)
84819323693SBrad Bishop    if os.path.isdir(local):
84919323693SBrad Bishop        return
850eb8dc403SDave Cobbley    base, ext = os.path.splitext(os.path.basename(local))
851eb8dc403SDave Cobbley    if ext in ('.gz', '.bz2', '.xz', '.Z'):
852eb8dc403SDave Cobbley        if expand:
853eb8dc403SDave Cobbley            local = os.path.join(workdir, base)
854eb8dc403SDave Cobbley        ext = os.path.splitext(base)[1]
855eb8dc403SDave Cobbley
856eb8dc403SDave Cobbley    urldata = fetch.ud[url]
857eb8dc403SDave Cobbley    if "apply" in urldata.parm:
858eb8dc403SDave Cobbley        apply = oe.types.boolean(urldata.parm["apply"])
859eb8dc403SDave Cobbley        if not apply:
860eb8dc403SDave Cobbley            return
861eb8dc403SDave Cobbley    elif ext not in (".diff", ".patch"):
862eb8dc403SDave Cobbley        return
863eb8dc403SDave Cobbley
864eb8dc403SDave Cobbley    return local
865eb8dc403SDave Cobbley
866eb8dc403SDave Cobbleydef src_patches(d, all=False, expand=True):
867eb8dc403SDave Cobbley    workdir = d.getVar('WORKDIR')
868eb8dc403SDave Cobbley    fetch = bb.fetch2.Fetch([], d)
869eb8dc403SDave Cobbley    patches = []
870eb8dc403SDave Cobbley    sources = []
871eb8dc403SDave Cobbley    for url in fetch.urls:
872eb8dc403SDave Cobbley        local = patch_path(url, fetch, workdir, expand)
873eb8dc403SDave Cobbley        if not local:
874eb8dc403SDave Cobbley            if all:
875eb8dc403SDave Cobbley                local = fetch.localpath(url)
876eb8dc403SDave Cobbley                sources.append(local)
877eb8dc403SDave Cobbley            continue
878eb8dc403SDave Cobbley
879eb8dc403SDave Cobbley        urldata = fetch.ud[url]
880eb8dc403SDave Cobbley        parm = urldata.parm
881eb8dc403SDave Cobbley        patchname = parm.get('pname') or os.path.basename(local)
882eb8dc403SDave Cobbley
883eb8dc403SDave Cobbley        apply, reason = should_apply(parm, d)
884eb8dc403SDave Cobbley        if not apply:
885eb8dc403SDave Cobbley            if reason:
886eb8dc403SDave Cobbley                bb.note("Patch %s %s" % (patchname, reason))
887eb8dc403SDave Cobbley            continue
888eb8dc403SDave Cobbley
889eb8dc403SDave Cobbley        patchparm = {'patchname': patchname}
890eb8dc403SDave Cobbley        if "striplevel" in parm:
891eb8dc403SDave Cobbley            striplevel = parm["striplevel"]
892eb8dc403SDave Cobbley        elif "pnum" in parm:
893eb8dc403SDave Cobbley            #bb.msg.warn(None, "Deprecated usage of 'pnum' url parameter in '%s', please use 'striplevel'" % url)
894eb8dc403SDave Cobbley            striplevel = parm["pnum"]
895eb8dc403SDave Cobbley        else:
896eb8dc403SDave Cobbley            striplevel = '1'
897eb8dc403SDave Cobbley        patchparm['striplevel'] = striplevel
898eb8dc403SDave Cobbley
899eb8dc403SDave Cobbley        patchdir = parm.get('patchdir')
900eb8dc403SDave Cobbley        if patchdir:
901eb8dc403SDave Cobbley            patchparm['patchdir'] = patchdir
902eb8dc403SDave Cobbley
903eb8dc403SDave Cobbley        localurl = bb.fetch.encodeurl(('file', '', local, '', '', patchparm))
904eb8dc403SDave Cobbley        patches.append(localurl)
905eb8dc403SDave Cobbley
906eb8dc403SDave Cobbley    if all:
907eb8dc403SDave Cobbley        return sources
908eb8dc403SDave Cobbley
909eb8dc403SDave Cobbley    return patches
910eb8dc403SDave Cobbley
911eb8dc403SDave Cobbley
912eb8dc403SDave Cobbleydef should_apply(parm, d):
913c342db35SBrad Bishop    import bb.utils
914eb8dc403SDave Cobbley    if "mindate" in parm or "maxdate" in parm:
915eb8dc403SDave Cobbley        pn = d.getVar('PN')
916eb8dc403SDave Cobbley        srcdate = d.getVar('SRCDATE_%s' % pn)
917eb8dc403SDave Cobbley        if not srcdate:
918eb8dc403SDave Cobbley            srcdate = d.getVar('SRCDATE')
919eb8dc403SDave Cobbley
920eb8dc403SDave Cobbley        if srcdate == "now":
921eb8dc403SDave Cobbley            srcdate = d.getVar('DATE')
922eb8dc403SDave Cobbley
923eb8dc403SDave Cobbley        if "maxdate" in parm and parm["maxdate"] < srcdate:
924eb8dc403SDave Cobbley            return False, 'is outdated'
925eb8dc403SDave Cobbley
926eb8dc403SDave Cobbley        if "mindate" in parm and parm["mindate"] > srcdate:
927eb8dc403SDave Cobbley            return False, 'is predated'
928eb8dc403SDave Cobbley
929eb8dc403SDave Cobbley
930eb8dc403SDave Cobbley    if "minrev" in parm:
931eb8dc403SDave Cobbley        srcrev = d.getVar('SRCREV')
932eb8dc403SDave Cobbley        if srcrev and srcrev < parm["minrev"]:
933eb8dc403SDave Cobbley            return False, 'applies to later revisions'
934eb8dc403SDave Cobbley
935eb8dc403SDave Cobbley    if "maxrev" in parm:
936eb8dc403SDave Cobbley        srcrev = d.getVar('SRCREV')
937eb8dc403SDave Cobbley        if srcrev and srcrev > parm["maxrev"]:
938eb8dc403SDave Cobbley            return False, 'applies to earlier revisions'
939eb8dc403SDave Cobbley
940eb8dc403SDave Cobbley    if "rev" in parm:
941eb8dc403SDave Cobbley        srcrev = d.getVar('SRCREV')
942eb8dc403SDave Cobbley        if srcrev and parm["rev"] not in srcrev:
943eb8dc403SDave Cobbley            return False, "doesn't apply to revision"
944eb8dc403SDave Cobbley
945eb8dc403SDave Cobbley    if "notrev" in parm:
946eb8dc403SDave Cobbley        srcrev = d.getVar('SRCREV')
947eb8dc403SDave Cobbley        if srcrev and parm["notrev"] in srcrev:
948eb8dc403SDave Cobbley            return False, "doesn't apply to revision"
949eb8dc403SDave Cobbley
950c342db35SBrad Bishop    if "maxver" in parm:
951c342db35SBrad Bishop        pv = d.getVar('PV')
952c342db35SBrad Bishop        if bb.utils.vercmp_string_op(pv, parm["maxver"], ">"):
953c342db35SBrad Bishop            return False, "applies to earlier version"
954c342db35SBrad Bishop
955c342db35SBrad Bishop    if "minver" in parm:
956c342db35SBrad Bishop        pv = d.getVar('PV')
957c342db35SBrad Bishop        if bb.utils.vercmp_string_op(pv, parm["minver"], "<"):
958c342db35SBrad Bishop            return False, "applies to later version"
959c342db35SBrad Bishop
960eb8dc403SDave Cobbley    return True, None
961