Make `gn edit move` idempotent.

`gn check --fix` currently outputs errors when it tries to apply the
same move twice. This fixes move to be a no-op when it's already
applied, similar to other commands.

Change-Id: Id0b8182d3fc8e9f9d6d308951f387f766a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/26220
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Commit-Queue: Matt Stark <msta@google.com>
diff --git a/src/gn/edit_command_unittest.cc b/src/gn/edit_command_unittest.cc
index 7673738..31276ea 100644
--- a/src/gn/edit_command_unittest.cc
+++ b/src/gn/edit_command_unittest.cc
@@ -338,6 +338,20 @@
   public_deps = [ "//a" ]
 }
 )"));
+
+  // Moving a value that already exists in the destination attribute should be
+  // a no-op with no warnings.
+  EXPECT_SUCCESS(DoEdit("move deps public_deps //a",
+                        R"(
+executable("foo") {
+  public_deps = [ "//a" ]
+}
+)"),
+                 Edited(R"(
+executable("foo") {
+  public_deps = [ "//a" ]
+}
+)"));
 }
 
 TEST_F(EditCommandTest, NewSubcommand) {
diff --git a/src/gn/edit_subcommands.cc b/src/gn/edit_subcommands.cc
index b2b7de4..9f6dd23 100644
--- a/src/gn/edit_subcommands.cc
+++ b/src/gn/edit_subcommands.cc
@@ -117,7 +117,8 @@
 bool RemoveFromTarget(const EditTarget& target,
                       const std::string& attribute,
                       const Value& value,
-                      EditState& state) {
+                      EditState& state,
+                      bool warn_if_missing = true) {
   bool done = false;
   for (auto& assignment : target.assignments(attribute)) {
     auto matches = FindListElementInAssignment(target, assignment, value);
@@ -156,7 +157,7 @@
         }
       }
     }
-  } else if (target.is_explicit) {
+  } else if (target.is_explicit && warn_if_missing) {
     target.add_warning(state, "does not contain the value " +
                                   value.ToString(true) + " in attribute \"" +
                                   attribute + "\".");
@@ -269,6 +270,23 @@
   });
 }
 
+// Returns whether |target| contains |value| in |attribute|.
+// Assignments using `-=` are filtered out.
+bool AttributeContainsValue(const EditTarget& target,
+                            std::string_view attribute,
+                            const Value& value) {
+  for (const auto& assignment : target.assignments(attribute)) {
+    if (const auto* op = assignment.node()->AsBinaryOp();
+        op && op->op().type() == Token::MINUS_EQUALS) {
+      continue;
+    }
+    if (!FindListElementInAssignment(target, assignment, value).empty()) {
+      return true;
+    }
+  }
+  return false;
+}
+
 EditCommand MoveCommand(std::string from_attribute,
                         std::string to_attribute,
                         std::vector<Value> values) {
@@ -279,7 +297,10 @@
                                EditState& state) -> Err {
     std::vector<Value> moved_values;
     for (const auto& value : values) {
-      if (RemoveFromTarget(target, from_attribute, value, state)) {
+      bool warn_if_missing =
+          !AttributeContainsValue(target, to_attribute, value);
+      if (RemoveFromTarget(target, from_attribute, value, state,
+                           warn_if_missing)) {
         moved_values.push_back(value);
       }
     }