public inbox for gentoo-portage-dev@lists.gentoo.org
 help / color / mirror / Atom feed
From: Zac Medico <zmedico@gentoo.org>
To: gentoo-portage-dev@lists.gentoo.org
Cc: Zac Medico <zmedico@gentoo.org>
Subject: [gentoo-portage-dev] [PATCH] dispatch-conf: symlink support for bug #485598
Date: Fri, 31 Oct 2014 06:34:32 -0700	[thread overview]
Message-ID: <1414762472-13482-1-git-send-email-zmedico@gentoo.org> (raw)
In-Reply-To: <1414321936-22851-1-git-send-email-zmedico@gentoo.org>

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 <gentoo@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



      parent reply	other threads:[~2014-10-31 13:34 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2014-10-26 11:12 [gentoo-portage-dev] [PATCH 1/3] etc-update: symlink support for bug #485598 zmedico
2014-10-26 11:12 ` [gentoo-portage-dev] [PATCH 2/3] dispatch-conf: " zmedico
2014-10-26 11:12 ` [gentoo-portage-dev] [PATCH 3/3] CONFIG_PROTECT: protect symlinks, " zmedico
2014-10-27  8:08   ` Alexander Berntsen
2014-10-27  9:07     ` Zac Medico
2014-10-27 20:35     ` Zac Medico
2014-11-03  3:54       ` Brian Dolbec
2014-10-27 22:57 ` [gentoo-portage-dev] [PATCH 1/3] etc-update: symlink support for " Zac Medico
2014-10-27 23:04 ` [gentoo-portage-dev] [PATCH 2/3] dispatch-conf: " Zac Medico
2014-10-31 13:34 ` Zac Medico [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=1414762472-13482-1-git-send-email-zmedico@gentoo.org \
    --to=zmedico@gentoo.org \
    --cc=gentoo-portage-dev@lists.gentoo.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox