#!/usr/bin/python # # dispatch-conf -- Integrate modified configs, post-emerge # # Copyright 2003 Gentoo Technologies, Inc. # Distributed under the terms of the GNU Public License v2 # # Jeremy Wohl (http://igmus.org) # $Id: dispatch-conf,v 1.2 2003/08/21 01:01:26 carpaski Exp $ # # TODO # dialog menus # from stat import * import os, sys, string, re, commands import portage, dispatch_conf FIND_EXTANT_CONFIGS = "find %s -iname '._cfg????_*'" DIFF_CONTENTS = 'diff -Nau %s %s' DIFF_CVS_INTERP = 'diff -Nau %s %s | grep "^[+-][^+-]" | grep -v "# .Header:.*"' DIFF_WSCOMMENTS = 'diff -Nau %s %s | grep "^[+-][^+-]" | grep -v "^[-+]#" | grep -v "^[-+][:space:]*$"' MERGE = 'sdiff --suppress-common-lines --output=%s %s %s' LOG_CHANGES_TO = '/var/log/dispatch-conf.changes' MANDATORY_OPTS = [ 'archive-dir', 'diff', 'pager', 'replace-cvs', 'replace-wscomments' ] class dispatch: options = {} def grind (self, config_paths): confs = [] count = 0 if os.path.exists(LOG_CHANGES_TO): os.rename(LOG_CHANGES_TO, LOG_CHANGES_TO + '.old') self.options = dispatch_conf.read_config(MANDATORY_OPTS) # # Build list of extant configs # for path in config_paths.split (): if not os.path.exists (path): continue confs += self.massage (os.popen (FIND_EXTANT_CONFIGS % (path,)).readlines ()) # # Remove new configs identical to current # and # Auto-replace configs a) whose differences are simply CVS interpolations, # or b) whose differences are simply ws or comments, # or c) in paths now unprotected by CONFIG_PROTECT_MASK, # def f (conf): mrgconf = re.sub(r'\._cfg', '._mrg', conf['new']) archive = os.path.join(self.options['archive-dir'], conf['current'].lstrip('/')) if self.options['use-rcs'] == 'yes': dispatch_conf.rcs_archive(archive, conf['current'], conf['new'], mrgconf) else: dispatch_conf.file_archive(archive, conf['current'], conf['new'], mrgconf) if os.path.exists(archive + '.dist'): unmodified = len(commands.getoutput(DIFF_CONTENTS % (conf['current'], archive + '.dist'))) == 0 else: unmodified = 0 if os.path.exists(mrgconf): if len(commands.getoutput(DIFF_CONTENTS % (conf['new'], mrgconf))) == 0: os.unlink(mrgconf) newconf = conf['new'] else: newconf = mrgconf else: newconf = conf['new'] same_file = len(commands.getoutput (DIFF_CONTENTS % (conf ['current'], newconf))) == 0 same_cvs = len(commands.getoutput (DIFF_CVS_INTERP % (conf ['current'], newconf))) == 0 same_wsc = len(commands.getoutput (DIFF_WSCOMMENTS % (conf ['current'], newconf))) == 0 # Do options permit? same_cvs = same_cvs and self.options['replace-cvs'] == 'yes' same_wsc = same_wsc and self.options['replace-wscomments'] == 'yes' unmodified = unmodified and self.options['replace-unmodified'] == 'yes' if same_file: os.unlink (conf ['new']) self.post_process(conf['current']) if os.path.exists(mrgconf): os.unlink(mrgconf) return False elif unmodified or same_cvs or same_wsc or conf ['dir'] in portage.settings ['CONFIG_PROTECT_MASK'].split (): self.replace(newconf, conf['current']) self.post_process(conf['current']) if newconf == mrgconf: os.unlink(conf['new']) elif os.path.exists(mrgconf): os.unlink(mrgconf) return False else: return True confs = filter (f, confs) # # Interactively process remaining # for conf in confs: count = count + 1 newconf = conf['new'] mrgconf = re.sub(r'\._cfg', '._mrg', newconf) if os.path.exists(mrgconf): newconf = mrgconf show_new_diff = 0 while 1: if show_new_diff: os.system((self.options['diff'] + '| %s') % (conf['new'], mrgconf, self.options['pager'])) show_new_diff = 0 else: os.system((self.options['diff'] + '| %s') % (conf['current'], newconf, self.options['pager'])) print print '>> (%i of %i) -- %s' % (count, len(confs), conf ['current']) print '>> q quit, h help, n next, e edit-new, z zap-new, u use-new\n m merge, t toggle-merge, l look-merge: ', c = getch () if c == 'q': sys.exit (0) if c == 'h': self.do_help () continue elif c == 't': if newconf == mrgconf: newconf = conf['new'] elif os.path.exists(mrgconf): newconf = mrgconf continue elif c == 'n': break elif c == 'm': merged = re.sub(r'\._mrg', '._tmp', mrgconf) print os.system (MERGE % (merged, conf ['current'], newconf)) os.rename (merged, mrgconf) mystat = os.lstat(conf['new']) os.chmod(mrgconf, mystat[ST_MODE]) os.chown(mrgconf, mystat[ST_UID], mystat[ST_GID]) newconf = mrgconf continue elif c == 'l': show_new_diff = 1 continue elif c == 'e': os.system(os.environ['EDITOR'] + ' ' + newconf) continue elif c == 'z': self.post_process(conf['current']) os.unlink(conf['new']) if os.path.exists(mrgconf): os.unlink(mrgconf) break elif c == 'u': self.replace(newconf, conf ['current']) self.post_process(conf['current']) if newconf == mrgconf: os.unlink(conf['new']) elif os.path.exists(mrgconf): os.unlink(mrgconf) break else: continue def replace (self, newconf, curconf): """Replace current config with the new/merged version. Also logs the diff of what changed into the LOG_CHANGES_TO file.""" os.system((DIFF_CONTENTS % (curconf, newconf)) + '>>' + LOG_CHANGES_TO) try: os.rename (newconf, curconf) except (IOError, os.error), why: print >> sys.stderr, 'dispatch-conf: Error renaming %s to %s: %s; fatal' % \ (newconf, curconf, str(why)) def post_process(self, curconf): archive = os.path.join(self.options['archive-dir'], curconf.lstrip('/')) if self.options['use-rcs'] == 'yes': dispatch_conf.rcs_archive_post_process(archive) else: dispatch_conf.file_archive_post_process(archive) def massage (self, newconfigs): """Sort, rstrip, remove old versions, break into triad hash. Triad is dictionary of current (/etc/make.conf), new (/etc/._cfg0003_make.conf) and dir (/etc). We keep ._cfg0002_conf over ._cfg0001_conf and ._cfg0000_conf. """ h = {} newconfigs.sort () for nconf in newconfigs: nconf = nconf.rstrip () conf = re.sub (r'\._cfg\d+_', '', nconf) dir = re.match (r'^(.+)/', nconf).group (1) if h.has_key (conf): mrgconf = re.sub(r'\._cfg', '._mrg', h[conf]['new']) if os.path.exists(mrgconf): os.unlink(mrgconf) os.unlink(h[conf]['new']) h [conf] = { 'current' : conf, 'dir' : dir, 'new' : nconf } configs = h.values () configs.sort (lambda a, b: cmp(a ['current'], b ['current'])) return configs def do_help (self): print; print print ' u -- update current config with new config and continue' print ' z -- zap (delete) new config and continue' print ' n -- skip to next config, leave all intact' print ' e -- edit new config' print ' m -- interactively merge current and new configs' print ' l -- look at diff between pre-merged and merged configs' print ' t -- toggle new config between merged and pre-merged state' print ' h -- this screen' print ' q -- quit' print; print 'press any key to return to diff...', getch () def getch (): # from ASPN - Danny Yoo # import sys, tty, termios fd = sys.stdin.fileno() old_settings = termios.tcgetattr(fd) try: tty.setraw(sys.stdin.fileno()) ch = sys.stdin.read(1) finally: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) return ch # run d = dispatch () if len(sys.argv) > 1: # for testing d.grind (string.join (sys.argv [1:])) else: d.grind (portage.settings ['CONFIG_PROTECT'])