Merge branch 'main' into avm99963-monorail

Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266

GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/tracker/tracker_bizobj.py b/tracker/tracker_bizobj.py
index f3f2594..f90b24f 100644
--- a/tracker/tracker_bizobj.py
+++ b/tracker/tracker_bizobj.py
@@ -1,7 +1,6 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style
-# license that can be found in the LICENSE file or at
-# https://developers.google.com/open-source/licenses/bsd
+# Copyright 2016 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
 
 """Business objects for the Monorail issue tracker.
 
@@ -26,7 +25,7 @@
 from framework import framework_helpers
 from framework import timestr
 from framework import urls
-from proto import tracker_pb2
+from mrproto import tracker_pb2
 from tracker import tracker_constants
 
 
@@ -435,15 +434,15 @@
 
 def GetIssueComponentsAndAncestors(issue, config):
   """Return a list of all the components that an issue is in."""
-  result = set()
+  result = []
   for component_id in issue.component_ids:
     cd = FindComponentDefByID(component_id, config)
     if cd is None:
       logging.error('Tried to look up non-existent component %r' % component_id)
       continue
     ancestors = FindAncestorComponents(config, cd)
-    result.add(cd)
-    result.update(ancestors)
+    result.append(cd)
+    result.extend(ancestors)
 
   return sorted(result, key=lambda cd: cd.path)
 
@@ -854,8 +853,8 @@
 
 
 def UsersInvolvedInApprovalDefs(approval_defs, matching_fds):
-  # type: (Sequence[proto.tracker_pb2.ApprovalDef],
-  #     Sequence[proto.tracker_pb2.FieldDef]) -> Collection[int]
+  # type: (Sequence[mrproto.tracker_pb2.ApprovalDef],
+  #     Sequence[mrproto.tracker_pb2.FieldDef]) -> Collection[int]
   """Return a set of user IDs referenced in the approval_defs and field defs"""
   result = set()
   for ad in approval_defs:
@@ -986,8 +985,8 @@
         fd_removed_values_by_phase[fv.phase_id].append(fv)
       # Use all_fv_phase_ids to create Amendments, so no empty amendments
       # are created for issue phases that had no field value changes.
-      all_fv_phase_ids = set(
-          fd_removed_values_by_phase.keys() + fd_added_values_by_phase.keys())
+      all_fv_phase_ids = set(fd_removed_values_by_phase.keys()) | set(
+          fd_added_values_by_phase.keys())
       for phase_id in all_fv_phase_ids:
         new_values = [GetFieldValue(fv, {}) for fv
                       in fd_added_values_by_phase.get(phase_id, [])]
@@ -1226,8 +1225,12 @@
   impacted_iids = set()
 
   def addAmendment(add_iids, remove_iids, amendment_func):
-    add_refs = issue_service.LookupIssueRefs(cnxn, add_iids).values()
-    remove_refs = issue_service.LookupIssueRefs(cnxn, remove_iids).values()
+    add_refs_dict = issue_service.LookupIssueRefs(cnxn, add_iids)
+    add_refs = [add_refs_dict[iid] for iid in add_iids if iid in add_refs_dict]
+    remove_refs_dict = issue_service.LookupIssueRefs(cnxn, remove_iids)
+    remove_refs = [
+        remove_refs_dict[iid] for iid in remove_iids if iid in remove_refs_dict
+    ]
     new_am = amendment_func(
         add_refs, remove_refs, default_project_name=issue.project_name)
     amendments.append(new_am)
@@ -1269,8 +1272,14 @@
 
 
 def MakeAmendment(
-    field, new_value, added_ids, removed_ids, custom_field_name=None,
-    old_value=None):
+    field,
+    new_value,
+    added_ids,
+    removed_ids,
+    custom_field_name=None,
+    old_value=None,
+    added_component_ids=None,
+    removed_component_ids=None):
   """Utility function to populate an Amendment PB.
 
   Args:
@@ -1296,6 +1305,12 @@
   if custom_field_name is not None:
     amendment.custom_field_name = custom_field_name
 
+  if added_component_ids is not None:
+    amendment.added_component_ids.extend(added_component_ids)
+
+  if removed_component_ids is not None:
+    amendment.removed_component_ids.extend(removed_component_ids)
+
   return amendment
 
 
@@ -1505,20 +1520,26 @@
   # lookups (and maybe permission checks in the future).  But, what
   # about history that references deleleted components?
   added_comp_paths = []
+  valid_added_comp_ids = []
   for comp_id in added_comp_ids:
     cd = FindComponentDefByID(comp_id, config)
     if cd:
       added_comp_paths.append(cd.path)
+      valid_added_comp_ids.append(comp_id)
 
   removed_comp_paths = []
+  valid_removed_comp_ids = []
   for comp_id in removed_comp_ids:
     cd = FindComponentDefByID(comp_id, config)
     if cd:
       removed_comp_paths.append(cd.path)
-
-  return _PlusMinusAmendment(
+      valid_removed_comp_ids.append(comp_id)
+  values = _PlusMinusString(added_comp_paths, removed_comp_paths)
+  return MakeAmendment(
       tracker_pb2.FieldID.COMPONENTS,
-      added_comp_paths, removed_comp_paths)
+      values, [], [],
+      added_component_ids=valid_added_comp_ids,
+      removed_component_ids=valid_removed_comp_ids)
 
 
 def MakeBlockedOnAmendment(