This includes numerous logic adjustments that are needed to support
protected symlinks. The new diff_mixed function is used for diffs
between arbitrary file types. For example, a diff between two symlinks
looks like this:

-SYM: /foo/bar -> baz
+SYM: /foo/bar -> blah

X-Gentoo-Bug: 485598
X-Gentoo-Bug-URL: https://bugs.gentoo.org/show_bug.cgi?id=485598
---
This updated patch adds a diff_mixed_wrapper class which is used to simplify
diff_mixed usage.

 bin/dispatch-conf            |  39 ++++++-----
 pym/portage/dispatch_conf.py | 150 +++++++++++++++++++++++++++++++++++++------
 2 files changed, 151 insertions(+), 38 deletions(-)

diff --git a/bin/dispatch-conf b/bin/dispatch-conf
index 6d2ae94..412dcdc 100755
--- a/bin/dispatch-conf
+++ b/bin/dispatch-conf
@@ -15,15 +15,15 @@ from __future__ import print_function
 
 from stat import ST_GID, ST_MODE, ST_UID
 from random import random
-import atexit, re, shutil, stat, sys
+import atexit, io, re, functools, shutil, sys
 from os import path as osp
 if osp.isfile(osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), 
".portage_not_installed")):
        sys.path.insert(0, 
osp.join(osp.dirname(osp.dirname(osp.realpath(__file__))), "pym"))
 import portage
 portage._internal_caller = True
 from portage import os
-from portage import _unicode_decode
-from portage.dispatch_conf import diffstatusoutput
+from portage import _encodings, _unicode_decode
+from portage.dispatch_conf import diffstatusoutput, diff_mixed_wrapper
 from portage.process import find_binary, spawn
 
 FIND_EXTANT_CONFIGS  = "find '%s' %s -name '._cfg????_%s' ! -name '.*~' ! 
-iname '.*.bak' -print"
@@ -72,6 +72,8 @@ def cmd_var_is_valid(cmd):
 
     return find_binary(cmd[0]) is not None
 
+diff = diff_mixed_wrapper(diffstatusoutput, DIFF_CONTENTS)
+
 class dispatch:
     options = {}
 
@@ -89,8 +91,6 @@ class dispatch:
                or not os.path.exists(self.options["log-file"]):
                 open(self.options["log-file"], 'w').close() # Truncate it
                 os.chmod(self.options["log-file"], 0o600)
-        else:
-            self.options["log-file"] = "/dev/null"
 
         pager = self.options.get("pager")
         if pager is None or not cmd_var_is_valid(pager):
@@ -148,9 +148,6 @@ class dispatch:
             portage.util.shlex_split(
             portage.settings.get('CONFIG_PROTECT_MASK', '')))
 
-        def diff(file1, file2):
-            return diffstatusoutput(DIFF_CONTENTS, file1, file2)
-
         #
         # Remove new configs identical to current
         #                  and
@@ -166,7 +163,7 @@ class dispatch:
                 mrgfail = portage.dispatch_conf.rcs_archive(archive, 
conf['current'], conf['new'], mrgconf)
             else:
                 mrgfail = portage.dispatch_conf.file_archive(archive, 
conf['current'], conf['new'], mrgconf)
-            if os.path.exists(archive + '.dist'):
+            if os.path.lexists(archive + '.dist'):
                 unmodified = len(diff(conf['current'], archive + '.dist')[1]) 
== 0
             else:
                 unmodified = 0
@@ -181,7 +178,7 @@ class dispatch:
 
             if newconf == mrgconf and \
                 self.options.get('ignore-previously-merged') != 'yes' and \
-                os.path.exists(archive+'.dist') and \
+                os.path.lexists(archive+'.dist') and \
                 len(diff(archive+'.dist', conf['new'])[1]) == 0:
                 # The current update is identical to the archived .dist
                 # version that has previously been merged.
@@ -254,6 +251,13 @@ class dispatch:
 
         valid_input = "qhtnmlezu"
 
+        def diff_pager(file1, file2):
+            cmd = self.options['diff'] % (file1, file2)
+            cmd += pager
+            spawn_shell(cmd)
+
+        diff_pager = diff_mixed_wrapper(diff_pager)
+
         for conf in confs:
             count = count + 1
 
@@ -266,14 +270,10 @@ class dispatch:
             while 1:
                 clear_screen()
                 if show_new_diff:
-                    cmd = self.options['diff'] % (conf['new'], mrgconf)
-                    cmd += pager
-                    spawn_shell(cmd)
+                    diff_pager(conf['new'], mrgconf)
                     show_new_diff = 0
                 else:
-                    cmd = self.options['diff'] % (conf['current'], newconf)
-                    cmd += pager
-                    spawn_shell(cmd)
+                    diff_pager(conf['current'], newconf)
 
                 print()
                 print('>> (%i of %i) -- %s' % (count, len(confs), conf 
['current']))
@@ -357,7 +357,12 @@ class dispatch:
     def replace (self, newconf, curconf):
         """Replace current config with the new/merged version.  Also logs
         the diff of what changed into the configured log file."""
-        os.system((DIFF_CONTENTS % (curconf, newconf)) + '>>' + 
self.options["log-file"])
+        if "log-file" in self.options:
+            status, output = diff(curconf, newconf)
+            with io.open(self.options["log-file"], mode="a",
+                encoding=_encodings["stdio"]) as f:
+                f.write(output + "\n")
+
         try:
             os.rename(newconf, curconf)
         except (IOError, os.error) as why:
diff --git a/pym/portage/dispatch_conf.py b/pym/portage/dispatch_conf.py
index 113d965..cda45d5 100644
--- a/pym/portage/dispatch_conf.py
+++ b/pym/portage/dispatch_conf.py
@@ -6,11 +6,12 @@
 # Library by Wayne Davison <gen...@blorf.net>, derived from code
 # written by Jeremy Wohl (http://igmus.org)
 
-from __future__ import print_function
+from __future__ import print_function, unicode_literals
 
-import os, shutil, subprocess, sys
+import functools, io, os, shutil, stat, subprocess, sys, tempfile
 
 import portage
+from portage import _encodings
 from portage.env.loaders import KeyValuePairFileLoader
 from portage.localization import _
 from portage.util import shlex_split, varexpand
@@ -50,6 +51,66 @@ def diffstatusoutput(cmd, file1, file2):
                output = output[:-1]
        return (proc.wait(), output)
 
+def diff_mixed(func, file1, file2):
+       tempdir = None
+       try:
+               if os.path.islink(file1) and \
+                       not os.path.islink(file2) and \
+                       os.path.isfile(file1) and \
+                       os.path.isfile(file2):
+                       # If a regular file replaces a symlink to a regular
+                       # file, then show the diff between the regular files
+                       # (bug #330221).
+                       diff_files = (file2, file2)
+               else:
+                       files = [file1, file2]
+                       diff_files = [file1, file2]
+                       for i in range(len(diff_files)):
+                               try:
+                                       st = os.lstat(diff_files[i])
+                               except OSError:
+                                       st = None
+                               if st is not None and stat.S_ISREG(st.st_mode):
+                                       continue
+
+                               if tempdir is None:
+                                       tempdir = tempfile.mkdtemp()
+                               diff_files[i] = os.path.join(tempdir, "%d" % i)
+                               if st is None:
+                                       content = "/dev/null\n"
+                               elif stat.S_ISLNK(st.st_mode):
+                                       link_dest = os.readlink(files[i])
+                                       content = "SYM: %s -> %s\n" % \
+                                               (file1, link_dest)
+                               elif stat.S_ISDIR(st.st_mode):
+                                       content = "DIR: %s\n" % (file1,)
+                               elif stat.S_ISFIFO(st.st_mode):
+                                       content = "FIF: %s\n" % (file1,)
+                               else:
+                                       content = "DEV: %s\n" % (file1,)
+                               with io.open(diff_files[i], mode='w',
+                                       encoding=_encodings['stdio']) as f:
+                                       f.write(content)
+
+               return func(diff_files[0], diff_files[1])
+
+       finally:
+               if tempdir is not None:
+                       shutil.rmtree(tempdir)
+
+class diff_mixed_wrapper(object):
+
+       def __init__(self, f, *args):
+               self._func = f
+               self._args = args
+
+       def __call__(self, *args):
+               return diff_mixed(
+                       functools.partial(self._func, *(self._args + 
args[:-2])),
+                       *args[-2:])
+
+diffstatusoutput_mixed = diff_mixed_wrapper(diffstatusoutput)
+
 def read_config(mandatory_opts):
        eprefix = portage.settings["EPREFIX"]
        if portage._not_installed:
@@ -103,35 +164,57 @@ def rcs_archive(archive, curconf, newconf, mrgconf):
        except OSError:
                pass
 
-       if os.path.isfile(curconf):
+       try:
+               curconf_st = os.lstat(curconf)
+       except OSError:
+               curconf_st = None
+
+       if curconf_st is not None and \
+               (stat.S_ISREG(curconf_st.st_mode) or
+               stat.S_ISLNK(curconf_st.st_mode)):
                try:
-                       shutil.copy2(curconf, archive)
+                       if stat.S_ISLNK(curconf_st.st_mode):
+                               os.symlink(os.readlink(curconf), archive)
+                       else:
+                               shutil.copy2(curconf, archive)
                except(IOError, os.error) as why:
                        print(_('dispatch-conf: Error copying %(curconf)s to 
%(archive)s: %(reason)s; fatal') % \
                                {"curconf": curconf, "archive": archive, 
"reason": str(why)}, file=sys.stderr)
 
-       if os.path.exists(archive + ',v'):
+       if os.path.lexists(archive + ',v'):
                os.system(RCS_LOCK + ' ' + archive)
        os.system(RCS_PUT + ' ' + archive)
 
        ret = 0
-       if newconf != '':
+       mystat = None
+       if newconf:
+               try:
+                       mystat = os.lstat(newconf)
+               except OSError:
+                       pass
+
+       if mystat is not None and \
+               (stat.S_ISREG(mystat.st_mode) or
+               stat.S_ISLNK(mystat.st_mode)):
                os.system(RCS_GET + ' -r' + RCS_BRANCH + ' ' + archive)
-               has_branch = os.path.exists(archive)
+               has_branch = os.path.lexists(archive)
                if has_branch:
                        os.rename(archive, archive + '.dist')
 
                try:
-                       shutil.copy2(newconf, archive)
+                       if stat.S_ISLNK(mystat.st_mode):
+                               os.symlink(os.readlink(newconf), archive)
+                       else:
+                               shutil.copy2(newconf, archive)
                except(IOError, os.error) as why:
                        print(_('dispatch-conf: Error copying %(newconf)s to 
%(archive)s: %(reason)s; fatal') % \
                                {"newconf": newconf, "archive": archive, 
"reason": str(why)}, file=sys.stderr)
 
                if has_branch:
-                       if mrgconf != '':
+                       if mrgconf and os.path.isfile(archive) and \
+                               os.path.isfile(mrgconf):
                                # This puts the results of the merge into 
mrgconf.
                                ret = os.system(RCS_MERGE % (archive, mrgconf))
-                               mystat = os.lstat(newconf)
                                os.chmod(mrgconf, mystat.st_mode)
                                os.chown(mrgconf, mystat.st_uid, mystat.st_gid)
                os.rename(archive, archive + '.dist.new')
@@ -153,10 +236,11 @@ def file_archive(archive, curconf, newconf, mrgconf):
                pass
 
        # Archive the current config file if it isn't already saved
-       if (os.path.exists(archive) and
-               len(diffstatusoutput("diff -aq '%s' '%s'", curconf, 
archive)[1]) != 0):
+       if (os.path.lexists(archive) and
+               len(diffstatusoutput_mixed(
+               "diff -aq '%s' '%s'", curconf, archive)[1]) != 0):
                suf = 1
-               while suf < 9 and os.path.exists(archive + '.' + str(suf)):
+               while suf < 9 and os.path.lexists(archive + '.' + str(suf)):
                        suf += 1
 
                while suf > 1:
@@ -165,26 +249,50 @@ def file_archive(archive, curconf, newconf, mrgconf):
 
                os.rename(archive, archive + '.1')
 
-       if os.path.isfile(curconf):
+       try:
+               curconf_st = os.lstat(curconf)
+       except OSError:
+               curconf_st = None
+
+       if curconf_st is not None and \
+               (stat.S_ISREG(curconf_st.st_mode) or
+               stat.S_ISLNK(curconf_st.st_mode)):
                try:
-                       shutil.copy2(curconf, archive)
+                       if stat.S_ISLNK(curconf_st.st_mode):
+                               os.symlink(os.readlink(curconf), archive)
+                       else:
+                               shutil.copy2(curconf, archive)
                except(IOError, os.error) as why:
                        print(_('dispatch-conf: Error copying %(curconf)s to 
%(archive)s: %(reason)s; fatal') % \
                                {"curconf": curconf, "archive": archive, 
"reason": str(why)}, file=sys.stderr)
 
-       if newconf != '':
+       mystat = None
+       if newconf:
+               try:
+                       mystat = os.lstat(newconf)
+               except OSError:
+                       pass
+
+       if mystat is not None and \
+               (stat.S_ISREG(mystat.st_mode) or
+               stat.S_ISLNK(mystat.st_mode)):
                # Save off new config file in the archive dir with .dist.new 
suffix
+               newconf_archive = archive + '.dist.new'
                try:
-                       shutil.copy2(newconf, archive + '.dist.new')
+                       if stat.S_ISLNK(mystat.st_mode):
+                               os.symlink(os.readlink(newconf), 
newconf_archive)
+                       else:
+                               shutil.copy2(newconf, newconf_archive)
                except(IOError, os.error) as why:
                        print(_('dispatch-conf: Error copying %(newconf)s to 
%(archive)s: %(reason)s; fatal') % \
                                {"newconf": newconf, "archive": archive + 
'.dist.new', "reason": str(why)}, file=sys.stderr)
 
                ret = 0
-               if mrgconf != '' and os.path.exists(archive + '.dist'):
+               if mrgconf and os.path.isfile(curconf) and \
+                       os.path.isfile(newconf) and \
+                       os.path.isfile(archive + '.dist'):
                        # This puts the results of the merge into mrgconf.
                        ret = os.system(DIFF3_MERGE % (curconf, archive + 
'.dist', newconf, mrgconf))
-                       mystat = os.lstat(newconf)
                        os.chmod(mrgconf, mystat.st_mode)
                        os.chown(mrgconf, mystat.st_uid, mystat.st_gid)
 
@@ -195,7 +303,7 @@ def rcs_archive_post_process(archive):
        """Check in the archive file with the .dist.new suffix on the branch
        and remove the one with the .dist suffix."""
        os.rename(archive + '.dist.new', archive)
-       if os.path.exists(archive + '.dist'):
+       if os.path.lexists(archive + '.dist'):
                # Commit the last-distributed version onto the branch.
                os.system(RCS_LOCK + RCS_BRANCH + ' ' + archive)
                os.system(RCS_PUT + ' -r' + RCS_BRANCH + ' ' + archive)
@@ -207,5 +315,5 @@ def rcs_archive_post_process(archive):
 
 def file_archive_post_process(archive):
        """Rename the archive file with the .dist.new suffix to a .dist 
suffix"""
-       if os.path.exists(archive + '.dist.new'):
+       if os.path.lexists(archive + '.dist.new'):
                os.rename(archive + '.dist.new', archive + '.dist')
-- 
2.0.4


Reply via email to