Repository: conan-io/conan

Issue:
Bug in revisions: Create json output doesn't return the revision
From a server with revisions, I install a package that is stored with revisions using the json output:

`conan install <ref> -r remote --json file.json`

Apparently, the "id" field of the json (recipe and packages) doesn't contain the revision, and it should.

Discussion:

Patch:
diff --git a/conans/client/cmd/export.py b/conans/client/cmd/export.py
index 3bdfd1a37..89c2efb0e 100644
--- a/conans/client/cmd/export.py
+++ b/conans/client/cmd/export.py
@@ -93,11 +93,11 @@ def cmd_export(package_layout, conanfile_path, conanfile, keep_source, revisions
         digest.save(package_layout.export())
 
     # Compute the revision for the recipe
-    _update_revision_in_metadata(package_layout=package_layout,
-                                 revisions_enabled=revisions_enabled,
-                                 output=output,
-                                 path=os.path.dirname(conanfile_path),
-                                 digest=digest)
+    revision = _update_revision_in_metadata(package_layout=package_layout,
+                                            revisions_enabled=revisions_enabled,
+                                            output=output,
+                                            path=os.path.dirname(conanfile_path),
+                                            digest=digest)
 
     # FIXME: Conan 2.0 Clear the registry entry if the recipe has changed
     source_folder = package_layout.source()
@@ -128,6 +128,8 @@ def cmd_export(package_layout, conanfile_path, conanfile, keep_source, revisions
             remover = DiskRemover()
             remover.remove_packages(package_layout, ids_filter=to_remove)
 
+    return package_layout.ref.copy_with_rev(revision)
+
 
 def _capture_export_scm_data(conanfile, conanfile_dir, destination_folder, output, scm_src_file):
 
@@ -251,6 +253,8 @@ def _update_revision_in_metadata(package_layout, revisions_enabled, output, path
     with package_layout.update_metadata() as metadata:
         metadata.recipe.revision = revision
 
+    return revision
+
 
 def _recreate_folders(destination_folder, destination_src_folder):
     try:
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
index d774c564b..46f5ba8db 100644
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -363,11 +363,11 @@ class ConanAPIV1(object):
             if not not_export:
                 check_casing_conflict(cache=self._cache, ref=ref)
                 package_layout = self._cache.package_layout(ref, short_paths=conanfile.short_paths)
-                cmd_export(package_layout, conanfile_path, conanfile, keep_source,
-                           self._cache.config.revisions_enabled, self._user_io.out,
-                           self._hook_manager)
-
-                recorder.recipe_exported(ref)
+                new_ref = cmd_export(package_layout, conanfile_path, conanfile, keep_source,
+                                     self._cache.config.revisions_enabled, self._user_io.out,
+                                     self._hook_manager)
+                # The new_ref contains the revision
+                recorder.recipe_exported(new_ref)
 
             if build_modes is None:  # Not specified, force build the tested library
                 build_modes = [conanfile.name]
@@ -384,11 +384,11 @@ class ConanAPIV1(object):
                    manifest_folder, manifest_verify, manifest_interactive, keep_build,
                    test_build_folder, test_folder, conanfile_path)
 
-            return recorder.get_info()
+            return recorder.get_info(self._cache.config.revisions_enabled)
 
         except ConanException as exc:
             recorder.error = True
-            exc.info = recorder.get_info()
+            exc.info = recorder.get_info(self._cache.config.revisions_enabled)
             raise
 
     @api_method
@@ -428,22 +428,24 @@ class ConanAPIV1(object):
             conanfile = self._loader.load_export(conanfile_path, name, version, user, channel)
             ref = ConanFileReference(conanfile.name, conanfile.version, user, channel)
 
-            recorder.recipe_exported(ref)
-            recorder.add_recipe_being_developed(ref)
             check_casing_conflict(cache=self._cache, ref=ref)
             package_layout = self._cache.package_layout(ref, short_paths=conanfile.short_paths)
-            cmd_export(package_layout, conanfile_path, conanfile, False,
-                       self._cache.config.revisions_enabled, self._user_io.out,
-                       self._hook_manager)
+            new_ref = cmd_export(package_layout, conanfile_path, conanfile, False,
+                                 self._cache.config.revisions_enabled, self._user_io.out,
+                                 self._hook_manager)
+            # new_ref has revision
+            recorder.recipe_exported(new_ref)
+            recorder.add_recipe_being_developed(ref)
+
             export_pkg(self._cache, self._graph_manager, self._hook_manager, recorder,
                        self._user_io.out,
                        ref, source_folder=source_folder, build_folder=build_folder,
                        package_folder=package_folder, install_folder=install_folder,
                        graph_info=graph_info, force=force)
-            return recorder.get_info()
+            return recorder.get_info(self._cache.config.revisions_enabled)
         except ConanException as exc:
             recorder.error = True
-            exc.info = recorder.get_info()
+            exc.info = recorder.get_info(self._cache.config.revisions_enabled)
             raise
 
     @api_method
@@ -531,10 +533,10 @@ class ConanAPIV1(object):
                             manifest_verify=manifest_verify,
                             manifest_interactive=manifest_interactive,
                             generators=generators)
-            return recorder.get_info()
+            return recorder.get_info(self._cache.config.revisions_enabled)
         except ConanException as exc:
             recorder.error = True
-            exc.info = recorder.get_info()
+            exc.info = recorder.get_info(self._cache.config.revisions_enabled)
             raise
 
     @api_method
@@ -568,10 +570,10 @@ class ConanAPIV1(object):
                             manifest_interactive=manifest_interactive,
                             generators=generators,
                             no_imports=no_imports)
-            return recorder.get_info()
+            return recorder.get_info(self._cache.config.revisions_enabled)
         except ConanException as exc:
             recorder.error = True
-            exc.info = recorder.get_info()
+            exc.info = recorder.get_info(self._cache.config.revisions_enabled)
             raise
 
     @api_method
diff --git a/conans/client/conf/__init__.py b/conans/client/conf/__init__.py
index 35ca1392e..cab97be9d 100644
--- a/conans/client/conf/__init__.py
+++ b/conans/client/conf/__init__.py
@@ -351,6 +351,13 @@ class ConanClientConfigParser(ConfigParser, object):
         except ConanException:
             return False
 
+    @property
+    def default_package_id_mode(self):
+        try:
+            return self.get_item("general.default_package_id_mode")
+        except ConanException:
+            return "semver_direct_mode"
+
     @property
     def storage_path(self):
         # Try with CONAN_STORAGE_PATH
diff --git a/conans/client/graph/graph.py b/conans/client/graph/graph.py
index d79011d26..7d7ebbca7 100644
--- a/conans/client/graph/graph.py
+++ b/conans/client/graph/graph.py
@@ -1,5 +1,3 @@
-from collections import OrderedDict
-
 from conans.model.ref import PackageReference
 
 RECIPE_DOWNLOADED = "Downloaded"
@@ -25,7 +23,7 @@ BINARY_EDITABLE = "Editable"
 class Node(object):
     def __init__(self, ref, conanfile, recipe=None):
         self.ref = ref
-        self.package_id = None
+        self._package_id = None
         self.prev = None
         self.conanfile = conanfile
         self.dependencies = []  # Ordered Edges
@@ -36,12 +34,31 @@ class Node(object):
         self.binary_remote = None
         self.build_require = False
         self.revision_pinned = False  # The revision has been specified by the user
+        # all the public deps to which this node belongs
+        self.public_deps = None  # {ref.name: Node}
+        # all the public deps only in the closure of this node
+        self.public_closure = None  # {ref.name: Node}
+        self.ancestors = None  # set{ref.name}
+
+    @property
+    def package_id(self):
+        return self._package_id
+
+    @package_id.setter
+    def package_id(self, pkg_id):
+        assert self._package_id is None, "Trying to override an existing package_id"
+        self._package_id = pkg_id
+
+    @property
+    def name(self):
+        return self.ref.name if self.ref else None
 
     @property
     def pref(self):
         return PackageReference(self.ref, self.package_id, self.prev)
 
     def partial_copy(self):
+        # Used for collapse_graph
         result = Node(self.ref, self.conanfile)
         result.dependants = set()
         result.dependencies = []
@@ -62,12 +79,8 @@ class Node(object):
     def neighbors(self):
         return [edge.dst for edge in self.dependencies]
 
-    def public_neighbors(self):
-        return [edge.dst for edge in self.dependencies
-                if not edge.private and not edge.build_require]
-
     def private_neighbors(self):
-        return [edge.dst for edge in self.dependencies if edge.private or edge.build_require]
+        return [edge.dst for edge in self.dependencies if edge.private]
 
     def inverse_neighbors(self):
         return [edge.src for edge in self.dependants]
@@ -146,18 +159,9 @@ class DepsGraph(object):
     def __init__(self):
         self.nodes = set()
         self.root = None
-
-    def add_graph(self, node, graph, build_require=False):
-        for n in graph.nodes:
-            if n != graph.root:
-                n.build_require = build_require
-                self.add_node(n)
-
-        for e in graph.root.dependencies:
-            e.src = node
-            e.build_require = build_require
-
-        node.dependencies = graph.root.dependencies + node.dependencies
+        self.aliased = {}
+        # These are the nodes with pref (not including PREV) that have been evaluated
+        self.evaluated = {}  # {pref: [nodes]}
 
     def add_node(self, node):
         if not self.nodes:
@@ -176,37 +180,6 @@ class DepsGraph(object):
             for node in level:
                 yield node
 
-    def full_closure(self, node, private=False):
-        # Needed to propagate correctly the cpp_info even with privates
-        closure = OrderedDict()
-        current = node.neighbors()
-        while current:
-            new_current = []
-            for n in current:
-                closure[n] = n
-            for n in current:
-                neighbors = n.public_neighbors() if not private else n.neighbors()
-                for neigh in neighbors:
-                    if neigh not in new_current and neigh not in closure:
-                        new_current.append(neigh)
-            current = new_current
-        return closure
-
-    def closure(self, node):
-        closure = OrderedDict()
-        current = node.neighbors()
-        while current:
-            new_current = []
-            for n in current:
-                closure[n.ref.name] = n
-            for n in current:
-                neighs = n.public_neighbors()
-                for neigh in neighs:
-                    if neigh not in new_current and neigh.ref.name not in closure:
-                        new_current.append(neigh)
-            current = new_current
-        return closure
-
     def _inverse_closure(self, references):
         closure = set()
         current = [n for n in self.nodes if str(n.ref) in references or "ALL" in references]
diff --git a/conans/client/graph/graph_binaries.py b/conans/client/graph/graph_binaries.py
index a285a448c..6edf11ed3 100644
--- a/conans/client/graph/graph_binaries.py
+++ b/conans/client/graph/graph_binaries.py
@@ -38,22 +38,27 @@ class GraphBinariesAnalyzer(object):
         pref = PackageReference(ref, node.package_id)
 
         # Check that this same reference hasn't already been checked
-        previous_node = evaluated_nodes.get(pref)
-        if previous_node:
+        previous_nodes = evaluated_nodes.get(pref)
+        if previous_nodes:
+            previous_nodes.append(node)
+            previous_node = previous_nodes[0]
             node.binary = previous_node.binary
             node.binary_remote = previous_node.binary_remote
+            node.prev = previous_node.prev
             return
-        evaluated_nodes[pref] = node
+        evaluated_nodes[pref] = [node]
 
         output = conanfile.output
 
         if node.recipe == RECIPE_EDITABLE:
             node.binary = BINARY_EDITABLE
+            # TODO: PREV?
             return
 
         if build_mode.forced(conanfile, ref):
             output.warn('Forced build from source')
             node.binary = BINARY_BUILD
+            node.prev = None
             return
 
         package_folder = self._cache.package(pref, short_paths=conanfile.short_paths)
@@ -105,6 +110,8 @@ class GraphBinariesAnalyzer(object):
                     output.warn("Can't update, no remote defined")
             if not node.binary:
                 node.binary = BINARY_CACHE
+                metadata = self._cache.package_layout(pref.ref).load_metadata()
+                node.prev = metadata.packages[pref.id].revision
                 package_hash = ConanInfo.load_from_package(package_folder).recipe_hash
 
         else:  # Binary does NOT exist locally
@@ -138,6 +145,7 @@ class GraphBinariesAnalyzer(object):
                     node.binary = BINARY_BUILD
                 else:
                     node.binary = BINARY_MISSING
+                node.prev = None
 
         if build_mode.outdated:
             if node.binary in (BINARY_CACHE, BINARY_DOWNLOAD, BINARY_UPDATE):
@@ -145,21 +153,21 @@ class GraphBinariesAnalyzer(object):
                 if local_recipe_hash != package_hash:
                     output.info("Outdated package!")
                     node.binary = BINARY_BUILD
+                    node.prev = None
                 else:
                     output.info("Package is up to date")
 
         node.binary_remote = remote
 
     @staticmethod
-    def _compute_package_id(node):
+    def _compute_package_id(node, default_package_id_mode):
         conanfile = node.conanfile
         neighbors = node.neighbors()
         direct_reqs = []  # of PackageReference
         indirect_reqs = set()   # of PackageReference, avoid duplicates
         for neighbor in neighbors:
             ref, nconan = neighbor.ref, neighbor.conanfile
-            pref = PackageReference(ref, neighbor.package_id)
-            direct_reqs.append(pref)
+            direct_reqs.append(neighbor.pref)
             indirect_reqs.update(nconan.info.requires.refs())
             conanfile.options.propagate_downstream(ref, nconan.info.full_options)
             # Might be never used, but update original requirement, just in case
@@ -171,34 +179,36 @@ class GraphBinariesAnalyzer(object):
         # for build-requires
         conanfile.build_requires_options = conanfile.options.values
         conanfile.options.clear_unused(indirect_reqs.union(direct_reqs))
+        conanfile.options.freeze()
 
         conanfile.info = ConanInfo.create(conanfile.settings.values,
                                           conanfile.options.values,
                                           direct_reqs,
-                                          indirect_reqs)
+                                          indirect_reqs,
+                                          default_package_id_mode=default_package_id_mode)
 
         # Once we are done, call package_id() to narrow and change possible values
         with conanfile_exception_formatter(str(conanfile), "package_id"):
             conanfile.package_id()
 
-        info = node.conanfile.info
+        info = conanfile.info
         node.package_id = info.package_id()
 
-    def _handle_private(self, node, deps_graph):
-        private_neighbours = node.private_neighbors()
-        if private_neighbours:
-            if node.binary in (BINARY_CACHE, BINARY_DOWNLOAD, BINARY_UPDATE):
-                for neigh in private_neighbours:
-                    neigh.binary = BINARY_SKIP
-                    closure = deps_graph.full_closure(neigh, private=True)
-                    for n in closure:
-                        n.binary = BINARY_SKIP
+    def _handle_private(self, node):
+        if node.binary in (BINARY_CACHE, BINARY_DOWNLOAD, BINARY_UPDATE, BINARY_SKIP):
+            private_neighbours = node.private_neighbors()
+            for neigh in private_neighbours:
+                # Current closure contains own node to be skipped
+                for n in neigh.public_closure.values():
+                    n.binary = BINARY_SKIP
+                    self._handle_private(n)
 
     def evaluate_graph(self, deps_graph, build_mode, update, remote_name):
-        evaluated_nodes = {}
+        default_package_id_mode = self._cache.config.default_package_id_mode
+        evaluated = deps_graph.evaluated
         for node in deps_graph.ordered_iterate():
-            self._compute_package_id(node)
+            self._compute_package_id(node, default_package_id_mode)
             if node.recipe in (RECIPE_CONSUMER, RECIPE_VIRTUAL):
                 continue
-            self._evaluate_node(node, build_mode, update, evaluated_nodes, remote_name)
-            self._handle_private(node, deps_graph)
+            self._evaluate_node(node, build_mode, update, evaluated, remote_name)
+            self._handle_private(node)
diff --git a/conans/client/graph/graph_builder.py b/conans/client/graph/graph_builder.py
index 7d1a6587e..caeab2a9a 100644
--- a/conans/client/graph/graph_builder.py
+++ b/conans/client/graph/graph_builder.py
@@ -1,14 +1,15 @@
 import time
-
+from collections import OrderedDict
 
 from conans.client.graph.graph import DepsGraph, Node, RECIPE_EDITABLE
 from conans.errors import (ConanException, ConanExceptionInUserConanfileMethod,
                            conanfile_exception_formatter)
 from conans.model.conan_file import get_env_context_manager
 from conans.model.ref import ConanFileReference
-from conans.model.requires import Requirements
+from conans.model.requires import Requirements, Requirement
 from conans.util.log import logger
 
+
 REFERENCE_CONFLICT, REVISION_CONFLICT = 1, 2
 
 
@@ -26,33 +27,75 @@ class DepsGraphBuilder(object):
         check_updates = check_updates or update
         dep_graph = DepsGraph()
         # compute the conanfile entry point for this dependency graph
+        name = root_node.name
+        root_node.public_deps = {name: root_node}
+        root_node.public_closure = OrderedDict([(name, root_node)])
+        root_node.ancestors = set()
         dep_graph.add_node(root_node)
-        public_deps = {}  # {name: Node} dict with public nodes, so they are not added again
-        aliased = {}
+
         # enter recursive computation
         t1 = time.time()
-        loop_ancestors = []
-        self._load_deps(root_node, Requirements(), dep_graph, public_deps, None, None,
-                        loop_ancestors, aliased, check_updates, update, remote_name,
+        self._load_deps(dep_graph, root_node, Requirements(), None, None,
+                        check_updates, update, remote_name,
                         processed_profile)
         logger.debug("GRAPH: Time to load deps %s" % (time.time() - t1))
         return dep_graph
 
-    def _resolve_deps(self, node, aliased, update, remote_name):
-        # Resolve possible version ranges of the current node requirements
-        # new_reqs is a shallow copy of what is propagated upstream, so changes done by the
-        # RangeResolver are also done in new_reqs, and then propagated!
+    def extend_build_requires(self, graph, node, build_requires_refs, check_updates, update,
+                              remote_name, processed_profile):
+
+        # The options that will be defined in the node will be the real options values that have
+        # been already propagated downstream from the dependency graph. This will override any
+        # other possible option in the build_requires dependency graph. This means that in theory
+        # an option conflict while expanding the build_requires is impossible
+        node.conanfile.build_requires_options.clear_unscoped_options()
+        new_options = node.conanfile.build_requires_options._reqs_options
+        new_reqs = Requirements()
+
         conanfile = node.conanfile
         scope = conanfile.display_name
-        for _, require in conanfile.requires.items():
+        requires = [Requirement(ref) for ref in build_requires_refs]
+        self._resolve_ranges(graph, requires, scope, update, remote_name)
+
+        for require in requires:
+            name = require.ref.name
+            require.build_require = True
+            self._handle_require(name, node, require, graph, check_updates, update,
+                                 remote_name, processed_profile, new_reqs, new_options)
+
+        new_nodes = set([n for n in graph.nodes if n.package_id is None])
+        # This is to make sure that build_requires have precedence over the normal requires
+        ordered_closure = list(node.public_closure.items())
+        ordered_closure.sort(key=lambda x: x[1] not in new_nodes)
+        node.public_closure = OrderedDict(ordered_closure)
+
+        subgraph = DepsGraph()
+        subgraph.aliased = graph.aliased
+        subgraph.evaluated = graph.evaluated
+        subgraph.nodes = new_nodes
+        for n in subgraph.nodes:
+            n.build_require = True
+
+        return subgraph
+
+    def _resolve_ranges(self, graph, requires, scope, update, remote_name):
+        for require in requires:
             self._resolver.resolve(require, scope, update, remote_name)
 
         # After resolving ranges,
-        for require in conanfile.requires.values():
-            alias = aliased.get(require.ref)
+        for require in requires:
+            alias = graph.aliased.get(require.ref)
             if alias:
                 require.ref = alias
 
+    def _resolve_deps(self, graph, node, update, remote_name):
+        # Resolve possible version ranges of the current node requirements
+        # new_reqs is a shallow copy of what is propagated upstream, so changes done by the
+        # RangeResolver are also done in new_reqs, and then propagated!
+        conanfile = node.conanfile
+        scope = conanfile.display_name
+        self._resolve_ranges(graph, conanfile.requires.values(), scope, update, remote_name)
+
         if not hasattr(conanfile, "_conan_evaluated_requires"):
             conanfile._conan_evaluated_requires = conanfile.requires.copy()
         elif conanfile.requires != conanfile._conan_evaluated_requires:
@@ -63,70 +106,85 @@ class DepsGraphBuilder(object):
                                  % (scope, list(conanfile._conan_evaluated_requires.values()),
                                     list(conanfile.requires.values())))
 
-    def _load_deps(self, node, down_reqs, dep_graph, public_deps, down_ref, down_options,
-                   loop_ancestors, aliased, check_updates, update, remote_name, processed_profile):
+    def _load_deps(self,  dep_graph, node, down_reqs, down_ref, down_options,
+                   check_updates, update, remote_name, processed_profile):
         """ loads a Conan object from the given file
         param node: Node object to be expanded in this step
         down_reqs: the Requirements as coming from downstream, which can overwrite current
                     values
-        param deps: DepsGraph result
-        param public_deps: {name: Node} of already expanded public Nodes, not to be repeated
-                           in graph
         param down_ref: ConanFileReference of who is depending on current node for this expansion
         """
         # basic node configuration
-        new_reqs, new_options = self._config_node(node, down_reqs, down_ref, down_options, aliased)
+        new_reqs, new_options = self._config_node(dep_graph, node, down_reqs, down_ref, down_options)
 
-        self._resolve_deps(node, aliased, update, remote_name)
+        self._resolve_deps(dep_graph, node, update, remote_name)
 
         # Expand each one of the current requirements
         for name, require in node.conanfile.requires.items():
             if require.override:
                 continue
-            if require.ref in loop_ancestors:
-                raise ConanException("Loop detected: %s"
-                                     % "->".join(str(r) for r in loop_ancestors))
-            new_loop_ancestors = loop_ancestors[:]  # Copy for propagating
-            new_loop_ancestors.append(require.ref)
-            previous = public_deps.get(name)
-            if require.private or not previous:  # new node, must be added and expanded
-                new_node = self._create_new_node(node, dep_graph, require, public_deps, name,
-                                                 aliased, check_updates, update, remote_name,
-                                                 processed_profile)
-                # RECURSION!
-                # Make sure the subgraph is truly private
-                new_public_deps = {} if require.private else public_deps
-                self._load_deps(new_node, new_reqs, dep_graph, new_public_deps, node.ref,
-                                new_options, new_loop_ancestors, aliased, check_updates, update,
+
+            self._handle_require(name, node, require, dep_graph, check_updates, update,
+                                 remote_name, processed_profile, new_reqs, new_options)
+
+    def _handle_require(self, name, node, require, dep_graph, check_updates, update,
+                        remote_name, processed_profile, new_reqs, new_options):
+        if name in node.ancestors:
+            raise ConanException("Loop detected: '%s' requires '%s' which is an ancestor too"
+                                 % (node.ref, require.ref))
+
+        previous = node.public_deps.get(name)
+        if require.private or require.build_require or not previous:
+            # new node, must be added and expanded
+            new_node = self._create_new_node(node, dep_graph, require, name,
+                                             check_updates, update, remote_name,
+                                             processed_profile)
+
+            new_node.public_closure = OrderedDict([(new_node.ref.name, new_node)])
+            node.public_closure[name] = new_node
+            if require.private or require.build_require:
+                new_node.public_deps = node.public_closure
+            else:
+                new_node.public_deps = node.public_deps
+
+                # add this new node to the public deps
+                node.public_deps[name] = new_node
+
+                # Update the closure of each dependent
+                for dep_node_name, dep_node in node.public_deps.items():
+                    if dep_node is node:
+                        continue
+                    if dep_node_name in new_node.ancestors:
+                        dep_node.public_closure[new_node.name] = new_node
+
+            # RECURSION!
+            self._load_deps(dep_graph, new_node, new_reqs, node.ref,
+                            new_options, check_updates, update,
+                            remote_name, processed_profile)
+        else:  # a public node already exist with this name
+            alias_ref = dep_graph.aliased.get(require.ref)
+            # Necessary to make sure that it is pointing to the correct aliased
+            if alias_ref:
+                require.ref = alias_ref
+            conflict = self._conflicting_references(previous.ref, require.ref)
+            if conflict == REVISION_CONFLICT:  # Revisions conflict
+                raise ConanException("Conflict in %s\n"
+                                     "    Different revisions of %s has been requested"
+                                     % (node.ref, require.ref))
+            elif conflict == REFERENCE_CONFLICT:
+                raise ConanException("Conflict in %s\n"
+                                     "    Requirement %s conflicts with already defined %s\n"
+                                     "    To change it, override it in your base requirements"
+                                     % (node.ref, require.ref, previous.ref))
+
+            previous.ancestors.add(node.name)
+            node.public_closure[name] = previous
+            dep_graph.add_edge(node, previous, require.private, require.build_require)
+            # RECURSION!
+            if self._recurse(previous.public_closure, new_reqs, new_options):
+                self._load_deps(dep_graph, previous, new_reqs, node.ref,
+                                new_options, check_updates, update,
                                 remote_name, processed_profile)
-            else:  # a public node already exist with this name
-                previous_node, closure = previous
-                alias_ref = aliased.get(require.ref, None)
-                # Necessary to make sure that it is pointing to the correct aliased
-                if alias_ref:
-                    require.ref = alias_ref
-                conflict = self._conflicting_references(previous_node.ref, require.ref)
-                if conflict == REVISION_CONFLICT:  # Revisions conflict
-                    raise ConanException("Conflict in %s\n"
-                                         "    Different revisions of %s has been requested"
-                                         % (node.ref, require.ref))
-                elif conflict == REFERENCE_CONFLICT:
-                    raise ConanException("Conflict in %s\n"
-                                         "    Requirement %s conflicts with already defined %s\n"
-                                         "    Keeping %s\n"
-                                         "    To change it, override it in your base requirements"
-                                         % (node.ref, require.ref,
-                                            previous_node.ref, previous_node.ref))
-
-                dep_graph.add_edge(node, previous_node)
-                # RECURSION!
-                if closure is None:
-                    closure = dep_graph.closure(node)
-                    public_deps[name] = previous_node, closure
-                if self._recurse(closure, new_reqs, new_options):
-                    self._load_deps(previous_node, new_reqs, dep_graph, public_deps, node.ref,
-                                    new_options, new_loop_ancestors, aliased, check_updates, update,
-                                    remote_name, processed_profile)
 
     @staticmethod
     def _conflicting_references(previous_ref, new_ref):
@@ -156,7 +214,7 @@ class DepsGraphBuilder(object):
                         return True
         return False
 
-    def _config_node(self, node, down_reqs, down_ref, down_options, aliased):
+    def _config_node(self, graph, node, down_reqs, down_ref, down_options):
         """ update settings and option in the current ConanFile, computing actual
         requirement values, cause they can be overridden by downstream requires
         param settings: dict of settings values => {"os": "windows"}
@@ -202,9 +260,9 @@ class DepsGraphBuilder(object):
                         conanfile.requirements()
 
                 new_options = conanfile.options.deps_package_values
-                if aliased:
+                if graph.aliased:
                     for req in conanfile.requires.values():
-                        req.ref = aliased.get(req.ref, req.ref)
+                        req.ref = graph.aliased.get(req.ref, req.ref)
                 new_down_reqs = conanfile.requires.update(down_reqs, self._output, ref, down_ref)
         except ConanExceptionInUserConanfileMethod:
             raise
@@ -215,7 +273,7 @@ class DepsGraphBuilder(object):
 
         return new_down_reqs, new_options
 
-    def _create_new_node(self, current_node, dep_graph, requirement, public_deps, name_req, aliased,
+    def _create_new_node(self, current_node, dep_graph, requirement, name_req,
                          check_updates, update, remote_name, processed_profile, alias_ref=None):
         """ creates and adds a new node to the dependency graph
         """
@@ -240,9 +298,9 @@ class DepsGraphBuilder(object):
         if getattr(dep_conanfile, "alias", None):
             alias_ref = alias_ref or new_ref.copy_clear_rev()
             requirement.ref = ConanFileReference.loads(dep_conanfile.alias)
-            aliased[alias_ref] = requirement.ref
-            return self._create_new_node(current_node, dep_graph, requirement, public_deps,
-                                         name_req, aliased, check_updates, update,
+            dep_graph.aliased[alias_ref] = requirement.ref
+            return self._create_new_node(current_node, dep_graph, requirement,
+                                         name_req, check_updates, update,
                                          remote_name, processed_profile,
                                          alias_ref=alias_ref)
 
@@ -251,8 +309,8 @@ class DepsGraphBuilder(object):
         new_node.revision_pinned = requirement.ref.revision is not None
         new_node.recipe = recipe_status
         new_node.remote = remote
+        new_node.ancestors = current_node.ancestors.copy()
+        new_node.ancestors.add(current_node.name)
         dep_graph.add_node(new_node)
-        dep_graph.add_edge(current_node, new_node, requirement.private)
-        if not requirement.private:
-            public_deps[name_req] = new_node, None
+        dep_graph.add_edge(current_node, new_node, requirement.private, requirement.build_require)
         return new_node
diff --git a/conans/client/graph/graph_manager.py b/conans/client/graph/graph_manager.py
index 54eebc1e9..9fb42d985 100644
--- a/conans/client/graph/graph_manager.py
+++ b/conans/client/graph/graph_manager.py
@@ -170,9 +170,15 @@ class GraphManager(object):
 
         return conanfile.build_requires
 
-    def _recurse_build_requires(self, graph, check_updates, update, build_mode, remote_name,
+    def _recurse_build_requires(self, graph, builder, binaries_analyzer, check_updates, update,
+                                build_mode, remote_name,
                                 profile_build_requires, recorder, processed_profile):
-        for node in list(graph.nodes):
+
+        binaries_analyzer.evaluate_graph(graph, build_mode, update, remote_name)
+
+        for node in graph.ordered_iterate():
+            # Virtual conanfiles doesn't have output, but conanfile.py and conanfile.txt do
+            # FIXME: To be improved and build a explicit model for this
             if node.recipe == RECIPE_VIRTUAL:
                 continue
             if (node.binary not in (BINARY_BUILD, BINARY_EDITABLE)
@@ -188,42 +194,33 @@ class GraphManager(object):
                         fnmatch.fnmatch(str_ref, pattern)):
                             for build_require in build_requires:
                                 if build_require.name in package_build_requires:  # Override existing
-                                    # this is a way to have only one package Name for all versions (no conflicts)
+                                    # this is a way to have only one package Name for all versions
+                                    # (no conflicts)
                                     # but the dict key is not used at all
                                     package_build_requires[build_require.name] = build_require
                                 else:  # Profile one
                                     new_profile_build_requires.append(build_require)
 
             if package_build_requires:
-                node.conanfile.build_requires_options.clear_unscoped_options()
-                build_requires_options = node.conanfile.build_requires_options
-                virtual = self._loader.load_virtual(package_build_requires.values(),
-                                                    scope_options=False,
-                                                    build_requires_options=build_requires_options,
-                                                    processed_profile=processed_profile)
-                virtual_node = Node(None, virtual, recipe=RECIPE_VIRTUAL)
-                build_requires_package_graph = self._load_graph(virtual_node, check_updates, update,
-                                                                build_mode, remote_name,
-                                                                profile_build_requires,
-                                                                recorder,
-                                                                processed_profile)
-                graph.add_graph(node, build_requires_package_graph, build_require=True)
+                subgraph = builder.extend_build_requires(graph, node,
+                                                         package_build_requires.values(),
+                                                         check_updates, update, remote_name,
+                                                         processed_profile)
+                self._recurse_build_requires(subgraph, builder, binaries_analyzer, check_updates,
+                                             update, build_mode,
+                                             remote_name, profile_build_requires, recorder,
+                                             processed_profile)
+                graph.nodes.update(subgraph.nodes)
 
             if new_profile_build_requires:
-                node.conanfile.build_requires_options.clear_unscoped_options()
-                build_requires_options = node.conanfile.build_requires_options
-                virtual = self._loader.load_virtual(new_profile_build_requires,
-                                                    scope_options=False,
-                                                    build_requires_options=build_requires_options,
-                                                    processed_profile=processed_profile)
-                virtual_node = Node(None, virtual, recipe=RECIPE_VIRTUAL)
-                # Profile build-requires do NOT recurse
-                build_requires_profile_graph = self._load_graph(virtual_node, check_updates, update,
-                                                                build_mode, remote_name,
-                                                                {},  # profile_build_requires
-                                                                recorder,
-                                                                processed_profile)
-                graph.add_graph(node, build_requires_profile_graph, build_require=True)
+                subgraph = builder.extend_build_requires(graph, node, new_profile_build_requires,
+                                                         check_updates, update, remote_name,
+                                                         processed_profile)
+                self._recurse_build_requires(subgraph, builder, binaries_analyzer, check_updates,
+                                             update, build_mode,
+                                             remote_name, {}, recorder,
+                                             processed_profile)
+                graph.nodes.update(subgraph.nodes)
 
     def _load_graph(self, root_node, check_updates, update, build_mode, remote_name,
                     profile_build_requires, recorder, processed_profile):
@@ -234,10 +231,21 @@ class GraphManager(object):
         graph = builder.load_graph(root_node, check_updates, update, remote_name, processed_profile)
         binaries_analyzer = GraphBinariesAnalyzer(self._cache, self._output,
                                                   self._remote_manager)
-        binaries_analyzer.evaluate_graph(graph, build_mode, update, remote_name)
 
-        self._recurse_build_requires(graph, check_updates, update, build_mode, remote_name,
+        self._recurse_build_requires(graph, builder, binaries_analyzer, check_updates, update,
+                                     build_mode, remote_name,
                                      profile_build_requires, recorder, processed_profile)
+
+        # Sort of closures, for linking order
+        inverse_levels = {n: i for i, level in enumerate(graph.inverse_levels()) for n in level}
+        for node in graph.nodes:
+            closure = node.public_closure
+            closure.pop(node.name)
+            node_order = list(closure.values())
+            # List sort is stable, will keep the original order of the closure, but prioritize levels
+            node_order.sort(key=lambda n: inverse_levels[n])
+            node.public_closure = node_order
+
         return graph
 
 
diff --git a/conans/client/installer.py b/conans/client/installer.py
index 5e5c0fce8..5a81447df 100644
--- a/conans/client/installer.py
+++ b/conans/client/installer.py
@@ -270,8 +270,6 @@ class BinaryInstaller(object):
         self._build(nodes_by_level, deps_graph, keep_build, root_node, graph_info)
 
     def _build(self, nodes_by_level, deps_graph, keep_build, root_node, graph_info):
-        inverse_levels = {n: i for i, level in enumerate(deps_graph.inverse_levels()) for n in level}
-
         processed_package_refs = set()
         for level in nodes_by_level:
             for node in level:
@@ -283,7 +281,7 @@ class BinaryInstaller(object):
                     raise_package_not_found_error(conan_file, ref, package_id, dependencies,
                                                   out=output, recorder=self._recorder)
 
-                self._propagate_info(node, inverse_levels, deps_graph)
+                self._propagate_info(node)
                 if node.binary == BINARY_EDITABLE:
                     self._handle_node_editable(node, graph_info)
                 else:
@@ -293,7 +291,7 @@ class BinaryInstaller(object):
                     self._handle_node_cache(node, keep_build, processed_package_refs)
 
         # Finally, propagate information to root node (ref=None)
-        self._propagate_info(root_node, inverse_levels, deps_graph)
+        self._propagate_info(root_node)
 
     def _node_concurrently_installed(self, node, package_folder):
         if node.binary == BINARY_DOWNLOAD and os.path.exists(package_folder):
@@ -429,13 +427,10 @@ class BinaryInstaller(object):
         self._recorder.package_built(pref)
 
     @staticmethod
-    def _propagate_info(node, inverse_levels, deps_graph):
+    def _propagate_info(node):
         # Get deps_cpp_info from upstream nodes
-        closure = deps_graph.full_closure(node)
-        node_order = [n for n in closure.values() if n.binary != BINARY_SKIP]
+        node_order = [n for n in node.public_closure if n.binary != BINARY_SKIP]
         # List sort is stable, will keep the original order of the closure, but prioritize levels
-        node_order.sort(key=lambda n: inverse_levels[n])
-
         conan_file = node.conanfile
         for n in node_order:
             if n.build_require:
diff --git a/conans/client/recorder/action_recorder.py b/conans/client/recorder/action_recorder.py
index 2358d71e6..b5e81d895 100644
--- a/conans/client/recorder/action_recorder.py
+++ b/conans/client/recorder/action_recorder.py
@@ -39,12 +39,12 @@ def _cpp_info_to_dict(cpp_info):
     return doc
 
 
-class Action(namedtuple("Action", "type, doc, time")):
+class Action(namedtuple("Action", "type, full_ref, doc, time")):
 
-    def __new__(cls, the_type, doc=None):
+    def __new__(cls, the_type, full_ref, doc=None):
         doc = doc or {}
         the_time = datetime.utcnow()
-        return super(cls, Action).__new__(cls, the_type, doc, the_time)
+        return super(cls, Action).__new__(cls, the_type, full_ref, doc, the_time)
 
 
 class ActionRecorder(object):
@@ -59,7 +59,7 @@ class ActionRecorder(object):
     # ###### INSTALL METHODS ############
     def add_recipe_being_developed(self, ref):
         assert(isinstance(ref, ConanFileReference))
-        self._inst_recipes_develop.add(ref)
+        self._inst_recipes_develop.add(ref.copy_clear_rev())
 
     def _add_recipe_action(self, ref, action):
         assert(isinstance(ref, ConanFileReference))
@@ -70,51 +70,49 @@ class ActionRecorder(object):
 
     def _add_package_action(self, pref, action):
         assert(isinstance(pref, PackageReference))
-        pref = pref.copy_clear_rev()
+        pref = pref.copy_clear_revs()
         if pref not in self._inst_packages_actions:
             self._inst_packages_actions[pref] = []
         self._inst_packages_actions[pref].append(action)
 
     # RECIPE METHODS
     def recipe_exported(self, ref):
-        self._add_recipe_action(ref, Action(INSTALL_EXPORTED))
+        self._add_recipe_action(ref, Action(INSTALL_EXPORTED, ref))
 
     def recipe_fetched_from_cache(self, ref):
-        self._add_recipe_action(ref, Action(INSTALL_CACHE))
+        self._add_recipe_action(ref, Action(INSTALL_CACHE, ref))
 
     def recipe_downloaded(self, ref, remote_name):
-        self._add_recipe_action(ref, Action(INSTALL_DOWNLOADED, {"remote": remote_name}))
+        self._add_recipe_action(ref, Action(INSTALL_DOWNLOADED, ref, {"remote": remote_name}))
 
     def recipe_install_error(self, ref, error_type, description, remote_name):
         doc = {"type": error_type, "description": description, "remote": remote_name}
-        self._add_recipe_action(ref, Action(INSTALL_ERROR, doc))
+        self._add_recipe_action(ref, Action(INSTALL_ERROR, ref, doc))
 
     # PACKAGE METHODS
     def package_exported(self, pref):
-        self._add_package_action(pref, Action(INSTALL_EXPORTED))
+        self._add_package_action(pref, Action(INSTALL_EXPORTED, pref))
 
     def package_built(self, pref):
-        self._add_package_action(pref, Action(INSTALL_BUILT))
+        self._add_package_action(pref, Action(INSTALL_BUILT, pref))
 
     def package_fetched_from_cache(self, pref):
-        self._add_package_action(pref, Action(INSTALL_CACHE))
+        self._add_package_action(pref, Action(INSTALL_CACHE, pref))
 
     def package_downloaded(self, pref, remote_name):
-        self._add_package_action(pref, Action(INSTALL_DOWNLOADED, {"remote": remote_name}))
+        self._add_package_action(pref, Action(INSTALL_DOWNLOADED, pref, {"remote": remote_name}))
 
     def package_install_error(self, pref, error_type, description, remote_name=None):
         assert(isinstance(pref, PackageReference))
-        pref = pref.copy_clear_rev()
         if pref not in self._inst_packages_actions:
-            self._inst_packages_actions[pref] = []
+            self._inst_packages_actions[pref.copy_clear_revs()] = []
         doc = {"type": error_type, "description": description, "remote": remote_name}
-        self._inst_packages_actions[pref].append(Action(INSTALL_ERROR, doc))
+        self._inst_packages_actions[pref.copy_clear_revs()].append(Action(INSTALL_ERROR, pref, doc))
 
     def package_cpp_info(self, pref, cpp_info):
         assert isinstance(pref, PackageReference)
-        pref = pref.copy_clear_rev()
         # assert isinstance(cpp_info, CppInfo)
-        self._inst_packages_info[pref]['cpp_info'] = _cpp_info_to_dict(cpp_info)
+        self._inst_packages_info[pref.copy_clear_revs()]['cpp_info'] = _cpp_info_to_dict(cpp_info)
 
     @property
     def install_errored(self):
@@ -138,10 +136,10 @@ class ActionRecorder(object):
     def in_development_recipe(self, ref):
         return ref in self._inst_recipes_develop
 
-    def get_info(self):
-        return self.get_install_info()
+    def get_info(self, revisions_enabled):
+        return self.get_install_info(revisions_enabled)
 
-    def get_install_info(self):
+    def get_install_info(self, revisions_enabled):
         ret = {"error": self.install_errored or self.error,
                "installed": []}
 
@@ -153,8 +151,12 @@ class ActionRecorder(object):
             remote = None if not remotes else remotes[0]
             action_types = [action.type for action in the_actions]
             time = the_actions[0].time
+            if revisions_enabled and isinstance(the_ref, ConanFileReference):
+                the_id = the_actions[0].full_ref.full_repr()
+            else:
+                the_id = str(the_ref)
 
-            doc = {"id": str(the_ref),
+            doc = {"id": the_id,
                    "downloaded": INSTALL_DOWNLOADED in action_types,
                    "exported": INSTALL_EXPORTED in action_types,
                    "error": error,
diff --git a/conans/model/info.py b/conans/model/info.py
index 4eb8b0b71..475034bfa 100644
--- a/conans/model/info.py
+++ b/conans/model/info.py
@@ -14,22 +14,28 @@ from conans.util.sha import sha1
 
 
 class RequirementInfo(object):
-    def __init__(self, value_str, indirect=False):
-        """ parse the input into fields name, version...
-        """
-        pref = PackageReference.loads(value_str)
+    def __init__(self, pref, default_package_id_mode, indirect=False):
         self.package = pref
         self.full_name = pref.ref.name
         self.full_version = pref.ref.version
         self.full_user = pref.ref.user
         self.full_channel = pref.ref.channel
         self.full_package_id = pref.id
+        self._indirect = indirect
 
-        # sha values
-        if indirect:
-            self.unrelated_mode()
-        else:
-            self.semver()
+        try:
+            getattr(self, default_package_id_mode)()
+        except AttributeError:
+            raise ConanException("'%s' is not a known package_id_mode" % default_package_id_mode)
+
+    def copy(self):
+        # Useful for build_id()
+        result = RequirementInfo(self.package, "unrelated_mode")
+        for f in ("name", "version", "user", "channel", "package_id"):
+            setattr(result, f, getattr(self, f))
+            f = "full_%s" % f
+            setattr(result, f, getattr(self, f))
+        return result
 
     def dumps(self):
         if not self.name:
@@ -43,18 +49,25 @@ class RequirementInfo(object):
 
     @property
     def sha(self):
-        return "/".join([str(n) for n in [self.name, self.version, self.user, self.channel,
-                                          self.package_id]])
+        vals = [str(n) for n in (self.name, self.version, self.user, self.channel, self.package_id)]
+        # This is done later to NOT affect existing package-IDs (before revisions)
+        return "/".join(vals)
 
     def unrelated_mode(self):
         self.name = self.version = self.user = self.channel = self.package_id = None
 
+    def semver_direct_mode(self):
+        if self._indirect:
+            self.unrelated_mode()
+        else:
+            self.semver_mode()
+
     def semver_mode(self):
         self.name = self.full_name
         self.version = self.full_version.stable()
         self.user = self.channel = self.package_id = None
 
-    semver = semver_mode
+    semver = semver_mode  # Remove Conan 2.0
 
     def full_version_mode(self):
         self.name = self.full_name
@@ -97,12 +110,16 @@ class RequirementInfo(object):
 
 
 class RequirementsInfo(object):
-    def __init__(self, requires):
+    def __init__(self, prefs, default_package_id_mode):
         # {PackageReference: RequirementInfo}
-        self._data = {r: RequirementInfo(str(r)) for r in requires}
+        self._data = {pref: RequirementInfo(pref, default_package_id_mode=default_package_id_mode)
+                      for pref in prefs}
 
     def copy(self):
-        return RequirementsInfo(self._data.keys())
+        # For build_id() implementation
+        result = RequirementsInfo([], None)
+        result._data = {pref: req_info.copy() for pref, req_info in self._data.items()}
+        return result
 
     def clear(self):
         self._data = {}
@@ -111,12 +128,13 @@ class RequirementsInfo(object):
         for name in args:
             del self._data[self._get_key(name)]
 
-    def add(self, indirect_reqs):
+    def add(self, prefs_indirect, default_package_id_mode):
         """ necessary to propagate from upstream the real
         package requirements
         """
-        for r in indirect_reqs:
-            self._data[r] = RequirementInfo(str(r), indirect=True)
+        for r in prefs_indirect:
+            self._data[r] = RequirementInfo(r, indirect=True,
+                                            default_package_id_mode=default_package_id_mode)
 
     def refs(self):
         """ used for updating downstream requirements with this
@@ -146,7 +164,8 @@ class RequirementsInfo(object):
         # Remove requirements without a name, i.e. indirect transitive requirements
         data = {k: v for k, v in self._data.items() if v.name}
         for key in sorted(data):
-            result.append(data[key].sha)
+            s = data[key].sha
+            result.append(s)
         return sha1('\n'.join(result).encode())
 
     def dumps(self):
@@ -160,6 +179,10 @@ class RequirementsInfo(object):
     def unrelated_mode(self):
         self.clear()
 
+    def semver_direct_mode(self):
+        for r in self._data.values():
+            r.semver_direct_mode()
+
     def semver_mode(self):
         for r in self._data.values():
             r.semver_mode()
@@ -218,17 +241,17 @@ class ConanInfo(object):
         return result
 
     @staticmethod
-    def create(settings, options, requires, indirect_requires):
+    def create(settings, options, prefs_direct, prefs_indirect, default_package_id_mode):
         result = ConanInfo()
         result.full_settings = settings
         result.settings = settings.copy()
         result.full_options = options
         result.options = options.copy()
         result.options.clear_indirect()
-        result.full_requires = _PackageReferenceList(requires)
-        result.requires = RequirementsInfo(requires)
-        result.requires.add(indirect_requires)
-        result.full_requires.extend(indirect_requires)
+        result.full_requires = _PackageReferenceList(prefs_direct)
+        result.requires = RequirementsInfo(prefs_direct, default_package_id_mode)
+        result.requires.add(prefs_indirect, default_package_id_mode)
+        result.full_requires.extend(prefs_indirect)
         result.recipe_hash = None
         result.env_values = EnvValues()
         result.vs_toolset_compatible()
@@ -239,6 +262,9 @@ class ConanInfo(object):
 
     @staticmethod
     def loads(text):
+        # This is used for search functionality, search prints info from this file
+        # Other use is from the BinariesAnalyzer, to get the recipe_hash and know
+        # if package is outdated
         parser = ConfigParser(text, ["settings", "full_settings", "options", "full_options",
                                      "requires", "full_requires", "scope", "recipe_hash", "env"],
                               raise_unexpected_field=False)
@@ -248,7 +274,8 @@ class ConanInfo(object):
         result.options = OptionsValues.loads(parser.options)
         result.full_options = OptionsValues.loads(parser.full_options)
         result.full_requires = _PackageReferenceList.loads(parser.full_requires)
-        result.requires = RequirementsInfo(result.full_requires)
+        # Requires after load are not used for any purpose, CAN'T be used, they are not correct
+        result.requires = RequirementsInfo(result.full_requires, "semver_direct_mode")
         result.recipe_hash = parser.recipe_hash or None
 
         # TODO: Missing handling paring of requires, but not necessary now
@@ -308,18 +335,15 @@ class ConanInfo(object):
         """ The package_id of a conans is the sha1 of its specific requirements,
         options and settings
         """
-        package_id = getattr(self, "_package_id", None)
-        if package_id:
-            return package_id
-
         result = []
         result.append(self.settings.sha)
         # Only are valid requires for OPtions those Non-Dev who are still in requires
         self.options.filter_used(self.requires.pkg_names)
         result.append(self.options.sha)
-        result.append(self.requires.sha)
+        requires_sha = self.requires.sha
+        result.append(requires_sha)
+
         package_id = sha1('\n'.join(result).encode())
-        self._package_id = package_id
         return package_id
 
     def serialize_min(self):
@@ -335,7 +359,7 @@ class ConanInfo(object):
     def header_only(self):
         self.settings.clear()
         self.options.clear()
-        self.requires.unrelated_mode()
+        self.requires.clear()
 
     def vs_toolset_compatible(self):
         """Default behaviour, same package for toolset v140 with compiler=Visual Studio 15 than
diff --git a/conans/model/options.py b/conans/model/options.py
index 0d13a02ac..651647d52 100644
--- a/conans/model/options.py
+++ b/conans/model/options.py
@@ -115,6 +115,9 @@ class PackageOptionValues(object):
     def remove(self, option_name):
         del self._dict[option_name]
 
+    def freeze(self):
+        self._freeze = True
+
     def propagate_upstream(self, down_package_values, down_ref, own_ref, package_name):
         if not down_package_values:
             return
@@ -124,6 +127,11 @@ class PackageOptionValues(object):
             if name in self._dict and self._dict.get(name) == value:
                 continue
 
+            if self._freeze:
+                raise ConanException("%s tried to change %s option %s to %s\n"
+                                     "but it was already defined as %s"
+                                     % (down_ref, own_ref, name, value, self._dict.get(name)))
+
             modified = self._modified.get(name)
             if modified is not None:
                 modified_value, modified_ref = modified
@@ -362,6 +370,7 @@ class PackageOptions(object):
         self._data = {str(k): PackageOption(v, str(k))
                       for k, v in definition.items()}
         self._modified = {}
+        self._freeze = False
 
     def __contains__(self, option):
         return str(option) in self._data
@@ -444,6 +453,9 @@ class PackageOptions(object):
             if option in self._data:
                 self._data[option].value = value
 
+    def freeze(self):
+        self._freeze = True
+
     def propagate_upstream(self, package_values, down_ref, own_ref, pattern_options):
         """
         :param: package_values: PackageOptionValues({"shared": "True"}
@@ -457,6 +469,10 @@ class PackageOptions(object):
             if name in self._data and self._data.get(name) == value:
                 continue
 
+            if self._freeze:
+                raise ConanException("%s tried to change %s option %s to %s\n"
+                                     "but it was already defined as %s"
+                                     % (down_ref, own_ref, name, value, self._data.get(name)))
             modified = self._modified.get(name)
             if modified is not None:
                 modified_value, modified_ref = modified
@@ -487,6 +503,11 @@ class Options(object):
         # are not public, not overridable
         self._deps_package_values = {}  # {name("Boost": PackageOptionValues}
 
+    def freeze(self):
+        self._package_options.freeze()
+        for v in self._deps_package_values.values():
+            v.freeze()
+
     @property
     def deps_package_values(self):
         return self._deps_package_values
diff --git a/conans/model/ref.py b/conans/model/ref.py
index cdb58e074..52776f1bc 100644
--- a/conans/model/ref.py
+++ b/conans/model/ref.py
@@ -174,3 +174,6 @@ class PackageReference(namedtuple("PackageReference", "ref id revision")):
     def copy_clear_rev(self):
         ref = self.ref.copy_clear_rev()
         return PackageReference(ref, self.id, revision=None)
+
+    def copy_clear_revs(self):
+        return self.copy_with_revs(None, None)
diff --git a/conans/model/requires.py b/conans/model/requires.py
index 612c59fca..bd7aa3fe0 100644
--- a/conans/model/requires.py
+++ b/conans/model/requires.py
@@ -19,6 +19,7 @@ class Requirement(object):
         self.range_ref = ref
         self.override = override
         self.private = private
+        self.build_require = False
 
     @property
     def version_range(self):


Test patch:
diff --git a/conans/test/functional/command/export_pkg_test.py b/conans/test/functional/command/export_pkg_test.py
index 8f67e6ee6..dbde9c941 100644
--- a/conans/test/functional/command/export_pkg_test.py
+++ b/conans/test/functional/command/export_pkg_test.py
@@ -450,8 +450,10 @@ class TestConan(ConanFile):
             json_content = load(json_path)
             output = json.loads(json_content)
             self.assertEqual(output["error"], with_error)
-            self.assertEqual(output["installed"][0]["recipe"]["id"],
-                             "mypackage/0.1.0@danimtb/testing")
+            tmp = ConanFileReference.loads(output["installed"][0]["recipe"]["id"])
+            if self.client.cache.config.revisions_enabled:
+                self.assertIsNotNone(tmp.revision)
+            self.assertEqual(str(tmp), "mypackage/0.1.0@danimtb/testing")
             self.assertFalse(output["installed"][0]["recipe"]["dependency"])
             self.assertTrue(output["installed"][0]["recipe"]["exported"])
             if with_error:
@@ -498,8 +500,10 @@ class MyConan(ConanFile):
             json_content = load(json_path)
             output = json.loads(json_content)
             self.assertEqual(output["error"], with_error)
-            self.assertEqual(output["installed"][0]["recipe"]["id"],
-                             "pkg2/1.0@danimtb/testing")
+            tmp = ConanFileReference.loads(output["installed"][0]["recipe"]["id"])
+            if self.client.cache.config.revisions_enabled:
+                self.assertIsNotNone(tmp.revision)
+            self.assertEqual(str(tmp), "pkg2/1.0@danimtb/testing")
             self.assertFalse(output["installed"][0]["recipe"]["dependency"])
             self.assertTrue(output["installed"][0]["recipe"]["exported"])
             if with_error:
@@ -508,8 +512,10 @@ class MyConan(ConanFile):
                 self.assertEqual(output["installed"][0]["packages"][0]["id"],
                                  "5825778de2dc9312952d865df314547576f129b3")
                 self.assertTrue(output["installed"][0]["packages"][0]["exported"])
-                self.assertEqual(output["installed"][1]["recipe"]["id"],
-                                 "pkg1/1.0@danimtb/testing")
+                tmp = ConanFileReference.loads(output["installed"][1]["recipe"]["id"])
+                if self.client.cache.config.revisions_enabled:
+                    self.assertIsNotNone(tmp.revision)
+                self.assertEqual(str(tmp), "pkg1/1.0@danimtb/testing")
                 self.assertTrue(output["installed"][1]["recipe"]["dependency"])
 
         conanfile = """from conans import ConanFile
diff --git a/conans/test/functional/command/json_output_test.py b/conans/test/functional/command/json_output_test.py
index 8b57e36a3..2a6499376 100644
--- a/conans/test/functional/command/json_output_test.py
+++ b/conans/test/functional/command/json_output_test.py
@@ -4,6 +4,7 @@ import textwrap
 import unittest
 
 from conans.model.build_info import DEFAULT_LIB
+from conans.model.ref import ConanFileReference
 from conans.test.utils.cpp_test_files import cpp_hello_conan_files
 from conans.test.utils.tools import TestClient, TestServer
 from conans.util.files import load, save
@@ -22,7 +23,10 @@ class JsonOutputTest(unittest.TestCase):
         self.client.run("create . private_user/channel --json=myfile.json")
         my_json = json.loads(load(os.path.join(self.client.current_folder, "myfile.json")))
         self.assertFalse(my_json["error"])
-        self.assertEquals(my_json["installed"][0]["recipe"]["id"], "CC/1.0@private_user/channel")
+        tmp = ConanFileReference.loads(my_json["installed"][0]["recipe"]["id"])
+        self.assertEquals(str(tmp), "CC/1.0@private_user/channel")
+        if self.client.cache.config.revisions_enabled:
+            self.assertIsNotNone(tmp.revision)
         self.assertFalse(my_json["installed"][0]["recipe"]["dependency"])
         self.assertTrue(my_json["installed"][0]["recipe"]["exported"])
         self.assertFalse(my_json["installed"][0]["recipe"]["downloaded"])
@@ -167,7 +171,10 @@ AA*: CC/1.0@private_user/channel
 
         # Installed the build require CC with two options
         self.assertEquals(len(my_json["installed"][2]["packages"]), 2)
-        self.assertEquals(my_json["installed"][2]["recipe"]["id"], "CC/1.0@private_user/channel")
+        tmp = ConanFileReference.loads(my_json["installed"][2]["recipe"]["id"])
+        if self.client.cache.config.revisions_enabled:
+            self.assertIsNotNone(tmp.revision)
+        self.assertEquals(str(tmp), "CC/1.0@private_user/channel")
         self.assertFalse(my_json["installed"][2]["recipe"]["downloaded"])
         self.assertFalse(my_json["installed"][2]["packages"][0]["downloaded"])
         self.assertFalse(my_json["installed"][2]["packages"][1]["downloaded"])
diff --git a/conans/test/functional/graph/graph_manager_base.py b/conans/test/functional/graph/graph_manager_base.py
new file mode 100644
index 000000000..b3446bd9f
--- /dev/null
+++ b/conans/test/functional/graph/graph_manager_base.py
@@ -0,0 +1,103 @@
+import os
+import unittest
+
+from mock import Mock
+
+from conans.client.cache.cache import ClientCache
+from conans.client.graph.graph_manager import GraphManager
+from conans.client.graph.proxy import ConanProxy
+from conans.client.graph.python_requires import ConanPythonRequire
+from conans.client.graph.range_resolver import RangeResolver
+from conans.client.installer import BinaryInstaller
+from conans.client.loader import ConanFileLoader
+from conans.client.recorder.action_recorder import ActionRecorder
+from conans.model.graph_info import GraphInfo
+from conans.model.manifest import FileTreeManifest
+from conans.model.options import OptionsValues
+from conans.model.profile import Profile
+from conans.model.ref import ConanFileReference
+from conans.test.unittests.model.transitive_reqs_test import MockSearchRemote
+from conans.test.utils.conanfile import TestConanFile
+from conans.test.utils.test_files import temp_folder
+from conans.test.utils.tools import TestBufferConanOutput
+from conans.util.files import save
+
+
+class GraphManagerTest(unittest.TestCase):
+
+    def setUp(self):
+        self.output = TestBufferConanOutput()
+        cache_folder = temp_folder()
+        cache = ClientCache(cache_folder, os.path.join(cache_folder, ".conan"), self.output)
+        self.cache = cache
+        self.remote_search = MockSearchRemote()
+        remote_manager = None
+        self.resolver = RangeResolver(cache, self.remote_search)
+        proxy = ConanProxy(cache, self.output, remote_manager)
+        self.loader = ConanFileLoader(None, self.output, ConanPythonRequire(None, None))
+        self.manager = GraphManager(self.output, cache, remote_manager, self.loader, proxy,
+                                    self.resolver)
+        hook_manager = Mock()
+        recorder = Mock()
+        self.binary_installer = BinaryInstaller(cache, self.output, remote_manager, recorder,
+                                                hook_manager)
+
+    def _cache_recipe(self, reference, test_conanfile, revision=None):
+        if isinstance(test_conanfile, TestConanFile):
+            test_conanfile.info = True
+        ref = ConanFileReference.loads(reference)
+        save(self.cache.conanfile(ref), str(test_conanfile))
+        with self.cache.package_layout(ref).update_metadata() as metadata:
+            metadata.recipe.revision = revision or "123"
+        manifest = FileTreeManifest.create(self.cache.export(ref))
+        manifest.save(self.cache.export(ref))
+
+    def build_graph(self, content, profile_build_requires=None, ref=None):
+        path = temp_folder()
+        path = os.path.join(path, "conanfile.py")
+        save(path, str(content))
+        self.loader.cached_conanfiles = {}
+
+        profile = Profile()
+        if profile_build_requires:
+            profile.build_requires = profile_build_requires
+        profile.process_settings(self.cache)
+        update = check_updates = False
+        recorder = ActionRecorder()
+        remote_name = None
+        build_mode = []
+        ref = ref or ConanFileReference(None, None, None, None, validate=False)
+        options = OptionsValues()
+        graph_info = GraphInfo(profile, options, root_ref=ref)
+        deps_graph, _ = self.manager.load_graph(path, None, graph_info,
+                                                build_mode, check_updates, update,
+                                                remote_name, recorder)
+        self.binary_installer.install(deps_graph, False, graph_info)
+        return deps_graph
+
+    def _check_node(self, node, ref, deps, build_deps, dependents, closure, public_deps):
+        conanfile = node.conanfile
+        ref = ConanFileReference.loads(str(ref))
+        self.assertEqual(node.ref.full_repr(), ref.full_repr())
+        self.assertEqual(conanfile.name, ref.name)
+        self.assertEqual(len(node.dependencies), len(deps) + len(build_deps))
+        self.assertEqual(len(node.dependants), len(dependents))
+
+        public_deps = {n.name: n for n in public_deps}
+        self.assertEqual(node.public_deps, public_deps)
+
+        # The recipe requires is resolved to the reference WITH revision!
+        self.assertEqual(len(deps), len(conanfile.requires))
+        for dep in deps:
+            self.assertEqual(conanfile.requires[dep.name].ref,
+                             dep.ref)
+
+        self.assertEqual(closure, node.public_closure)
+        libs = []
+        envs = []
+        for n in closure:
+            libs.append("mylib%s%slib" % (n.ref.name, n.ref.version))
+            envs.append("myenv%s%senv" % (n.ref.name, n.ref.version))
+        self.assertEqual(conanfile.deps_cpp_info.libs, libs)
+        env = {"MYENV": envs} if envs else {}
+        self.assertEqual(conanfile.deps_env_info.vars, env)
diff --git a/conans/test/functional/graph/graph_manager_test.py b/conans/test/functional/graph/graph_manager_test.py
new file mode 100644
index 000000000..3919ee951
--- /dev/null
+++ b/conans/test/functional/graph/graph_manager_test.py
@@ -0,0 +1,465 @@
+import os
+import unittest
+
+from mock import Mock
+
+from conans.client.cache.cache import ClientCache
+from conans.client.graph.graph import RECIPE_CONSUMER, RECIPE_INCACHE
+from conans.client.graph.graph_manager import GraphManager
+from conans.client.graph.proxy import ConanProxy
+from conans.client.graph.python_requires import ConanPythonRequire
+from conans.client.graph.range_resolver import RangeResolver
+from conans.client.installer import BinaryInstaller
+from conans.client.loader import ConanFileLoader
+from conans.client.recorder.action_recorder import ActionRecorder
+from conans.model.graph_info import GraphInfo
+from conans.model.manifest import FileTreeManifest
+from conans.model.options import OptionsValues
+from conans.model.profile import Profile
+from conans.model.ref import ConanFileReference
+from conans.test.unittests.model.transitive_reqs_test import MockSearchRemote
+from conans.test.utils.conanfile import TestConanFile
+from conans.test.utils.test_files import temp_folder
+from conans.test.utils.tools import TestBufferConanOutput
+from conans.util.files import save
+from conans.errors import ConanException
+from parameterized import parameterized
+
+
+class GraphManagerTest(unittest.TestCase):
+
+    def setUp(self):
+        self.output = TestBufferConanOutput()
+        cache_folder = temp_folder()
+        cache = ClientCache(cache_folder, os.path.join(cache_folder, ".conan"), self.output)
+        self.cache = cache
+        self.remote_search = MockSearchRemote()
+        remote_manager = None
+        self.resolver = RangeResolver(cache, self.remote_search)
+        proxy = ConanProxy(cache, self.output, remote_manager)
+        self.loader = ConanFileLoader(None, self.output, ConanPythonRequire(None, None))
+        self.manager = GraphManager(self.output, cache, remote_manager, self.loader, proxy,
+                                    self.resolver)
+        hook_manager = Mock()
+        recorder = Mock()
+        self.binary_installer = BinaryInstaller(cache, self.output, remote_manager, recorder,
+                                                hook_manager)
+
+    def _cache_recipe(self, reference, test_conanfile, revision=None):
+        if isinstance(test_conanfile, TestConanFile):
+            test_conanfile.info = True
+        ref = ConanFileReference.loads(reference)
+        save(self.cache.conanfile(ref), str(test_conanfile))
+        with self.cache.package_layout(ref).update_metadata() as metadata:
+            metadata.recipe.revision = revision or "123"
+        manifest = FileTreeManifest.create(self.cache.export(ref))
+        manifest.save(self.cache.export(ref))
+
+    def build_graph(self, content, profile_build_requires=None, ref=None):
+        path = temp_folder()
+        path = os.path.join(path, "conanfile.py")
+        save(path, str(content))
+        self.loader.cached_conanfiles = {}
+
+        profile = Profile()
+        if profile_build_requires:
+            profile.build_requires = profile_build_requires
+        profile.process_settings(self.cache)
+        update = check_updates = False
+        recorder = ActionRecorder()
+        remote_name = None
+        build_mode = []
+        ref = ref or ConanFileReference(None, None, None, None, validate=False)
+        options = OptionsValues()
+        graph_info = GraphInfo(profile, options, root_ref=ref)
+        deps_graph, _ = self.manager.load_graph(path, None, graph_info,
+                                                build_mode, check_updates, update,
+                                                remote_name, recorder)
+        self.binary_installer.install(deps_graph, False, graph_info)
+        return deps_graph
+
+    def _check_node(self, node, ref, deps, build_deps, dependents, closure, public_deps):
+        conanfile = node.conanfile
+        ref = ConanFileReference.loads(str(ref))
+        self.assertEqual(node.ref.full_repr(), ref.full_repr())
+        self.assertEqual(conanfile.name, ref.name)
+        self.assertEqual(len(node.dependencies), len(deps) + len(build_deps))
+        self.assertEqual(len(node.dependants), len(dependents))
+
+        public_deps = {n.name: n for n in public_deps}
+        self.assertEqual(node.public_deps, public_deps)
+
+        # The recipe requires is resolved to the reference WITH revision!
+        self.assertEqual(len(deps), len(conanfile.requires))
+        for dep in deps:
+            self.assertEqual(conanfile.requires[dep.name].ref,
+                             dep.ref)
+
+        self.assertEqual(closure, node.public_closure)
+        libs = []
+        envs = []
+        for n in closure:
+            libs.append("mylib%s%slib" % (n.ref.name, n.ref.version))
+            envs.append("myenv%s%senv" % (n.ref.name, n.ref.version))
+        self.assertEqual(conanfile.deps_cpp_info.libs, libs)
+        env = {"MYENV": envs} if envs else {}
+        self.assertEqual(conanfile.deps_env_info.vars, env)
+
+
+class TransitiveGraphTest(GraphManagerTest):
+    def test_basic(self):
+        # say/0.1
+        deps_graph = self.build_graph(TestConanFile("Say", "0.1"))
+        self.assertEqual(1, len(deps_graph.nodes))
+        node = deps_graph.root
+        self.assertEqual(node.conanfile.name, "Say")
+        self.assertEqual(len(node.dependencies), 0)
+        self.assertEqual(len(node.dependants), 0)
+
+    def test_transitive(self):
+        # app -> libb0.1
+        libb_ref = "libb/0.1@user/testing"
+        self._cache_recipe(libb_ref, TestConanFile("libb", "0.1"))
+        deps_graph = self.build_graph(TestConanFile("app", "0.1",
+                                                    requires=[libb_ref]))
+        self.assertEqual(2, len(deps_graph.nodes))
+        app = deps_graph.root
+        self.assertEqual(app.conanfile.name, "app")
+        self.assertEqual(len(app.dependencies), 1)
+        self.assertEqual(len(app.dependants), 0)
+        self.assertEqual(app.recipe, RECIPE_CONSUMER)
+
+        libb = app.dependencies[0].dst
+        self.assertEqual(libb.conanfile.name, "libb")
+        self.assertEqual(len(libb.dependencies), 0)
+        self.assertEqual(len(libb.dependants), 1)
+        self.assertEqual(libb.inverse_neighbors(), [app])
+        self.assertEqual(libb.ancestors, set([app.ref.name]))
+        self.assertEqual(libb.recipe, RECIPE_INCACHE)
+
+        self.assertEqual(app.public_closure, [libb])
+        self.assertEqual(libb.public_closure, [])
+        self.assertEqual(app.public_deps, {"app": app, "libb": libb})
+        self.assertEqual(libb.public_deps, app.public_deps)
+
+    def test_transitive_two_levels(self):
+        # app -> libb0.1 -> liba0.1
+        liba_ref = "liba/0.1@user/testing"
+        libb_ref = "libb/0.1@user/testing"
+        self._cache_recipe(liba_ref, TestConanFile("liba", "0.1"))
+        self._cache_recipe(libb_ref, TestConanFile("libb", "0.1", requires=[liba_ref]))
+        deps_graph = self.build_graph(TestConanFile("app", "0.1", requires=[libb_ref]))
+
+        self.assertEqual(3, len(deps_graph.nodes))
+        app = deps_graph.root
+        libb = app.dependencies[0].dst
+        liba = libb.dependencies[0].dst
+
+        # No Revision??? Because of consumer?
+        self._check_node(app, "app/0.1@None/None", deps=[libb], build_deps=[], dependents=[],
+                         closure=[libb, liba], public_deps=[app, libb, liba])
+        self._check_node(libb, "libb/0.1@user/testing#123", deps=[liba], build_deps=[],
+                         dependents=[app], closure=[liba], public_deps=[app, libb, liba])
+        self._check_node(liba, "liba/0.1@user/testing#123", deps=[], build_deps=[],
+                         dependents=[libb], closure=[], public_deps=[app, libb, liba])
+
+    def test_diamond(self):
+        # app -> libb0.1 -> liba0.1
+        #    \-> libc0.1 ->/
+        liba_ref = "liba/0.1@user/testing"
+        libb_ref = "libb/0.1@user/testing"
+        libc_ref = "libc/0.1@user/testing"
+        self._cache_recipe(liba_ref, TestConanFile("liba", "0.1"))
+        self._cache_recipe(libb_ref, TestConanFile("libb", "0.1", requires=[liba_ref]))
+        self._cache_recipe(libc_ref, TestConanFile("libc", "0.1", requires=[liba_ref]))
+        deps_graph = self.build_graph(TestConanFile("app", "0.1", requires=[libb_ref, libc_ref]))
+
+        self.assertEqual(4, len(deps_graph.nodes))
+        app = deps_graph.root
+        libb = app.dependencies[0].dst
+        libc = app.dependencies[1].dst
+        liba = libb.dependencies[0].dst
+
+        # No Revision??? Because of consumer?
+        self._check_node(app, "app/0.1@None/None", deps=[libb, libc], build_deps=[], dependents=[],
+                         closure=[libb, libc, liba], public_deps=[app, libb, libc, liba])
+        self._check_node(libb, "libb/0.1@user/testing#123", deps=[liba], build_deps=[],
+                         dependents=[app], closure=[liba], public_deps=[app, libb, libc, liba])
+        self._check_node(libb, "libb/0.1@user/testing#123", deps=[liba], build_deps=[],
+                         dependents=[app], closure=[liba], public_deps=[app, libb, libc, liba])
+        self._check_node(liba, "liba/0.1@user/testing#123", deps=[], build_deps=[],
+                         dependents=[libb, libc], closure=[], public_deps=[app, libb, libc, liba])
+
+    def test_diamond_conflict(self):
+        # app -> libb0.1 -> liba0.1
+        #    \-> libc0.1 -> liba0.2
+        liba_ref = "liba/0.1@user/testing"
+        liba_ref2 = "liba/0.2@user/testing"
+        libb_ref = "libb/0.1@user/testing"
+        libc_ref = "libc/0.1@user/testing"
+        self._cache_recipe(liba_ref, TestConanFile("liba", "0.1"))
+        self._cache_recipe(liba_ref2, TestConanFile("liba", "0.2"))
+        self._cache_recipe(libb_ref, TestConanFile("libb", "0.1", requires=[liba_ref]))
+        self._cache_recipe(libc_ref, TestConanFile("libc", "0.1", requires=[liba_ref2]))
+        with self.assertRaisesRegexp(ConanException, "Requirement liba/0.2@user/testing conflicts"):
+            self.build_graph(TestConanFile("app", "0.1", requires=[libb_ref, libc_ref]))
+
+    def test_loop(self):
+        # app -> libc0.1 -> libb0.1 -> liba0.1 ->|
+        #             \<-------------------------|
+        liba_ref = "liba/0.1@user/testing"
+        libb_ref = "libb/0.1@user/testing"
+        libc_ref = "libc/0.1@user/testing"
+        self._cache_recipe(liba_ref, TestConanFile("liba", "0.1", requires=[libc_ref]))
+        self._cache_recipe(libb_ref, TestConanFile("libb", "0.1", requires=[liba_ref]))
+        self._cache_recipe(libc_ref, TestConanFile("libc", "0.1", requires=[libb_ref]))
+        with self.assertRaisesRegexp(ConanException, "Loop detected: 'liba/0.1@user/testing' "
+                                     "requires 'libc/0.1@user/testing'"):
+            self.build_graph(TestConanFile("app", "0.1", requires=[libc_ref]))
+
+    @parameterized.expand([("recipe", ), ("profile", )])
+    def test_basic_build_require(self, build_require):
+        # app -(br)-> tool0.1
+        self._cache_recipe("tool/0.1@user/testing", TestConanFile("tool", "0.1"))
+        if build_require == "recipe":
+            deps_graph = self.build_graph(TestConanFile("app", "0.1",
+                                                        build_requires=["tool/0.1@user/testing"]))
+        else:
+            profile_build_requires = {"*": [ConanFileReference.loads("tool/0.1@user/testing")]}
+            deps_graph = self.build_graph(TestConanFile("app", "0.1"),
+                                          profile_build_requires=profile_build_requires)
+
+        # Build requires always apply to the consumer
+        self.assertEqual(2, len(deps_graph.nodes))
+        app = deps_graph.root
+        tool = app.dependencies[0].dst
+
+        self._check_node(app, "app/0.1@None/None", deps=[], build_deps=[tool], dependents=[],
+                         closure=[tool], public_deps=[app])
+        self._check_node(tool, "tool/0.1@user/testing#123", deps=[], build_deps=[],
+                         dependents=[app], closure=[], public_deps=[tool, app])
+
+    def test_transitive_build_require_recipe(self):
+        # app -> lib -(br)-> tool
+        self._cache_recipe("tool/0.1@user/testing", TestConanFile("tool", "0.1"))
+        self._cache_recipe("lib/0.1@user/testing",
+                           TestConanFile("lib", "0.1",
+                                         build_requires=["tool/0.1@user/testing"]))
+        deps_graph = self.build_graph(TestConanFile("app", "0.1",
+                                                    requires=["lib/0.1@user/testing"]))
+
+        self.assertEqual(3, len(deps_graph.nodes))
+        app = deps_graph.root
+        lib = app.dependencies[0].dst
+        tool = lib.dependencies[0].dst
+
+        self._check_node(app, "app/0.1@None/None", deps=[lib], build_deps=[], dependents=[],
+                         closure=[lib], public_deps=[app, lib])
+
+        self._check_node(lib, "lib/0.1@user/testing#123", deps=[], build_deps=[tool],
+                         dependents=[app], closure=[tool], public_deps=[app, lib])
+
+        self._check_node(tool, "tool/0.1@user/testing#123", deps=[], build_deps=[],
+                         dependents=[lib], closure=[], public_deps=[tool, lib])
+
+    def test_loop_build_require(self):
+        # app -> lib -(br)-> tool ->|
+        #          \<---------------|
+        lib_ref = "lib/0.1@user/testing"
+        self._cache_recipe("tool/0.1@user/testing", TestConanFile("tool", "0.1", requires=[lib_ref]))
+        self._cache_recipe(lib_ref,
+                           TestConanFile("lib", "0.1",
+                                         build_requires=["tool/0.1@user/testing"]))
+        with self.assertRaisesRegexp(ConanException, "Loop detected: 'tool/0.1@user/testing' "
+                                     "requires 'lib/0.1@user/testing'"):
+            self.build_graph(TestConanFile("app", "0.1", requires=["lib/0.1@user/testing"]))
+
+    def test_transitive_build_require_recipe_profile(self):
+        # app -> lib -(br)-> gtest -(br)-> mingw
+        # profile \---(br)-> mingw
+        # app -(br)-> mingw
+        self._cache_recipe("mingw/0.1@user/testing", TestConanFile("mingw", "0.1"))
+        self._cache_recipe("gtest/0.1@user/testing", TestConanFile("gtest", "0.1"))
+        self._cache_recipe("lib/0.1@user/testing",
+                           TestConanFile("lib", "0.1",
+                                         build_requires=["gtest/0.1@user/testing"]))
+        profile_build_requires = {"*": [ConanFileReference.loads("mingw/0.1@user/testing")]}
+        deps_graph = self.build_graph(TestConanFile("app", "0.1",
+                                                    requires=["lib/0.1@user/testing"]),
+                                      profile_build_requires=profile_build_requires)
+
+        self.assertEqual(6, len(deps_graph.nodes))
+        app = deps_graph.root
+        lib = app.dependencies[0].dst
+        gtest = lib.dependencies[0].dst
+        mingw_gtest = gtest.dependencies[0].dst
+        mingw_lib = lib.dependencies[1].dst
+        mingw_app = app.dependencies[1].dst
+
+        self._check_node(app, "app/0.1@None/None", deps=[lib], build_deps=[mingw_app], dependents=[],
+                         closure=[mingw_app, lib], public_deps=[app, lib])
+
+        self._check_node(lib, "lib/0.1@user/testing#123", deps=[], build_deps=[mingw_lib, gtest],
+                         dependents=[lib], closure=[mingw_lib, gtest], public_deps=[app, lib])
+        self._check_node(gtest, "gtest/0.1@user/testing#123", deps=[], build_deps=[mingw_gtest],
+                         dependents=[lib], closure=[mingw_gtest], public_deps=[gtest, lib])
+        # MinGW leaf nodes
+        self._check_node(mingw_gtest, "mingw/0.1@user/testing#123", deps=[], build_deps=[],
+                         dependents=[gtest], closure=[], public_deps=[mingw_gtest, gtest])
+        self._check_node(mingw_lib, "mingw/0.1@user/testing#123", deps=[], build_deps=[],
+                         dependents=[lib], closure=[], public_deps=[mingw_lib, gtest, lib])
+        self._check_node(mingw_app, "mingw/0.1@user/testing#123", deps=[], build_deps=[],
+                         dependents=[app], closure=[], public_deps=[mingw_app, lib, app])
+
+    def test_conflict_transitive_build_requires(self):
+        zlib_ref = "zlib/0.1@user/testing"
+        zlib_ref2 = "zlib/0.2@user/testing"
+        self._cache_recipe(zlib_ref, TestConanFile("zlib", "0.1"))
+        self._cache_recipe(zlib_ref2, TestConanFile("zlib", "0.2"))
+
+        self._cache_recipe("gtest/0.1@user/testing", TestConanFile("gtest", "0.1",
+                                                                   requires=[zlib_ref2]))
+        self._cache_recipe("lib/0.1@user/testing",
+                           TestConanFile("lib", "0.1", requires=[zlib_ref],
+                                         build_requires=["gtest/0.1@user/testing"]))
+
+        with self.assertRaisesRegexp(ConanException, "Requirement zlib/0.2@user/testing conflicts"):
+            self.build_graph(TestConanFile("app", "0.1", requires=["lib/0.1@user/testing"]))
+
+    def test_not_conflict_transitive_build_requires(self):
+        # Same as above, but gtest->(build_require)->zlib2
+        zlib_ref = "zlib/0.1@user/testing"
+        zlib_ref2 = "zlib/0.2@user/testing"
+        self._cache_recipe(zlib_ref, TestConanFile("zlib", "0.1"))
+        self._cache_recipe(zlib_ref2, TestConanFile("zlib", "0.2"))
+
+        self._cache_recipe("gtest/0.1@user/testing", TestConanFile("gtest", "0.1",
+                                                                   build_requires=[zlib_ref2]))
+        self._cache_recipe("lib/0.1@user/testing",
+                           TestConanFile("lib", "0.1", requires=[zlib_ref],
+                                         build_requires=["gtest/0.1@user/testing"]))
+
+        graph = self.build_graph(TestConanFile("app", "0.1", requires=["lib/0.1@user/testing"]))
+
+        self.assertEqual(5, len(graph.nodes))
+        app = graph.root
+        lib = app.dependencies[0].dst
+        zlib = lib.dependencies[0].dst
+        gtest = lib.dependencies[1].dst
+        zlib2 = gtest.dependencies[0].dst
+        self._check_node(app, "app/0.1@None/None", deps=[lib], build_deps=[], dependents=[],
+                         closure=[lib, zlib], public_deps=[app, lib, zlib])
+
+        self._check_node(lib, "lib/0.1@user/testing#123", deps=[zlib], build_deps=[gtest],
+                         dependents=[app], closure=[gtest, zlib], public_deps=[app, lib, zlib])
+
+        self._check_node(gtest, "gtest/0.1@user/testing#123", deps=[], build_deps=[zlib2],
+                         dependents=[lib], closure=[zlib2], public_deps=[gtest, lib, zlib])
+
+    def test_diamond_no_option_conflict_build_requires(self):
+        # Same as above, but gtest->(build_require)->zlib2
+        zlib_ref = "zlib/0.1@user/testing"
+        self._cache_recipe(zlib_ref, TestConanFile("zlib", "0.1",
+                                                   options='{"shared": [True, False]}',
+                                                   default_options='shared=False'))
+
+        self._cache_recipe("gtest/0.1@user/testing",
+                           TestConanFile("gtest", "0.1", requires=[zlib_ref],
+                                         default_options='zlib:shared=True'))
+        self._cache_recipe("lib/0.1@user/testing",
+                           TestConanFile("lib", "0.1", requires=[zlib_ref],
+                                         build_requires=["gtest/0.1@user/testing"]))
+
+        graph = self.build_graph(TestConanFile("app", "0.1", requires=["lib/0.1@user/testing"]))
+
+        self.assertEqual(4, len(graph.nodes))
+        app = graph.root
+        lib = app.dependencies[0].dst
+        zlib = lib.dependencies[0].dst
+        self.assertFalse(bool(zlib.conanfile.options.shared))
+        gtest = lib.dependencies[1].dst
+        zlib2 = gtest.dependencies[0].dst
+        self.assertIs(zlib, zlib2)
+        self._check_node(app, "app/0.1@None/None", deps=[lib], build_deps=[], dependents=[],
+                         closure=[lib, zlib], public_deps=[app, lib, zlib])
+
+        self._check_node(lib, "lib/0.1@user/testing#123", deps=[zlib], build_deps=[gtest],
+                         dependents=[app], closure=[gtest, zlib], public_deps=[app, lib, zlib])
+
+        self._check_node(gtest, "gtest/0.1@user/testing#123", deps=[zlib2], build_deps=[],
+                         dependents=[lib], closure=[zlib2], public_deps=[gtest, lib, zlib])
+
+    def test_diamond_option_conflict_build_requires(self):
+        # Same as above, but gtest->(build_require)->zlib2
+        zlib_ref = "zlib/0.1@user/testing"
+        self._cache_recipe(zlib_ref, TestConanFile("zlib", "0.1",
+                                                   options='{"shared": [True, False]}',
+                                                   default_options='shared=False'))
+        configure = """
+    def configure(self):
+        self.options["zlib"].shared=True
+        """
+        gtest = str(TestConanFile("gtest", "0.1", requires=[zlib_ref])) + configure
+        self._cache_recipe("gtest/0.1@user/testing", gtest)
+        self._cache_recipe("lib/0.1@user/testing",
+                           TestConanFile("lib", "0.1", requires=[zlib_ref],
+                                         build_requires=["gtest/0.1@user/testing"]))
+
+        with self.assertRaisesRegexp(ConanException,
+                                     "tried to change zlib/0.1@user/testing option shared to True"):
+            self.build_graph(TestConanFile("app", "0.1", requires=["lib/0.1@user/testing"]))
+
+    def test_conflict_private(self):
+        liba_ref = "liba/0.1@user/testing"
+        liba_ref2 = "liba/0.2@user/testing"
+        libb_ref = "libb/0.1@user/testing"
+        libc_ref = "libc/0.1@user/testing"
+        self._cache_recipe(liba_ref, TestConanFile("liba", "0.1"))
+        self._cache_recipe(liba_ref2, TestConanFile("liba", "0.2"))
+        self._cache_recipe(libb_ref, TestConanFile("libb", "0.1", requires=[liba_ref]))
+        self._cache_recipe(libc_ref, TestConanFile("libc", "0.1", requires=[liba_ref2]))
+        with self.assertRaisesRegexp(ConanException, "Requirement liba/0.2@user/testing conflicts"):
+            self.build_graph(TestConanFile("app", "0.1", private_requires=[libb_ref, libc_ref]))
+
+    def test_loop_private(self):
+        # app -> lib -(private)-> tool ->|
+        #          \<-----(private)------|
+        lib_ref = "lib/0.1@user/testing"
+        self._cache_recipe("tool/0.1@user/testing", TestConanFile("tool", "0.1",
+                                                                  private_requires=[lib_ref]))
+        self._cache_recipe(lib_ref,
+                           TestConanFile("lib", "0.1",
+                                         private_requires=["tool/0.1@user/testing"]))
+        with self.assertRaisesRegexp(ConanException, "Loop detected: 'tool/0.1@user/testing' "
+                                     "requires 'lib/0.1@user/testing'"):
+            self.build_graph(TestConanFile("app", "0.1", requires=[lib_ref]))
+
+    def test_build_require_private(self):
+        # app -> lib -(br)-> tool -(private)-> zlib
+        zlib_ref = "zlib/0.1@user/testing"
+        self._cache_recipe(zlib_ref, TestConanFile("zlib", "0.1"))
+        self._cache_recipe("tool/0.1@user/testing", TestConanFile("tool", "0.1",
+                                                                  private_requires=[zlib_ref]))
+        self._cache_recipe("lib/0.1@user/testing",
+                           TestConanFile("lib", "0.1",
+                                         build_requires=["tool/0.1@user/testing"]))
+        deps_graph = self.build_graph(TestConanFile("app", "0.1",
+                                                    requires=["lib/0.1@user/testing"]))
+
+        self.assertEqual(4, len(deps_graph.nodes))
+        app = deps_graph.root
+        lib = app.dependencies[0].dst
+        tool = lib.dependencies[0].dst
+        zlib = tool.dependencies[0].dst
+
+        self._check_node(app, "app/0.1@None/None", deps=[lib], build_deps=[], dependents=[],
+                         closure=[lib], public_deps=[app, lib])
+
+        self._check_node(lib, "lib/0.1@user/testing#123", deps=[], build_deps=[tool],
+                         dependents=[app], closure=[tool], public_deps=[app, lib])
+
+        self._check_node(tool, "tool/0.1@user/testing#123", deps=[zlib], build_deps=[],
+                         dependents=[lib], closure=[zlib], public_deps=[tool, lib])
+
+        self._check_node(zlib, "zlib/0.1@user/testing#123", deps=[], build_deps=[],
+                         dependents=[tool], closure=[], public_deps=[zlib])
diff --git a/conans/test/functional/graph/loop_detection_test.py b/conans/test/functional/graph/loop_detection_test.py
index e9114e9c4..ba30b1fdf 100644
--- a/conans/test/functional/graph/loop_detection_test.py
+++ b/conans/test/functional/graph/loop_detection_test.py
@@ -23,6 +23,6 @@ class Package{number}Conan(ConanFile):
             client.run("export . lasote/stable")
 
         client.run("install Package3/0.1@lasote/stable --build", assert_error=True)
-        self.assertIn("ERROR: Loop detected: Package3/0.1@lasote/stable->"
-                      "Package1/0.1@lasote/stable->Package2/0.1@lasote/stable",
-                      client.user_io.out)
+        self.assertIn("ERROR: Loop detected: 'Package2/0.1@lasote/stable' requires "
+                      "'Package3/0.1@lasote/stable' which is an ancestor too",
+                      client.out)
diff --git a/conans/test/functional/graph/package_id_modes_test.py b/conans/test/functional/graph/package_id_modes_test.py
new file mode 100644
index 000000000..b8faeffe5
--- /dev/null
+++ b/conans/test/functional/graph/package_id_modes_test.py
@@ -0,0 +1,35 @@
+from conans.test.functional.graph.graph_manager_base import GraphManagerTest
+from conans.test.utils.conanfile import TestConanFile
+
+
+class PackageIDGraphTests(GraphManagerTest):
+
+    def test_recipe_modes(self):
+        configs = []
+        mode = "semver_mode"
+        configs.append((mode, "liba/1.1.1@user/testing", "8ecbf93ba63522ffb32573610c80ab4dcb399b52"))
+        configs.append((mode, "liba/1.1.2@user/testing", "8ecbf93ba63522ffb32573610c80ab4dcb399b52"))
+        configs.append((mode, "liba/1.2.1@other/stable", "8ecbf93ba63522ffb32573610c80ab4dcb399b52"))
+        mode = "patch_mode"
+        configs.append((mode, "liba/1.1.1@user/testing", "bd664570d5174c601d5d417bc19257c4dba48f2e"))
+        configs.append((mode, "liba/1.1.2@user/testing", "fb1f766173191d44b67156c6b9ac667422e99286"))
+        configs.append((mode, "liba/1.1.1@other/stable", "bd664570d5174c601d5d417bc19257c4dba48f2e"))
+        mode = "full_recipe_mode"
+        configs.append((mode, "liba/1.1.1@user/testing", "9cbe703e1dee73a2a6807f71d8551c5f1e1b08fd"))
+        configs.append((mode, "liba/1.1.2@user/testing", "42a9ff9024adabbd54849331cf01be7d95139948"))
+        configs.append((mode, "liba/1.1.1@user/stable", "b41d6c026473cffed4abded4b0eaa453497be1d2"))
+
+        for package_id_mode, ref, package_id in configs:
+            self.cache.config.set_item("general.default_package_id_mode", package_id_mode)
+            libb_ref = "libb/0.1@user/testing"
+            self._cache_recipe(ref, TestConanFile("liba", "0.1.1"))
+            self._cache_recipe(libb_ref, TestConanFile("libb", "0.1", requires=[ref]))
+            deps_graph = self.build_graph(TestConanFile("app", "0.1", requires=[libb_ref]))
+
+            self.assertEqual(3, len(deps_graph.nodes))
+            app = deps_graph.root
+            libb = app.dependencies[0].dst
+            liba = libb.dependencies[0].dst
+
+            self.assertEqual(liba.package_id, "5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9")
+            self.assertEqual(libb.package_id, package_id)
diff --git a/conans/test/integration/build_id_test.py b/conans/test/integration/build_id_test.py
index 82b6bf35f..3a5405148 100644
--- a/conans/test/integration/build_id_test.py
+++ b/conans/test/integration/build_id_test.py
@@ -9,22 +9,18 @@ from conans.util.files import load
 
 conanfile = """from conans import ConanFile
 from conans.util.files import save
-
 class MyTest(ConanFile):
     name = "Pkg"
     version = "0.1"
     settings = "os", "build_type"
     build_policy = "missing"
-
     def build_id(self):
         if self.settings.os == "Windows":
             self.info_build.settings.build_type = "Any"
-
     def build(self):
         self.output.info("Building my code!")
         save("debug/file1.txt", "Debug file1")
         save("release/file1.txt", "Release file1")
-
     def package(self):
         self.output.info("Packaging %s!" % self.settings.build_type)
         if self.settings.build_type == "Debug":
@@ -40,18 +36,14 @@ Pkg/0.1@user/channel
 """
 
 consumer_py = """from conans import ConanFile
-
-
 class MyTest(ConanFile):
     name = "MyTest"
     version = "0.1"
     settings = "os", "build_type"
     requires = "Pkg/0.1@user/channel"
-
     def build_id(self):
         self.info_build.settings.build_type = "Any"
         self.info_build.requires.clear()
-
     def imports(self):
         self.copy("*")
 """
@@ -120,6 +112,7 @@ class BuildIdTest(unittest.TestCase):
             client.save({"conanfile.py": consumer_py}, clean_first=True)
         else:
             client.save({"conanfile.txt": consumer}, clean_first=True)
+        # Windows Debug
         client.run('install . -s os=Windows -s build_type=Debug')
         self.assertIn("Building package from source as defined by build_policy='missing'",
                       client.user_io.out)
@@ -127,12 +120,14 @@ class BuildIdTest(unittest.TestCase):
         self.assertIn("Packaging Debug!", client.user_io.out)
         content = load(os.path.join(client.current_folder, "file1.txt"))
         self.assertEqual("Debug file1", content)
+        # Package Windows Release, it will reuse the previous build
         client.run('install . -s os=Windows -s build_type=Release')
         self.assertNotIn("Building my code!", client.user_io.out)
         self.assertIn("Packaging Release!", client.user_io.out)
         content = load(os.path.join(client.current_folder, "file1.txt"))
         self.assertEqual("Release file1", content)
-        # Now Linux
+
+        # Now Linux Debug
         client.run('install . -s os=Linux -s build_type=Debug')
         self.assertIn("Building package from source as defined by build_policy='missing'",
                       client.user_io.out)
@@ -140,6 +135,7 @@ class BuildIdTest(unittest.TestCase):
         self.assertIn("Packaging Debug!", client.user_io.out)
         content = load(os.path.join(client.current_folder, "file1.txt"))
         self.assertEqual("Debug file1", content)
+        # Linux Release must build again, as it is not affected by build_id()
         client.run('install . -s os=Linux -s build_type=Release')
         self.assertIn("Building my code!", client.user_io.out)
         self.assertIn("Packaging Release!", client.user_io.out)
@@ -149,11 +145,13 @@ class BuildIdTest(unittest.TestCase):
 
         # Check that repackaging works, not necessary to re-build
         client.run("remove Pkg/0.1@user/channel -p -f")
+        # Windows Debug
         client.run('install . -s os=Windows -s build_type=Debug')
         self.assertNotIn("Building my code!", client.user_io.out)
         self.assertIn("Packaging Debug!", client.user_io.out)
         content = load(os.path.join(client.current_folder, "file1.txt"))
         self.assertEqual("Debug file1", content)
+        # Windows Release
         client.run('install . -s os=Windows -s build_type=Release')
         self.assertNotIn("Building my code!", client.user_io.out)
         self.assertIn("Packaging Release!", client.user_io.out)
diff --git a/conans/test/integration/revisions_test.py b/conans/test/integration/revisions_test.py
index adc5dec04..3a65e6b63 100644
--- a/conans/test/integration/revisions_test.py
+++ b/conans/test/integration/revisions_test.py
@@ -365,6 +365,16 @@ class InstallingPackagesWithRevisionsTest(unittest.TestCase):
             client.run(command, assert_error=True)
             self.assertIn("Can't find a '{}' package".format(self.ref), client.out)
 
+    def test_json_output(self):
+        client = TurboTestClient()
+        client.save({"conanfile.py": str(GenConanfile())})
+        client.run("create . {} --json file.json".format(self.ref.full_repr()))
+        json_path = os.path.join(client.current_folder, "file.json")
+        import json
+        data = json.loads(load(json_path))
+        ref = ConanFileReference.loads(data["installed"][0]["recipe"]["id"])
+        self.assertIsNotNone(ref.revision)
+
 
 @unittest.skipUnless(get_env("TESTING_REVISIONS_ENABLED", False), "Only revisions")
 class RevisionsInLocalCacheTest(unittest.TestCase):
diff --git a/conans/test/unittests/client/recorder/action_recorder_test.py b/conans/test/unittests/client/recorder/action_recorder_test.py
index a3a925e31..a799dda72 100644
--- a/conans/test/unittests/client/recorder/action_recorder_test.py
+++ b/conans/test/unittests/client/recorder/action_recorder_test.py
@@ -20,7 +20,7 @@ class ActionRecorderTest(unittest.TestCase):
         tracer = ActionRecorder()
         tracer.recipe_install_error(self.ref1, INSTALL_ERROR_NETWORK, "SSL wtf", "http://drl.com")
         tracer.add_recipe_being_developed(self.ref1)
-        install_info = tracer.get_info()
+        install_info = tracer.get_info(False)
         self.assertTrue(install_info["error"])
         self.assertEquals(install_info["installed"][0]["packages"], [])
         self.assertEquals(install_info["installed"][0]["recipe"]["dependency"], False)
@@ -32,7 +32,7 @@ class ActionRecorderTest(unittest.TestCase):
         tracer.package_downloaded(self.pref1, "http://drl.com")
         tracer.package_fetched_from_cache(self.pref1)
 
-        install_info = tracer.get_info()
+        install_info = tracer.get_info(False)
         self.assertFalse(install_info["error"])
 
         first_installed = install_info["installed"][0]
@@ -59,7 +59,7 @@ class ActionRecorderTest(unittest.TestCase):
         tracer.package_built(self.pref3)
         tracer.add_recipe_being_developed(self.ref1)
 
-        install_info = tracer.get_info()
+        install_info = tracer.get_info(False)
         self.assertTrue(install_info["error"])
 
         first_installed = install_info["installed"][0]
diff --git a/conans/test/unittests/model/transitive_reqs_test.py b/conans/test/unittests/model/transitive_reqs_test.py
index 1fd63b587..852ed0d66 100644
--- a/conans/test/unittests/model/transitive_reqs_test.py
+++ b/conans/test/unittests/model/transitive_reqs_test.py
@@ -1,10 +1,16 @@
 import unittest
 from collections import namedtuple, Counter
 
+from mock import Mock
+
 from conans import DEFAULT_REVISION_V1
+from conans.client.cache.cache import ClientCache
 from conans.client.conf import default_settings_yml
+from conans.client.graph.build_mode import BuildMode
+from conans.client.graph.graph_binaries import GraphBinariesAnalyzer
 from conans.client.graph.graph_builder import DepsGraphBuilder
 from conans.client.graph.python_requires import ConanPythonRequire
+from conans.client.graph.range_resolver import RangeResolver
 from conans.client.loader import ConanFileLoader
 from conans.errors import ConanException
 from conans.model.options import OptionsValues, option_not_exist_msg, option_wrong_value_msg
@@ -14,17 +20,9 @@ from conans.model.requires import Requirements
 from conans.model.settings import Settings, bad_value_msg
 from conans.model.values import Values
 from conans.test.unittests.model.fake_retriever import Retriever
-from conans.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestBufferConanOutput,\
-    test_processed_profile
-import os
-
-from mock import Mock
-
-from conans.client.cache.cache import ClientCache
-from conans.client.graph.build_mode import BuildMode
-from conans.client.graph.graph_binaries import GraphBinariesAnalyzer
-from conans.client.graph.range_resolver import RangeResolver
 from conans.test.utils.conanfile import TestConanFile
+from conans.test.utils.tools import (NO_SETTINGS_PACKAGE_ID, TestBufferConanOutput,
+                                     test_processed_profile)
 
 
 say_content = TestConanFile("Say", "0.1")
@@ -88,6 +86,7 @@ class GraphTest(unittest.TestCase):
         self.builder = DepsGraphBuilder(self.retriever, self.output, self.loader,
                                         self.resolver, None)
         cache = Mock()
+        cache.config.default_package_id_mode = "semver_direct_mode"
         remote_manager = None
         self.binaries_analyzer = GraphBinariesAnalyzer(cache, self.output, remote_manager)
 
@@ -1100,8 +1099,8 @@ class HelloConan(ConanFile):
     Previous requirements: [Base/0.1@user/testing, png/0.1@user/testing]
     New requirements: [Base/0.1@user/testing, Zlib/0.1@user/testing]"""
         try:
-            _ = self.build_graph(TestConanFile("Chat", "2.3", requires=["Say/0.1@user/testing",
-                                                                        "Hello/1.2@user/testing"]))
+            self.build_graph(TestConanFile("Chat", "2.3", requires=["Say/0.1@user/testing",
+                                                                    "Hello/1.2@user/testing"]))
             self.assert_(False, "Exception not thrown")
         except ConanException as e:
             self.assertEqual(str(e), expected)
diff --git a/conans/test/utils/conanfile.py b/conans/test/utils/conanfile.py
index 0be3bb807..6fbfe986e 100644
--- a/conans/test/utils/conanfile.py
+++ b/conans/test/utils/conanfile.py
@@ -58,26 +58,36 @@ class MockConanfile(ConanFile):
 
 class TestConanFile(object):
     def __init__(self, name="Hello", version="0.1", settings=None, requires=None, options=None,
-                 default_options=None, package_id=None):
+                 default_options=None, package_id=None, build_requires=None, info=None,
+                 private_requires=None):
         self.name = name
         self.version = version
         self.settings = settings
         self.requires = requires
+        self.private_requires = private_requires
+        self.build_requires = build_requires
         self.options = options
         self.default_options = default_options
         self.package_id = package_id
+        self.info = info
 
     def __repr__(self):
         base = """from conans import ConanFile
 
-class %sConan(ConanFile):
-    name = "%s"
-    version = "%s"
-""" % (self.name, self.name, self.version)
+class {name}Conan(ConanFile):
+    name = "{name}"
+    version = "{version}"
+""".format(name=self.name, version=self.version)
         if self.settings:
             base += "    settings = %s\n" % self.settings
         if self.requires:
             base += "    requires = %s\n" % (", ".join('"%s"' % r for r in self.requires))
+        if self.private_requires:
+            base += "    requires = %s,\n" % (", ".join("('%s', 'private')" % r
+                                                        for r in self.private_requires))
+        if self.build_requires:
+            base += "    build_requires = %s\n" % (", ".join('"%s"' % r
+                                                             for r in self.build_requires))
         if self.options:
             base += "    options = %s\n" % str(self.options)
         if self.default_options:
@@ -87,6 +97,12 @@ class %sConan(ConanFile):
                 base += "    default_options = %s\n" % str(self.default_options)
         if self.package_id:
             base += "    def package_id(self):\n        %s\n" % self.package_id
+        if self.info:
+            base += """
+    def package_info(self):
+        self.cpp_info.libs = ["mylib{name}{version}lib"]
+        self.env_info.MYENV = ["myenv{name}{version}env"]
+""".format(name=self.name, version=self.version)
         return base