Implement `gn edit "move attr attr value(s)"`

Change-Id: Iadb0b09205bdf7bde614fe7fcd37ae8b6a6a6964
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/25460
Reviewed-by: Takuto Ikuta <tikuta@google.com>
Commit-Queue: Matt Stark <msta@google.com>
diff --git a/docs/reference.md b/docs/reference.md
index 33e1c6d..26ab83c 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -741,6 +741,13 @@
       Example:
         gn edit "delete" //src/tools:old_target
 
+  move <from_attribute> <to_attribute> <value(s)>
+      Moves <value(s)> from the list <from_attribute> to <to_attribute>.
+      If <to_attribute> does not exist, it is created.
+
+      Example:
+        gn edit "move deps public_deps //base" //src/tools:*
+
   remove <attribute>
       Removes <attribute> entirely.
 
diff --git a/src/gn/edit_command.cc b/src/gn/edit_command.cc
index ff4d127..8f02524 100644
--- a/src/gn/edit_command.cc
+++ b/src/gn/edit_command.cc
@@ -48,6 +48,13 @@
     "      Example:\n"
     "        gn edit \"delete\" //src/tools:old_target\n"
     "\n"
+    "  move <from_attribute> <to_attribute> <value(s)>\n"
+    "      Moves <value(s)> from the list <from_attribute> to <to_attribute>.\n"
+    "      If <to_attribute> does not exist, it is created.\n"
+    "\n"
+    "      Example:\n"
+    "        gn edit \"move deps public_deps //base\" //src/tools:*\n"
+    "\n"
     "  remove <attribute>\n"
     "      Removes <attribute> entirely.\n"
     "\n"
diff --git a/src/gn/edit_command_unittest.cc b/src/gn/edit_command_unittest.cc
index 0ec3cfc..e6a0e2c 100644
--- a/src/gn/edit_command_unittest.cc
+++ b/src/gn/edit_command_unittest.cc
@@ -261,6 +261,35 @@
                         EditState({Label(SourceDir("//"), "bar")})));
 }
 
+TEST_F(EditCommandTest, MoveSubcommand) {
+  EXPECT_SUCCESS(
+      DoEdit("move deps public_deps //a //b //nonexistent", {"//:foo"},
+             R"(
+executable("foo") {
+  deps = [
+    "//a",
+    "//b",
+    "//c",
+  ]
+  public_deps = [ "//d" ]
+}
+)"),
+      Edited(R"(
+executable("foo") {
+  deps = [ "//c" ]
+  public_deps = [
+    "//a",
+    "//b",
+    "//d",
+  ]
+}
+)",
+             EditState{{},
+                       {Err(Location(),
+                            "Target \"//:foo\" does not contain the value "
+                            "\"//nonexistent\" in attribute \"deps\".")}}));
+}
+
 TEST_F(EditCommandTest, RemoveAttributeSubcommand) {
   EXPECT_SUCCESS(DoEdit("remove testonly",
                         R"(
diff --git a/src/gn/edit_subcommands.cc b/src/gn/edit_subcommands.cc
index 9be56c8..34455f1 100644
--- a/src/gn/edit_subcommands.cc
+++ b/src/gn/edit_subcommands.cc
@@ -184,6 +184,27 @@
   });
 }
 
+EditCommand MoveCommand(std::string from_attribute,
+                        std::string to_attribute,
+                        std::vector<Value> values) {
+  return EditTargetCommand([from_attribute = std::move(from_attribute),
+                            to_attribute = std::move(to_attribute),
+                            values = std::move(values)](
+                               BuildFile& build_file, const EditTarget& target,
+                               EditState& state) -> Err {
+    std::vector<Value> moved_values;
+    for (const auto& value : values) {
+      if (RemoveFromTarget(target, from_attribute, value, state)) {
+        moved_values.push_back(value);
+      }
+    }
+    if (!moved_values.empty()) {
+      AddToTarget(build_file, target, to_attribute, moved_values);
+    }
+    return Ok();
+  });
+}
+
 EditCommand RemoveAttributeCommand(std::string attribute) {
   return EditTargetCommand([attribute = std::move(attribute)](
                                BuildFile& build_file, const EditTarget& target,
@@ -256,6 +277,14 @@
       return Err(Location(), "Invalid delete command.", "Usage: delete");
     }
     return DeleteCommand();
+  } else if (args[0] == "move") {
+    if (args.size() < 4) {
+      return Err(Location(), "Invalid move command.",
+                 "Usage: move <from_attribute> <to_attribute> <value(s)>");
+    }
+    ASSIGN_OR_RETURN(std::vector<Value> values,
+                     ParseValues(base::make_span(args).subspan(3)));
+    return MoveCommand(args[1], args[2], std::move(values));
   } else if (args[0] == "remove") {
     if (args.size() < 2) {
       return Err(Location(), "Invalid remove command.",