1# Report significant differences in the buildhistory repository since a specific revision
2#
3# Copyright (C) 2012-2013, 2016-2017 Intel Corporation
4# Author: Paul Eggleton <paul.eggleton@linux.intel.com>
5#
6# SPDX-License-Identifier: GPL-2.0-only
7#
8# Note: requires GitPython 0.3.1+
9#
10# You can use this from the command line by running scripts/buildhistory-diff
11#
12
13import sys
14import os.path
15import difflib
16import git
17import re
18import shlex
19import hashlib
20import collections
21import bb.utils
22import bb.tinfoil
23
24
25# How to display fields
26list_fields = ['DEPENDS', 'RPROVIDES', 'RDEPENDS', 'RRECOMMENDS', 'RSUGGESTS', 'RREPLACES', 'RCONFLICTS', 'FILES', 'FILELIST', 'USER_CLASSES', 'IMAGE_CLASSES', 'IMAGE_FEATURES', 'IMAGE_LINGUAS', 'IMAGE_INSTALL', 'BAD_RECOMMENDATIONS', 'PACKAGE_EXCLUDE']
27list_order_fields = ['PACKAGES']
28defaultval_map = {'PKG': 'PKG', 'PKGE': 'PE', 'PKGV': 'PV', 'PKGR': 'PR'}
29numeric_fields = ['PKGSIZE', 'IMAGESIZE']
30# Fields to monitor
31monitor_fields = ['RPROVIDES', 'RDEPENDS', 'RRECOMMENDS', 'RREPLACES', 'RCONFLICTS', 'PACKAGES', 'FILELIST', 'PKGSIZE', 'IMAGESIZE', 'PKG']
32ver_monitor_fields = ['PKGE', 'PKGV', 'PKGR']
33# Percentage change to alert for numeric fields
34monitor_numeric_threshold = 10
35# Image files to monitor (note that image-info.txt is handled separately)
36img_monitor_files = ['installed-package-names.txt', 'files-in-image.txt']
37
38colours = {
39    'colour_default': '',
40    'colour_add':     '',
41    'colour_remove':  '',
42}
43
44def init_colours(use_colours):
45    global colours
46    if use_colours:
47        colours = {
48            'colour_default': '\033[0m',
49            'colour_add':     '\033[1;32m',
50            'colour_remove':  '\033[1;31m',
51        }
52    else:
53        colours = {
54            'colour_default': '',
55            'colour_add':     '',
56            'colour_remove':  '',
57        }
58
59class ChangeRecord:
60    def __init__(self, path, fieldname, oldvalue, newvalue, monitored):
61        self.path = path
62        self.fieldname = fieldname
63        self.oldvalue = oldvalue
64        self.newvalue = newvalue
65        self.monitored = monitored
66        self.filechanges = None
67
68    def __str__(self):
69        return self._str_internal(True)
70
71    def _str_internal(self, outer):
72        if outer:
73            if '/image-files/' in self.path:
74                prefix = '%s: ' % self.path.split('/image-files/')[0]
75            else:
76                prefix = '%s: ' % self.path
77        else:
78            prefix = ''
79
80        def pkglist_combine(depver):
81            pkglist = []
82            for k,v in depver.items():
83                if v:
84                    pkglist.append("%s (%s)" % (k,v))
85                else:
86                    pkglist.append(k)
87            return pkglist
88
89        def detect_renamed_dirs(aitems, bitems):
90            adirs = set(map(os.path.dirname, aitems))
91            bdirs = set(map(os.path.dirname, bitems))
92            files_ab = [(name, sorted(os.path.basename(item) for item in aitems if os.path.dirname(item) == name)) \
93                                for name in adirs - bdirs]
94            files_ba = [(name, sorted(os.path.basename(item) for item in bitems if os.path.dirname(item) == name)) \
95                                for name in bdirs - adirs]
96            renamed_dirs = []
97            for dir1, files1 in files_ab:
98                rename = False
99                for dir2, files2 in files_ba:
100                    if files1 == files2 and not rename:
101                        renamed_dirs.append((dir1,dir2))
102                        # Make sure that we don't use this (dir, files) pair again.
103                        files_ba.remove((dir2,files2))
104                        # If a dir has already been found to have a rename, stop and go no further.
105                        rename = True
106
107            # remove files that belong to renamed dirs from aitems and bitems
108            for dir1, dir2 in renamed_dirs:
109                aitems = [item for item in aitems if os.path.dirname(item) not in (dir1, dir2)]
110                bitems = [item for item in bitems if os.path.dirname(item) not in (dir1, dir2)]
111            return renamed_dirs, aitems, bitems
112
113        if self.fieldname in list_fields or self.fieldname in list_order_fields:
114            renamed_dirs = []
115            changed_order = False
116            if self.fieldname in ['RPROVIDES', 'RDEPENDS', 'RRECOMMENDS', 'RSUGGESTS', 'RREPLACES', 'RCONFLICTS']:
117                (depvera, depverb) = compare_pkg_lists(self.oldvalue, self.newvalue)
118                aitems = pkglist_combine(depvera)
119                bitems = pkglist_combine(depverb)
120            else:
121                if self.fieldname == 'FILELIST':
122                    aitems = shlex.split(self.oldvalue)
123                    bitems = shlex.split(self.newvalue)
124                    renamed_dirs, aitems, bitems = detect_renamed_dirs(aitems, bitems)
125                else:
126                    aitems = self.oldvalue.split()
127                    bitems = self.newvalue.split()
128
129            removed = list(set(aitems) - set(bitems))
130            added = list(set(bitems) - set(aitems))
131
132            if not removed and not added and self.fieldname in ['RPROVIDES', 'RDEPENDS', 'RRECOMMENDS', 'RSUGGESTS', 'RREPLACES', 'RCONFLICTS']:
133                depvera = bb.utils.explode_dep_versions2(self.oldvalue, sort=False)
134                depverb = bb.utils.explode_dep_versions2(self.newvalue, sort=False)
135                for i, j in zip(depvera.items(), depverb.items()):
136                    if i[0] != j[0]:
137                        changed_order = True
138                        break
139
140            lines = []
141            if renamed_dirs:
142                for dfrom, dto in renamed_dirs:
143                    lines.append('directory renamed {colour_remove}{}{colour_default} -> {colour_add}{}{colour_default}'.format(dfrom, dto, **colours))
144            if removed or added:
145                if removed and not bitems:
146                    lines.append('removed all items "{colour_remove}{}{colour_default}"'.format(' '.join(removed), **colours))
147                else:
148                    if removed:
149                        lines.append('removed "{colour_remove}{value}{colour_default}"'.format(value=' '.join(removed), **colours))
150                    if added:
151                        lines.append('added "{colour_add}{value}{colour_default}"'.format(value=' '.join(added), **colours))
152            else:
153                lines.append('changed order')
154
155            if not (removed or added or changed_order):
156                out = ''
157            else:
158                out = '%s: %s' % (self.fieldname, ', '.join(lines))
159
160        elif self.fieldname in numeric_fields:
161            aval = int(self.oldvalue or 0)
162            bval = int(self.newvalue or 0)
163            if aval != 0:
164                percentchg = ((bval - aval) / float(aval)) * 100
165            else:
166                percentchg = 100
167            out = '{} changed from {colour_remove}{}{colour_default} to {colour_add}{}{colour_default} ({}{:.0f}%)'.format(self.fieldname, self.oldvalue or "''", self.newvalue or "''", '+' if percentchg > 0 else '', percentchg, **colours)
168        elif self.fieldname in defaultval_map:
169            out = '{} changed from {colour_remove}{}{colour_default} to {colour_add}{}{colour_default}'.format(self.fieldname, self.oldvalue, self.newvalue, **colours)
170            if self.fieldname == 'PKG' and '[default]' in self.newvalue:
171                out += ' - may indicate debian renaming failure'
172        elif self.fieldname in ['pkg_preinst', 'pkg_postinst', 'pkg_prerm', 'pkg_postrm']:
173            if self.oldvalue and self.newvalue:
174                out = '%s changed:\n  ' % self.fieldname
175            elif self.newvalue:
176                out = '%s added:\n  ' % self.fieldname
177            elif self.oldvalue:
178                out = '%s cleared:\n  ' % self.fieldname
179            alines = self.oldvalue.splitlines()
180            blines = self.newvalue.splitlines()
181            diff = difflib.unified_diff(alines, blines, self.fieldname, self.fieldname, lineterm='')
182            out += '\n  '.join(list(diff)[2:])
183            out += '\n  --'
184        elif self.fieldname in img_monitor_files or '/image-files/' in self.path or self.fieldname == "sysroot":
185            if self.filechanges or (self.oldvalue and self.newvalue):
186                fieldname = self.fieldname
187                if '/image-files/' in self.path:
188                    fieldname = os.path.join('/' + self.path.split('/image-files/')[1], self.fieldname)
189                    out = 'Changes to %s:\n  ' % fieldname
190                else:
191                    if outer:
192                        prefix = 'Changes to %s ' % self.path
193                    out = '(%s):\n  ' % self.fieldname
194                if self.filechanges:
195                    out += '\n  '.join(['%s' % i for i in self.filechanges])
196                else:
197                    alines = self.oldvalue.splitlines()
198                    blines = self.newvalue.splitlines()
199                    diff = difflib.unified_diff(alines, blines, fieldname, fieldname, lineterm='')
200                    out += '\n  '.join(list(diff))
201                    out += '\n  --'
202            else:
203                out = ''
204        else:
205            out = '{} changed from "{colour_remove}{}{colour_default}" to "{colour_add}{}{colour_default}"'.format(self.fieldname, self.oldvalue, self.newvalue, **colours)
206
207        return '%s%s' % (prefix, out) if out else ''
208
209class FileChange:
210    changetype_add = 'A'
211    changetype_remove = 'R'
212    changetype_type = 'T'
213    changetype_perms = 'P'
214    changetype_ownergroup = 'O'
215    changetype_link = 'L'
216
217    def __init__(self, path, changetype, oldvalue = None, newvalue = None):
218        self.path = path
219        self.changetype = changetype
220        self.oldvalue = oldvalue
221        self.newvalue = newvalue
222
223    def _ftype_str(self, ftype):
224        if ftype == '-':
225            return 'file'
226        elif ftype == 'd':
227            return 'directory'
228        elif ftype == 'l':
229            return 'symlink'
230        elif ftype == 'c':
231            return 'char device'
232        elif ftype == 'b':
233            return 'block device'
234        elif ftype == 'p':
235            return 'fifo'
236        elif ftype == 's':
237            return 'socket'
238        else:
239            return 'unknown (%s)' % ftype
240
241    def __str__(self):
242        if self.changetype == self.changetype_add:
243            return '%s was added' % self.path
244        elif self.changetype == self.changetype_remove:
245            return '%s was removed' % self.path
246        elif self.changetype == self.changetype_type:
247            return '%s changed type from %s to %s' % (self.path, self._ftype_str(self.oldvalue), self._ftype_str(self.newvalue))
248        elif self.changetype == self.changetype_perms:
249            return '%s changed permissions from %s to %s' % (self.path, self.oldvalue, self.newvalue)
250        elif self.changetype == self.changetype_ownergroup:
251            return '%s changed owner/group from %s to %s' % (self.path, self.oldvalue, self.newvalue)
252        elif self.changetype == self.changetype_link:
253            return '%s changed symlink target from %s to %s' % (self.path, self.oldvalue, self.newvalue)
254        else:
255            return '%s changed (unknown)' % self.path
256
257
258def blob_to_dict(blob):
259    alines = [line for line in blob.data_stream.read().decode('utf-8').splitlines()]
260    adict = {}
261    for line in alines:
262        splitv = [i.strip() for i in line.split('=',1)]
263        if len(splitv) > 1:
264            adict[splitv[0]] = splitv[1]
265    return adict
266
267
268def file_list_to_dict(lines):
269    adict = {}
270    for line in lines:
271        # Leave the last few fields intact so we handle file names containing spaces
272        splitv = line.split(None,4)
273        # Grab the path and remove the leading .
274        path = splitv[4][1:].strip()
275        # Handle symlinks
276        if(' -> ' in path):
277            target = path.split(' -> ')[1]
278            path = path.split(' -> ')[0]
279            adict[path] = splitv[0:3] + [target]
280        else:
281            adict[path] = splitv[0:3]
282    return adict
283
284
285def compare_file_lists(alines, blines, compare_ownership=True):
286    adict = file_list_to_dict(alines)
287    bdict = file_list_to_dict(blines)
288    filechanges = []
289    for path, splitv in adict.items():
290        newsplitv = bdict.pop(path, None)
291        if newsplitv:
292            # Check type
293            oldvalue = splitv[0][0]
294            newvalue = newsplitv[0][0]
295            if oldvalue != newvalue:
296                filechanges.append(FileChange(path, FileChange.changetype_type, oldvalue, newvalue))
297
298            # Check permissions
299            oldvalue = splitv[0][1:]
300            newvalue = newsplitv[0][1:]
301            if oldvalue != newvalue:
302                filechanges.append(FileChange(path, FileChange.changetype_perms, oldvalue, newvalue))
303
304            if compare_ownership:
305                # Check owner/group
306                oldvalue = '%s/%s' % (splitv[1], splitv[2])
307                newvalue = '%s/%s' % (newsplitv[1], newsplitv[2])
308                if oldvalue != newvalue:
309                    filechanges.append(FileChange(path, FileChange.changetype_ownergroup, oldvalue, newvalue))
310
311            # Check symlink target
312            if newsplitv[0][0] == 'l':
313                if len(splitv) > 3:
314                    oldvalue = splitv[3]
315                else:
316                    oldvalue = None
317                newvalue = newsplitv[3]
318                if oldvalue != newvalue:
319                    filechanges.append(FileChange(path, FileChange.changetype_link, oldvalue, newvalue))
320        else:
321            filechanges.append(FileChange(path, FileChange.changetype_remove))
322
323    # Whatever is left over has been added
324    for path in bdict:
325        filechanges.append(FileChange(path, FileChange.changetype_add))
326
327    return filechanges
328
329
330def compare_lists(alines, blines):
331    removed = list(set(alines) - set(blines))
332    added = list(set(blines) - set(alines))
333
334    filechanges = []
335    for pkg in removed:
336        filechanges.append(FileChange(pkg, FileChange.changetype_remove))
337    for pkg in added:
338        filechanges.append(FileChange(pkg, FileChange.changetype_add))
339
340    return filechanges
341
342
343def compare_pkg_lists(astr, bstr):
344    depvera = bb.utils.explode_dep_versions2(astr)
345    depverb = bb.utils.explode_dep_versions2(bstr)
346
347    # Strip out changes where the version has increased
348    remove = []
349    for k in depvera:
350        if k in depverb:
351            dva = depvera[k]
352            dvb = depverb[k]
353            if dva and dvb and len(dva) == len(dvb):
354                # Since length is the same, sort so that prefixes (e.g. >=) will line up
355                dva.sort()
356                dvb.sort()
357                removeit = True
358                for dvai, dvbi in zip(dva, dvb):
359                    if dvai != dvbi:
360                        aiprefix = dvai.split(' ')[0]
361                        biprefix = dvbi.split(' ')[0]
362                        if aiprefix == biprefix and aiprefix in ['>=', '=']:
363                            if bb.utils.vercmp(bb.utils.split_version(dvai), bb.utils.split_version(dvbi)) > 0:
364                                removeit = False
365                                break
366                        else:
367                            removeit = False
368                            break
369                if removeit:
370                    remove.append(k)
371
372    for k in remove:
373        depvera.pop(k)
374        depverb.pop(k)
375
376    return (depvera, depverb)
377
378
379def compare_dict_blobs(path, ablob, bblob, report_all, report_ver):
380    adict = blob_to_dict(ablob)
381    bdict = blob_to_dict(bblob)
382
383    pkgname = os.path.basename(path)
384
385    defaultvals = {}
386    defaultvals['PKG'] = pkgname
387    defaultvals['PKGE'] = '0'
388
389    changes = []
390    keys = list(set(adict.keys()) | set(bdict.keys()) | set(defaultval_map.keys()))
391    for key in keys:
392        astr = adict.get(key, '')
393        bstr = bdict.get(key, '')
394        if key in ver_monitor_fields:
395            monitored = report_ver or astr or bstr
396        else:
397            monitored = key in monitor_fields
398        mapped_key = defaultval_map.get(key, '')
399        if mapped_key:
400            if not astr:
401                astr = '%s [default]' % adict.get(mapped_key, defaultvals.get(key, ''))
402            if not bstr:
403                bstr = '%s [default]' % bdict.get(mapped_key, defaultvals.get(key, ''))
404
405        if astr != bstr:
406            if (not report_all) and key in numeric_fields:
407                aval = int(astr or 0)
408                bval = int(bstr or 0)
409                if aval != 0:
410                    percentchg = ((bval - aval) / float(aval)) * 100
411                else:
412                    percentchg = 100
413                if abs(percentchg) < monitor_numeric_threshold:
414                    continue
415            elif (not report_all) and key in list_fields:
416                if key == "FILELIST" and path.endswith("-dbg") and bstr.strip() != '':
417                    continue
418                if key in ['RPROVIDES', 'RDEPENDS', 'RRECOMMENDS', 'RSUGGESTS', 'RREPLACES', 'RCONFLICTS']:
419                    (depvera, depverb) = compare_pkg_lists(astr, bstr)
420                    if depvera == depverb:
421                        continue
422                if key == 'FILELIST':
423                    alist = shlex.split(astr)
424                    blist = shlex.split(bstr)
425                else:
426                    alist = astr.split()
427                    blist = bstr.split()
428                alist.sort()
429                blist.sort()
430                # We don't care about the removal of self-dependencies
431                if pkgname in alist and not pkgname in blist:
432                    alist.remove(pkgname)
433                if ' '.join(alist) == ' '.join(blist):
434                    continue
435
436            if key == 'PKGR' and not report_all:
437                vers = []
438                # strip leading 'r' and dots
439                for ver in (astr.split()[0], bstr.split()[0]):
440                    if ver.startswith('r'):
441                        ver = ver[1:]
442                    vers.append(ver.replace('.', ''))
443                maxlen = max(len(vers[0]), len(vers[1]))
444                try:
445                    # pad with '0' and convert to int
446                    vers = [int(ver.ljust(maxlen, '0')) for ver in vers]
447                except ValueError:
448                    pass
449                else:
450                     # skip decrements and increments
451                    if abs(vers[0] - vers[1]) == 1:
452                        continue
453
454            chg = ChangeRecord(path, key, astr, bstr, monitored)
455            changes.append(chg)
456    return changes
457
458
459def compare_siglists(a_blob, b_blob, taskdiff=False):
460    # FIXME collapse down a recipe's tasks?
461    alines = a_blob.data_stream.read().decode('utf-8').splitlines()
462    blines = b_blob.data_stream.read().decode('utf-8').splitlines()
463    keys = []
464    pnmap = {}
465    def readsigs(lines):
466        sigs = {}
467        for line in lines:
468            linesplit = line.split()
469            if len(linesplit) > 2:
470                sigs[linesplit[0]] = linesplit[2]
471                if not linesplit[0] in keys:
472                    keys.append(linesplit[0])
473                pnmap[linesplit[1]] = linesplit[0].rsplit('.', 1)[0]
474        return sigs
475    adict = readsigs(alines)
476    bdict = readsigs(blines)
477    out = []
478
479    changecount = 0
480    addcount = 0
481    removecount = 0
482    if taskdiff:
483        with bb.tinfoil.Tinfoil() as tinfoil:
484            tinfoil.prepare(config_only=True)
485
486            changes = collections.OrderedDict()
487
488            def compare_hashfiles(pn, taskname, hash1, hash2):
489                hashes = [hash1, hash2]
490                hashfiles = bb.siggen.find_siginfo(pn, taskname, hashes, tinfoil.config_data)
491
492                if not taskname:
493                    (pn, taskname) = pn.rsplit('.', 1)
494                    pn = pnmap.get(pn, pn)
495                desc = '%s.%s' % (pn, taskname)
496
497                if len(hashfiles) == 0:
498                    out.append("Unable to find matching sigdata for %s with hashes %s or %s" % (desc, hash1, hash2))
499                elif not hash1 in hashfiles:
500                    out.append("Unable to find matching sigdata for %s with hash %s" % (desc, hash1))
501                elif not hash2 in hashfiles:
502                    out.append("Unable to find matching sigdata for %s with hash %s" % (desc, hash2))
503                else:
504                    out2 = bb.siggen.compare_sigfiles(hashfiles[hash1], hashfiles[hash2], recursecb, collapsed=True)
505                    for line in out2:
506                        m = hashlib.sha256()
507                        m.update(line.encode('utf-8'))
508                        entry = changes.get(m.hexdigest(), (line, []))
509                        if desc not in entry[1]:
510                            changes[m.hexdigest()] = (line, entry[1] + [desc])
511
512            # Define recursion callback
513            def recursecb(key, hash1, hash2):
514                compare_hashfiles(key, None, hash1, hash2)
515                return []
516
517            for key in keys:
518                siga = adict.get(key, None)
519                sigb = bdict.get(key, None)
520                if siga is not None and sigb is not None and siga != sigb:
521                    changecount += 1
522                    (pn, taskname) = key.rsplit('.', 1)
523                    compare_hashfiles(pn, taskname, siga, sigb)
524                elif siga is None:
525                    addcount += 1
526                elif sigb is None:
527                    removecount += 1
528        for key, item in changes.items():
529            line, tasks = item
530            if len(tasks) == 1:
531                desc = tasks[0]
532            elif len(tasks) == 2:
533                desc = '%s and %s' % (tasks[0], tasks[1])
534            else:
535                desc = '%s and %d others' % (tasks[-1], len(tasks)-1)
536            out.append('%s: %s' % (desc, line))
537    else:
538        for key in keys:
539            siga = adict.get(key, None)
540            sigb = bdict.get(key, None)
541            if siga is not None and sigb is not None and siga != sigb:
542                out.append('%s changed from %s to %s' % (key, siga, sigb))
543                changecount += 1
544            elif siga is None:
545                out.append('%s was added' % key)
546                addcount += 1
547            elif sigb is None:
548                out.append('%s was removed' % key)
549                removecount += 1
550    out.append('Summary: %d tasks added, %d tasks removed, %d tasks modified (%.1f%%)' % (addcount, removecount, changecount, (changecount / float(len(bdict)) * 100)))
551    return '\n'.join(out)
552
553
554def process_changes(repopath, revision1, revision2='HEAD', report_all=False, report_ver=False,
555                    sigs=False, sigsdiff=False, exclude_path=None):
556    repo = git.Repo(repopath)
557    assert repo.bare == False
558    commit = repo.commit(revision1)
559    diff = commit.diff(revision2)
560
561    changes = []
562
563    if sigs or sigsdiff:
564        for d in diff.iter_change_type('M'):
565            if d.a_blob.path == 'siglist.txt':
566                changes.append(compare_siglists(d.a_blob, d.b_blob, taskdiff=sigsdiff))
567        return changes
568
569    for d in diff.iter_change_type('M'):
570        path = os.path.dirname(d.a_blob.path)
571        if path.startswith('packages/'):
572            filename = os.path.basename(d.a_blob.path)
573            if filename == 'latest':
574                changes.extend(compare_dict_blobs(path, d.a_blob, d.b_blob, report_all, report_ver))
575            elif filename.startswith('latest.'):
576                chg = ChangeRecord(path, filename, d.a_blob.data_stream.read().decode('utf-8'), d.b_blob.data_stream.read().decode('utf-8'), True)
577                changes.append(chg)
578            elif filename == 'sysroot':
579                alines = d.a_blob.data_stream.read().decode('utf-8').splitlines()
580                blines = d.b_blob.data_stream.read().decode('utf-8').splitlines()
581                filechanges = compare_file_lists(alines,blines, compare_ownership=False)
582                if filechanges:
583                    chg = ChangeRecord(path, filename, None, None, True)
584                    chg.filechanges = filechanges
585                    changes.append(chg)
586
587        elif path.startswith('images/'):
588            filename = os.path.basename(d.a_blob.path)
589            if filename in img_monitor_files:
590                if filename == 'files-in-image.txt':
591                    alines = d.a_blob.data_stream.read().decode('utf-8').splitlines()
592                    blines = d.b_blob.data_stream.read().decode('utf-8').splitlines()
593                    filechanges = compare_file_lists(alines,blines)
594                    if filechanges:
595                        chg = ChangeRecord(path, filename, None, None, True)
596                        chg.filechanges = filechanges
597                        changes.append(chg)
598                elif filename == 'installed-package-names.txt':
599                    alines = d.a_blob.data_stream.read().decode('utf-8').splitlines()
600                    blines = d.b_blob.data_stream.read().decode('utf-8').splitlines()
601                    filechanges = compare_lists(alines,blines)
602                    if filechanges:
603                        chg = ChangeRecord(path, filename, None, None, True)
604                        chg.filechanges = filechanges
605                        changes.append(chg)
606                else:
607                    chg = ChangeRecord(path, filename, d.a_blob.data_stream.read().decode('utf-8'), d.b_blob.data_stream.read().decode('utf-8'), True)
608                    changes.append(chg)
609            elif filename == 'image-info.txt':
610                changes.extend(compare_dict_blobs(path, d.a_blob, d.b_blob, report_all, report_ver))
611            elif '/image-files/' in path:
612                chg = ChangeRecord(path, filename, d.a_blob.data_stream.read().decode('utf-8'), d.b_blob.data_stream.read().decode('utf-8'), True)
613                changes.append(chg)
614
615    # Look for added preinst/postinst/prerm/postrm
616    # (without reporting newly added recipes)
617    addedpkgs = []
618    addedchanges = []
619    for d in diff.iter_change_type('A'):
620        path = os.path.dirname(d.b_blob.path)
621        if path.startswith('packages/'):
622            filename = os.path.basename(d.b_blob.path)
623            if filename == 'latest':
624                addedpkgs.append(path)
625            elif filename.startswith('latest.'):
626                chg = ChangeRecord(path, filename[7:], '', d.b_blob.data_stream.read().decode('utf-8'), True)
627                addedchanges.append(chg)
628    for chg in addedchanges:
629        found = False
630        for pkg in addedpkgs:
631            if chg.path.startswith(pkg):
632                found = True
633                break
634        if not found:
635            changes.append(chg)
636
637    # Look for cleared preinst/postinst/prerm/postrm
638    for d in diff.iter_change_type('D'):
639        path = os.path.dirname(d.a_blob.path)
640        if path.startswith('packages/'):
641            filename = os.path.basename(d.a_blob.path)
642            if filename != 'latest' and filename.startswith('latest.'):
643                chg = ChangeRecord(path, filename[7:], d.a_blob.data_stream.read().decode('utf-8'), '', True)
644                changes.append(chg)
645
646    # filter out unwanted paths
647    if exclude_path:
648        for chg in changes:
649            if chg.filechanges:
650                fchgs = []
651                for fchg in chg.filechanges:
652                    for epath in exclude_path:
653                        if fchg.path.startswith(epath):
654                           break
655                    else:
656                        fchgs.append(fchg)
657                chg.filechanges = fchgs
658
659    if report_all:
660        return changes
661    else:
662        return [chg for chg in changes if chg.monitored]
663