Repository: conan-io/conan

Issue:
[feature] Flush stdout before executing commands
I run with `print_run_commands = True` 24/7, as does our CI server. I'm really happy the setting exists. Unfortunately, the output is often jumbled because of buffering. I think if you called `sys.stdout.flush()` and `sys.stderr.flush()` before running an external process, that would solve the issue. Probably? Here's an example of what I'm talking about

```
...
...
...
[13:38:41]	[100%] Linking CXX executable ../bin/iodb_read_write_test
[13:38:41]	[100%] Built target iodb_read_write_test
[13:38:41]	[100%] Built target iodb_read_write_test-mocked
[13:38:41]	Running tests...
[13:38:41]	Test project /home/captain/.conan/data/TagIO/1.0.4/wsbu/stable/build/4df8410dd1e5005908c5e4796a6e566752fe7a41
[13:38:41]	    Start 1: conversion_test-mocked
[13:38:41]	1/9 Test #1: conversion_test-mocked ......................   Passed    0.05 sec
[13:38:41]	    Start 2: iodb_address_ranges_test-mocked
[13:38:42]	2/9 Test #2: iodb_address_ranges_test-mocked .............   Passed    0.05 sec
[13:38:42]	    Start 3: tags_csv_file_change_checking_test-mocked
[13:38:43]	3/9 Test #3: tags_csv_file_change_checking_test-mocked ...***Failed    1.35 sec
[13:38:43]	    Start 4: back_end_utility_functions_test-mocked
[13:38:43]	4/9 Test #4: back_end_utility_functions_test-mocked ......   Passed    0.10 sec
[13:38:43]	    Start 5: iodb_read_write_test-mocked
[13:38:43]	5/9 Test #5: iodb_read_write_test-mocked .................   Passed    0.19 sec
[13:38:43]	    Start 6: overloaded_operators_test-mocked
[13:38:43]	6/9 Test #6: overloaded_operators_test-mocked ............   Passed    0.06 sec
[13:38:43]	    Start 7: tag_management_test-mocked
[13:38:47]	7/9 Test #7: tag_management_test-mocked ..................   Passed    3.86 sec
[13:38:47]	    Start 8: tagio_wrapper_test-mocked
[13:38:47]	8/9 Test #8: tagio_wrapper_test-mocked ...................   Passed    0.09 sec
[13:38:47]	    Start 9: tagio_cli_test-mocked
[13:38:48]	9/9 Test #9: tagio_cli_test-mocked .......................   Passed    0.53 sec
[13:38:48]	
[13:38:48]	89% tests passed, 1 tests failed out of 9
[13:38:48]	
[13:38:48]	Total Test time (real) =   6.28 sec
[13:38:48]	
[13:38:48]	The following tests FAILED:
[13:38:48]		  3 - tags_csv_file_change_checking_test-mocked (Failed)
[13:38:48]	Errors while running CTest
[13:38:48]	make: *** [test] Error 8
[13:38:48]	Makefile:129: recipe for target 'test' failed
[13:38:48]	
[13:38:48]	----Running------
[13:38:48]	> cd '/home/captain/.conan/data/TagIO/1.0.4/wsbu/stable/build/4df8410dd1e5005908c5e4796a6e566752fe7a41' && cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE="Debug" -DCMAKE_TOOLCHAIN_FILE="/opt/toolchain-linaro-armhf.cmake" -DCONAN_EXPORTED="1" -DCONAN_IN_LOCAL_CACHE="ON" -DCONAN_COMPILER="gcc" -DCONAN_COMPILER_VERSION="4.9" -DCONAN_LIBCXX="libstdc++" -DBUILD_SHARED_LIBS="ON" -DCMAKE_INSTALL_PREFIX="/home/captain/.conan/data/TagIO/1.0.4/wsbu/stable/package/4df8410dd1e5005908c5e4796a6e566752fe7a41" -DCMAKE_INSTALL_BINDIR="bin" -DCMAKE_INSTALL_SBINDIR="bin" -DCMAKE_INSTALL_LIBEXECDIR="bin" -DCMAKE_INSTALL_LIBDIR="lib" -DCMAKE_INSTALL_INCLUDEDIR="include" -DCMAKE_INSTALL_OLDINCLUDEDIR="include" -DCMAKE_INSTALL_DATAROOTDIR="share" -DCMAKE_EXPORT_NO_PACKAGE_REGISTRY="ON" -Wno-dev '/home/captain/.conan/data/TagIO/1.0.4/wsbu/stable/build/4df8410dd1e5005908c5e4796a6e566752fe7a41'
[13:38:48]	-----------------
[13:38:48]	
[13:38:48]	----Running------
[13:38:48]	> cmake --build '/home/captain/.conan/data/TagIO/1.0.4/wsbu/stable/build/4df8410dd1e5005908c5e4796a6e566752fe7a41' '--' '-j8'
[13:38:48]	-----------------
[13:38:48]	
[13:38:48]	----Running------
[13:38:48]	> cmake --build '/home/captain/.conan/data/TagIO/1.0.4/wsbu/stable/build/4df8410dd1e5005908c5e4796a6e566752fe7a41' '--target' 'test'
[13:38:48]	-----------------
[13:38:48]	TagIO/1.0.4@wsbu/stable: 
[13:38:48]	TagIO/1.0.4@wsbu/stable: ERROR: Package '4df8410dd1e5005908c5e4796a6e566752fe7a41' build failed
[13:38:48]	TagIO/1.0.4@wsbu/stable: WARN: Build folder /home/captain/.conan/data/TagIO/1.0.4/wsbu/stable/build/4df8410dd1e5005908c5e4796a6e566752fe7a41
[13:38:48]	ERROR: TagIO/1.0.4@wsbu/stable: Error in build() method, line 35
[13:38:48]		cmake.test()
[13:38:48]		ConanException: Error 512 while executing cmake --build '/home/captain/.conan/data/TagIO/1.0.4/wsbu/stable/build/4df8410dd1e5005908c5e4796a6e566752fe7a41' '--target' 'test'
[13:38:48]	Process exited with code 1
```

Discussion:

Patch:
diff --git a/conans/__init__.py b/conans/__init__.py
index e045befcd..704268205 100644
--- a/conans/__init__.py
+++ b/conans/__init__.py
@@ -20,3 +20,4 @@ SERVER_CAPABILITIES = [COMPLEX_SEARCH_CAPABILITY, REVISIONS]  # Server is always
 DEFAULT_REVISION_V1 = "0"
 
 __version__ = '1.15.0-dev'
+
diff --git a/conans/client/command.py b/conans/client/command.py
index 08ce1705a..6de474501 100644
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -208,10 +208,21 @@ class Command(object):
                             action=OnceArgument)
         parser.add_argument("-j", "--json", default=None, action=OnceArgument,
                             help='json output file')
+        parser.add_argument('--raw', default=None, action=OnceArgument,
+                            help='Print just the value of the requested attribute')
 
         args = parser.parse_args(*args)
-        result = self._conan.inspect(args.path_or_reference, args.attribute, args.remote)
-        Printer(self._user_io.out).print_inspect(result)
+
+        if args.raw and args.attribute:
+            raise ConanException("Argument '--raw' is incompatible with '-a'")
+
+        if args.raw and args.json:
+            raise ConanException("Argument '--raw' is incompatible with '--json'")
+
+        attributes = [args.raw, ] if args.raw else args.attribute
+
+        result = self._conan.inspect(args.path_or_reference, attributes, args.remote)
+        Printer(self._user_io.out).print_inspect(result, raw=args.raw)
         if args.json:
             json_output = json.dumps(result)
             if not os.path.isabs(args.json):
@@ -419,6 +430,8 @@ class Command(object):
         parser = argparse.ArgumentParser(description=self.config.__doc__, prog="conan config")
 
         subparsers = parser.add_subparsers(dest='subcommand', help='sub-command help')
+        subparsers.required = True
+
         rm_subparser = subparsers.add_parser('rm', help='Remove an existing config element')
         set_subparser = subparsers.add_parser('set', help='Set a value for a configuration item')
         get_subparser = subparsers.add_parser('get', help='Get the value of configuration item')
@@ -746,12 +759,12 @@ class Command(object):
         return self._conan.imports(args.path, args.import_folder, args.install_folder)
 
     def export_pkg(self, *args):
-        """Exports a recipe, then creates a package from local source and build folders.
+        """
+        Exports a recipe and create a binary package from local files.
 
-        The package is created by calling the package() method applied to the
-        local folders '--source-folder' and '--build-folder' It's created in
-        the local cache for the specified 'reference' and for the specified
-        '--settings', '--options' and or '--profile'.
+        If '--package-folder' is provided it will copy the files from there, otherwise it
+        will execute package() method over '--source-folder' and '--build-folder' to create
+        the binary package.
         """
         parser = argparse.ArgumentParser(description=self.export_pkg.__doc__,
                                          prog="conan export-pkg")
@@ -1192,6 +1205,7 @@ class Command(object):
         """
         parser = argparse.ArgumentParser(description=self.remote.__doc__, prog="conan remote")
         subparsers = parser.add_subparsers(dest='subcommand', help='sub-command help')
+        subparsers.required = True
 
         # create the parser for the "a" command
         parser_list = subparsers.add_parser('list', help='List current remotes')
@@ -1307,6 +1321,7 @@ class Command(object):
         """
         parser = argparse.ArgumentParser(description=self.profile.__doc__, prog="conan profile")
         subparsers = parser.add_subparsers(dest='subcommand')
+        subparsers.required = True
 
         # create the parser for the "profile" command
         subparsers.add_parser('list', help='List current profiles')
@@ -1431,10 +1446,16 @@ class Command(object):
         parser = argparse.ArgumentParser(description=self.workspace.__doc__,
                                          prog="conan workspace")
         subparsers = parser.add_subparsers(dest='subcommand', help='sub-command help')
-
-        install_parser = subparsers.add_parser('install', help='same as a "conan install" command '
-                                               'but using the workspace data from the file')
-        install_parser.add_argument('path', help='path to workspace definition file')
+        subparsers.required = True
+
+        install_parser = subparsers.add_parser('install',
+                                               help='same as a "conan install" command'
+                                                    ' but using the workspace data from the file. If'
+                                                    ' no file is provided, it will look for a file'
+                                                    ' named "conanws.yml"')
+        install_parser.add_argument('path', help='path to workspace definition file (it will look'
+                                                 ' for a "conanws.yml" inside if a directory is'
+                                                 ' given)')
         _add_common_install_arguments(install_parser, build_help=_help_build_policies)
 
         args = parser.parse_args(*args)
@@ -1450,6 +1471,7 @@ class Command(object):
         parser = argparse.ArgumentParser(description=self.editable.__doc__,
                                          prog="conan editable")
         subparsers = parser.add_subparsers(dest='subcommand', help='sub-command help')
+        subparsers.required = True
 
         add_parser = subparsers.add_parser('add', help='Put a package in editable mode')
         add_parser.add_argument('path', help='Path to the package folder in the user workspace')
diff --git a/conans/client/graph/graph_builder.py b/conans/client/graph/graph_builder.py
index b606b1727..7d215b1e4 100644
--- a/conans/client/graph/graph_builder.py
+++ b/conans/client/graph/graph_builder.py
@@ -136,7 +136,8 @@ class DepsGraphBuilder(object):
                                  % (node.ref, require.ref))
 
         previous = node.public_deps.get(name)
-        if require.private or require.build_require or not previous:
+        previous_closure = node.public_closure.get(name)
+        if not previous or ((require.build_require or require.private) and not previous_closure):
             # new node, must be added and expanded
             # node -> new_node
             new_node = self._create_new_node(node, dep_graph, require, name,
diff --git a/conans/client/migrations.py b/conans/client/migrations.py
index c9590df2b..db5a91efb 100644
--- a/conans/client/migrations.py
+++ b/conans/client/migrations.py
@@ -52,11 +52,25 @@ class ClientMigrator(Migrator):
         if old_version is None:
             return
 
-        if old_version < Version("1.15.0"):
-            migrate_registry_file(self.cache, self.out)
+        if old_version < Version("0.25"):
+            from conans.paths import DEFAULT_PROFILE_NAME
+            default_profile_path = os.path.join(self.cache.conan_folder, PROFILES_FOLDER,
+                                                DEFAULT_PROFILE_NAME)
+            if not os.path.exists(default_profile_path):
+                self.out.warn("Migration: Moving default settings from %s file to %s"
+                              % (CONAN_CONF, DEFAULT_PROFILE_NAME))
+                conf_path = os.path.join(self.cache.conan_folder, CONAN_CONF)
 
-        if old_version < Version("1.14.0"):
-            migrate_config_install(self.cache)
+                migrate_to_default_profile(conf_path, default_profile_path)
+
+                self.out.warn("Migration: export_source cache new layout")
+                migrate_c_src_export_source(self.cache, self.out)
+
+        if old_version < Version("1.0"):
+            _migrate_lock_files(self.cache, self.out)
+
+        if old_version < Version("1.12.0"):
+            migrate_plugins_to_hooks(self.cache)
 
         if old_version < Version("1.13.0"):
             old_settings = """
@@ -131,25 +145,14 @@ cppstd: [None, 98, gnu98, 11, gnu11, 14, gnu14, 17, gnu17, 20, gnu20]
             # MIGRATE LOCAL CACHE TO GENERATE MISSING METADATA.json
             _migrate_create_metadata(self.cache, self.out)
 
-        if old_version < Version("1.12.0"):
-            migrate_plugins_to_hooks(self.cache)
-
-        if old_version < Version("1.0"):
-            _migrate_lock_files(self.cache, self.out)
-
-        if old_version < Version("0.25"):
-            from conans.paths import DEFAULT_PROFILE_NAME
-            default_profile_path = os.path.join(self.cache.conan_folder, PROFILES_FOLDER,
-                                                DEFAULT_PROFILE_NAME)
-            if not os.path.exists(default_profile_path):
-                self.out.warn("Migration: Moving default settings from %s file to %s"
-                              % (CONAN_CONF, DEFAULT_PROFILE_NAME))
-                conf_path = os.path.join(self.cache.conan_folder, CONAN_CONF)
+        if old_version < Version("1.14.0"):
+            migrate_config_install(self.cache)
 
-                migrate_to_default_profile(conf_path, default_profile_path)
+        if old_version < Version("1.14.2"):
+            _migrate_full_metadata(self.cache, self.out)
 
-                self.out.warn("Migration: export_source cache new layout")
-                migrate_c_src_export_source(self.cache, self.out)
+        if old_version < Version("1.15.0"):
+            migrate_registry_file(self.cache, self.out)
 
 
 def _get_refs(cache):
@@ -163,6 +166,50 @@ def _get_prefs(layout):
     return [PackageReference(layout.ref, s) for s in folders]
 
 
+def _migrate_full_metadata(cache, out):
+    # Fix for https://github.com/conan-io/conan/issues/4898
+    out.warn("Running a full revision metadata migration")
+    refs = _get_refs(cache)
+    for ref in refs:
+        try:
+            base_folder = os.path.normpath(os.path.join(cache.store, ref.dir_repr()))
+            layout = PackageCacheLayout(base_folder=base_folder, ref=ref, short_paths=None,
+                                        no_lock=True)
+            with layout.update_metadata() as metadata:
+                # Updating the RREV
+                if metadata.recipe.revision is None:
+                    out.warn("Package %s metadata had recipe revision None, migrating" % str(ref))
+                    folder = layout.export()
+                    try:
+                        manifest = FileTreeManifest.load(folder)
+                        rrev = manifest.summary_hash
+                    except Exception:
+                        rrev = DEFAULT_REVISION_V1
+                    metadata.recipe.revision = rrev
+
+                prefs = _get_prefs(layout)
+                existing_ids = [pref.id for pref in prefs]
+                for pkg_id in list(metadata.packages.keys()):
+                    if pkg_id not in existing_ids:
+                        out.warn("Package %s metadata had stalled package information %s, removing"
+                                 % (str(ref), pkg_id))
+                        del metadata.packages[pkg_id]
+                # UPDATING PREVS
+                for pref in prefs:
+                    try:
+                        pmanifest = FileTreeManifest.load(layout.package(pref))
+                        prev = pmanifest.summary_hash
+                    except Exception:
+                        prev = DEFAULT_REVISION_V1
+                    metadata.packages[pref.id].revision = prev
+                    metadata.packages[pref.id].recipe_revision = metadata.recipe.revision
+
+        except Exception as e:
+            raise ConanException("Something went wrong while migrating metadata.json files "
+                                 "in the cache, please try to fix the issue or wipe the cache: {}"
+                                 ":{}".format(ref, e))
+
+
 def _migrate_create_metadata(cache, out):
     out.warn("Migration: Generating missing metadata files")
     refs = _get_refs(cache)
@@ -178,7 +225,7 @@ def _migrate_create_metadata(cache, out):
             try:
                 manifest = FileTreeManifest.load(folder)
                 rrev = manifest.summary_hash
-            except:
+            except Exception:
                 rrev = DEFAULT_REVISION_V1
             metadata_path = layout.package_metadata()
             if not os.path.exists(metadata_path):
@@ -190,7 +237,7 @@ def _migrate_create_metadata(cache, out):
                     try:
                         pmanifest = FileTreeManifest.load(layout.package(pref))
                         prev = pmanifest.summary_hash
-                    except:
+                    except Exception:
                         prev = DEFAULT_REVISION_V1
                     metadata.packages[pref.id].revision = prev
                     metadata.packages[pref.id].recipe_revision = metadata.recipe.revision
diff --git a/conans/client/printer.py b/conans/client/printer.py
index e1ab99f00..9f2a17a7d 100644
--- a/conans/client/printer.py
+++ b/conans/client/printer.py
@@ -20,7 +20,7 @@ class Printer(object):
     def __init__(self, out):
         self._out = out
 
-    def print_inspect(self, inspect):
+    def print_inspect(self, inspect, raw=False):
         for k, v in inspect.items():
             if k == "default_options":
                 if isinstance(v, str):
@@ -29,12 +29,18 @@ class Printer(object):
                     v = OptionsValues(v)
                 elif isinstance(v, list):
                     v = OptionsValues(tuple(v))
-            if isinstance(v, (dict, OptionsValues)):
-                self._out.writeln("%s:" % k)
-                for ok, ov in sorted(v.items()):
-                    self._out.writeln("    %s: %s" % (ok, ov))
+                elif isinstance(v, dict):
+                    v = OptionsValues(v)
+
+            if raw:
+                self._out.write(str(v))
             else:
-                self._out.writeln("%s: %s" % (k, str(v)))
+                if isinstance(v, (dict, OptionsValues)):
+                    self._out.writeln("%s:" % k)
+                    for ok, ov in sorted(v.items()):
+                        self._out.writeln("    %s: %s" % (ok, ov))
+                else:
+                    self._out.writeln("%s: %s" % (k, str(v)))
 
     def print_info(self, data, _info, package_filter=None, show_paths=False, show_revisions=False):
         """ Print in console the dependency information for a conan file
diff --git a/conans/client/remover.py b/conans/client/remover.py
index b25fe740c..de6ca8675 100644
--- a/conans/client/remover.py
+++ b/conans/client/remover.py
@@ -101,8 +101,8 @@ class ConanRemover(object):
 
     @staticmethod
     def _message_removing_editable(ref):
-        return "Package '{r}' is installed as editable, unlink it first using " \
-               "command 'conan link {r} --remove'".format(r=ref)
+        return "Package '{r}' is installed as editable, remove it first using " \
+               "command 'conan editable remove {r}'".format(r=ref)
 
     def _local_remove(self, ref, src, build_ids, package_ids):
         if self._cache.installed_as_editable(ref):
diff --git a/conans/client/runner.py b/conans/client/runner.py
index 17ee54c5a..e063e497f 100644
--- a/conans/client/runner.py
+++ b/conans/client/runner.py
@@ -11,6 +11,15 @@ from conans.unicode import get_cwd
 from conans.util.files import decode_text
 
 
+class _UnbufferedWrite(object):
+    def __init__(self, stream):
+        self._stream = stream
+
+    def write(self, *args, **kwargs):
+        self._stream.write(*args, **kwargs)
+        self._stream.flush()
+
+
 class ConanRunner(object):
 
     def __init__(self, print_commands_to_output=False, generate_run_log_file=False,
@@ -32,6 +41,9 @@ class ConanRunner(object):
                   "use six.StringIO() instead ***")
 
         stream_output = output if output and hasattr(output, "write") else sys.stdout
+        if hasattr(output, "flush"):
+            # We do not want output from different streams to get mixed (sys.stdout, os.system)
+            stream_output = _UnbufferedWrite(stream_output)
 
         if not self._generate_run_log_file:
             log_filepath = None
diff --git a/conans/model/conan_generator.py b/conans/model/conan_generator.py
index fc882838e..f60b48d69 100644
--- a/conans/model/conan_generator.py
+++ b/conans/model/conan_generator.py
@@ -1,8 +1,10 @@
 from abc import ABCMeta, abstractproperty
 
+import six
 
+
+@six.add_metaclass(ABCMeta)
 class Generator(object):
-    __metaclass__ = ABCMeta
 
     def __init__(self, conanfile):
         self.conanfile = conanfile
diff --git a/conans/model/workspace.py b/conans/model/workspace.py
index e8d9762cf..3703d0012 100644
--- a/conans/model/workspace.py
+++ b/conans/model/workspace.py
@@ -39,6 +39,7 @@ class LocalPackage(object):
 
 
 class Workspace(object):
+    default_filename = "conanws.yml"
 
     def generate(self, cwd, graph, output):
         if self._ws_generator == "cmake":
@@ -90,6 +91,10 @@ class Workspace(object):
         self._cache = cache
         self._ws_generator = None
         self._workspace_packages = OrderedDict()  # {reference: LocalPackage}
+
+        if not os.path.isfile(path):
+            path = os.path.join(path, self.default_filename)
+
         self._base_folder = os.path.dirname(path)
         try:
             content = load(path)
diff --git a/conans/requirements.txt b/conans/requirements.txt
index a1c3a2632..5cbdf6d92 100644
--- a/conans/requirements.txt
+++ b/conans/requirements.txt
@@ -1,7 +1,7 @@
 PyJWT>=1.4.0, <2.0.0
 requests>=2.7.0, <3.0.0
 colorama>=0.3.3, <0.5.0
-PyYAML>=3.11, <3.14.0
+PyYAML>=3.11, <6.0
 patch==1.16
 fasteners>=0.14.1
 six>=1.10.0
diff --git a/conans/server/rest/bottle_plugins/authorization_header.py b/conans/server/rest/bottle_plugins/authorization_header.py
index 7ee988023..ba2b4f180 100644
--- a/conans/server/rest/bottle_plugins/authorization_header.py
+++ b/conans/server/rest/bottle_plugins/authorization_header.py
@@ -7,10 +7,10 @@ from bottle import PluginError, request
 from conans.util.log import logger
 
 
+@six.add_metaclass(ABCMeta)
 class AuthorizationHeader(object):
     """ Generic plugin to handle Authorization header. Must be extended and implement
     some abstract methods in subclasses """
-    __metaclass__ = ABCMeta
 
     name = 'authorizationheader'
     api = 2
diff --git a/conans/server/service/authorize.py b/conans/server/service/authorize.py
index b447e29ab..1456133d1 100644
--- a/conans/server/service/authorize.py
+++ b/conans/server/service/authorize.py
@@ -9,9 +9,10 @@ Replace this module with other that keeps the interface or super class.
 
 '''
 
-
 from abc import ABCMeta, abstractmethod
 
+import six
+
 from conans.errors import AuthenticationException, ForbiddenException, InternalErrorException
 from conans.model.ref import ConanFileReference
 
@@ -20,12 +21,11 @@ from conans.model.ref import ConanFileReference
 #  ############ ABSTRACT CLASSES ##############
 #  ############################################
 
-
+@six.add_metaclass(ABCMeta)
 class Authorizer(object):
     """
     Handles the access permissions to conans and packages
     """
-    __metaclass__ = ABCMeta
 
     @abstractmethod
     def check_read_conan(self, username, ref):
@@ -60,12 +60,11 @@ class Authorizer(object):
         raise NotImplemented()
 
 
+@six.add_metaclass(ABCMeta)
 class Authenticator(object):
     """
     Handles the user authentication
     """
-    __metaclass__ = ABCMeta
-
     @abstractmethod
     def valid_user(self, username, plain_password):
         """


Test patch:
diff --git a/conans/test/functional/command/config_test.py b/conans/test/functional/command/config_test.py
index d4534fb8c..1b92932d4 100644
--- a/conans/test/functional/command/config_test.py
+++ b/conans/test/functional/command/config_test.py
@@ -84,3 +84,7 @@ class ConfigTest(unittest.TestCase):
         self.client.run('config rm env.MY_VAR')
         conf_file = load(self.client.cache.conan_conf_path)
         self.assertNotIn('MY_VAR', conf_file)
+
+    def missing_subarguments_test(self):
+        self.client.run("config", assert_error=True)
+        self.assertIn("ERROR: Exiting with code: 2", self.client.out)
diff --git a/conans/test/functional/command/inspect_test.py b/conans/test/functional/command/inspect_test.py
index e43f61f3e..4dd3edf34 100644
--- a/conans/test/functional/command/inspect_test.py
+++ b/conans/test/functional/command/inspect_test.py
@@ -338,3 +338,84 @@ default_options:
     bar: True
     foo: True
 """, client.out)
+
+
+class InspectRawTest(unittest.TestCase):
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile
+        
+        class Pkg(ConanFile):
+            name = "MyPkg"
+            version = "1.2.3"
+            author = "John Doe"
+            url = "https://john.doe.com"
+            settings = "os", "arch", "build_type", "compiler"
+            options = {"foo": [True, False], "bar": [True, False]}
+            default_options = "foo=True", "bar=True"
+            
+            _private = "Nothing"
+
+            def build(self):
+                pass
+        """)
+
+    def test_no_field_or_multiple(self):
+        client = TestClient()
+        client.save({"conanfile.py": self.conanfile})
+        client.run("inspect . --raw", assert_error=True)
+        client.run("inspect . --raw=name --raw=version", assert_error=True)
+
+    def test_incompatible_commands(self):
+        client = TestClient()
+        client.save({"conanfile.py": self.conanfile})
+
+        client.run("inspect . --raw=name -a=version", assert_error=True)
+        self.assertIn("ERROR: Argument '--raw' is incompatible with '-a'", client.out)
+
+        client.run("inspect . --raw=name --json=tmp.json", assert_error=True)
+        self.assertIn("ERROR: Argument '--raw' is incompatible with '--json'", client.out)
+
+    def test_invalid_field(self):
+        client = TestClient()
+        client.save({"conanfile.py": self.conanfile})
+        client.run("inspect . --raw=not_exists", assert_error=True)
+        self.assertIn("ERROR: 'Pkg' object has no attribute 'not_exists'", client.out)
+
+    def test_private_field(self):
+        client = TestClient()
+        client.save({"conanfile.py": self.conanfile})
+        client.run("inspect . --raw=_private")
+        self.assertEqual("Nothing", client.out)
+
+    def test_settings(self):
+        client = TestClient()
+        client.save({"conanfile.py": self.conanfile})
+        client.run("inspect . --raw=settings")
+        self.assertEqual("('os', 'arch', 'build_type', 'compiler')", client.out)
+
+    def test_options_dict(self):
+        client = TestClient()
+        client.save({"conanfile.py": self.conanfile})
+        client.run("inspect . --raw=options")
+        # The output is a dictionary, no order guaranteed, we need to compare as dict
+        import ast
+        output = ast.literal_eval(str(client.out))
+        self.assertDictEqual(output, {'bar': [True, False], 'foo': [True, False]})
+
+    def test_default_options_list(self):
+        client = TestClient()
+        client.save({"conanfile.py": self.conanfile})
+        client.run("inspect . --raw=default_options")
+        self.assertEqual("bar=True\nfoo=True", client.out)
+
+    def test_default_options_dict(self):
+        client = TestClient()
+        conanfile = textwrap.dedent("""
+            from conans import ConanFile
+            
+            class Lib(ConanFile):
+                default_options = {"dict": True, "list": False}
+            """)
+        client.save({"conanfile.py": conanfile})
+        client.run("inspect . --raw=default_options")
+        self.assertEqual("dict=True\nlist=False", client.out)
diff --git a/conans/test/functional/command/profile_test.py b/conans/test/functional/command/profile_test.py
index 74eb2da8b..24690875c 100644
--- a/conans/test/functional/command/profile_test.py
+++ b/conans/test/functional/command/profile_test.py
@@ -216,3 +216,8 @@ class ProfileTest(unittest.TestCase):
         self.assertNotIn("\nos=FakeOS", load(pr_path))
         self.assertNotIn("[env]\nMyEnv=MYVALUe", load(pr_path))
         self.assertEqual(load(pr_path), detected_profile)
+
+    def missing_subarguments_test(self):
+        client = TestClient()
+        client.run("profile", assert_error=True)
+        self.assertIn("ERROR: Exiting with code: 2", client.out)
diff --git a/conans/test/functional/command/remote_test.py b/conans/test/functional/command/remote_test.py
index 5f1d8d6a1..dc138bad5 100644
--- a/conans/test/functional/command/remote_test.py
+++ b/conans/test/functional/command/remote_test.py
@@ -364,3 +364,7 @@ class HelloConan(ConanFile):
         self.client.run("remote list_pref Hello1/0.1@user/testing")
         self.assertIn("Hello1/0.1@user/testing:555: remote2", self.client.user_io.out)
         self.assertIn("Hello1/0.1@user/testing:666: remote1", self.client.user_io.out)
+
+    def missing_subarguments_test(self):
+        self.client.run("remote", assert_error=True)
+        self.assertIn("ERROR: Exiting with code: 2", self.client.out)
diff --git a/conans/test/functional/editable/editable_add_test.py b/conans/test/functional/editable/editable_add_test.py
index 9c6b16722..0d3026212 100644
--- a/conans/test/functional/editable/editable_add_test.py
+++ b/conans/test/functional/editable/editable_add_test.py
@@ -63,3 +63,8 @@ class CreateEditablePackageTest(unittest.TestCase):
         t.run('editable add . wrong/version@user/channel', assert_error=True)
         self.assertIn("ERROR: Name and version from reference (wrong/version@user/channel) and "
                       "target conanfile.py (lib/version) must match", t.out)
+
+    def missing_subarguments_test(self):
+        t = TestClient()
+        t.run("editable", assert_error=True)
+        self.assertIn("ERROR: Exiting with code: 2", t.out)
diff --git a/conans/test/functional/editable/forbidden_commands_test.py b/conans/test/functional/editable/forbidden_commands_test.py
index e2d9e503a..1d96c1e1b 100644
--- a/conans/test/functional/editable/forbidden_commands_test.py
+++ b/conans/test/functional/editable/forbidden_commands_test.py
@@ -24,13 +24,13 @@ class ForbiddenRemoveTest(unittest.TestCase):
         t.run('editable add . {}'.format(ref))
         self.assertTrue(t.cache.installed_as_editable(ref))
         t.run('remove {} --force'.format(ref), assert_error=True)
-        self.assertIn("Package 'lib/version@user/name' is installed as editable, unlink it first "
-                      "using command 'conan link lib/version@user/name --remove'", t.out)
+        self.assertIn("Package 'lib/version@user/name' is installed as editable, remove it first "
+                      "using command 'conan editable remove lib/version@user/name'", t.out)
 
         # Also with a pattern, but only a warning
         t.run('remove lib* --force')
         self.assertIn("WARN: Package 'lib/version@user/name' is installed as editable, "
-                      "unlink it first using command 'conan link lib/version@user/name --remove'",
+                      "remove it first using command 'conan editable remove lib/version@user/name'",
                       t.out)
         self.assertTrue(t.cache.installed_as_editable(ref))
 
diff --git a/conans/test/functional/generators/cmake_find_package_multi_test.py b/conans/test/functional/generators/cmake_find_package_multi_test.py
index d6318ebdc..f3a4373d9 100644
--- a/conans/test/functional/generators/cmake_find_package_multi_test.py
+++ b/conans/test/functional/generators/cmake_find_package_multi_test.py
@@ -6,7 +6,6 @@ import unittest
 from nose.plugins.attrib import attr
 
 from conans.test.utils.tools import TestClient
-from conans.util.files import mkdir
 
 
 @attr('slow')
@@ -25,30 +24,28 @@ class CMakeFindPathMultiGeneratorTest(unittest.TestCase):
 
         # Create packages for hello and bye
         for p in ("hello", "bye"):
-            with c.chdir(p):
-                for bt in ("Debug", "Release"):
-                    c.run("create . user/channel -s build_type={}".format(bt))
+            for bt in ("Debug", "Release"):
+                c.run("create {} user/channel -s build_type={}".format(p, bt))
 
         with c.chdir(project_folder_name):
             # Save conanfile and example
             conanfile = textwrap.dedent("""
                 [requires]
                 bye/1.0@user/channel
-                
+
                 [generators]
                 cmake_find_package_multi
                 """)
             example_cpp = textwrap.dedent("""
                 #include <iostream>
                 #include "bye.h"
-                
+
                 int main() {
                     bye();
                 }
                 """)
             c.save({"conanfile.txt": conanfile, "example.cpp": example_cpp})
 
-            mkdir("./build")
             with c.chdir("build"):
                 for bt in ("Debug", "Release"):
                     c.run("install .. user/channel -s build_type={}".format(bt))
diff --git a/conans/test/functional/graph/graph_manager_test.py b/conans/test/functional/graph/graph_manager_test.py
index 821394aa5..ed24953ec 100644
--- a/conans/test/functional/graph/graph_manager_test.py
+++ b/conans/test/functional/graph/graph_manager_test.py
@@ -665,3 +665,58 @@ class TransitiveGraphTest(GraphManagerTest):
 
         self._check_node(zlib, "zlib/0.1@user/testing#123", deps=[], build_deps=[],
                          dependents=[tool], closure=[], public_deps=[zlib])
+
+    def test_transitive_private_conflict(self):
+        # https://github.com/conan-io/conan/issues/4931
+        # cheetah -> gazelle -> grass
+        #    \--(private)------0.2----/
+        self._cache_recipe("grass/0.1@user/testing", TestConanFile("grass", "0.1"))
+        self._cache_recipe("grass/0.2@user/testing", TestConanFile("grass", "0.2"))
+        self._cache_recipe("gazelle/0.1@user/testing",
+                           TestConanFile("gazelle", "0.1",
+                                         requires=["grass/0.1@user/testing"]))
+
+        with six.assertRaisesRegex(self, ConanException,
+                                   "Requirement grass/0.2@user/testing conflicts"):
+            self.build_graph(TestConanFile("cheetah", "0.1", requires=["gazelle/0.1@user/testing"],
+                                           private_requires=["grass/0.2@user/testing"]))
+
+    def test_build_require_conflict(self):
+        # https://github.com/conan-io/conan/issues/4931
+        # cheetah -> gazelle -> grass
+        #    \--(br)------0.2----/
+        self._cache_recipe("grass/0.1@user/testing", TestConanFile("grass", "0.1"))
+        self._cache_recipe("grass/0.2@user/testing", TestConanFile("grass", "0.2"))
+        self._cache_recipe("gazelle/0.1@user/testing",
+                           TestConanFile("gazelle", "0.1",
+                                         requires=["grass/0.1@user/testing"]))
+
+        with six.assertRaisesRegex(self, ConanException,
+                                   "Requirement grass/0.2@user/testing conflicts"):
+            self.build_graph(TestConanFile("cheetah", "0.1", requires=["gazelle/0.1@user/testing"],
+                                           build_requires=["grass/0.2@user/testing"]))
+
+    def test_build_require_link_order(self):
+        # https://github.com/conan-io/conan/issues/4931
+        # cheetah -> gazelle -> grass
+        #    \--(br)------------/
+        self._cache_recipe("grass/0.1@user/testing", TestConanFile("grass", "0.1"))
+        self._cache_recipe("gazelle/0.1@user/testing",
+                           TestConanFile("gazelle", "0.1",
+                                         requires=["grass/0.1@user/testing"]))
+
+        deps_graph = self.build_graph(TestConanFile("cheetah", "0.1",
+                                                    requires=["gazelle/0.1@user/testing"],
+                                                    build_requires=["grass/0.1@user/testing"]))
+
+        self.assertEqual(3, len(deps_graph.nodes))
+        cheetah = deps_graph.root
+        gazelle = cheetah.dependencies[0].dst
+        grass = gazelle.dependencies[0].dst
+        grass2 = cheetah.dependencies[1].dst
+
+        self._check_node(cheetah, "cheetah/0.1@None/None", deps=[gazelle], build_deps=[grass2],
+                         dependents=[], closure=[gazelle, grass],
+                         public_deps=[cheetah, gazelle, grass])
+        self.assertEqual(cheetah.conanfile.deps_cpp_info.libs,
+                         ['mylibgazelle0.1lib', 'mylibgrass0.1lib'])
diff --git a/conans/test/functional/old/test_migrations.py b/conans/test/functional/old/test_migrations.py
index 4f048c6ae..bbb08d950 100644
--- a/conans/test/functional/old/test_migrations.py
+++ b/conans/test/functional/old/test_migrations.py
@@ -1,3 +1,4 @@
+import json
 import os
 import unittest
 
@@ -7,6 +8,8 @@ from conans import __version__
 from conans.client.migrations import migrate_plugins_to_hooks, migrate_to_default_profile
 from conans.client.output import ConanOutput
 from conans.migrations import CONAN_VERSION
+from conans.model.ref import ConanFileReference
+from conans.test.utils.conanfile import TestConanFile
 from conans.test.utils.test_files import temp_folder
 from conans.test.utils.tools import TestClient
 from conans.util.files import load, save
@@ -14,6 +17,42 @@ from conans.util.files import load, save
 
 class TestMigrations(unittest.TestCase):
 
+    def test_migrate_revision_metadata(self):
+        # https://github.com/conan-io/conan/issues/4898
+        client = TestClient()
+        client.save({"conanfile.py": str(TestConanFile("Hello", "0.1"))})
+        client.run("create . user/testing")
+        ref = ConanFileReference.loads("Hello/0.1@user/testing")
+        layout1 = client.cache.package_layout(ref)
+        metadata = json.loads(load(layout1.package_metadata()))
+        metadata["recipe"]["revision"] = None
+        metadata["packages"]["WRONG"] = {"revision": ""}
+        metadata["packages"]["5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9"]["revision"] = None
+        metadata["packages"]["5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9"]["recipe_revision"] = None
+        save(layout1.package_metadata(), json.dumps(metadata))
+
+        client.run("create . user/stable")
+        ref2 = ConanFileReference.loads("Hello/0.1@user/stable")
+        layout2 = client.cache.package_layout(ref2)
+        metadata = json.loads(load(layout2.package_metadata()))
+        metadata["recipe"]["revision"] = "Other"
+        save(layout2.package_metadata(), json.dumps(metadata))
+
+        version_file = os.path.join(client.cache.conan_folder, CONAN_VERSION)
+        save(version_file, "1.14.1")
+        client.run("search")  # This will fire a migration
+
+        metadata_ref1 = client.cache.package_layout(ref).load_metadata()
+        self.assertEqual(metadata_ref1.recipe.revision, "2e48797069e65568befd81854aa8aaf0")
+        pkg_metadata = metadata_ref1.packages["5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9"]
+        self.assertEqual(pkg_metadata.recipe_revision, "2e48797069e65568befd81854aa8aaf0")
+        self.assertEqual(pkg_metadata.revision, "38d05b42210ca2becb43a05f9265abbe")
+        self.assertEqual(list(metadata_ref1.packages.keys()),
+                         ["5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9"])
+
+        metadata_ref2 = client.cache.package_layout(ref2).load_metadata()
+        self.assertEqual(metadata_ref2.recipe.revision, "Other")
+
     def test_migrate_config_install(self):
         client = TestClient()
         client.run('config set general.config_install="url, http:/fake.url, None, None"')
diff --git a/conans/test/functional/workspace/workspace_test.py b/conans/test/functional/workspace/workspace_test.py
index 0373e6ee6..6764e7313 100644
--- a/conans/test/functional/workspace/workspace_test.py
+++ b/conans/test/functional/workspace/workspace_test.py
@@ -924,3 +924,60 @@ class Pkg(ConanFile):
         for p in ("HelloC", "HelloB", "HelloA"):
             self.assertIn("add_subdirectory(${PACKAGE_%s_SRC} ${PACKAGE_%s_BUILD})" % (p, p),
                           conanws_cmake)
+
+    def test_default_filename(self):
+        client = TestClient()
+        path_to_editable = os.path.join(client.current_folder, "A")
+
+        project = dedent("""
+            editables:
+                HelloA/0.1@lasote/stable:
+                    path: {path_to_editable}
+            layout: layout
+            workspace_generator: cmake
+            root: HelloA/0.1@lasote/stable
+            """.format(path_to_editable=path_to_editable))
+
+        conanfile = dedent("""
+            from conans import ConanFile
+
+            class Lib(ConanFile):
+                pass
+            """)
+
+        layout = dedent("""
+            [build_folder]
+            [source_folder]
+            """)
+
+        client.save({"conanfile.py": conanfile}, path=path_to_editable)
+        ws_folder = temp_folder()
+        client.save({os.path.join(ws_folder, Workspace.default_filename): project,
+                     os.path.join(ws_folder, "layout"): layout})
+
+        # For the right folder, it works
+        client.run('workspace install "{}"'.format(ws_folder))
+        conanws_cmake = load(os.path.join(client.current_folder, "conanworkspace.cmake"))
+        self.assertIn("macro(conan_workspace_subdirectories)", conanws_cmake)
+
+        # For a non existing folder, it will try to load the default filename (it fails)
+        non_existing = temp_folder()
+        client.run('workspace install "{}"'.format(non_existing), assert_error=True)
+        trial_path = os.path.join(non_existing, Workspace.default_filename)
+        self.assertIn("ERROR: Couldn't load workspace file in {}".format(trial_path), client.out)
+
+        # For an existing file, it will try to use it (will fail because of the format)
+        invalid_file = os.path.join(ws_folder, "layout")
+        client.run('workspace install "{}"'.format(invalid_file), assert_error=True)
+        self.assertIn("ERROR: There was an error parsing", client.out)
+
+        # For an existing folder, without the default file (it will fail looking for it)
+        no_default_file = os.path.join(client.current_folder)
+        client.run('workspace install "{}"'.format(no_default_file), assert_error=True)
+        trial_path = os.path.join(no_default_file, Workspace.default_filename)
+        self.assertIn("ERROR: Couldn't load workspace file in {}".format(trial_path), client.out)
+
+    def missing_subarguments_test(self):
+        client = TestClient()
+        client.run("workspace", assert_error=True)
+        self.assertIn("ERROR: Exiting with code: 2", client.out)