Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/api/converters.py b/api/converters.py
new file mode 100644
index 0000000..4f01a8b
--- /dev/null
+++ b/api/converters.py
@@ -0,0 +1,1147 @@
+# Copyright 2018 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
+
+"""Functions that convert protorpc business objects into protoc objects.
+
+Monorail uses protorpc objects internally, whereas the API uses protoc
+objects.  The difference is not just the choice of protobuf library, there
+will always be a need for conversion because out internal objects may have
+field that we do not want to expose externally, or the format of some fields
+may be different than how we want to expose them.
+"""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+
+from six import string_types
+
+import settings
+from api.api_proto import common_pb2
+from api.api_proto import features_objects_pb2
+from api.api_proto import issue_objects_pb2
+from api.api_proto import project_objects_pb2
+from api.api_proto import user_objects_pb2
+from features import federated
+from framework import exceptions
+from framework import filecontent
+from framework import framework_constants
+from framework import framework_helpers
+from framework import permissions
+from framework import validate
+from services import features_svc
+from tracker import attachment_helpers
+from tracker import field_helpers
+from tracker import tracker_bizobj
+from tracker import tracker_helpers
+from proto import tracker_pb2
+from proto import user_pb2
+
+
+# Convert and ingest objects in issue_objects.proto.
+
+
+def ConvertApprovalValues(approval_values, phases, users_by_id, config):
+  """Convert a list of ApprovalValue into protoc Approvals."""
+  phases_by_id = {
+    phase.phase_id: phase
+    for phase in phases}
+  result = [
+    ConvertApproval(
+      av, users_by_id, config, phase=phases_by_id.get(av.phase_id))
+    for av in approval_values]
+  result = [av for av in result if av]
+  return result
+
+
+def ConvertApproval(approval_value, users_by_id, config, phase=None):
+  """Use the given ApprovalValue to create a protoc Approval."""
+  approval_name = ''
+  fd = tracker_bizobj.FindFieldDefByID(approval_value.approval_id, config)
+  if fd:
+    approval_name = fd.field_name
+  else:
+    logging.info(
+        'Ignoring approval value referencing a non-existing field: %r',
+        approval_value)
+    return None
+
+  field_ref = ConvertFieldRef(
+      approval_value.approval_id, approval_name,
+      tracker_pb2.FieldTypes.APPROVAL_TYPE, None)
+  approver_refs = ConvertUserRefs(approval_value.approver_ids, [], users_by_id,
+                                  False)
+  setter_ref = ConvertUserRef(approval_value.setter_id, None, users_by_id)
+
+  status = ConvertApprovalStatus(approval_value.status)
+  set_on = approval_value.set_on
+
+  phase_ref = issue_objects_pb2.PhaseRef()
+  if phase:
+    phase_ref.phase_name = phase.name
+
+  result = issue_objects_pb2.Approval(
+      field_ref=field_ref, approver_refs=approver_refs,
+      status=status, set_on=set_on, setter_ref=setter_ref,
+      phase_ref=phase_ref)
+  return result
+
+
+def ConvertStatusRef(explicit_status, derived_status, config):
+  """Use the given status strings to create a StatusRef."""
+  status = explicit_status or derived_status
+  is_derived = not explicit_status
+  if not status:
+    return common_pb2.StatusRef(
+        status=framework_constants.NO_VALUES, is_derived=False, means_open=True)
+
+  return common_pb2.StatusRef(
+      status=status,
+      is_derived=is_derived,
+      means_open=tracker_helpers.MeansOpenInProject(status, config))
+
+
+def ConvertApprovalStatus(status):
+  """Use the given protorpc ApprovalStatus to create a protoc ApprovalStatus"""
+  return issue_objects_pb2.ApprovalStatus.Value(status.name)
+
+
+def ConvertUserRef(explicit_user_id, derived_user_id, users_by_id):
+  """Use the given user IDs to create a UserRef."""
+  user_id = explicit_user_id or derived_user_id
+  is_derived = not explicit_user_id
+  if not user_id:
+    return None;
+
+  return common_pb2.UserRef(
+      user_id=user_id,
+      is_derived=is_derived,
+      display_name=users_by_id[user_id].display_name)
+
+# TODO(jojwang): Rewrite method, ConvertUserRefs should be able to
+# call ConvertUserRef
+def ConvertUserRefs(explicit_user_ids, derived_user_ids, users_by_id,
+                    use_email):
+  # (List(int), List(int), Dict(int: UserView), bool) -> List(UserRef)
+  """Use the given user ID lists to create a list of UserRef.
+
+  Args:
+    explicit_user_ids: list of user_ids for users that are not derived.
+    derived_user_ids: list of user_ids for users derived from FilterRules.
+    users_by_id: dict of {user_id: UserView, ...} for all users in
+      explicit_user_ids and derived_user_ids.
+    use_email: boolean true if the UserView.email should be used as
+      the display_name instead of UserView.display_name, which may be obscured.
+
+  Returns:
+    A single list of UserRefs.
+  """
+  result = []
+  for user_id in explicit_user_ids:
+    result.append(common_pb2.UserRef(
+      user_id=user_id,
+      is_derived=False,
+      display_name=(
+          users_by_id[user_id].email
+          if use_email
+          else users_by_id[user_id].display_name)))
+  for user_id in derived_user_ids:
+    result.append(common_pb2.UserRef(
+      user_id=user_id,
+      is_derived=True,
+      display_name=(
+          users_by_id[user_id].email
+          if use_email
+          else users_by_id[user_id].display_name)))
+  return result
+
+
+def ConvertUsers(users, users_by_id):
+  """Use the given protorpc Users to create protoc Users.
+
+  Args:
+    users: list of protorpc Users to convert.
+    users_by_id: dict {user_id: UserView} of all Users linked
+      from the users list.
+
+  Returns:
+    A list of protoc Users.
+  """
+  result = []
+  for user in users:
+    linked_parent_ref = None
+    if user.linked_parent_id:
+      linked_parent_ref = ConvertUserRefs(
+          [user.linked_parent_id], [], users_by_id, False)[0]
+    linked_child_refs = ConvertUserRefs(
+        user.linked_child_ids, [], users_by_id, False)
+    converted_user = user_objects_pb2.User(
+        user_id=user.user_id,
+        display_name=user.email,
+        is_site_admin=user.is_site_admin,
+        availability=framework_helpers.GetUserAvailability(user)[0],
+        linked_parent_ref=linked_parent_ref,
+        linked_child_refs=linked_child_refs)
+    result.append(converted_user)
+  return result
+
+
+def ConvertPrefValues(userprefvalues):
+  """Convert a list of protorpc UserPrefValue to protoc UserPrefValues."""
+  return [
+      user_objects_pb2.UserPrefValue(name=upv.name, value=upv.value)
+      for upv in userprefvalues]
+
+
+def ConvertLabels(explicit_labels, derived_labels):
+  """Combine the given explicit and derived lists into LabelRefs."""
+  explicit_refs = [common_pb2.LabelRef(label=lab, is_derived=False)
+                   for lab in explicit_labels]
+  derived_refs = [common_pb2.LabelRef(label=lab, is_derived=True)
+                  for lab in derived_labels]
+  return explicit_refs + derived_refs
+
+
+def ConvertComponentRef(component_id, config, is_derived=False):
+  """Make a ComponentRef from the component_id and project config."""
+  component_def = tracker_bizobj.FindComponentDefByID(component_id, config)
+  if not component_def:
+    logging.info('Ignoring non-existing component id %s', component_id)
+    return None
+  result = common_pb2.ComponentRef(
+      path=component_def.path,
+      is_derived=is_derived)
+  return result
+
+# TODO(jojwang): rename to ConvertComponentRefs
+def ConvertComponents(explicit_component_ids, derived_component_ids, config):
+  """Make a ComponentRef for each component_id."""
+  result = [ConvertComponentRef(cid, config) for cid in explicit_component_ids]
+  result += [
+      ConvertComponentRef(cid, config, is_derived=True)
+      for cid in derived_component_ids]
+  result = [cr for cr in result if cr]
+  return result
+
+
+def ConvertIssueRef(issue_ref_pair, ext_id=''):
+  """Convert (project_name, local_id) to an IssueRef protoc object.
+
+  With optional external ref in ext_id.
+  """
+  project_name, local_id = issue_ref_pair
+  ref = common_pb2.IssueRef(project_name=project_name, local_id=local_id,
+      ext_identifier=ext_id)
+  return ref
+
+
+def ConvertIssueRefs(issue_ids, related_refs_dict):
+  """Convert a list of iids to IssueRef protoc objects."""
+  return [ConvertIssueRef(related_refs_dict[iid]) for iid in issue_ids]
+
+
+def ConvertFieldValue(field_id, field_name, value, field_type,
+                      approval_name=None, phase_name=None, is_derived=False):
+  """Convert one field value view item into a protoc FieldValue."""
+  if not isinstance(value, string_types):
+    value = str(value)
+  fv = issue_objects_pb2.FieldValue(
+      field_ref=ConvertFieldRef(field_id, field_name, field_type,
+                                approval_name),
+      value=value,
+      is_derived=is_derived)
+  if phase_name:
+    fv.phase_ref.phase_name = phase_name
+
+  return fv
+
+
+def ConvertFieldType(field_type):
+  """Use the given protorpc FieldTypes enum to create a protoc FieldType."""
+  return common_pb2.FieldType.Value(field_type.name)
+
+
+def ConvertFieldRef(field_id, field_name, field_type, approval_name):
+  """Convert a field name and protorpc FieldType into a protoc FieldRef."""
+  return common_pb2.FieldRef(field_id=field_id,
+                             field_name=field_name,
+                             type=ConvertFieldType(field_type),
+                             approval_name=approval_name)
+
+
+def ConvertFieldValues(
+    config, labels, derived_labels, field_values, users_by_id, phases=None):
+  """Convert lists of labels and field_values to protoc FieldValues."""
+  fvs = []
+  phase_names_by_id = {phase.phase_id: phase.name for phase in phases or []}
+  fds_by_id = {fd.field_id:fd for fd in config.field_defs}
+  fids_by_name = {fd.field_name:fd.field_id for fd in config.field_defs}
+  enum_names_by_lower = {
+      fd.field_name.lower(): fd.field_name for fd in config.field_defs
+      if fd.field_type == tracker_pb2.FieldTypes.ENUM_TYPE}
+
+  labels_by_prefix = tracker_bizobj.LabelsByPrefix(
+      labels, list(enum_names_by_lower.keys()))
+  der_labels_by_prefix = tracker_bizobj.LabelsByPrefix(
+      derived_labels, list(enum_names_by_lower.keys()))
+
+  for lower_field_name, values in labels_by_prefix.items():
+    field_name = enum_names_by_lower.get(lower_field_name)
+    if not field_name:
+      continue
+    fvs.extend(
+        [ConvertFieldValue(
+            fids_by_name.get(field_name), field_name, value,
+            tracker_pb2.FieldTypes.ENUM_TYPE)
+         for value in values])
+
+  for lower_field_name, values in der_labels_by_prefix.items():
+    field_name = enum_names_by_lower.get(lower_field_name)
+    if not field_name:
+      continue
+    fvs.extend(
+        [ConvertFieldValue(
+            fids_by_name.get(field_name), field_name, value,
+            tracker_pb2.FieldTypes.ENUM_TYPE, is_derived=True)
+         for value in values])
+
+  for fv in field_values:
+    field_def = fds_by_id.get(fv.field_id)
+    if not field_def:
+      logging.info(
+          'Ignoring field value referencing a non-existent field: %r', fv)
+      continue
+
+    value = tracker_bizobj.GetFieldValue(fv, users_by_id)
+    field_name = field_def.field_name
+    field_type = field_def.field_type
+    approval_name = None
+
+    if field_def.approval_id is not None:
+      approval_def = fds_by_id.get(field_def.approval_id)
+      if approval_def:
+        approval_name = approval_def.field_name
+
+    fvs.append(ConvertFieldValue(
+        fv.field_id, field_name, value, field_type, approval_name=approval_name,
+        phase_name=phase_names_by_id.get(fv.phase_id), is_derived=fv.derived))
+
+  return fvs
+
+
+def ConvertIssue(issue, users_by_id, related_refs, config):
+  """Convert our protorpc Issue to a protoc Issue.
+
+  Args:
+    issue: protorpc issue used by monorail internally.
+    users_by_id: dict {user_id: UserViews} for all users mentioned in issue.
+    related_refs: dict {issue_id: (project_name, local_id)} of all blocked-on,
+        blocking, or merged-into issues referenced from this issue, regardless
+        of perms.
+    config: ProjectIssueConfig for this issue.
+
+  Returns: A protoc Issue object.
+  """
+  status_ref = ConvertStatusRef(issue.status, issue.derived_status, config)
+  owner_ref = ConvertUserRef(
+      issue.owner_id, issue.derived_owner_id, users_by_id)
+  cc_refs = ConvertUserRefs(
+      issue.cc_ids, issue.derived_cc_ids, users_by_id, False)
+  labels, derived_labels = tracker_bizobj.ExplicitAndDerivedNonMaskedLabels(
+      issue.labels, issue.derived_labels, config)
+  label_refs = ConvertLabels(labels, derived_labels)
+  component_refs = ConvertComponents(
+      issue.component_ids, issue.derived_component_ids, config)
+  blocked_on_issue_refs = ConvertIssueRefs(
+      issue.blocked_on_iids, related_refs)
+  dangling_blocked_on_refs = [
+      ConvertIssueRef((dangling_issue.project, dangling_issue.issue_id),
+          ext_id=dangling_issue.ext_issue_identifier)
+      for dangling_issue in issue.dangling_blocked_on_refs]
+  blocking_issue_refs = ConvertIssueRefs(
+      issue.blocking_iids, related_refs)
+  dangling_blocking_refs = [
+      ConvertIssueRef((dangling_issue.project, dangling_issue.issue_id),
+          ext_id=dangling_issue.ext_issue_identifier)
+      for dangling_issue in issue.dangling_blocking_refs]
+  merged_into_issue_ref = None
+  if issue.merged_into:
+    merged_into_issue_ref = ConvertIssueRef(related_refs[issue.merged_into])
+  if issue.merged_into_external:
+    merged_into_issue_ref = ConvertIssueRef((None, None),
+        ext_id=issue.merged_into_external)
+
+  field_values = ConvertFieldValues(
+      config, issue.labels, issue.derived_labels,
+      issue.field_values, users_by_id, phases=issue.phases)
+  approval_values = ConvertApprovalValues(
+      issue.approval_values, issue.phases, users_by_id, config)
+  reporter_ref = ConvertUserRef(issue.reporter_id, None, users_by_id)
+  phases = [ConvertPhaseDef(phase) for phase in issue.phases]
+  result = issue_objects_pb2.Issue(
+      project_name=issue.project_name, local_id=issue.local_id,
+      summary=issue.summary, status_ref=status_ref, owner_ref=owner_ref,
+      cc_refs=cc_refs, label_refs=label_refs, component_refs=component_refs,
+      blocked_on_issue_refs=blocked_on_issue_refs,
+      dangling_blocked_on_refs=dangling_blocked_on_refs,
+      blocking_issue_refs=blocking_issue_refs,
+      dangling_blocking_refs=dangling_blocking_refs,
+      merged_into_issue_ref=merged_into_issue_ref, field_values=field_values,
+      is_deleted=issue.deleted, reporter_ref=reporter_ref,
+      opened_timestamp=issue.opened_timestamp,
+      closed_timestamp=issue.closed_timestamp,
+      modified_timestamp=issue.modified_timestamp,
+      component_modified_timestamp=issue.component_modified_timestamp,
+      status_modified_timestamp=issue.status_modified_timestamp,
+      owner_modified_timestamp=issue.owner_modified_timestamp,
+      star_count=issue.star_count, is_spam=issue.is_spam,
+      approval_values=approval_values, phases=phases)
+
+  # TODO(crbug.com/monorail/5857): Set attachment_count unconditionally
+  # after the underlying source of negative attachment counts has been
+  # resolved and database has been repaired.
+  if issue.attachment_count >= 0:
+    result.attachment_count = issue.attachment_count
+
+  return result
+
+
+def ConvertPhaseDef(phase):
+  """Convert a protorpc Phase to a protoc PhaseDef."""
+  phase_def = issue_objects_pb2.PhaseDef(
+      phase_ref=issue_objects_pb2.PhaseRef(phase_name=phase.name),
+      rank=phase.rank)
+  return phase_def
+
+
+def ConvertAmendment(amendment, users_by_id):
+  """Convert a protorpc Amendment to a protoc Amendment."""
+  field_name = tracker_bizobj.GetAmendmentFieldName(amendment)
+  new_value = tracker_bizobj.AmendmentString(amendment, users_by_id)
+  result = issue_objects_pb2.Amendment(
+      field_name=field_name, new_or_delta_value=new_value,
+      old_value=amendment.oldvalue)
+  return result
+
+
+def ConvertAttachment(attach, project_name):
+  """Convert a protorpc Attachment to a protoc Attachment."""
+  size, thumbnail_url, view_url, download_url = None, None, None, None
+  if not attach.deleted:
+    size = attach.filesize
+    download_url = attachment_helpers.GetDownloadURL(attach.attachment_id)
+    view_url = attachment_helpers.GetViewURL(attach, download_url, project_name)
+    thumbnail_url = attachment_helpers.GetThumbnailURL(attach, download_url)
+
+  result = issue_objects_pb2.Attachment(
+      attachment_id=attach.attachment_id, filename=attach.filename,
+      size=size, content_type=attach.mimetype,
+      is_deleted=attach.deleted, thumbnail_url=thumbnail_url,
+      view_url=view_url, download_url=download_url)
+  return result
+
+
+def ConvertComment(
+    issue, comment, config, users_by_id, comment_reporters, description_nums,
+    user_id, perms):
+  """Convert a protorpc IssueComment to a protoc Comment."""
+  commenter = users_by_id[comment.user_id]
+
+  can_delete = permissions.CanDeleteComment(
+      comment, commenter, user_id, perms)
+  can_flag, is_flagged = permissions.CanFlagComment(
+      comment, commenter, comment_reporters, user_id, perms)
+  can_view = permissions.CanViewComment(
+      comment, commenter, user_id, perms)
+  can_view_inbound_message = permissions.CanViewInboundMessage(
+      comment, user_id, perms)
+
+  is_deleted = bool(comment.deleted_by or is_flagged or commenter.banned)
+
+  result = issue_objects_pb2.Comment(
+      project_name=issue.project_name,
+      local_id=issue.local_id,
+      sequence_num=comment.sequence,
+      is_deleted=is_deleted,
+      can_delete=can_delete,
+      is_spam=is_flagged,
+      can_flag=can_flag,
+      timestamp=comment.timestamp)
+
+  if can_view:
+    result.commenter.CopyFrom(
+        ConvertUserRef(comment.user_id, None, users_by_id))
+    result.content = comment.content
+    if comment.inbound_message and can_view_inbound_message:
+      result.inbound_message = comment.inbound_message
+    result.amendments.extend([
+        ConvertAmendment(amend, users_by_id)
+        for amend in comment.amendments])
+    result.attachments.extend([
+        ConvertAttachment(attach, issue.project_name)
+        for attach in comment.attachments])
+
+  if comment.id in description_nums:
+    result.description_num = description_nums[comment.id]
+
+  fd = tracker_bizobj.FindFieldDefByID(comment.approval_id, config)
+  if fd:
+    result.approval_ref.field_name = fd.field_name
+
+  return result
+
+
+def ConvertCommentList(
+    issue, comments, config, users_by_id, comment_reporters, user_id, perms):
+  """Convert a list of protorpc IssueComments to protoc Comments."""
+  description_nums = {}
+  for comment in comments:
+    if (comment.is_description and not users_by_id[comment.user_id].banned and
+        not comment.deleted_by and not comment.is_spam):
+      description_nums[comment.id] = len(description_nums) + 1
+
+  result = [
+    ConvertComment(
+       issue, c, config, users_by_id, comment_reporters.get(c.id, []),
+       description_nums, user_id, perms)
+    for c in comments]
+  return result
+
+
+def IngestUserRef(cnxn, user_ref, user_service, autocreate=False):
+  """Return ID of specified user or raise NoSuchUserException."""
+  try:
+    return IngestUserRefs(
+        cnxn, [user_ref], user_service, autocreate=autocreate)[0]
+  except IndexError:
+    # user_ref.display_name was not a valid email.
+    raise exceptions.NoSuchUserException
+
+
+def IngestUserRefs(cnxn, user_refs, user_service, autocreate=False):
+  """Return IDs of specified users or raise NoSuchUserException."""
+
+  # Filter out user_refs with invalid display_names.
+  # Invalid emails won't get auto-created in LookupUserIds, but un-specified
+  # user_ref.user_id values have the zero-value 0. So invalid user_ref's
+  # need to be filtered out here to prevent these resulting in '0's in
+  # the 'result' array.
+  if autocreate:
+    user_refs = [user_ref for user_ref in user_refs
+                 if (not user_ref.display_name) or
+                 validate.IsValidEmail(user_ref.display_name)]
+
+  # 1. Verify that all specified user IDs actually match existing users.
+  user_ids_to_verify = [user_ref.user_id for user_ref in user_refs
+                        if user_ref.user_id]
+  user_service.LookupUserEmails(cnxn, user_ids_to_verify)
+
+  # 2. Lookup or create any users that are specified by email address.
+  needed_emails = [user_ref.display_name for user_ref in user_refs
+                   if not user_ref.user_id and user_ref.display_name]
+  emails_to_ids = user_service.LookupUserIDs(
+      cnxn, needed_emails, autocreate=autocreate)
+
+  # 3. Build the result from emails_to_ids or straight from user_ref's
+  # user_id.
+  # Note: user_id can be specified as 0 to clear the issue owner.
+  result = [
+      emails_to_ids.get(user_ref.display_name.lower(), user_ref.user_id)
+      for user_ref in user_refs
+  ]
+  return result
+
+
+def IngestPrefValues(pref_values):
+  """Return protorpc UserPrefValues for the given values."""
+  return [user_pb2.UserPrefValue(name=upv.name, value=upv.value)
+          for upv in pref_values]
+
+
+def IngestComponentRefs(comp_refs, config, ignore_missing_objects=False):
+  """Return IDs of specified components or raise NoSuchComponentException."""
+  cids_by_path = {cd.path.lower(): cd.component_id
+                  for cd in config.component_defs}
+  result = []
+  for comp_ref in comp_refs:
+    cid = cids_by_path.get(comp_ref.path.lower())
+    if cid:
+      result.append(cid)
+    else:
+      if not ignore_missing_objects:
+        raise exceptions.NoSuchComponentException()
+  return result
+
+
+def IngestFieldRefs(field_refs, config):
+  """Return IDs of specified fields or raise NoSuchFieldDefException."""
+  fids_by_name = {fd.field_name.lower(): fd.field_id
+                  for fd in config.field_defs}
+  result = []
+  for field_ref in field_refs:
+    fid = fids_by_name.get(field_ref.field_name.lower())
+    if fid:
+      result.append(fid)
+    else:
+      raise exceptions.NoSuchFieldDefException()
+  return result
+
+
+def IngestIssueRefs(cnxn, issue_refs, services):
+  """Look up issue IDs for the specified issues."""
+  project_names = set(ref.project_name for ref in issue_refs)
+  project_names_to_id = services.project.LookupProjectIDs(cnxn, project_names)
+  project_local_id_pairs = []
+  for ref in issue_refs:
+    if ref.ext_identifier:
+      # TODO(jeffcarp): For external tracker refs, once we have the classes
+      # set up, validate that the tracker for this specific ref is supported
+      # and store the external ref in the issue properly.
+      if '/' not in ref.ext_identifier:
+        raise exceptions.InvalidExternalIssueReference()
+      continue
+    if ref.project_name in project_names_to_id:
+      pair = (project_names_to_id[ref.project_name], ref.local_id)
+      project_local_id_pairs.append(pair)
+    else:
+      raise exceptions.NoSuchProjectException()
+  issue_ids, misses = services.issue.LookupIssueIDs(
+      cnxn, project_local_id_pairs)
+  if misses:
+    raise exceptions.NoSuchIssueException()
+  return issue_ids
+
+
+def IngestExtIssueRefs(issue_refs):
+  """Validate and return external issue refs."""
+  return [
+      ref.ext_identifier
+      for ref in issue_refs
+      if ref.ext_identifier
+      and federated.IsShortlinkValid(ref.ext_identifier)]
+
+
+def IngestIssueDelta(
+    cnxn, services, delta, config, phases, ignore_missing_objects=False):
+  """Ingest a protoc IssueDelta and create a protorpc IssueDelta."""
+  status = None
+  if delta.HasField('status'):
+    status = delta.status.value
+  owner_id = None
+  if delta.HasField('owner_ref'):
+    try:
+      owner_id = IngestUserRef(cnxn, delta.owner_ref, services.user)
+    except exceptions.NoSuchUserException as e:
+      if not ignore_missing_objects:
+        raise e
+  summary = None
+  if delta.HasField('summary'):
+    summary = delta.summary.value
+
+  cc_ids_add = IngestUserRefs(
+      cnxn, delta.cc_refs_add, services.user, autocreate=True)
+  cc_ids_remove = IngestUserRefs(cnxn, delta.cc_refs_remove, services.user)
+
+  comp_ids_add = IngestComponentRefs(
+      delta.comp_refs_add, config,
+      ignore_missing_objects=ignore_missing_objects)
+  comp_ids_remove = IngestComponentRefs(
+      delta.comp_refs_remove, config,
+      ignore_missing_objects=ignore_missing_objects)
+
+  labels_add = [lab_ref.label for lab_ref in delta.label_refs_add]
+  labels_remove = [lab_ref.label for lab_ref in delta.label_refs_remove]
+
+  field_vals_add, field_vals_remove = _RedistributeEnumFieldsIntoLabels(
+      labels_add, labels_remove,
+      delta.field_vals_add, delta.field_vals_remove,
+      config)
+
+  field_vals_add = IngestFieldValues(
+      cnxn, services.user, field_vals_add, config, phases=phases)
+  field_vals_remove = IngestFieldValues(
+      cnxn, services.user, field_vals_remove, config, phases=phases)
+  fields_clear = IngestFieldRefs(delta.fields_clear, config)
+
+  # Ingest intra-tracker issue refs.
+  blocked_on_add = IngestIssueRefs(
+      cnxn, delta.blocked_on_refs_add, services)
+  blocked_on_remove = IngestIssueRefs(
+      cnxn, delta.blocked_on_refs_remove, services)
+  blocking_add = IngestIssueRefs(
+      cnxn, delta.blocking_refs_add, services)
+  blocking_remove = IngestIssueRefs(
+      cnxn, delta.blocking_refs_remove, services)
+
+  # Ingest inter-tracker issue refs.
+  ext_blocked_on_add = IngestExtIssueRefs(delta.blocked_on_refs_add)
+  ext_blocked_on_remove = IngestExtIssueRefs(delta.blocked_on_refs_remove)
+  ext_blocking_add = IngestExtIssueRefs(delta.blocking_refs_add)
+  ext_blocking_remove = IngestExtIssueRefs(delta.blocking_refs_remove)
+
+  merged_into = None
+  merged_into_external = None
+  if delta.HasField('merged_into_ref'):
+    if delta.merged_into_ref.ext_identifier:  # Adding an external merged.
+      merged_into_external = delta.merged_into_ref.ext_identifier
+    elif not delta.merged_into_ref.local_id:  # Clearing an internal merged.
+      merged_into = 0
+    else:  # Adding an internal merged.
+      merged_into = IngestIssueRefs(cnxn, [delta.merged_into_ref], services)[0]
+
+  result = tracker_bizobj.MakeIssueDelta(
+      status, owner_id, cc_ids_add, cc_ids_remove, comp_ids_add,
+      comp_ids_remove, labels_add, labels_remove, field_vals_add,
+      field_vals_remove, fields_clear, blocked_on_add, blocked_on_remove,
+      blocking_add, blocking_remove, merged_into, summary,
+      ext_blocked_on_add=ext_blocked_on_add,
+      ext_blocked_on_remove=ext_blocked_on_remove,
+      ext_blocking_add=ext_blocking_add,
+      ext_blocking_remove=ext_blocking_remove,
+      merged_into_external=merged_into_external)
+  return result
+
+def IngestAttachmentUploads(attachment_uploads):
+  """Ingest protoc AttachmentUpload objects as tuples."""
+  result = []
+  for up in attachment_uploads:
+    if not up.filename:
+      raise exceptions.InputException('Missing attachment name')
+    if not up.content:
+      raise exceptions.InputException('Missing attachment content')
+    mimetype = filecontent.GuessContentTypeFromFilename(up.filename)
+    attachment_tuple = (up.filename, up.content, mimetype)
+    result.append(attachment_tuple)
+  return result
+
+
+def IngestApprovalDelta(cnxn, user_service, approval_delta, setter_id, config):
+  """Ingest a protoc ApprovalDelta and create a protorpc ApprovalDelta."""
+  fids_by_name = {fd.field_name.lower(): fd.field_id for
+                       fd in config.field_defs}
+
+  approver_ids_add = IngestUserRefs(
+      cnxn, approval_delta.approver_refs_add, user_service, autocreate=True)
+  approver_ids_remove = IngestUserRefs(
+      cnxn, approval_delta.approver_refs_remove, user_service, autocreate=True)
+
+  labels_add, labels_remove = [], []
+  # TODO(jojwang): monorail:4673, validate enum values all belong to approval.
+  field_vals_add, field_vals_remove = _RedistributeEnumFieldsIntoLabels(
+      labels_add, labels_remove,
+      approval_delta.field_vals_add, approval_delta.field_vals_remove,
+      config)
+
+  sub_fvs_add = IngestFieldValues(cnxn, user_service, field_vals_add, config)
+  sub_fvs_remove = IngestFieldValues(
+      cnxn, user_service, field_vals_remove, config)
+  sub_fields_clear = [fids_by_name.get(clear.field_name.lower()) for
+                      clear in approval_delta.fields_clear
+                      if clear.field_name.lower() in fids_by_name]
+
+  # protoc ENUMs default to the zero value (in this case: NOT_SET).
+  # NOT_SET should only be allowed when an issue is first created.
+  # Once a user changes it to something else, no one should be allowed
+  # to set it back.
+  status = None
+  if approval_delta.status != issue_objects_pb2.NOT_SET:
+    status = IngestApprovalStatus(approval_delta.status)
+
+  return tracker_bizobj.MakeApprovalDelta(
+      status, setter_id, approver_ids_add, approver_ids_remove,
+      sub_fvs_add, sub_fvs_remove, sub_fields_clear, labels_add, labels_remove)
+
+
+def IngestApprovalStatus(approval_status):
+  """Ingest a protoc ApprovalStatus and create a protorpc ApprovalStatus. """
+  if approval_status == issue_objects_pb2.NOT_SET:
+    return tracker_pb2.ApprovalStatus.NOT_SET
+  return tracker_pb2.ApprovalStatus(approval_status)
+
+
+def IngestFieldValues(cnxn, user_service, field_values, config, phases=None):
+  """Ingest a list of protoc FieldValues and create protorpc FieldValues.
+
+  Args:
+    cnxn: connection to the DB.
+    user_service: interface to user data storage.
+    field_values: a list of protoc FieldValue used by the API.
+    config: ProjectIssueConfig for this field_value's project.
+    phases: a list of the issue's protorpc Phases.
+
+
+  Returns: A protorpc FieldValue object.
+  """
+  fds_by_name = {fd.field_name.lower(): fd for fd in config.field_defs}
+  phases_by_name = {phase.name: phase.phase_id for phase in phases or []}
+
+  ingested_fvs = []
+  for fv in field_values:
+    fd = fds_by_name.get(fv.field_ref.field_name.lower())
+    if fd:
+      if not fv.value:
+        logging.info('Ignoring blank field value: %r', fv)
+        continue
+      ingested_fv = field_helpers.ParseOneFieldValue(
+          cnxn, user_service, fd, fv.value)
+      if not ingested_fv:
+        raise exceptions.InputException(
+          'Unparsable value for field %s' % fv.field_ref.field_name)
+      if ingested_fv.user_id == field_helpers.INVALID_USER_ID:
+        raise exceptions.NoSuchUserException()
+      if fd.is_phase_field:
+        ingested_fv.phase_id = phases_by_name.get(fv.phase_ref.phase_name)
+      ingested_fvs.append(ingested_fv)
+
+  return ingested_fvs
+
+
+def IngestSavedQueries(cnxn, project_service, saved_queries):
+  """Ingest a list of protoc SavedQuery and create protorpc SavedQuery.
+
+  Args:
+    cnxn: connection to the DB.
+    project_service: interface to project data storage.
+    saved_queries: a list of protoc Savedquery.
+
+  Returns: A protorpc SavedQuery object.
+  """
+  if not saved_queries:
+    return []
+
+  project_ids = set()
+  for sq in saved_queries:
+    project_ids.update(sq.executes_in_project_ids)
+
+  project_name_dict = project_service.LookupProjectNames(cnxn,
+      project_ids)
+  return [
+      common_pb2.SavedQuery(
+          query_id=sq.query_id,
+          name=sq.name,
+          query=sq.query,
+          project_names=[project_name_dict[project_id]
+              for project_id in sq.executes_in_project_ids]
+      )
+      for sq in saved_queries]
+
+
+def IngestHotlistRefs(cnxn, user_service, features_service, hotlist_refs):
+  return [IngestHotlistRef(cnxn, user_service, features_service, hotlist_ref)
+        for hotlist_ref in hotlist_refs]
+
+
+def IngestHotlistRef(cnxn, user_service, features_service, hotlist_ref):
+  hotlist_id = None
+
+  if hotlist_ref.hotlist_id:
+    # If a hotlist ID was specified, verify it actually match existing hotlists.
+    features_service.GetHotlist(cnxn, hotlist_ref.hotlist_id)
+    hotlist_id = hotlist_ref.hotlist_id
+
+  if hotlist_ref.name and hotlist_ref.owner:
+    name = hotlist_ref.name
+    owner_id = IngestUserRef(cnxn, hotlist_ref.owner, user_service)
+    hotlists = features_service.LookupHotlistIDs(cnxn, [name], [owner_id])
+    # Verify there is a hotlist with that name and owner.
+    if (name.lower(), owner_id) not in hotlists:
+      raise features_svc.NoSuchHotlistException()
+    found_id = hotlists[name.lower(), owner_id]
+    # If a hotlist_id was also given, verify it correspond to the name and
+    # owner.
+    if hotlist_id is not None and found_id != hotlist_id:
+      raise features_svc.NoSuchHotlistException()
+    hotlist_id = found_id
+
+  # Neither an ID, nor a name-owner ref were given.
+  if hotlist_id is None:
+    raise features_svc.NoSuchHotlistException()
+
+  return hotlist_id
+
+
+def IngestPagination(pagination):
+  max_items = settings.max_artifact_search_results_per_page
+  if pagination.max_items:
+    max_items = min(max_items, pagination.max_items)
+  return pagination.start, max_items
+
+# Convert and ingest objects in project_objects.proto.
+
+def ConvertStatusDef(status_def):
+  """Convert a protorpc StatusDef into a protoc StatusDef."""
+  result = project_objects_pb2.StatusDef(
+      status=status_def.status,
+      means_open=status_def.means_open,
+      docstring=status_def.status_docstring,
+      deprecated=status_def.deprecated)
+  return result
+
+
+def ConvertLabelDef(label_def):
+  """Convert a protorpc LabelDef into a protoc LabelDef."""
+  result = project_objects_pb2.LabelDef(
+      label=label_def.label,
+      docstring=label_def.label_docstring,
+      deprecated=label_def.deprecated)
+  return result
+
+
+def ConvertComponentDef(
+  component_def, users_by_id, labels_by_id, include_admin_info):
+  """Convert a protorpc ComponentDef into a protoc ComponentDef."""
+  if not include_admin_info:
+    return project_objects_pb2.ComponentDef(
+        path=component_def.path,
+        docstring=component_def.docstring,
+        deprecated=component_def.deprecated)
+
+  admin_refs = ConvertUserRefs(component_def.admin_ids, [], users_by_id, False)
+  cc_refs = ConvertUserRefs(component_def.cc_ids, [], users_by_id, False)
+  labels = [labels_by_id[lid] for lid in component_def.label_ids]
+  label_refs = ConvertLabels(labels, [])
+  creator_ref = ConvertUserRef(component_def.creator_id, None, users_by_id)
+  modifier_ref = ConvertUserRef(component_def.modifier_id, None, users_by_id)
+  return project_objects_pb2.ComponentDef(
+      path=component_def.path,
+      docstring=component_def.docstring,
+      admin_refs=admin_refs,
+      cc_refs=cc_refs,
+      deprecated=component_def.deprecated,
+      created=component_def.created,
+      creator_ref=creator_ref,
+      modified=component_def.modified,
+      modifier_ref=modifier_ref,
+      label_refs=label_refs)
+
+
+def ConvertFieldDef(field_def, user_choices, users_by_id, config,
+                    include_admin_info):
+  """Convert a protorpc FieldDef into a protoc FieldDef."""
+  parent_approval_name = None
+  if field_def.approval_id:
+    parent_fd = tracker_bizobj.FindFieldDefByID(field_def.approval_id, config)
+    if parent_fd:
+      parent_approval_name = parent_fd.field_name
+  field_ref = ConvertFieldRef(
+      field_def.field_id, field_def.field_name, field_def.field_type,
+      parent_approval_name)
+
+  enum_choices = []
+  if field_def.field_type == tracker_pb2.FieldTypes.ENUM_TYPE:
+    masked_labels = tracker_helpers.LabelsMaskedByFields(
+        config, [field_def.field_name], True)
+    enum_choices = [
+        project_objects_pb2.LabelDef(
+            label=label.name,
+            docstring=label.docstring,
+            deprecated=(label.commented == '#'))
+        for label in masked_labels]
+
+  if not include_admin_info:
+    return project_objects_pb2.FieldDef(
+        field_ref=field_ref,
+        docstring=field_def.docstring,
+        # Display full email address for user choices.
+        user_choices=ConvertUserRefs(user_choices, [], users_by_id, True),
+        enum_choices=enum_choices)
+
+  admin_refs = ConvertUserRefs(field_def.admin_ids, [], users_by_id, False)
+  # TODO(jrobbins): validation, permission granting, and notification options.
+
+  return project_objects_pb2.FieldDef(
+      field_ref=field_ref,
+      applicable_type=field_def.applicable_type,
+      is_required=field_def.is_required,
+      is_niche=field_def.is_niche,
+      is_multivalued=field_def.is_multivalued,
+      docstring=field_def.docstring,
+      admin_refs=admin_refs,
+      is_phase_field=field_def.is_phase_field,
+      enum_choices=enum_choices)
+
+
+def ConvertApprovalDef(approval_def, users_by_id, config, include_admin_info):
+  """Convert a protorpc ApprovalDef into a protoc ApprovalDef."""
+  field_def = tracker_bizobj.FindFieldDefByID(approval_def.approval_id, config)
+  field_ref = ConvertFieldRef(field_def.field_id, field_def.field_name,
+                              field_def.field_type, None)
+  if not include_admin_info:
+    return project_objects_pb2.ApprovalDef(field_ref=field_ref)
+
+  approver_refs = ConvertUserRefs(approval_def.approver_ids, [], users_by_id,
+                                  False)
+  return project_objects_pb2.ApprovalDef(
+      field_ref=field_ref,
+      approver_refs=approver_refs,
+      survey=approval_def.survey)
+
+
+def ConvertConfig(
+    project, config, users_by_id, labels_by_id):
+  """Convert a protorpc ProjectIssueConfig into a protoc Config."""
+  status_defs = [
+      ConvertStatusDef(sd)
+      for sd in config.well_known_statuses]
+  statuses_offer_merge = [
+      ConvertStatusRef(sd.status, None, config)
+      for sd in config.well_known_statuses
+      if sd.status in config.statuses_offer_merge]
+  label_defs = [
+      ConvertLabelDef(ld)
+      for ld in config.well_known_labels]
+  component_defs = [
+      ConvertComponentDef(
+          cd, users_by_id, labels_by_id, True)
+      for cd in config.component_defs]
+  field_defs = [
+      ConvertFieldDef(fd, [], users_by_id, config, True)
+      for fd in config.field_defs
+      if not fd.is_deleted]
+  approval_defs = [
+      ConvertApprovalDef(ad, users_by_id, config, True)
+      for ad in config.approval_defs]
+  result = project_objects_pb2.Config(
+      project_name=project.project_name,
+      status_defs=status_defs,
+      statuses_offer_merge=statuses_offer_merge,
+      label_defs=label_defs,
+      exclusive_label_prefixes=config.exclusive_label_prefixes,
+      component_defs=component_defs,
+      field_defs=field_defs,
+      approval_defs=approval_defs,
+      restrict_to_known=config.restrict_to_known)
+  return result
+
+
+def ConvertProjectTemplateDefs(templates, users_by_id, config):
+  """Convert a project's protorpc TemplateDefs into protoc TemplateDefs."""
+  converted_templates = []
+  for template in templates:
+    owner_ref = ConvertUserRef(template.owner_id, None, users_by_id)
+    status_ref = ConvertStatusRef(template.status, None, config)
+    labels, _derived_labels = tracker_bizobj.ExplicitAndDerivedNonMaskedLabels(
+        template.labels, [], config)
+    label_refs = ConvertLabels(labels, [])
+    admin_refs = ConvertUserRefs(template.admin_ids, [], users_by_id, False)
+    field_values = ConvertFieldValues(
+        config, template.labels, [], template.field_values, users_by_id,
+        phases=template.phases)
+    component_refs = ConvertComponents(template.component_ids, [], config)
+    approval_values = ConvertApprovalValues(
+        template.approval_values, template.phases, users_by_id, config)
+    phases = [ConvertPhaseDef(phase) for phase in template.phases]
+
+    converted_templates.append(
+        project_objects_pb2.TemplateDef(
+            template_name=template.name, content=template.content,
+            summary=template.summary,
+            summary_must_be_edited=template.summary_must_be_edited,
+            owner_ref=owner_ref, status_ref=status_ref, label_refs=label_refs,
+            members_only=template.members_only,
+            owner_defaults_to_member=template.owner_defaults_to_member,
+            admin_refs=admin_refs, field_values=field_values,
+            component_refs=component_refs,
+            component_required=template.component_required,
+            approval_values=approval_values, phases=phases)
+    )
+  return converted_templates
+
+
+def ConvertHotlist(hotlist, users_by_id):
+  """Convert a protorpc Hotlist into a protoc Hotlist."""
+  owner_ref = ConvertUserRef(
+      hotlist.owner_ids[0], None, users_by_id)
+  editor_refs = ConvertUserRefs(hotlist.editor_ids, [], users_by_id, False)
+  follower_refs = ConvertUserRefs(
+      hotlist.follower_ids, [], users_by_id, False)
+  result = features_objects_pb2.Hotlist(
+      owner_ref=owner_ref,
+      editor_refs=editor_refs,
+      follower_refs=follower_refs,
+      name=hotlist.name,
+      summary=hotlist.summary,
+      description=hotlist.description,
+      default_col_spec=hotlist.default_col_spec,
+      is_private=hotlist.is_private,
+  )
+  return result
+
+
+def ConvertHotlistItems(hotlist_items, issues_by_id, users_by_id, related_refs,
+                        harmonized_config):
+  # Note: hotlist_items are not always sorted by 'rank'
+  sorted_ranks = sorted(item.rank for item in hotlist_items)
+  friendly_ranks_dict = {
+      rank: friendly_rank for friendly_rank, rank in
+      enumerate(sorted_ranks, 1)}
+  converted_items = []
+  for item in hotlist_items:
+    issue_pb = issues_by_id[item.issue_id]
+    issue = ConvertIssue(
+        issue_pb, users_by_id, related_refs, harmonized_config)
+    adder_ref = ConvertUserRef(item.adder_id, None, users_by_id)
+    converted_items.append(features_objects_pb2.HotlistItem(
+        issue=issue,
+        rank=friendly_ranks_dict[item.rank],
+        adder_ref=adder_ref,
+        added_timestamp=item.date_added,
+        note=item.note))
+  return converted_items
+
+
+def ConvertValueAndWhy(value_and_why):
+  return common_pb2.ValueAndWhy(
+      value=value_and_why.get('value'),
+      why=value_and_why.get('why'))
+
+
+def ConvertValueAndWhyList(value_and_why_list):
+  return [ConvertValueAndWhy(vnw) for vnw in value_and_why_list]
+
+
+def _RedistributeEnumFieldsIntoLabels(
+    labels_add, labels_remove, field_vals_add, field_vals_remove, config):
+  """Look at the custom field values and treat enum fields as labels.
+
+  Args:
+    labels_add: list of labels to add/set on the issue.
+    labels_remove: list of labels to remove from the issue.
+    field_val_add: list of protoc FieldValues to be added.
+    field_val_remove: list of protoc FieldValues to be removed.
+        remove.
+    config: ProjectIssueConfig PB including custom field definitions.
+
+  Returns:
+    Two revised lists of protoc FieldValues to be added and removed,
+      without enum_types.
+
+  SIDE-EFFECT: the labels and labels_remove lists will be extended with
+  key-value labels corresponding to the enum field values.
+  """
+  field_val_strs_add = {}
+  for field_val in field_vals_add:
+    field_val_strs_add.setdefault(field_val.field_ref.field_id, []).append(
+        field_val.value)
+
+  field_val_strs_remove = {}
+  for field_val in field_vals_remove:
+    field_val_strs_remove.setdefault(field_val.field_ref.field_id, []).append(
+        field_val.value)
+
+  field_helpers.ShiftEnumFieldsIntoLabels(
+      labels_add, labels_remove, field_val_strs_add, field_val_strs_remove,
+      config)
+
+  # Filter out the fields that were shifted into labels
+  updated_field_vals_add = [
+      fv for fv in field_vals_add
+      if fv.field_ref.field_id in field_val_strs_add]
+  updated_field_vals_remove = [
+      fv for fv in field_vals_remove
+      if fv.field_ref.field_id in field_val_strs_remove]
+
+  return updated_field_vals_add, updated_field_vals_remove