Repository: conan-io/conan

Issue:
[question] Is there a way to remove "modified" fields from lockfiles?
<!-- What is your question? Please be as specific as possible! -->

Hello,

I'm playing with lockfiles right now, and my goal is to start a CI job, doing:

* generating a `conan.lock` from the top-level recipe
* rebuilding everything from the modified dependency up to the top-level recipe
* pushing the lockfile to a repo, so that devs and further CI jobs can reuse it

Problem is, the lockfile will contain `modified=built` fields, which make the lockfile unusable by further CI jobs.

@memsharded told me that there was a previous `clean` command, that was removed earlier, reintroducing it might solve the problem.

- [x] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).


Discussion:

Resolving patch:
diff --git a/conans/client/command.py b/conans/client/command.py
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -1782,6 +1782,9 @@ def graph(self, *args):
         build_order_cmd.add_argument("--json", action=OnceArgument,
                                      help="generate output file in json format")
 
+        clean_cmd = subparsers.add_parser('clean-modified', help='Clean modified')
+        clean_cmd.add_argument('lockfile', help='lockfile folder')
+
         lock_cmd = subparsers.add_parser('lock', help='create a lockfile')
         lock_cmd.add_argument("path_or_reference", help="Path to a folder containing a recipe"
                               " (conanfile.py or conanfile.txt) or to a recipe file. e.g., "
@@ -1803,6 +1806,8 @@ def graph(self, *args):
             if args.json:
                 json_file = _make_abs_path(args.json)
                 save(json_file, json.dumps(build_order, indent=True))
+        elif args.subcommand == "clean-modified":
+            self._conan.lock_clean_modified(args.lockfile)
         elif args.subcommand == "lock":
             self._conan.create_lock(args.path_or_reference,
                                     remote_name=args.remote,
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -1225,6 +1225,14 @@ def build_order(self, lockfile, build=None, cwd=None):
             level[:] = [(id_, repr(pref)) for id_, pref in level]
         return build_order
 
+    @api_method
+    def lock_clean_modified(self, lockfile, cwd=None):
+        cwd = cwd or os.getcwd()
+        lockfile = _make_abs_path(lockfile, cwd)
+        lock = GraphLockFile.load(lockfile, self.app.config.revisions_enabled)
+        lock.graph_lock.clean_modified()
+        lock.save(lockfile)
+
     @api_method
     def create_lock(self, reference, remote_name=None, settings=None, options=None, env=None,
                     profile_names=None, update=False, lockfile=None, build=None,):
diff --git a/conans/model/graph_lock.py b/conans/model/graph_lock.py
--- a/conans/model/graph_lock.py
+++ b/conans/model/graph_lock.py
@@ -219,6 +219,12 @@ def update_lock(self, new_lock):
             if node.modified:
                 self._nodes[id_] = node
 
+    def clean_modified(self):
+        """ remove all the "modified" flags from the lockfile
+        """
+        for _, node in self._nodes.items():
+            node.modified = None
+
     def _closure_affected(self):
         """ returns all the IDs of the nodes that depend directly or indirectly of some
         package marked as "modified"


Test patch:
diff --git a/conans/test/functional/graph_lock/graph_lock_ci_test.py b/conans/test/functional/graph_lock/graph_lock_ci_test.py
--- a/conans/test/functional/graph_lock/graph_lock_ci_test.py
+++ b/conans/test/functional/graph_lock/graph_lock_ci_test.py
@@ -4,10 +4,10 @@
 import unittest
 
 from conans.model.graph_lock import LOCKFILE, GraphLockNode
-from conans.model.ref import PackageReference
 from conans.test.utils.tools import TestClient, TestServer
 from conans.util.env_reader import get_env
 from conans.util.files import load
+from conans.model.ref import PackageReference
 
 
 class GraphLockCITest(unittest.TestCase):
@@ -396,6 +396,13 @@ def package_info(self):
         self.assertIn("PkgC/0.1@user/channel: DEP FILE PkgB: ByeB World!!", client.out)
         self.assertIn("PkgD/0.1@user/channel: DEP FILE PkgB: ByeB World!!", client.out)
 
+        dirty_lockfile = client.load(LOCKFILE)
+        self.assertIn('"modified": "%s"' % GraphLockNode.MODIFIED_BUILT, dirty_lockfile)
+        client.run("graph clean-modified .")
+        clean_lockfile = client.load(LOCKFILE)
+        self.assertNotIn('modified', clean_lockfile)
+
+
     def test_version_ranges_diamond(self):
         conanfile = textwrap.dedent("""
             from conans import ConanFile, load