#!/usr/bin/python # # dispatch-conf -- Integrate modified configs, post-emerge # # Copyright 2002 Gentoo Technologies, Inc. # Distributed under the terms of the GNU Public License v2 # # Jeremy Wohl (http://igmus.org) # $Id: dispatch-conf,v 1.13 2003/02/05 09:39:31 jeremyw Exp $ # # TODO # dialog menus # import os, sys, string, re, commands, shutil import portage 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' RCS_BRANCH = '1.1.1' RCS_LOCK = 'rcs -ko -M -l' RCS_PUT = 'ci -t-"Archived config file." -m"dispatch-conf update."' RCS_GET = 'co' RCS_MERGE = 'rcsmerge -p -r' + RCS_BRANCH + ' %s >%s' DIFF3_MERGE = 'diff3 -mE %s %s %s >%s' MANDATORY_OPTS = [ 'archive-dir', 'diff', 'pager', 'replace-cvs', 'replace-wscomments' ] class dispatch: options = {} def grind (self, config_paths): confs = [] count = 0 self.read_my_config () # # 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']) if os.path.exists (mrgconf): newconf = mrgconf unmodified = 0 else: if self.options ['use-rcs'] == 'yes': newconf = self.do_rcs (conf ['new'], conf ['current'], mrgconf) else: newconf = self.do_archive (conf ['new'], conf ['current'], mrgconf) if newconf == mrgconf: unmodified = len(commands.getoutput (DIFF_CONTENTS % (conf ['new'], mrgconf))) == 0 else: unmodified = 0 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 or unmodified: os.unlink (conf ['new']) if os.path.exists (mrgconf): os.unlink (mrgconf) return False elif same_cvs or same_wsc or conf ['dir'] in portage.settings ['CONFIG_PROTECT_MASK'].split (): self.replace (newconf, 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 = '/tmp/dispatch-conf.merged.%i' % (os.getpid (),) print os.system (MERGE % (merged, conf ['current'], newconf)) os.rename (merged, mrgconf) newconf = mrgconf continue elif c == 'l': show_new_diff = 1 continue elif c == 'e': os.system ('vim %s' % (newconf)) continue elif c == 'z': os.unlink (conf ['new']) if os.path.exists (mrgconf): os.unlink (mrgconf) break elif c == 'u': self.replace (newconf, conf ['current']) if newconf == mrgconf: os.unlink (conf ['new']) elif os.path.exists (mrgconf): os.unlink (mrgconf) break else: continue def do_rcs (self, newconf, curconf, mrgconf): """Archive existing config in rcs (on trunk). Then, if an old branch version exists, merge the user's changes and the distributed changes and put the result into mrgconf. Next, archive the new distributed version on the 1.1.1 branch of the rcs file. We return the mrgconf name if a merge happened, else newconf.""" archive = os.path.join (self.options ['archive-dir'], curconf.lstrip ('/')) try: os.makedirs (os.path.dirname (archive)) except: pass try: shutil.copy2 (curconf, archive) except (IOError, os.error), why: print >> sys.stderr, 'dispatch-conf: Error copying %s to %s: %s; fatal' % \ (curconf, archive, str(why)) if (os.path.exists (archive + ',v')): os.system (RCS_LOCK + ' ' + archive) os.system (RCS_PUT + ' ' + archive) # There is probably a better way of checking for the branch... os.system (RCS_GET + ' -r' + RCS_BRANCH + ' ' + archive) has_branch = os.path.exists (archive) try: shutil.copy2 (newconf, archive) except (IOError, os.error), why: print >> sys.stderr, 'dispatch-conf: Error copying %s to %s: %s; fatal' % \ (newconf, archive, str(why)) if has_branch: # This puts the results of the merge into mrgconf. os.system (RCS_MERGE % (archive, mrgconf)) # Commit the last-distributed version onto the branch. os.system (RCS_LOCK + RCS_BRANCH + ' ' + archive) os.system (RCS_PUT + ' -r' + RCS_BRANCH + ' ' + archive) return mrgconf # Forcefully commit the last-distributed version onto the branch. os.system (RCS_PUT + ' -f -r' + RCS_BRANCH + ' ' + archive) return newconf def do_archive (self, newconf, curconf, mrgconf): """Archive existing config using a .# suffix. Then, if a non-suffixed version exists, merge the user's changes and the distributed changes and put the result into mrgconf. Next, archive the new distributed version (as the non-suffixed version). We return the mrgconf name if a merge happened, else newconf.""" archive = os.path.join (self.options ['archive-dir'], curconf.lstrip ('/')) try: os.makedirs (os.path.dirname (archive)) except: pass count = 1 while count < 1000: withsuf = archive + '.' + str(count) if os.path.exists (withsuf): count = count + 1 continue else: # Don't save a duplicate of the previously archived version if count > 1: prevsuf = archive + '.' + str(count-1) if len(commands.getoutput (DIFF_CONTENTS % (curconf, prevsuf))) == 0: break try: shutil.copy2 (curconf, withsuf) except (IOError, os.error), why: print >> sys.stderr, 'dispatch-conf: Error copying %s to %s: %s; fatal' % \ (curconf, withsuf, str(why)) break else: print >> sys.stderr, 'dispatch-conf: Error archiving files -- exhausted slots???; fatal' sys.exit (1) if os.path.exists (archive): # This puts the results of the merge into mrgconf. os.system (DIFF3_MERGE % (curconf, archive, newconf, mrgconf)) ret = mrgconf else: ret = newconf # Save off the new config file in the archive dir (sans suffix) try: shutil.copy2 (newconf, archive) except (IOError, os.error), why: print >> sys.stderr, 'dispatch-conf: Error copying %s to %s: %s; fatal' % \ (newconf, archive, str(why)) return ret def replace (self, newconf, curconf): """Replace current config with the new/merged version""" 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 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): os.remove (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 read_my_config (self): try: opts = portage.getconfig ('/etc/dispatch-conf.conf') except: opts = None if not opts: print >> sys.stderr, 'dispatch-conf: Error reading /etc/dispatch-conf.conf; fatal' sys.exit (1) for key in MANDATORY_OPTS: if not opts.has_key (key): print >> sys.stderr, 'dispatch-conf: Missing option "%s" in /etc/dispatch-conf.conf; fatal' % (key,) if not (os.path.exists (opts ['archive-dir']) and os.path.isdir (opts ['archive-dir'])): print >> sys.stderr, 'dispatch-conf: Config archive dir [%s] must exist; fatal' % (opts ['archive-dir'],) sys.exit (1) self.options = opts 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'])