# This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Library General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # by Panu Matilainen # James Antill # # TODO: # - In 'pre' mode we could get the changelogs from rpmdb thus avoiding # the costly 'otherdata' import. import time from rpmUtils.miscutils import splitFilename from yum.plugins import TYPE_INTERACTIVE from yum.plugins import PluginYumExit from yum import logginglevels import logging from yum.i18n import to_unicode, to_str from yum.update_md import UpdateMetadata try: import dateutil.parser as dateutil_parser except ImportError: dateutil_parser = None requires_api_version = '2.5' plugin_type = (TYPE_INTERACTIVE,) origpkgs = {} changelog = False orignots = set() also_updateinfo = False updateinfo = False def changelog_delta(pkg, olddate): out = [] for date, author, message in pkg.returnChangelog(): if int(date) > olddate: out.append("* %s %s\n%s" % (time.ctime(int(date)), author, message)) return out def srpmname(pkg): n,v,r,e,a = splitFilename(pkg.returnSimple('sourcerpm')) return n def _show_changes_changelog(conduit, srpms): for name in sorted(srpms.keys()): rpms = [] if name in origpkgs: for rpm in srpms[name]: rpms.append("%s" % rpm) done = False kvf = conduit._base.fmtKeyValFill rpm_names = ", ".join(sorted(rpms)) for line in changelog_delta(srpms[name][0], origpkgs[name]): if not done: conduit.info(2,kvf("ChangeLog for: ", rpm_names)) done = True conduit.info(2, "%s\n" % to_unicode(line)) if not done: conduit.info(2, "%s\n" % kvf("** No ChangeLog for: ",rpm_names)) def _show_changes_updateinfo(conduit): for note in sorted(orignots): conduit.info(2, note) def show_changes(conduit, msg): # Group by src.rpm name, not binary to avoid showing duplicate changelogs # for subpackages srpms = {} ts = conduit.getTsInfo() for tsmem in ts.getMembers(): if not tsmem.updates: continue name = srpmname(tsmem.po) if name in srpms: srpms[name].append(tsmem.po) else: srpms[name] = [tsmem.po] conduit.info(2, "\n%s\n" % msg) if changelog: _show_changes_changelog(conduit, srpms) if updateinfo: _show_changes_updateinfo(conduit) class ChangeLogCommand: def getNames(self): return ['changelog', 'ChangeLog'] def getUsage(self): return "[||all] [PACKAGE|all|installed|updates|extras|obsoletes|recent]" def getSummary(self): return """\ Display changelog data, since a specified time, on a group of packages""" def doCheck(self, base, basecmd, extcmds): pass def changelog(self, pkg): if self._since_all: for date, author, message in pkg.returnChangelog(): yield "* %s %s\n%s" % (time.ctime(int(date)), author, message) return if self._since_num is not None: num = self._since_num for date, author, message in pkg.returnChangelog(): if num <= 0: return num -= 1 yield "* %s %s\n%s" % (time.ctime(int(date)), author, message) return if True: for date, author, message in pkg.returnChangelog(): if int(date) < self._since_tt: return yield "* %s %s\n%s" % (time.ctime(int(date)), author, message) def show_data(self, msg, pkgs, name): done = False for pkg in pkgs: self._pkgs += 1 if pkg.sourcerpm in self._done_spkgs: continue self._spkgs += 1 for line in self.changelog(pkg): if pkg.sourcerpm not in self._done_spkgs: if not self._done_spkgs: msg('') if self._since_all: msg('Listing all changelogs') elif self._since_num is not None: sn = "s" if self._since_num == 1: sn = "" msg('Listing %d changelog%s' % (self._since_num,sn)) else: msg('Listing changelogs since ' + str(self._since_dto.date())) msg('') if not done: msg("%s %s %s" % ('=' * 20, name, '=' * 20)) done = True self._done_spkgs[pkg.sourcerpm] = True msg('%-40.40s %s' % (pkg, pkg.repoid)) self._changelogs += 1 msg(to_unicode(line)) msg('') def doCommand(self, base, basecmd, extcmds): logger = logging.getLogger("yum.verbose.main") def msg(x): logger.log(logginglevels.INFO_2, x) def msg_warn(x): logger.warn(x) self._done_spkgs = {} self._pkgs = 0 self._spkgs = 0 self._changelogs = 0 self._since_all = False self._since_dto = None self._since_tt = None self._since_num = None if not len(extcmds): return 1, [basecmd + " " + self.getUsage()] since = extcmds[0] extcmds = extcmds[1:] if since == 'all': self._since_all = True else: try: num = int(since) if num <= 0: raise ValueError self._since_num = num except: if dateutil_parser is None: msg = "Dateutil module not available, so can't parse dates" raise PluginYumExit(msg) try: self._since_dto = dateutil_parser.parse(since, fuzzy=True) except ValueError: msg = "Argument -- %s -- is not \"all\", a positive number or a date" % since raise PluginYumExit(msg) try: tt = self._since_dto.timetuple() self._since_tt = time.mktime(tt) except ValueError, e: msg = "Argument -- %s -- is not valid: %s" % (since, to_str(e)) raise PluginYumExit(msg) if self._since_dto == dateutil_parser.parse('garbage', fuzzy=True): # 'since' was a package name, not a date extcmds.insert(0, since) # assume since="all" self._since_all = True self._since_dto = None self._since_tt = None ypl = base.returnPkgLists(extcmds) self.show_data(msg, ypl.installed, 'Installed Packages') self.show_data(msg, ypl.available, 'Available Packages') self.show_data(msg, ypl.extras, 'Extra Packages') self.show_data(msg, ypl.updates, 'Updated Packages') self.show_data(msg, ypl.obsoletes, 'Obsoleting Packages') self.show_data(msg, ypl.recent, 'Recent Packages') ps = sps = cs = "" if self._pkgs != 1: ps = "s" if self._spkgs != 1: sps = "s" if self._changelogs != 1: cs = "s" return 0, [basecmd + ' stats. %d pkg%s, %d source pkg%s, %d changelog%s' % (self._pkgs, ps, self._spkgs, sps, self._changelogs, cs)] def needTs(self, base, basecmd, extcmds): if len(extcmds) and extcmds[0] == 'installed': return False return True def config_hook(conduit): conduit.registerCommand(ChangeLogCommand()) parser = conduit.getOptParser() if parser: if conduit.confBool('main', 'always', default=False): global changelog changelog = True if conduit.confBool('main', 'updateinfo', default=True): global also_updateinfo also_updateinfo = True if (also_updateinfo and conduit.confBool('main', 'updateinfo_always', default=changelog)): global updateinfo updateinfo = True if changelog and (not also_updateinfo or updateinfo): return if hasattr(parser, 'plugin_option_group'): parser = parser.plugin_option_group parser.add_option('--changelog', action='store_true', help='Show changelog delta of updated packages') def _setup_changelog_from_cmdline(conduit): global changelog global updateinfo opts, args = conduit.getCmdLine() if opts: if not changelog: changelog = opts.changelog if not updateinfo: updateinfo = opts.changelog def postresolve_hook(conduit): _setup_changelog_from_cmdline(conduit) if not (changelog or updateinfo) or conduit.resultcode == 1: return # Find currently installed versions of packages we're about to update ts = conduit.getTsInfo() rpmdb = conduit.getRpmDB() if updateinfo: repos = set() for tsmem in ts.getMembers(): if tsmem.po.repoid == 'installed': continue repos.add(tsmem.po.repo) mdi = UpdateMetadata(repos=list(repos)) for tsmem in ts.getMembers(): for po in rpmdb.searchNevra(name=tsmem.po.name, arch=tsmem.po.arch): times = po['changelogtime'] try: n,v,r,e,a = splitFilename(po.sourcerpm) except TypeError: n = po.name if len(times) == 0: # deal with packages without changelog origpkgs[n] = 0 else: origpkgs[n] = times[0] if updateinfo: for (pkgtup, notice) in mdi.get_applicable_notices(po.pkgtup): orignots.add(notice) if conduit.confString('main', 'when', default='post') == 'pre': show_changes(conduit, 'Changes in packages about to be updated:') def posttrans_hook(conduit): if not (changelog or updateinfo): return if conduit.confString('main', 'when', default='post') == "post": show_changes(conduit, 'Changes in updated packages:')