Merge branch 'main' into avm99963-monorail

Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266

GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/mrproto/__init__.py b/mrproto/__init__.py
new file mode 100644
index 0000000..68130d5
--- /dev/null
+++ b/mrproto/__init__.py
@@ -0,0 +1,3 @@
+# 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.
diff --git a/mrproto/api_clients_config.proto b/mrproto/api_clients_config.proto
new file mode 100644
index 0000000..5ae7561
--- /dev/null
+++ b/mrproto/api_clients_config.proto
@@ -0,0 +1,41 @@
+// 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.
+
+// Schemas for monorail api client configs.
+// Command to generate api_clients_config_pb2.py: in monorail/ directory:
+// protoc ./mrproto/api_clients_config.proto --proto_path=./mrproto/ --python_out=./mrproto
+
+
+syntax = "proto2";
+
+package monorail;
+
+message ProjectPermission {
+  enum Role {
+    committer = 1;
+    contributor = 2;
+  }
+
+  optional string project = 1;
+  optional Role role = 2 [default = contributor];
+  repeated string extra_permissions = 3;
+}
+
+// Next available tag: 11
+message Client {
+  optional string client_email = 1;
+  optional string display_name = 2;
+  optional string client_id = 3;
+  repeated string allowed_origins = 10;
+  optional string description = 4;
+  repeated ProjectPermission project_permissions = 5;
+  optional int32 period_limit = 6 [default = 100000];
+  optional int32 lifetime_limit = 7 [default = 1000000];
+  repeated string contacts = 8;
+  optional int32 qpm_limit = 9 [default = 100];
+}
+
+message ClientCfg {
+  repeated Client clients = 1;
+}
diff --git a/mrproto/api_clients_config_pb2.py b/mrproto/api_clients_config_pb2.py
new file mode 100644
index 0000000..1a1d4bd
--- /dev/null
+++ b/mrproto/api_clients_config_pb2.py
@@ -0,0 +1,260 @@
+# -*- coding: utf-8 -*-
+# 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.
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: api_clients_config.proto
+
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='api_clients_config.proto',
+  package='monorail',
+  syntax='proto2',
+  serialized_options=None,
+  create_key=_descriptor._internal_create_key,
+  serialized_pb=b'\n\x18\x61pi_clients_config.proto\x12\x08monorail\"\xa4\x01\n\x11ProjectPermission\x12\x0f\n\x07project\x18\x01 \x01(\t\x12;\n\x04role\x18\x02 \x01(\x0e\x32 .monorail.ProjectPermission.Role:\x0b\x63ontributor\x12\x19\n\x11\x65xtra_permissions\x18\x03 \x03(\t\"&\n\x04Role\x12\r\n\tcommitter\x10\x01\x12\x0f\n\x0b\x63ontributor\x10\x02\"\x98\x02\n\x06\x43lient\x12\x14\n\x0c\x63lient_email\x18\x01 \x01(\t\x12\x14\n\x0c\x64isplay_name\x18\x02 \x01(\t\x12\x11\n\tclient_id\x18\x03 \x01(\t\x12\x17\n\x0f\x61llowed_origins\x18\n \x03(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\x38\n\x13project_permissions\x18\x05 \x03(\x0b\x32\x1b.monorail.ProjectPermission\x12\x1c\n\x0cperiod_limit\x18\x06 \x01(\x05:\x06\x31\x30\x30\x30\x30\x30\x12\x1f\n\x0elifetime_limit\x18\x07 \x01(\x05:\x07\x31\x30\x30\x30\x30\x30\x30\x12\x10\n\x08\x63ontacts\x18\x08 \x03(\t\x12\x16\n\tqpm_limit\x18\t \x01(\x05:\x03\x31\x30\x30\".\n\tClientCfg\x12!\n\x07\x63lients\x18\x01 \x03(\x0b\x32\x10.monorail.Client'
+)
+
+
+
+_PROJECTPERMISSION_ROLE = _descriptor.EnumDescriptor(
+  name='Role',
+  full_name='monorail.ProjectPermission.Role',
+  filename=None,
+  file=DESCRIPTOR,
+  create_key=_descriptor._internal_create_key,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='committer', index=0, number=1,
+      serialized_options=None,
+      type=None,
+      create_key=_descriptor._internal_create_key),
+    _descriptor.EnumValueDescriptor(
+      name='contributor', index=1, number=2,
+      serialized_options=None,
+      type=None,
+      create_key=_descriptor._internal_create_key),
+  ],
+  containing_type=None,
+  serialized_options=None,
+  serialized_start=165,
+  serialized_end=203,
+)
+_sym_db.RegisterEnumDescriptor(_PROJECTPERMISSION_ROLE)
+
+
+_PROJECTPERMISSION = _descriptor.Descriptor(
+  name='ProjectPermission',
+  full_name='monorail.ProjectPermission',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='project', full_name='monorail.ProjectPermission.project', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='role', full_name='monorail.ProjectPermission.role', index=1,
+      number=2, type=14, cpp_type=8, label=1,
+      has_default_value=True, default_value=2,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='extra_permissions', full_name='monorail.ProjectPermission.extra_permissions', index=2,
+      number=3, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+    _PROJECTPERMISSION_ROLE,
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=39,
+  serialized_end=203,
+)
+
+
+_CLIENT = _descriptor.Descriptor(
+  name='Client',
+  full_name='monorail.Client',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='client_email', full_name='monorail.Client.client_email', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='display_name', full_name='monorail.Client.display_name', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='client_id', full_name='monorail.Client.client_id', index=2,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='allowed_origins', full_name='monorail.Client.allowed_origins', index=3,
+      number=10, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='description', full_name='monorail.Client.description', index=4,
+      number=4, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='project_permissions', full_name='monorail.Client.project_permissions', index=5,
+      number=5, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='period_limit', full_name='monorail.Client.period_limit', index=6,
+      number=6, type=5, cpp_type=1, label=1,
+      has_default_value=True, default_value=100000,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='lifetime_limit', full_name='monorail.Client.lifetime_limit', index=7,
+      number=7, type=5, cpp_type=1, label=1,
+      has_default_value=True, default_value=1000000,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='contacts', full_name='monorail.Client.contacts', index=8,
+      number=8, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='qpm_limit', full_name='monorail.Client.qpm_limit', index=9,
+      number=9, type=5, cpp_type=1, label=1,
+      has_default_value=True, default_value=100,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=206,
+  serialized_end=486,
+)
+
+
+_CLIENTCFG = _descriptor.Descriptor(
+  name='ClientCfg',
+  full_name='monorail.ClientCfg',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='clients', full_name='monorail.ClientCfg.clients', index=0,
+      number=1, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=488,
+  serialized_end=534,
+)
+
+_PROJECTPERMISSION.fields_by_name['role'].enum_type = _PROJECTPERMISSION_ROLE
+_PROJECTPERMISSION_ROLE.containing_type = _PROJECTPERMISSION
+_CLIENT.fields_by_name['project_permissions'].message_type = _PROJECTPERMISSION
+_CLIENTCFG.fields_by_name['clients'].message_type = _CLIENT
+DESCRIPTOR.message_types_by_name['ProjectPermission'] = _PROJECTPERMISSION
+DESCRIPTOR.message_types_by_name['Client'] = _CLIENT
+DESCRIPTOR.message_types_by_name['ClientCfg'] = _CLIENTCFG
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+ProjectPermission = _reflection.GeneratedProtocolMessageType('ProjectPermission', (_message.Message,), {
+  'DESCRIPTOR' : _PROJECTPERMISSION,
+  '__module__' : 'api_clients_config_pb2'
+  # @@protoc_insertion_point(class_scope:monorail.ProjectPermission)
+  })
+_sym_db.RegisterMessage(ProjectPermission)
+
+Client = _reflection.GeneratedProtocolMessageType('Client', (_message.Message,), {
+  'DESCRIPTOR' : _CLIENT,
+  '__module__' : 'api_clients_config_pb2'
+  # @@protoc_insertion_point(class_scope:monorail.Client)
+  })
+_sym_db.RegisterMessage(Client)
+
+ClientCfg = _reflection.GeneratedProtocolMessageType('ClientCfg', (_message.Message,), {
+  'DESCRIPTOR' : _CLIENTCFG,
+  '__module__' : 'api_clients_config_pb2'
+  # @@protoc_insertion_point(class_scope:monorail.ClientCfg)
+  })
+_sym_db.RegisterMessage(ClientCfg)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/mrproto/api_pb2_v1.py b/mrproto/api_pb2_v1.py
new file mode 100644
index 0000000..0bf1378
--- /dev/null
+++ b/mrproto/api_pb2_v1.py
@@ -0,0 +1,651 @@
+# 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.
+
+"""Protocol buffers for Monorail API."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from endpoints import ResourceContainer
+from protorpc import messages
+from protorpc import message_types
+
+from mrproto import usergroup_pb2
+
+
+########################## Helper Message ##########################
+
+
+class ErrorMessage(messages.Message):
+  """Request error."""
+  code = messages.IntegerField(
+      1, required=True, variant=messages.Variant.INT32)
+  reason = messages.StringField(2, required=True)
+  message = messages.StringField(3, required=True)
+
+
+class Status(messages.Message):
+  """Issue status."""
+  status = messages.StringField(1, required=True)
+  meansOpen = messages.BooleanField(2, required=True)
+  description = messages.StringField(3)
+
+
+class Label(messages.Message):
+  """Issue label."""
+  label = messages.StringField(1, required=True)
+  description = messages.StringField(2)
+
+
+class Prompt(messages.Message):
+  """Default issue template values."""
+  name = messages.StringField(1, required=True)
+  title = messages.StringField(2)
+  description = messages.StringField(3)
+  titleMustBeEdited = messages.BooleanField(4)
+  status = messages.StringField(5)
+  labels = messages.StringField(6, repeated=True)
+  membersOnly = messages.BooleanField(7)
+  defaultToMember = messages.BooleanField(8)
+  componentRequired = messages.BooleanField(9)
+
+
+class Role(messages.Enum):
+  """User role."""
+  owner = 1
+  member = 2
+  contributor = 3
+
+
+class IssueState(messages.Enum):
+  """Issue state."""
+  closed = 0
+  open = 1
+
+
+class CannedQuery(messages.Enum):
+  """Canned query to search issues."""
+  all = 0
+  new = 1
+  open = 2
+  owned = 3
+  reported = 4
+  starred = 5
+  to_verify = 6
+
+
+class AtomPerson(messages.Message):
+  """Atomic person."""
+  name = messages.StringField(1, required=True)
+  htmlLink = messages.StringField(2)
+  kind = messages.StringField(3)
+  last_visit_days_ago = messages.IntegerField(4)
+  email_bouncing = messages.BooleanField(5)
+  vacation_message = messages.StringField(6)
+
+
+class Attachment(messages.Message):
+  """Issue attachment."""
+  attachmentId = messages.IntegerField(
+      1, variant=messages.Variant.INT64, required=True)
+  fileName = messages.StringField(2, required=True)
+  fileSize = messages.IntegerField(
+      3, required=True, variant=messages.Variant.INT32)
+  mimetype = messages.StringField(4, required=True)
+  isDeleted = messages.BooleanField(5)
+
+
+class IssueRef(messages.Message):
+  "Issue reference."
+  issueId = messages.IntegerField(
+      1, required=True, variant=messages.Variant.INT32)
+  projectId = messages.StringField(2)
+  kind = messages.StringField(3)
+
+
+class FieldValueOperator(messages.Enum):
+  """Operator of field values."""
+  add = 1
+  remove = 2
+  clear = 3
+
+
+class FieldValue(messages.Message):
+  """Custom field values."""
+  fieldName = messages.StringField(1, required=True)
+  fieldValue = messages.StringField(2)
+  derived = messages.BooleanField(3, default=False)
+  operator = messages.EnumField(FieldValueOperator, 4, default='add')
+  phaseName = messages.StringField(5)
+  approvalName = messages.StringField(6)
+
+
+class Update(messages.Message):
+  """Issue update."""
+  summary = messages.StringField(1)
+  status = messages.StringField(2)
+  owner = messages.StringField(3)
+  labels = messages.StringField(4, repeated=True)
+  cc = messages.StringField(5, repeated=True)
+  blockedOn = messages.StringField(6, repeated=True)
+  blocking = messages.StringField(7, repeated=True)
+  mergedInto = messages.StringField(8)
+  kind = messages.StringField(9)
+  components = messages.StringField(10, repeated=True)
+  moveToProject = messages.StringField(11)
+  fieldValues = messages.MessageField(FieldValue, 12, repeated=True)
+  is_description = messages.BooleanField(13)
+
+
+class ApprovalUpdate(messages.Message):
+  """Approval update."""
+  approvers = messages.StringField(1, repeated=True)
+  status = messages.StringField(2)
+  kind = messages.StringField(3)
+  fieldValues = messages.MessageField(FieldValue, 4, repeated=True)
+
+
+class ProjectIssueConfig(messages.Message):
+  """Issue configuration of project."""
+  kind = messages.StringField(1)
+  restrictToKnown = messages.BooleanField(2)
+  defaultColumns = messages.StringField(3, repeated=True)
+  defaultSorting = messages.StringField(4, repeated=True)
+  statuses = messages.MessageField(Status, 5, repeated=True)
+  labels = messages.MessageField(Label, 6, repeated=True)
+  prompts = messages.MessageField(Prompt, 7, repeated=True)
+  defaultPromptForMembers = messages.IntegerField(
+      8, variant=messages.Variant.INT32)
+  defaultPromptForNonMembers = messages.IntegerField(
+      9, variant=messages.Variant.INT32)
+  usersCanSetLabels = messages.BooleanField(10)
+
+
+class Phase(messages.Message):
+  """Issue phase details."""
+  phaseName = messages.StringField(1)
+  rank = messages.IntegerField(2)
+
+
+class IssueCommentWrapper(messages.Message):
+  """Issue comment details."""
+  attachments = messages.MessageField(Attachment, 1, repeated=True)
+  author = messages.MessageField(AtomPerson, 2)
+  canDelete = messages.BooleanField(3)
+  content = messages.StringField(4)
+  deletedBy = messages.MessageField(AtomPerson, 5)
+  id = messages.IntegerField(6, variant=messages.Variant.INT32)
+  published = message_types.DateTimeField(7)
+  updates = messages.MessageField(Update, 8)
+  kind = messages.StringField(9)
+  is_description = messages.BooleanField(10)
+
+
+class ApprovalCommentWrapper(messages.Message):
+  """Approval comment details."""
+  attachments = messages.MessageField(Attachment, 1, repeated=True)
+  author = messages.MessageField(AtomPerson, 2)
+  canDelete = messages.BooleanField(3)
+  content = messages.StringField(4)
+  deletedBy = messages.MessageField(AtomPerson, 5)
+  id = messages.IntegerField(6, variant=messages.Variant.INT32)
+  published = message_types.DateTimeField(7)
+  approvalUpdates = messages.MessageField(ApprovalUpdate, 8)
+  kind = messages.StringField(9)
+  is_description = messages.BooleanField(10)
+
+
+class ApprovalStatus(messages.Enum):
+  """Allowed Approval Statuses."""
+  needsReview = 1
+  nA = 2
+  reviewRequested = 3
+  reviewStarted = 4
+  needInfo = 5
+  approved = 6
+  notApproved = 7
+  notSet = 8
+
+
+class Approval(messages.Message):
+  """Approval Value details"""
+  approvalName = messages.StringField(1)
+  approvers = messages.MessageField(AtomPerson, 2, repeated=True)
+  status = messages.EnumField(ApprovalStatus, 3)
+  setter = messages.MessageField(AtomPerson, 4)
+  setOn = message_types.DateTimeField(5)
+  phaseName = messages.StringField(6)
+
+
+class IssueWrapper(messages.Message):
+  """Issue details."""
+  author = messages.MessageField(AtomPerson, 1)
+  blockedOn = messages.MessageField(IssueRef, 2, repeated=True)
+  blocking = messages.MessageField(IssueRef, 3, repeated=True)
+  canComment = messages.BooleanField(4)
+  canEdit = messages.BooleanField(5)
+  cc = messages.MessageField(AtomPerson, 6, repeated=True)
+  closed = message_types.DateTimeField(7)
+  description = messages.StringField(8)
+  id = messages.IntegerField(9, variant=messages.Variant.INT32)
+  kind = messages.StringField(10)
+  labels = messages.StringField(11, repeated=True)
+  owner = messages.MessageField(AtomPerson, 12)
+  published = message_types.DateTimeField(13)
+  starred = messages.BooleanField(14)
+  stars = messages.IntegerField(15, variant=messages.Variant.INT32)
+  state = messages.EnumField(IssueState, 16)
+  status = messages.StringField(17, required=True)
+  summary = messages.StringField(18, required=True)
+  title = messages.StringField(19)
+  updated = message_types.DateTimeField(20)
+  components = messages.StringField(21, repeated=True)
+  projectId = messages.StringField(22, required=True)
+  mergedInto = messages.MessageField(IssueRef, 23)
+  fieldValues = messages.MessageField(FieldValue, 24, repeated=True)
+  owner_modified = message_types.DateTimeField(25)
+  status_modified = message_types.DateTimeField(26)
+  component_modified = message_types.DateTimeField(27)
+  approvalValues = messages.MessageField(Approval, 28, repeated=True)
+  phases = messages.MessageField(Phase, 29, repeated=True)
+  migrated_id = messages.StringField(30)
+
+class ProjectWrapper(messages.Message):
+  """Project details."""
+  kind = messages.StringField(1)
+  name = messages.StringField(2)
+  externalId = messages.StringField(3, required=True)
+  htmlLink = messages.StringField(4, required=True)
+  summary = messages.StringField(5)
+  description = messages.StringField(6)
+  versionControlSystem = messages.StringField(7)
+  repositoryUrls = messages.StringField(8, repeated=True)
+  issuesConfig = messages.MessageField(ProjectIssueConfig, 9)
+  role = messages.EnumField(Role, 10)
+  members = messages.MessageField(AtomPerson, 11, repeated=True)
+
+
+class UserGroupSettingsWrapper(messages.Message):
+  """User group settings."""
+  groupName = messages.StringField(1, required=True)
+  who_can_view_members = messages.EnumField(
+      usergroup_pb2.MemberVisibility, 2,
+      default=usergroup_pb2.MemberVisibility.MEMBERS)
+  ext_group_type = messages.EnumField(usergroup_pb2.GroupType, 3)
+  last_sync_time = messages.IntegerField(
+      4, default=0, variant=messages.Variant.INT32)
+
+
+class GroupCitizens(messages.Message):
+  """Group members and owners."""
+  groupOwners = messages.StringField(1, repeated=True)
+  groupMembers = messages.StringField(2, repeated=True)
+
+
+########################## Comments Message ##########################
+
+# pylint: disable=pointless-string-statement
+
+"""Request to delete/undelete an issue's comments."""
+ISSUES_COMMENTS_DELETE_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+    message_types.VoidMessage,
+    projectId=messages.StringField(1, required=True),
+    issueId=messages.IntegerField(
+        2, required=True, variant=messages.Variant.INT32),
+    commentId=messages.IntegerField(
+        3, required=True, variant=messages.Variant.INT32)
+)
+
+
+class IssuesCommentsDeleteResponse(messages.Message):
+  """Response message of request to delete/undelete an issue's comments."""
+  error = messages.MessageField(ErrorMessage, 1)
+
+
+"""Request to insert an issue's comments."""
+ISSUES_COMMENTS_INSERT_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+    IssueCommentWrapper,
+    projectId=messages.StringField(1, required=True),
+    issueId=messages.IntegerField(
+        2, required=True, variant=messages.Variant.INT32),
+    sendEmail=messages.BooleanField(3)
+)
+
+
+class IssuesCommentsInsertResponse(messages.Message):
+  """Response message of request to insert an issue's comments."""
+  error = messages.MessageField(ErrorMessage, 1)
+  id = messages.IntegerField(2, variant=messages.Variant.INT32)
+  kind = messages.StringField(3)
+  author = messages.MessageField(AtomPerson, 4)
+  content = messages.StringField(5)
+  published = message_types.DateTimeField(6)
+  updates = messages.MessageField(Update, 7)
+  canDelete = messages.BooleanField(8)
+
+
+"""Request to list an issue's comments."""
+ISSUES_COMMENTS_LIST_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+    message_types.VoidMessage,
+    projectId=messages.StringField(1, required=True),
+    issueId=messages.IntegerField(
+        2, required=True, variant=messages.Variant.INT32),
+    maxResults=messages.IntegerField(
+        3, default=100, variant=messages.Variant.INT32),
+    startIndex=messages.IntegerField(
+        4, default=0, variant=messages.Variant.INT32)
+)
+
+
+class IssuesCommentsListResponse(messages.Message):
+  """Response message of request to list an issue's comments."""
+  error = messages.MessageField(ErrorMessage, 1)
+  items = messages.MessageField(IssueCommentWrapper, 2, repeated=True)
+  totalResults = messages.IntegerField(3, variant=messages.Variant.INT32)
+  kind = messages.StringField(4)
+
+########################## ApprovalComments Message ################
+
+"""Request to insert an issue approval's comments."""
+APPROVALS_COMMENTS_INSERT_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+    ApprovalCommentWrapper,
+    projectId=messages.StringField(1, required=True),
+    issueId=messages.IntegerField(
+        2, required=True, variant=messages.Variant.INT32),
+    approvalName=messages.StringField(3, required=True),
+    sendEmail=messages.BooleanField(4)
+)
+
+
+class ApprovalsCommentsInsertResponse(messages.Message):
+  """Response message of request to insert an isuse's comments."""
+  error = messages.MessageField(ErrorMessage, 1)
+  id = messages.IntegerField(2, variant=messages.Variant.INT32)
+  kind = messages.StringField(3)
+  author = messages.MessageField(AtomPerson, 4)
+  content = messages.StringField(5)
+  published = message_types.DateTimeField(6)
+  approvalUpdates = messages.MessageField(ApprovalUpdate, 7)
+  canDelete = messages.BooleanField(8)
+  approvalName = messages.StringField(9)
+
+
+"""Requests to list an approval's comments."""
+APPROVALS_COMMENTS_LIST_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+    message_types.VoidMessage,
+    projectId=messages.StringField(1, required=True),
+    issueId=messages.IntegerField(
+        2, required=True, variant=messages.Variant.INT32),
+    approvalName=messages.StringField(3, required=True),
+    maxResults=messages.IntegerField(
+        4, default=100, variant=messages.Variant.INT32),
+    startIndex=messages.IntegerField(
+        5, default=0, variant=messages.Variant.INT32)
+)
+
+
+class ApprovalsCommentsListResponse(messages.Message):
+  """Response message of request to list an approval's comments."""
+  error = messages.MessageField(ErrorMessage, 1)
+  items = messages.MessageField(ApprovalCommentWrapper, 2, repeated=True)
+  totalResults = messages.IntegerField(3, variant=messages.Variant.INT32)
+  kind = messages.StringField(4)
+
+########################## Users Message ##########################
+
+"""Request to get a user."""
+USERS_GET_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+    message_types.VoidMessage,
+    userId=messages.StringField(1, required=True),
+    ownerProjectsOnly=messages.BooleanField(2, default=False)
+)
+
+
+class UsersGetResponse(messages.Message):
+  """Response message of request to get a user."""
+  error = messages.MessageField(ErrorMessage, 1)
+  id = messages.StringField(2)
+  kind = messages.StringField(3)
+  projects = messages.MessageField(ProjectWrapper, 4, repeated=True)
+
+
+########################## Issues Message ##########################
+
+"""Request to get an issue."""
+ISSUES_GET_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+    message_types.VoidMessage,
+    projectId=messages.StringField(1, required=True),
+    issueId=messages.IntegerField(
+        2, required=True, variant=messages.Variant.INT32)
+)
+
+
+"""Request to insert an issue."""
+ISSUES_INSERT_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+    IssueWrapper,
+    projectId=messages.StringField(1, required=True),
+    sendEmail=messages.BooleanField(2, default=True)
+)
+
+
+class IssuesGetInsertResponse(messages.Message):
+  """Response message of request to get/insert an issue."""
+  error = messages.MessageField(ErrorMessage, 1)
+  kind = messages.StringField(2)
+  id = messages.IntegerField(3, variant=messages.Variant.INT32)
+  title = messages.StringField(4)
+  summary = messages.StringField(5)
+  stars = messages.IntegerField(6, variant=messages.Variant.INT32)
+  starred = messages.BooleanField(7)
+  status = messages.StringField(8)
+  state = messages.EnumField(IssueState, 9)
+  labels = messages.StringField(10, repeated=True)
+  author = messages.MessageField(AtomPerson, 11)
+  owner = messages.MessageField(AtomPerson, 12)
+  cc = messages.MessageField(AtomPerson, 13, repeated=True)
+  updated = message_types.DateTimeField(14)
+  published = message_types.DateTimeField(15)
+  closed = message_types.DateTimeField(16)
+  blockedOn = messages.MessageField(IssueRef, 17, repeated=True)
+  blocking = messages.MessageField(IssueRef, 18, repeated=True)
+  projectId = messages.StringField(19)
+  canComment = messages.BooleanField(20)
+  canEdit = messages.BooleanField(21)
+  components = messages.StringField(22, repeated=True)
+  mergedInto = messages.MessageField(IssueRef, 23)
+  fieldValues = messages.MessageField(FieldValue, 24, repeated=True)
+  owner_modified = message_types.DateTimeField(25)
+  status_modified = message_types.DateTimeField(26)
+  component_modified = message_types.DateTimeField(27)
+  approvalValues = messages.MessageField(Approval, 28, repeated=True)
+  phases = messages.MessageField(Phase, 29, repeated=True)
+  migrated_id = messages.StringField(30)
+
+
+"""Request to list issues."""
+ISSUES_LIST_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+    message_types.VoidMessage,
+    projectId=messages.StringField(1, required=True),
+    additionalProject=messages.StringField(2, repeated=True),
+    can=messages.EnumField(CannedQuery, 3, default='all'),
+    label=messages.StringField(4),
+    maxResults=messages.IntegerField(
+        5, default=100, variant=messages.Variant.INT32),
+    owner=messages.StringField(6),
+    publishedMax=messages.IntegerField(7, variant=messages.Variant.INT64),
+    publishedMin=messages.IntegerField(8, variant=messages.Variant.INT64),
+    q=messages.StringField(9),
+    sort=messages.StringField(10),
+    startIndex=messages.IntegerField(
+        11, default=0, variant=messages.Variant.INT32),
+    status=messages.StringField(12),
+    updatedMax=messages.IntegerField(13, variant=messages.Variant.INT64),
+    updatedMin=messages.IntegerField(14, variant=messages.Variant.INT64)
+)
+
+
+class IssuesListResponse(messages.Message):
+  """Response message of request to list issues."""
+  error = messages.MessageField(ErrorMessage, 1)
+  items = messages.MessageField(IssueWrapper, 2, repeated=True)
+  totalResults = messages.IntegerField(3, variant=messages.Variant.INT32)
+  kind = messages.StringField(4)
+
+
+"""Request to list group settings."""
+GROUPS_SETTINGS_LIST_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+    message_types.VoidMessage,
+    importedGroupsOnly=messages.BooleanField(1, default=False)
+)
+
+
+class GroupsSettingsListResponse(messages.Message):
+  """Response message of request to list group settings."""
+  error = messages.MessageField(ErrorMessage, 1)
+  groupSettings = messages.MessageField(
+      UserGroupSettingsWrapper, 2, repeated=True)
+
+
+"""Request to create a group."""
+GROUPS_CREATE_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+    message_types.VoidMessage,
+    groupName = messages.StringField(1, required=True),
+    who_can_view_members = messages.EnumField(
+        usergroup_pb2.MemberVisibility, 2,
+        default=usergroup_pb2.MemberVisibility.MEMBERS, required=True),
+    ext_group_type = messages.EnumField(usergroup_pb2.GroupType, 3)
+)
+
+
+class GroupsCreateResponse(messages.Message):
+  """Response message of request to create a group."""
+  error = messages.MessageField(ErrorMessage, 1)
+  groupID = messages.IntegerField(
+      2, variant=messages.Variant.INT32)
+
+
+"""Request to get a group."""
+GROUPS_GET_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+    message_types.VoidMessage,
+    groupName = messages.StringField(1, required=True)
+)
+
+
+class GroupsGetResponse(messages.Message):
+  """Response message of request to create a group."""
+  error = messages.MessageField(ErrorMessage, 1)
+  groupID = messages.IntegerField(
+      2, variant=messages.Variant.INT32)
+  groupSettings = messages.MessageField(
+      UserGroupSettingsWrapper, 3)
+  groupOwners = messages.StringField(4, repeated=True)
+  groupMembers = messages.StringField(5, repeated=True)
+
+
+"""Request to update a group."""
+GROUPS_UPDATE_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+    GroupCitizens,
+    groupName = messages.StringField(1, required=True),
+    who_can_view_members = messages.EnumField(
+        usergroup_pb2.MemberVisibility, 2),
+    ext_group_type = messages.EnumField(usergroup_pb2.GroupType, 3),
+    last_sync_time = messages.IntegerField(
+        4, default=0, variant=messages.Variant.INT32),
+    friend_projects = messages.StringField(5, repeated=True),
+)
+
+
+class GroupsUpdateResponse(messages.Message):
+  """Response message of request to update a group."""
+  error = messages.MessageField(ErrorMessage, 1)
+
+
+########################## Component Message ##########################
+
+class Component(messages.Message):
+  """Component PB."""
+  componentId = messages.IntegerField(
+      1, required=True, variant=messages.Variant.INT32)
+  projectName = messages.StringField(2, required=True)
+  componentPath = messages.StringField(3, required=True)
+  description = messages.StringField(4)
+  admin = messages.StringField(5, repeated=True)
+  cc = messages.StringField(6, repeated=True)
+  deprecated = messages.BooleanField(7, default=False)
+  created = message_types.DateTimeField(8)
+  creator = messages.StringField(9)
+  modified = message_types.DateTimeField(10)
+  modifier = messages.StringField(11)
+
+
+"""Request to get components of a project."""
+COMPONENTS_LIST_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+    message_types.VoidMessage,
+    projectId=messages.StringField(1, required=True),
+)
+
+
+class ComponentsListResponse(messages.Message):
+  """Response to list components."""
+  components = messages.MessageField(
+      Component, 1, repeated=True)
+
+
+class ComponentCreateRequestBody(messages.Message):
+  """Request body to create a component."""
+  parentPath = messages.StringField(1)
+  description = messages.StringField(2)
+  admin = messages.StringField(3, repeated=True)
+  cc = messages.StringField(4, repeated=True)
+  deprecated = messages.BooleanField(5, default=False)
+
+
+"""Request to create component of a project."""
+COMPONENTS_CREATE_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+    ComponentCreateRequestBody,
+    projectId=messages.StringField(1, required=True),
+    componentName=messages.StringField(2, required=True),
+)
+
+
+"""Request to delete a component."""
+COMPONENTS_DELETE_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+    message_types.VoidMessage,
+    projectId=messages.StringField(1, required=True),
+    componentPath=messages.StringField(2, required=True),
+)
+
+
+class ComponentUpdateFieldID(messages.Enum):
+  """Possible fields that can be updated in a component."""
+  LEAF_NAME = 1
+  DESCRIPTION = 2
+  ADMIN = 3
+  CC = 4
+  DEPRECATED = 5
+
+
+class ComponentUpdate(messages.Message):
+  """Component update."""
+  # 'field' allows a field to be cleared
+  field = messages.EnumField(ComponentUpdateFieldID, 1, required=True)
+  leafName = messages.StringField(2)
+  description = messages.StringField(3)
+  admin = messages.StringField(4, repeated=True)
+  cc = messages.StringField(5, repeated=True)
+  deprecated = messages.BooleanField(6)
+
+
+class ComponentUpdateRequestBody(messages.Message):
+  """Request body to update a component."""
+  updates = messages.MessageField(ComponentUpdate, 1, repeated=True)
+
+
+"""Request to update a component."""
+COMPONENTS_UPDATE_REQUEST_RESOURCE_CONTAINER = ResourceContainer(
+    ComponentUpdateRequestBody,
+    projectId=messages.StringField(1, required=True),
+    componentPath=messages.StringField(2, required=True),
+)
diff --git a/mrproto/ast_pb2.py b/mrproto/ast_pb2.py
new file mode 100644
index 0000000..9ad9edf
--- /dev/null
+++ b/mrproto/ast_pb2.py
@@ -0,0 +1,110 @@
+# 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.
+
+"""Protocol buffers for user queries parsed into abstract syntax trees.
+
+A user issue query can look like [Type=Defect owner:jrobbins "memory leak"].
+In that simple form, all the individual search conditions are simply ANDed
+together.  In the code, a list of conditions to be ANDed is called a
+conjunction.
+
+Monorail also supports a quick-or feature: [Type=Defect,Enhancement].  That
+will match any issue that has labels Type-Defect or Type-Enhancement, or both.
+
+Monorail supports a top-level "OR" keyword that can
+be used to logically OR a series of conjunctions.  For example:
+[Type=Defect stars>10 OR Type=Enhancement stars>50].
+
+Parentheses groups and "OR" statements are preprocessed before the final
+QueryAST is constructed.
+
+So, QueryAST is always exactly two levels:  the overall tree
+consists of a list of conjunctions, and each conjunction consists of a list
+of conditions.
+
+A condition can look like [stars>10] or [summary:memory] or
+[Type=Defect,Enhancement].  Each condition has a single comparison operator.
+Most conditions refer to a single field definition, but in the case of
+cross-project search a single condition can have a list of field definitions
+from the different projects being searched.  Each condition can have a list
+of constant values to compare against.  The values may be all strings or all
+integers.
+
+Some conditions are procesed by the SQL database and others by the GAE
+search API.  All conditions are passed to each module and it is up to
+the module to decide which conditions to handle and which to ignore.
+"""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from protorpc import messages
+
+from mrproto import tracker_pb2
+
+
+# This is a special field_name for a FieldDef that means to do a fulltext
+# search for words that occur in any part of the issue.
+ANY_FIELD = 'any_field'
+
+
+class QueryOp(messages.Enum):
+  """Enumeration of possible query condition operators."""
+  EQ = 1
+  NE = 2
+  LT = 3
+  GT = 4
+  LE = 5
+  GE = 6
+  TEXT_HAS = 7
+  NOT_TEXT_HAS = 8
+  IS_DEFINED = 11
+  IS_NOT_DEFINED = 12
+  KEY_HAS = 13
+
+
+class TokenType(messages.Enum):
+  """Enumeration of query tokens used for parentheses parsing."""
+  SUBQUERY = 1
+  LEFT_PAREN = 2
+  RIGHT_PAREN = 3
+  OR = 4
+
+
+class QueryToken(messages.Message):
+  """Data structure to represent a single token for parentheses parsing."""
+  token_type = messages.EnumField(TokenType, 1, required=True)
+  value = messages.StringField(2)
+
+
+class Condition(messages.Message):
+  """Representation of one query condition.  E.g., [Type=Defect,Task]."""
+  op = messages.EnumField(QueryOp, 1, required=True)
+  field_defs = messages.MessageField(tracker_pb2.FieldDef, 2, repeated=True)
+  str_values = messages.StringField(3, repeated=True)
+  int_values = messages.IntegerField(4, repeated=True)
+  # The suffix of a search field
+  # eg. the 'approver' in 'UXReview-approver:user@mail.com'
+  key_suffix = messages.StringField(5)
+  # The name of the phase this field value should belong to.
+  phase_name = messages.StringField(6)
+
+
+class Conjunction(messages.Message):
+  """A list of conditions that are implicitly ANDed together."""
+  conds = messages.MessageField(Condition, 1, repeated=True)
+
+
+class QueryAST(messages.Message):
+  """Abstract syntax tree for the user's query."""
+  conjunctions = messages.MessageField(Conjunction, 1, repeated=True)
+
+
+def MakeCond(op, field_defs, str_values, int_values,
+             key_suffix=None, phase_name=None):
+  """Shorthand function to construct a Condition PB."""
+  return Condition(
+      op=op, field_defs=field_defs, str_values=str_values,
+      int_values=int_values, key_suffix=key_suffix, phase_name=phase_name)
diff --git a/mrproto/features_pb2.py b/mrproto/features_pb2.py
new file mode 100644
index 0000000..cc0dc44
--- /dev/null
+++ b/mrproto/features_pb2.py
@@ -0,0 +1,85 @@
+# 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.
+
+"""Protocol buffers for Monorail features."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from features import features_constants
+from protorpc import messages
+
+
+class Hotlist(messages.Message):
+  """This protocol buffer holds all the metadata associated with a hotlist."""
+  # A numeric identifier for this hotlist.
+  hotlist_id = messages.IntegerField(1, required=True)
+
+  # The short identifier for this hotlist.
+  name = messages.StringField(2, required=True)
+
+  # A one-line summary (human-readable) of the hotlist.
+  summary = messages.StringField(3, default='')
+
+  # A detailed description of the hotlist.
+  description = messages.StringField(4, default='')
+
+  # Hotlists can be marked private to prevent unwanted users from seeing them.
+  is_private = messages.BooleanField(5, default=False)
+
+  # Note that these lists are disjoint (a user ID will not appear twice).
+  owner_ids = messages.IntegerField(6, repeated=True)
+  editor_ids = messages.IntegerField(8, repeated=True)
+  follower_ids = messages.IntegerField(9, repeated=True)
+
+
+  class HotlistItem(messages.Message):
+    """Nested message for a hotlist to issue relation."""
+    issue_id = messages.IntegerField(1, required=True)
+    rank = messages.IntegerField(2, required=True)
+    adder_id = messages.IntegerField(3)
+    date_added = messages.IntegerField(4)
+    note = messages.StringField(5, default='')
+
+  items = messages.MessageField(HotlistItem, 10, repeated=True)
+
+  # The default columns to show on hotlist issues page
+  default_col_spec = messages.StringField(
+      11, default=features_constants.DEFAULT_COL_SPEC)
+
+def MakeHotlist(name, hotlist_item_fields=None, **kwargs):
+  """Returns a hotlist protocol buffer with the given attributes.
+    Args:
+      hotlist_item_fields: tuple of (iid, rank, user, date, note)
+  kwargs should only include the following:
+    hotlist_id, summary, description, is_private, owner_ids, editor_ids,
+    follower_ids, default_col_spec"""
+  hotlist = Hotlist(name=name, **kwargs)
+
+  if hotlist_item_fields is not None:
+    for iid, rank, user, date, note in hotlist_item_fields:
+      hotlist.items.append(Hotlist.HotlistItem(
+          issue_id=iid, rank=rank, adder_id=user, date_added=date, note=note))
+
+  return hotlist
+
+
+# For any issues that were added to hotlists before we started storing that
+# timestamp, just use the launch date of the feature as a default.
+ADDED_TS_FEATURE_LAUNCH_TS = 1484350000  # Jan 13, 2017
+
+
+def MakeHotlistItem(
+    issue_id, rank=None, adder_id=None, date_added=None, note=None):
+  item = Hotlist.HotlistItem(
+      issue_id=issue_id,
+      date_added=date_added or ADDED_TS_FEATURE_LAUNCH_TS)
+  if rank is not None:
+    item.rank = rank
+  if adder_id is not None:
+    item.adder_id = adder_id
+  if note is not None:
+    item.note = note
+  return item
diff --git a/mrproto/project_pb2.py b/mrproto/project_pb2.py
new file mode 100644
index 0000000..0edac8d
--- /dev/null
+++ b/mrproto/project_pb2.py
@@ -0,0 +1,238 @@
+# 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.
+
+"""Protocol buffers for Monorail projects."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from protorpc import messages
+
+# Project state affects permissions in that project, and project deletion.
+# It is edited on the project admin page.  If it is anything other that LIVE
+# it triggers a notice at the top of every project page.
+# For more info, see the "Project deletion in Monorail" design doc.
+class ProjectState(messages.Enum):
+  """Enum for states in the project lifecycle."""
+  # Project is visible and indexed. This is the typical state.
+  #
+  # If moved_to is set, this project is live but has been moved
+  # to another location, so redirects will be used or links shown.
+  LIVE = 1
+
+  # Project owner has requested the project be archived. Project is
+  # read-only to members only, off-limits to non-members.  Issues
+  # can be searched when in the project, but should not appear in
+  # site-wide searches.  The project name is still in-use by this
+  # project.
+  #
+  # If a delete_time is set, then the project is doomed: (1) the
+  # state can only be changed by a site admin, and (2) the project
+  # will automatically transition to DELETABLE after that time is
+  # reached.
+  ARCHIVED = 2
+
+  # Project can be deleted at any time.  The project name should
+  # have already been changed to a generated string, so it's
+  # impossible to navigate to this project, and the original name
+  # can be reused by a new project.
+  DELETABLE = 3
+
+
+# Project access affects permissions in that project.
+# It is edited on the project admin page.
+class ProjectAccess(messages.Enum):
+  """Enum for possible project access levels."""
+  # Anyone may view this project, even anonymous users.
+  ANYONE = 1
+
+  # Only project members may view the project.
+  MEMBERS_ONLY = 3
+
+
+# A Project PB represents a project in Monorail, which is a workspace for
+# project members to collaborate on issues.
+# A project is created on the project creation page, searched on the project
+# list page, and edited on the project admin page.
+# Next message: 74
+class Project(messages.Message):
+  """This protocol buffer holds all the metadata associated with a project."""
+  state = messages.EnumField(ProjectState, 1, required=True)
+  access = messages.EnumField(ProjectAccess, 18, default=ProjectAccess.ANYONE)
+
+  # The short identifier for this project. This value is lower-cased,
+  # and must be between 3 and 20 characters (inclusive). Alphanumeric
+  # and dashes are allowed, and it must start with an alpha character.
+  # Project names must be unique.
+  project_name = messages.StringField(2, required=True)
+
+  # A numeric identifier for this project.
+  project_id = messages.IntegerField(3, required=True)
+
+  # A one-line summary (human-readable) name of the project.
+  summary = messages.StringField(4, default='')
+
+  # A detailed description of the project.
+  description = messages.StringField(5, default='')
+
+  # Description of why this project has the state set as it is.
+  # This is used for administrative purposes to notify Owners that we
+  # are going to delete their project unless they can provide a good
+  # reason to not do so.
+  state_reason = messages.StringField(9)
+
+  # Time (in seconds) at which an ARCHIVED project may automatically
+  # be changed to state DELETABLE.  The state change is done by a
+  # cron job.
+  delete_time = messages.IntegerField(10)
+
+  # Note that these lists are disjoint (a user ID will not appear twice).
+  owner_ids = messages.IntegerField(11, repeated=True)
+  committer_ids = messages.IntegerField(12, repeated=True)
+  contributor_ids = messages.IntegerField(15, repeated=True)
+
+  class ExtraPerms(messages.Message):
+    """Nested message for each member's extra permissions in a project."""
+    member_id = messages.IntegerField(1, required=True)
+    # Each custom perm is a single word [a-zA-Z0-9].
+    perms = messages.StringField(2, repeated=True)
+
+  extra_perms = messages.MessageField(ExtraPerms, 16, repeated=True)
+
+  # Project owners may choose to have ALL issue change notifications go to a
+  # mailing list (in addition to going directly to the users interested
+  # in that issue).
+  issue_notify_address = messages.StringField(14)
+
+  # These fields keep track of the cumulative size of all issue attachments
+  # in a given project.  Normally, the number of bytes used is compared
+  # to a constant defined in the web application.  However, if a custom
+  # quota is specified here, it will be used instead.  An issue attachment
+  # will fail if its size would put the project over its quota.  Not all
+  # projects have these fields: they are only set when the first attachment
+  # is uploaded.
+  attachment_bytes_used = messages.IntegerField(38, default=0)
+  # If quota is not set, default from tracker_constants.py is used.
+  attachment_quota = messages.IntegerField(39)
+
+  # NOTE: open slots 40, 41
+
+  # Recent_activity is a timestamp (in seconds since the Epoch) of the
+  # last time that an issue was entered, updated, or commented on.
+  recent_activity = messages.IntegerField(42, default=0)
+
+  # NOTE: open slots 43...
+
+  # Timestamp (in seconds since the Epoch) of the most recent change
+  # to this project that would invalidate cached content.  It is set
+  # whenever project membership is edited, or any component config PB
+  # is edited.  HTTP requests for auto-complete feeds include this
+  # value in the URL.
+  cached_content_timestamp = messages.IntegerField(53, default=0)
+
+  # If set, this project has been moved elsewhere.  This can
+  # be an absolute URL, the name of another project on the same site.
+  moved_to = messages.StringField(60)
+
+  # Enable inbound email processing for issues.
+  process_inbound_email = messages.BooleanField(63, default=False)
+
+  # Limit removal of Restrict-* labels to project owners.
+  only_owners_remove_restrictions = messages.BooleanField(64, default=False)
+
+  # A per-project read-only lock. This lock (1) is meant to be
+  # long-lived (lasting as long as migration operations, project
+  # deletion, or anything else might take and (2) is meant to only
+  # limit user mutations; whether or not it limits automated actions
+  # that would change project data (such as workflow items) is
+  # determined based on the action.
+  #
+  # This lock is implemented as a user-visible string describing the
+  # reason for the project being in a read-only state. An absent or empty
+  # value indicates that the project is read-write; a present and
+  # non-empty value indicates that the project is read-only for the
+  # reason described.
+  read_only_reason = messages.StringField(65)
+
+  # This option is rarely used, but it makes sense for projects that aim for
+  # hub-and-spoke collaboration bewtween a vendor organization (like Google)
+  # and representatives of partner companies who are not supposed to know
+  # about each other.
+  # When true, it prevents project committers, contributors, and visitors
+  # from seeing the list of project members on the project summary page,
+  # on the People list page, and in autocomplete for issue owner and Cc.
+  # Project owners can always see the complete list of project members.
+  only_owners_see_contributors = messages.BooleanField(66, default=False)
+
+  # This configures the URLs generated when autolinking revision numbers.
+  # E.g., gitiles, viewvc, or crrev.com.
+  revision_url_format = messages.StringField(67)
+
+  # The home page of the Project.
+  home_page = messages.StringField(68)
+  # The url to redirect to for wiki/documentation links.
+  docs_url = messages.StringField(71)
+  # The url to redirect to for wiki/documentation links.
+  source_url = messages.StringField(72)
+  # The GCS object ID of the Project's logo.
+  logo_gcs_id = messages.StringField(69)
+  # The uploaded file name of the Project's logo.
+  logo_file_name = messages.StringField(70)
+
+  # Always send the full content of update in notifications.
+  issue_notify_always_detailed = messages.BooleanField(73, default=False)
+
+
+# This PB documents some of the duties of some of the members
+# in a given project.  This info is displayed on the project People page.
+class ProjectCommitments(messages.Message):
+  project_id = messages.IntegerField(50)
+
+  class MemberCommitment(messages.Message):
+    member_id = messages.IntegerField(11, required=True)
+    notes = messages.StringField(13)
+
+  commitments = messages.MessageField(MemberCommitment, 2, repeated=True)
+
+
+def MakeProject(
+    project_name, project_id=None, state=ProjectState.LIVE,
+    access=ProjectAccess.ANYONE, summary=None, description=None,
+    moved_to=None, cached_content_timestamp=None,
+    owner_ids=None, committer_ids=None, contributor_ids=None,
+    read_only_reason=None, home_page=None, docs_url=None, source_url=None,
+    logo_gcs_id=None, logo_file_name=None):
+  """Returns a project protocol buffer with the given attributes."""
+  project = Project(
+      project_name=project_name, access=access, state=state)
+  if project_id:
+    project.project_id = project_id
+  if moved_to:
+    project.moved_to = moved_to
+  if cached_content_timestamp:
+    project.cached_content_timestamp = cached_content_timestamp
+  if summary:
+    project.summary = summary
+  if description:
+    project.description = description
+  if home_page:
+    project.home_page = home_page
+  if docs_url:
+    project.docs_url = docs_url
+  if source_url:
+    project.source_url = source_url
+  if logo_gcs_id:
+    project.logo_gcs_id = logo_gcs_id
+  if logo_file_name:
+    project.logo_file_name = logo_file_name
+
+  project.owner_ids.extend(owner_ids or [])
+  project.committer_ids.extend(committer_ids or [])
+  project.contributor_ids.extend(contributor_ids or [])
+
+  if read_only_reason is not None:
+    project.read_only_reason = read_only_reason
+
+  return project
diff --git a/mrproto/secrets.proto b/mrproto/secrets.proto
new file mode 100644
index 0000000..bab34b5
--- /dev/null
+++ b/mrproto/secrets.proto
@@ -0,0 +1,36 @@
+// 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.
+
+// This file defines protobufs needed for handling Monorail secrets.
+
+syntax = "proto3";
+
+package monorail.secrets;
+
+
+// Next available tag: 7
+message ListRequestContents {
+  // The parent resource of the requested resources.
+  string parent = 1;
+  // The requested page size for listing the resources.
+  int32 page_size = 2;
+  // The requested sort order of the list of resources.
+  string order_by = 3;
+  // The query that may be used to filter which resources to show.
+  string query = 4;
+  // The resource names of projects to query within.
+  repeated string projects = 5;
+  // The string that may be used to filter which resources to show.
+  // See AIP-160.
+  string filter = 6;
+}
+
+
+// Next available tag: 3
+message PageTokenContents {
+  // The index of where the requested resource list should start.
+  int32 start = 1;
+  // An encrypted ListRequestContents message.
+  bytes encrypted_list_request_contents = 2;
+}
diff --git a/mrproto/secrets_pb2.py b/mrproto/secrets_pb2.py
new file mode 100644
index 0000000..0b64ced
--- /dev/null
+++ b/mrproto/secrets_pb2.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+# 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.
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: mrproto/secrets.proto
+
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='mrproto/secrets.proto',
+  package='monorail.secrets',
+  syntax='proto3',
+  serialized_options=None,
+  create_key=_descriptor._internal_create_key,
+  serialized_pb=b'\n\x13proto/secrets.proto\x12\x10monorail.secrets\"{\n\x13ListRequestContents\x12\x0e\n\x06parent\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x10\n\x08order_by\x18\x03 \x01(\t\x12\r\n\x05query\x18\x04 \x01(\t\x12\x10\n\x08projects\x18\x05 \x03(\t\x12\x0e\n\x06\x66ilter\x18\x06 \x01(\t\"K\n\x11PageTokenContents\x12\r\n\x05start\x18\x01 \x01(\x05\x12\'\n\x1f\x65ncrypted_list_request_contents\x18\x02 \x01(\x0c\x62\x06proto3'
+)
+
+
+
+
+_LISTREQUESTCONTENTS = _descriptor.Descriptor(
+  name='ListRequestContents',
+  full_name='monorail.secrets.ListRequestContents',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='parent', full_name='monorail.secrets.ListRequestContents.parent', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='page_size', full_name='monorail.secrets.ListRequestContents.page_size', index=1,
+      number=2, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='order_by', full_name='monorail.secrets.ListRequestContents.order_by', index=2,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='query', full_name='monorail.secrets.ListRequestContents.query', index=3,
+      number=4, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='projects', full_name='monorail.secrets.ListRequestContents.projects', index=4,
+      number=5, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='filter', full_name='monorail.secrets.ListRequestContents.filter', index=5,
+      number=6, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=41,
+  serialized_end=164,
+)
+
+
+_PAGETOKENCONTENTS = _descriptor.Descriptor(
+  name='PageTokenContents',
+  full_name='monorail.secrets.PageTokenContents',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='start', full_name='monorail.secrets.PageTokenContents.start', index=0,
+      number=1, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='encrypted_list_request_contents', full_name='monorail.secrets.PageTokenContents.encrypted_list_request_contents', index=1,
+      number=2, type=12, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"",
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=166,
+  serialized_end=241,
+)
+
+DESCRIPTOR.message_types_by_name['ListRequestContents'] = _LISTREQUESTCONTENTS
+DESCRIPTOR.message_types_by_name['PageTokenContents'] = _PAGETOKENCONTENTS
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+ListRequestContents = _reflection.GeneratedProtocolMessageType('ListRequestContents', (_message.Message,), {
+  'DESCRIPTOR' : _LISTREQUESTCONTENTS,
+  '__module__' : 'mrproto.secrets_pb2'
+  # @@protoc_insertion_point(class_scope:monorail.secrets.ListRequestContents)
+  })
+_sym_db.RegisterMessage(ListRequestContents)
+
+PageTokenContents = _reflection.GeneratedProtocolMessageType('PageTokenContents', (_message.Message,), {
+  'DESCRIPTOR' : _PAGETOKENCONTENTS,
+  '__module__' : 'mrproto.secrets_pb2'
+  # @@protoc_insertion_point(class_scope:monorail.secrets.PageTokenContents)
+  })
+_sym_db.RegisterMessage(PageTokenContents)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/mrproto/site_pb2.py b/mrproto/site_pb2.py
new file mode 100644
index 0000000..820417e
--- /dev/null
+++ b/mrproto/site_pb2.py
@@ -0,0 +1,25 @@
+# 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.
+
+"""Protocol buffers for Monorail site-wide features."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from protorpc import messages
+
+
+class UserTypeRestriction(messages.Enum):
+  """An enum for site-wide settings about who can take an action."""
+  # Anyone may do it.
+  ANYONE = 1
+
+  # Only domain admins may do it.
+  ADMIN_ONLY = 2
+
+  # No one may do it, the feature is basically disabled.
+  NO_ONE = 3
+
+  # TODO(jrobbins): implement same-domain users
diff --git a/mrproto/test/__init__.py b/mrproto/test/__init__.py
new file mode 100644
index 0000000..68130d5
--- /dev/null
+++ b/mrproto/test/__init__.py
@@ -0,0 +1,3 @@
+# 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.
diff --git a/mrproto/test/ast_pb2_test.py b/mrproto/test/ast_pb2_test.py
new file mode 100644
index 0000000..91bb8ec
--- /dev/null
+++ b/mrproto/test/ast_pb2_test.py
@@ -0,0 +1,27 @@
+# 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.
+
+"""Tests for ast_pb2 functions."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from mrproto import ast_pb2
+from mrproto import tracker_pb2
+
+
+class ASTPb2Test(unittest.TestCase):
+
+  def testCond(self):
+    fd = tracker_pb2.FieldDef(field_id=1, field_name='Size')
+    cond = ast_pb2.MakeCond(
+        ast_pb2.QueryOp.EQ, [fd], ['XL'], [], key_suffix='-approver')
+    self.assertEqual(ast_pb2.QueryOp.EQ, cond.op)
+    self.assertEqual([fd], cond.field_defs)
+    self.assertEqual(['XL'], cond.str_values)
+    self.assertEqual([], cond.int_values)
+    self.assertEqual(cond.key_suffix, '-approver')
diff --git a/mrproto/test/features_pb2_test.py b/mrproto/test/features_pb2_test.py
new file mode 100644
index 0000000..fd145dc
--- /dev/null
+++ b/mrproto/test/features_pb2_test.py
@@ -0,0 +1,55 @@
+# 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.
+
+"""Tests for features_pb2 functions."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from mrproto import features_pb2
+
+
+class FeaturesPb2Test(unittest.TestCase):
+
+  def testMakeHotlist_Defaults(self):
+    hotlist = features_pb2.MakeHotlist('summer-issues')
+    self.assertEqual('summer-issues', hotlist.name)
+    self.assertEqual([], hotlist.items)
+
+  def testMakeHotlist_Everything(self):
+    ts = 20011111111111
+    hotlist = features_pb2.MakeHotlist(
+        'summer-issues', [(1000, 1, 444, ts, ''), (1001, 2, 333, ts, ''),
+                          (1009, None, None, ts, '')],
+        description='desc')
+    self.assertEqual('summer-issues', hotlist.name)
+    self.assertEqual(
+        [features_pb2.MakeHotlistItem(
+            1000, rank=1, adder_id=444, date_added=ts, note=''),
+         features_pb2.MakeHotlistItem(
+             1001, rank=2, adder_id=333, date_added=ts, note=''),
+         features_pb2.MakeHotlistItem(1009, date_added=ts, note=''),
+         ],
+        hotlist.items)
+    self.assertEqual('desc', hotlist.description)
+
+  def testMakeHotlistItem(self):
+    ts = 20011111111111
+    item_1 = features_pb2.MakeHotlistItem(
+        1000, rank=1, adder_id=111, date_added=ts, note='short note')
+    self.assertEqual(1000, item_1.issue_id)
+    self.assertEqual(1, item_1.rank)
+    self.assertEqual(111, item_1.adder_id)
+    self.assertEqual(ts, item_1.date_added)
+    self.assertEqual('short note', item_1.note)
+
+    item_2 = features_pb2.MakeHotlistItem(1001)
+    self.assertEqual(1001, item_2.issue_id)
+    self.assertEqual(None, item_2.rank)
+    self.assertEqual(None, item_2.adder_id)
+    self.assertEqual('', item_2.note)
+    self.assertEqual(features_pb2.ADDED_TS_FEATURE_LAUNCH_TS, item_2.date_added)
diff --git a/mrproto/test/project_pb2_test.py b/mrproto/test/project_pb2_test.py
new file mode 100644
index 0000000..c9f8e61
--- /dev/null
+++ b/mrproto/test/project_pb2_test.py
@@ -0,0 +1,52 @@
+# 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.
+
+"""Tests for project_pb2 functions."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from mrproto import project_pb2
+
+
+class ProjectPb2Test(unittest.TestCase):
+
+  def testMakeProject_Defaults(self):
+    project = project_pb2.MakeProject('proj')
+    self.assertEqual('proj', project.project_name)
+    self.assertEqual(project_pb2.ProjectState.LIVE, project.state)
+    self.assertEqual(project_pb2.ProjectAccess.ANYONE, project.access)
+    self.assertFalse(project.read_only_reason)
+
+  def testMakeProject_Everything(self):
+    project = project_pb2.MakeProject(
+        'proj', project_id=789, state=project_pb2.ProjectState.ARCHIVED,
+        access=project_pb2.ProjectAccess.MEMBERS_ONLY, summary='sum',
+        description='desc', moved_to='example.com',
+        cached_content_timestamp=1234567890, owner_ids=[111],
+        committer_ids=[222], contributor_ids=[333],
+        read_only_reason='being migrated',
+        home_page='example.com', docs_url='example.com/docs',
+        source_url='example.com/src', logo_gcs_id='logo_id',
+        logo_file_name='logo.gif')
+    self.assertEqual('proj', project.project_name)
+    self.assertEqual(789, project.project_id)
+    self.assertEqual(project_pb2.ProjectState.ARCHIVED, project.state)
+    self.assertEqual(project_pb2.ProjectAccess.MEMBERS_ONLY, project.access)
+    self.assertEqual('sum', project.summary)
+    self.assertEqual('desc', project.description)
+    self.assertEqual('example.com', project.moved_to)
+    self.assertEqual(1234567890, project.cached_content_timestamp)
+    self.assertEqual([111], project.owner_ids)
+    self.assertEqual([222], project.committer_ids)
+    self.assertEqual([333], project.contributor_ids)
+    self.assertEqual('being migrated', project.read_only_reason)
+    self.assertEqual('example.com', project.home_page)
+    self.assertEqual('example.com/docs', project.docs_url)
+    self.assertEqual('example.com/src', project.source_url)
+    self.assertEqual('logo_id', project.logo_gcs_id)
+    self.assertEqual('logo.gif', project.logo_file_name)
diff --git a/mrproto/test/user_pb2_test.py b/mrproto/test/user_pb2_test.py
new file mode 100644
index 0000000..fcfed71
--- /dev/null
+++ b/mrproto/test/user_pb2_test.py
@@ -0,0 +1,28 @@
+# 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.
+
+"""Tests for user_pb2 functions."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from mrproto import user_pb2
+
+
+class UserPb2Test(unittest.TestCase):
+
+  def testUser_Defaults(self):
+    user = user_pb2.MakeUser(111)
+    self.assertEqual(111, user.user_id)
+    self.assertFalse(user.obscure_email)
+    self.assertIsNone(user.email)
+
+  def testUser_Everything(self):
+    user = user_pb2.MakeUser(111, email='user@example.com', obscure_email=True)
+    self.assertEqual(111, user.user_id)
+    self.assertTrue(user.obscure_email)
+    self.assertEqual('user@example.com', user.email)
diff --git a/mrproto/test/usergroup_pb2_test.py b/mrproto/test/usergroup_pb2_test.py
new file mode 100644
index 0000000..26f7166
--- /dev/null
+++ b/mrproto/test/usergroup_pb2_test.py
@@ -0,0 +1,36 @@
+# 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.
+
+"""Tests for usergroup_pb2 functions."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import unittest
+
+from mrproto import usergroup_pb2
+
+
+class UserGroupPb2Test(unittest.TestCase):
+
+  def testMakeSettings_Defaults(self):
+    usergroup = usergroup_pb2.MakeSettings('anyone')
+    self.assertEqual(
+        usergroup_pb2.MemberVisibility.ANYONE,
+        usergroup.who_can_view_members)
+    self.assertIsNone(usergroup.ext_group_type)
+    self.assertEqual(0, usergroup.last_sync_time)
+    self.assertEqual([], usergroup.friend_projects)
+
+  def testMakeSettings_Everything(self):
+    usergroup = usergroup_pb2.MakeSettings(
+        'Members', ext_group_type_str='mdb',
+        last_sync_time=1234567890, friend_projects=[789])
+    self.assertEqual(
+        usergroup_pb2.MemberVisibility.MEMBERS,
+        usergroup.who_can_view_members)
+    self.assertEqual(usergroup_pb2.GroupType.MDB, usergroup.ext_group_type)
+    self.assertEqual(1234567890, usergroup.last_sync_time)
+    self.assertEqual([789], usergroup.friend_projects)
diff --git a/mrproto/tracker_pb2.py b/mrproto/tracker_pb2.py
new file mode 100644
index 0000000..f5820e6
--- /dev/null
+++ b/mrproto/tracker_pb2.py
@@ -0,0 +1,553 @@
+# 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.
+
+"""The Monorail issue tracker uses ProtoRPC for storing business objects."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from protorpc import messages
+
+
+class FieldValue(messages.Message):
+  """Holds a single custom field value in an issue.
+
+  Multi-valued custom fields will have multiple such FieldValues on a given
+  issue. Note that enumerated type custom fields are represented as key-value
+  labels.
+  """
+  field_id = messages.IntegerField(1, required=True)
+  # Only one of the following fields will hve any value.
+  int_value = messages.IntegerField(2)
+  str_value = messages.StringField(3)
+  user_id = messages.IntegerField(4)
+  date_value = messages.IntegerField(6)
+  url_value = messages.StringField(7)
+
+  derived = messages.BooleanField(5, default=False)
+
+  # None if field is not a phse field.
+  phase_id = messages.IntegerField(8)
+
+
+class ApprovalStatus(messages.Enum):
+  """Statuses that an approval field could be set to."""
+  NEEDS_REVIEW = 1
+  NA = 2
+  REVIEW_REQUESTED = 3
+  REVIEW_STARTED = 4
+  NEED_INFO = 5
+  APPROVED = 6
+  NOT_APPROVED = 7
+  NOT_SET = 8
+
+
+class ApprovalValue(messages.Message):
+  """Holds a single approval field value in an issue."""
+  approval_id = messages.IntegerField(1)
+  status = messages.EnumField(ApprovalStatus, 2, default='NOT_SET')
+  setter_id = messages.IntegerField(3)
+  set_on = messages.IntegerField(4)
+  approver_ids = messages.IntegerField(5, repeated=True)
+  phase_id = messages.IntegerField(7)
+
+
+class ApprovalDelta(messages.Message):
+  """In-memory representation of requested changes to an issue's approval."""
+  status = messages.EnumField(ApprovalStatus, 1)
+  set_on = messages.IntegerField(2)
+  setter_id = messages.IntegerField(3)
+  approver_ids_add = messages.IntegerField(4, repeated=True)
+  approver_ids_remove = messages.IntegerField(5, repeated=True)
+  subfield_vals_add = messages.MessageField(FieldValue, 6, repeated=True)
+  subfield_vals_remove = messages.MessageField(FieldValue, 7, repeated=True)
+  subfields_clear = messages.IntegerField(8, repeated=True)
+  # Stores Approval's Enum subfield changes.
+  labels_add = messages.StringField(9, repeated=True)
+  labels_remove = messages.StringField(10, repeated=True)
+
+
+class Phase(messages.Message):
+  """Holds a single launch review phase."""
+  phase_id = messages.IntegerField(1)
+  name = messages.StringField(2)
+  rank = messages.IntegerField(4)
+
+
+class DanglingIssueRef(messages.Message):
+  """Holds a reference to an issue on Codesite or an external tracker."""
+  project = messages.StringField(1, required=True)
+  issue_id = messages.IntegerField(2, required=True)
+  ext_issue_identifier = messages.StringField(3, required=False)
+
+
+class Issue(messages.Message):
+  """Holds all the current metadata about an issue.
+
+  The most frequent searches can work by consulting solely the issue metadata.
+  Display of the issue list is done solely with this issue metadata.
+  Displaying one issue in detail with description and comments requires
+  more info from other objects.
+
+  The issue_id field is the unique primary key for retrieving issues.  Local ID
+  is a small integer that counts up in each project.
+
+  Summary, Status, Owner, CC, reporter, and opened_timestamp are hard
+  fields that are always there.  All other metadata is stored as
+  labels or custom fields.
+  Next available tag: 63.
+  """
+  # Globally unique issue ID.
+  issue_id = messages.IntegerField(42)
+  # project_name is not stored in the DB, only the project_id is stored.
+  # project_name is used in RAM to simplify formatting logic in lots of places.
+  project_name = messages.StringField(1, required=True)
+  project_id = messages.IntegerField(50)
+  local_id = messages.IntegerField(2, required=True)
+  summary = messages.StringField(3, default='')
+  status = messages.StringField(4, default='')
+  owner_id = messages.IntegerField(5)
+  cc_ids = messages.IntegerField(6, repeated=True)
+  labels = messages.StringField(7, repeated=True)
+  component_ids = messages.IntegerField(39, repeated=True)
+
+  # Denormalized count of stars on this Issue.
+  star_count = messages.IntegerField(8, required=True, default=0)
+  reporter_id = messages.IntegerField(9, required=True, default=0)
+  # Time that the issue was opened, in seconds since the Epoch.
+  opened_timestamp = messages.IntegerField(10, required=True, default=0)
+
+  # This should be set when an issue is closed and cleared when a
+  # closed issue is reopened.  Measured in seconds since the Epoch.
+  closed_timestamp = messages.IntegerField(12, default=0)
+
+  # This should be updated every time an issue is modified.  Measured
+  # in seconds since the Epoch.
+  modified_timestamp = messages.IntegerField(13, default=0)
+
+  # These timestamps are updated whenever owner, status, or components
+  # change, including when altered by a filter rule.
+  owner_modified_timestamp = messages.IntegerField(19, default=0)
+  status_modified_timestamp = messages.IntegerField(20, default=0)
+  component_modified_timestamp = messages.IntegerField(21, default=0)
+
+  # Enhanced version of modified_timestamp that also captures changes to
+  # subresources of issues like stars, comments, and attachments.
+  # See: go/monorail-enhanced-modified-time
+  migration_modified_timestamp = messages.IntegerField(62, default=0)
+
+  # Issue IDs of issues that this issue is blocked on.
+  blocked_on_iids = messages.IntegerField(16, repeated=True)
+
+  # Rank values of issue relations that are blocking this issue. The issue
+  # with id blocked_on_iids[i] has rank value blocked_on_ranks[i]
+  blocked_on_ranks = messages.IntegerField(54, repeated=True)
+
+  # Issue IDs of issues that this issue is blocking.
+  blocking_iids = messages.IntegerField(17, repeated=True)
+
+  # References to 'dangling' (still in codesite) issue relations.
+  dangling_blocked_on_refs = messages.MessageField(
+      DanglingIssueRef, 52, repeated=True)
+  dangling_blocking_refs = messages.MessageField(
+      DanglingIssueRef, 53, repeated=True)
+
+  # Issue ID of issue that this issue was merged into most recently.  When it
+  # is missing or 0, it is considered to be not merged into any other issue.
+  merged_into = messages.IntegerField(18)
+  # Use this when an issue is a duplicate of an issue in an external tracker.
+  merged_into_external = messages.StringField(61)
+
+  # Default derived via rules, used iff status == ''.
+  derived_status = messages.StringField(30, default='')
+  # Default derived via rules, used iff owner_id == 0.
+  derived_owner_id = messages.IntegerField(31, default=0)
+  # Additional CCs derived via rules.
+  derived_cc_ids = messages.IntegerField(32, repeated=True)
+  # Additional labels derived via rules.
+  derived_labels = messages.StringField(33, repeated=True)
+  # Additional notification email addresses derived via rules.
+  derived_notify_addrs = messages.StringField(34, repeated=True)
+  # Additional components derived via rules.
+  derived_component_ids = messages.IntegerField(40, repeated=True)
+  # Software development process warnings and errors generated by filter rules.
+  # TODO(jrobbins): these are not yet stored in the DB, they are only in RAM.
+  derived_warnings = messages.StringField(55, repeated=True)
+  derived_errors = messages.StringField(56, repeated=True)
+
+  # Soft delete of the entire issue.
+  deleted = messages.BooleanField(35, default=False)
+
+  # Total number of attachments in the issue
+  attachment_count = messages.IntegerField(36, default=0)
+
+  # Total number of comments on the issue (not counting the initial comment
+  # created when the issue is created).
+  comment_count = messages.IntegerField(37, default=0)
+
+  # Custom field values (other than enums)
+  field_values = messages.MessageField(FieldValue, 41, repeated=True)
+
+  is_spam = messages.BooleanField(51, default=False)
+  # assume_stale is used in RAM to ensure that a value saved to the DB was
+  # loaded from the DB in the same request handler (not via the cache).
+  assume_stale = messages.BooleanField(57, default=True)
+
+  phases = messages.MessageField(Phase, 59, repeated=True)
+  approval_values = messages.MessageField(ApprovalValue, 60, repeated=True)
+
+
+class FieldID(messages.Enum):
+  """Possible fields that can be updated in an Amendment."""
+  # The spelling of these names must match enum values in tracker.sql.
+  SUMMARY = 1
+  STATUS = 2
+  OWNER = 3
+  CC = 4
+  LABELS = 5
+  BLOCKEDON = 6
+  BLOCKING = 7
+  MERGEDINTO = 8
+  PROJECT = 9
+  COMPONENTS = 10
+  CUSTOM = 11
+  WARNING = 12
+  ERROR = 13
+
+
+class IssueDelta(messages.Message):
+  """In-memory representation of requested changes to an issue.
+
+  Next available tag: 23
+  """
+  status = messages.StringField(1)
+  owner_id = messages.IntegerField(2)
+  cc_ids_add = messages.IntegerField(3, repeated=True)
+  cc_ids_remove = messages.IntegerField(4, repeated=True)
+  comp_ids_add = messages.IntegerField(5, repeated=True)
+  comp_ids_remove = messages.IntegerField(6, repeated=True)
+  labels_add = messages.StringField(7, repeated=True)
+  labels_remove = messages.StringField(8, repeated=True)
+  field_vals_add = messages.MessageField(FieldValue, 9, repeated=True)
+  field_vals_remove = messages.MessageField(FieldValue, 10, repeated=True)
+  fields_clear = messages.IntegerField(11, repeated=True)
+  blocked_on_add = messages.IntegerField(12, repeated=True)
+  blocked_on_remove = messages.IntegerField(13, repeated=True)
+  blocking_add = messages.IntegerField(14, repeated=True)
+  blocking_remove = messages.IntegerField(15, repeated=True)
+  merged_into = messages.IntegerField(16)
+  merged_into_external = messages.StringField(22)
+  summary = messages.StringField(17)
+  ext_blocked_on_add = messages.StringField(18, repeated=True)
+  ext_blocked_on_remove = messages.StringField(19, repeated=True)
+  ext_blocking_add = messages.StringField(20, repeated=True)
+  ext_blocking_remove = messages.StringField(21, repeated=True)
+
+
+class Amendment(messages.Message):
+  """Holds info about one issue field change."""
+  field = messages.EnumField(FieldID, 11, required=True)
+  # User-visible string describing the change
+  newvalue = messages.StringField(12)
+  # Newvalue could have + or - characters to indicate that labels and CCs
+  # were added or removed
+  # Users added to owner or cc field
+  added_user_ids = messages.IntegerField(29, repeated=True)
+  # Users removed from owner or cc
+  removed_user_ids = messages.IntegerField(30, repeated=True)
+  custom_field_name = messages.StringField(31)
+  # When having newvalue be a +/- string doesn't make sense (e.g. status),
+  # store the old value here so that it can still be displayed.
+  oldvalue = messages.StringField(32)
+  # New Components value add to the issue
+  added_component_ids = messages.IntegerField(33, repeated=True)
+  # Old Components value removed from the issue
+  removed_component_ids = messages.IntegerField(34, repeated=True)
+
+
+class Attachment(messages.Message):
+  """Holds info about one attachment."""
+  attachment_id = messages.IntegerField(21, required=True)
+  # Client-side filename
+  filename = messages.StringField(22, required=True)
+  filesize = messages.IntegerField(23, required=True)
+  # File mime-type, or at least our best guess.
+  mimetype = messages.StringField(24, required=True)
+  deleted = messages.BooleanField(27, default=False)
+  gcs_object_id = messages.StringField(29, required=False)
+
+
+class IssueComment(messages.Message):
+  # TODO(lukasperaza): update first comment to is_description=True
+  """Holds one issue description or one additional comment on an issue.
+
+  The IssueComment with the lowest timestamp is the issue description,
+  if there is no IssueComment with is_description=True; otherwise, the
+  IssueComment with is_description=True and the highest timestamp is
+  the issue description.
+  Next available tag: 56
+  """
+  id = messages.IntegerField(32)
+  # Issue ID of the issue that was commented on.
+  issue_id = messages.IntegerField(31, required=True)
+  project_id = messages.IntegerField(50)
+  # User who entered the comment
+  user_id = messages.IntegerField(4, required=True, default=0)
+  # id of the APPROVAL_TYPE fielddef, if this is an approval comment.
+  approval_id = messages.IntegerField(54)
+  # Time when comment was entered (seconds).
+  timestamp = messages.IntegerField(5, required=True)
+  # Text of the comment
+  content = messages.StringField(6, required=True)
+  # Audit trail of changes made w/ this comment
+  amendments = messages.MessageField(Amendment, 10, repeated=True)
+
+  # Soft delete that can be undeleted.
+  # Deleted comments should not be shown to average users.
+  # If deleted, deleted_by contains the user id of user who deleted.
+  deleted_by = messages.IntegerField(13)
+
+  attachments = messages.MessageField(Attachment, 20, repeated=True)
+
+  # Sequence number of the comment
+  # The field is optional for compatibility with code existing before
+  # this field was added.
+  # In practice, issue_svc sets this for all comments in GetCommentsForIssue.
+  sequence = messages.IntegerField(26)
+
+  # The body text of the inbound email that caused this issue comment
+  # to be automatically entered.  If this field is non-empty, it means
+  # that the comment was added via an inbound email.  Headers and attachments
+  # are not included.
+  inbound_message = messages.StringField(28)
+
+  is_spam = messages.BooleanField(51, default=False)
+
+  is_description = messages.BooleanField(52, default=False)
+  description_num = messages.StringField(53)
+
+  # User ID of script that imported the comment on behalf of a user.
+  importer_id = messages.IntegerField(55, default=0)
+
+
+class SavedQuery(messages.Message):
+  """Store a saved query, for either a project or a user."""
+  query_id = messages.IntegerField(1)
+  name = messages.StringField(2)
+  base_query_id = messages.IntegerField(3)
+  query = messages.StringField(4, required=True)
+
+  # For personal cross-project queries.
+  executes_in_project_ids = messages.IntegerField(5, repeated=True)
+
+  # For user saved queries.
+  subscription_mode = messages.StringField(6)
+
+
+class NotifyTriggers(messages.Enum):
+  """Issue tracker events that can trigger notification emails."""
+  NEVER = 0
+  ANY_COMMENT = 1
+  # TODO(jrobbins): ANY_CHANGE, OPENED_CLOSED, ETC.
+
+
+class FieldTypes(messages.Enum):
+  """Types of custom fields that Monorail supports."""
+  ENUM_TYPE = 1
+  INT_TYPE = 2
+  STR_TYPE = 3
+  USER_TYPE = 4
+  DATE_TYPE = 5
+  BOOL_TYPE = 6
+  URL_TYPE = 7
+  APPROVAL_TYPE = 8
+  # TODO(jrobbins): more types, see tracker.sql for all TODOs.
+
+
+class DateAction(messages.Enum):
+  """What to do when a date field value arrives."""
+  NO_ACTION = 0
+  PING_OWNER_ONLY = 1
+  PING_PARTICIPANTS = 2
+
+
+class FieldDef(messages.Message):
+  """This PB stores info about one custom field definition."""
+  field_id = messages.IntegerField(1, required=True)
+  project_id = messages.IntegerField(2, required=True)
+  field_name = messages.StringField(3, required=True)
+  field_type = messages.EnumField(FieldTypes, 4, required=True)
+  applicable_type = messages.StringField(11)
+  applicable_predicate = messages.StringField(10)
+  is_required = messages.BooleanField(5, default=False)
+  is_niche = messages.BooleanField(19, default=False)
+  is_multivalued = messages.BooleanField(6, default=False)
+  docstring = messages.StringField(7)
+  is_deleted = messages.BooleanField(8, default=False)
+  admin_ids = messages.IntegerField(9, repeated=True)
+  editor_ids = messages.IntegerField(24, repeated=True)
+
+  # validation details for int_type
+  min_value = messages.IntegerField(12)
+  max_value = messages.IntegerField(13)
+  # validation details for str_type
+  regex = messages.StringField(14)
+  # validation details for user_type
+  needs_member = messages.BooleanField(15, default=False)
+  needs_perm = messages.StringField(16)
+
+  # semantics for user_type fields
+  grants_perm = messages.StringField(17)
+  notify_on = messages.EnumField(NotifyTriggers, 18)
+
+  # semantics for date_type fields
+  date_action = messages.EnumField(DateAction, 20)
+
+  # field_id of the approval this FieldDef belongs to
+  approval_id = messages.IntegerField(21)
+
+  # These fields should only be associated with issue phases
+  is_phase_field = messages.BooleanField(22, default=False)
+
+  # boolean that indicates if this field is restricted
+  is_restricted_field = messages.BooleanField(23, default=False)
+
+
+class ComponentDef(messages.Message):
+  """This stores info about a component in a project."""
+  component_id = messages.IntegerField(1, required=True)
+  project_id = messages.IntegerField(2, required=True)
+  path = messages.StringField(3, required=True)
+  docstring = messages.StringField(4)
+  admin_ids = messages.IntegerField(5, repeated=True)
+  cc_ids = messages.IntegerField(6, repeated=True)
+  deprecated = messages.BooleanField(7, default=False)
+  created = messages.IntegerField(8)
+  creator_id = messages.IntegerField(9)
+  modified = messages.IntegerField(10)
+  modifier_id = messages.IntegerField(11)
+  label_ids = messages.IntegerField(12, repeated=True)
+
+
+class FilterRule(messages.Message):
+  """Filter rules implement semantics as project-specific if-then rules."""
+  predicate = messages.StringField(10, required=True)
+
+  # If the predicate is satisfied, these actions set some of the derived_*
+  # fields on the issue: labels, status, owner, or CCs.
+  add_labels = messages.StringField(20, repeated=True)
+  default_status = messages.StringField(21)
+  default_owner_id = messages.IntegerField(22)
+  add_cc_ids = messages.IntegerField(23, repeated=True)
+  add_notify_addrs = messages.StringField(24, repeated=True)
+  warning = messages.StringField(25)
+  error = messages.StringField(26)
+
+
+class StatusDef(messages.Message):
+  """Definition of one well-known issue status."""
+  status = messages.StringField(11, required=True)
+  means_open = messages.BooleanField(12, default=False)
+  status_docstring = messages.StringField(13)
+  deprecated = messages.BooleanField(14, default=False)
+
+
+class LabelDef(messages.Message):
+  """Definition of one well-known issue label."""
+  label = messages.StringField(21, required=True)
+  label_docstring = messages.StringField(22)
+  deprecated = messages.BooleanField(23, default=False)
+
+
+class ApprovalDef(messages.Message):
+  """Definition of an approval type field def."""
+  # Note: approval_id is semantically required
+  approval_id = messages.IntegerField(1)
+  approver_ids = messages.IntegerField(4, repeated=True)
+  survey = messages.StringField(5)
+
+# Next available tag: 48
+class TemplateDef(messages.Message):
+  """Definition of one issue template."""
+  template_id = messages.IntegerField(57)
+  name = messages.StringField(31, required=True)
+  content = messages.StringField(32, required=True)
+  summary = messages.StringField(33)
+  summary_must_be_edited = messages.BooleanField(34, default=False)
+  owner_id = messages.IntegerField(35)
+  status = messages.StringField(36)
+  # Note: labels field is considered to have been set iff summary was set.
+  labels = messages.StringField(37, repeated=True)
+  # This controls what is listed in the template drop-down menu. Users
+  # could still select any template by editing the URL, and that's OK.
+  members_only = messages.BooleanField(38, default=False)
+  # If no owner_id is specified, and owner_defaults_to_member is
+  # true, then when an issue is entered by a member, fill in the initial
+  # owner field with the signed in user's name.
+  owner_defaults_to_member = messages.BooleanField(39, default=True)
+  admin_ids = messages.IntegerField(41, repeated=True)
+
+  # Custom field values (other than enums)
+  field_values = messages.MessageField(FieldValue, 42, repeated=True)
+  # Components.
+  component_ids = messages.IntegerField(43, repeated=True)
+  component_required = messages.BooleanField(44, default=False)
+  phases = messages.MessageField(Phase, 46, repeated=True)
+  approval_values = messages.MessageField(ApprovalValue, 47, repeated=True)
+
+
+class ProjectIssueConfig(messages.Message):
+  """This holds all configuration info for one project.
+
+  That includes canned queries, well-known issue statuses,
+  and well-known issue labels.
+
+  "Well-known" means that they are always offered to the user in
+  drop-downs, even if there are currently no open issues that have
+  that label or status value.  Deleting a well-known value from the
+  configuration does not change any issues that may still reference
+  that old label, and users are still free to use it.
+
+  Exclusive label prefixes mean that a given issue may only have one
+  label that begins with that prefix.  E.g., Priority should be
+  exclusive so that no issue can be labeled with both Priority-High
+  and Priority-Low.
+  Next available tag: 62
+  """
+
+  project_id = messages.IntegerField(60)
+  well_known_statuses = messages.MessageField(StatusDef, 10, repeated=True)
+  # If an issue's status is being set to one of these, show "Merge with:".
+  statuses_offer_merge = messages.StringField(14, repeated=True)
+
+  well_known_labels = messages.MessageField(LabelDef, 20, repeated=True)
+  exclusive_label_prefixes = messages.StringField(2, repeated=True)
+
+  approval_defs = messages.MessageField(ApprovalDef, 61, repeated=True)
+
+  field_defs = messages.MessageField(FieldDef, 5, repeated=True)
+  component_defs = messages.MessageField(ComponentDef, 6, repeated=True)
+
+  default_template_for_developers = messages.IntegerField(3, required=True)
+  default_template_for_users = messages.IntegerField(4, required=True)
+
+  # These options control the default appearance of the issue list or grid
+  # for non-members.
+  default_col_spec = messages.StringField(50, default='')
+  default_sort_spec = messages.StringField(51, default='')
+  default_x_attr = messages.StringField(52, default='')
+  default_y_attr = messages.StringField(53, default='')
+
+  # These options control the default appearance of the issue list or grid
+  # for project members.
+  member_default_query = messages.StringField(57, default='')
+
+  # This bool controls whether users are able to enter odd-ball
+  # labels and status values, or whether they are limited to only the
+  # well-known labels and status values defined on the admin subtab.
+  restrict_to_known = messages.BooleanField(16, default=False)
+
+  # Allow special projects to have a custom URL for the "New issue" link.
+  custom_issue_entry_url = messages.StringField(56)
diff --git a/mrproto/user_pb2.py b/mrproto/user_pb2.py
new file mode 100644
index 0000000..de1aa6e
--- /dev/null
+++ b/mrproto/user_pb2.py
@@ -0,0 +1,96 @@
+# 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.
+
+"""Protocol buffers for Monorail users."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from protorpc import messages
+
+
+class IssueUpdateNav(messages.Enum):
+  """Pref for where a project member goes after an issue update."""
+  UP_TO_LIST = 0       # Back to issue list or grid view.
+  STAY_SAME_ISSUE = 1  # Show the same issue with the update.
+  NEXT_IN_LIST = 2     # Triage mode: go to next issue, if any.
+
+
+class User(messages.Message):
+  """In-memory busines object for representing users."""
+  user_id = messages.IntegerField(1)  # TODO(jrobbins): make it required.
+
+  # Is this user a site administer?
+  is_site_admin = messages.BooleanField(4, required=True, default=False)
+
+  # User notification preferences.  These preferences describe when
+  # a user is sent a email notification after an issue has changed.
+  # The user is notified if either of the following is true:
+  # 1. notify_issue_change is True and the user is named in the
+  # issue's Owner or CC field.
+  # 2. notify_starred_issue_change is True and the user has starred
+  # the issue.
+  notify_issue_change = messages.BooleanField(5, default=True)
+  notify_starred_issue_change = messages.BooleanField(6, default=True)
+  # Opt-in to email subject lines like "proj:123: issue summary".
+  email_compact_subject = messages.BooleanField(14, default=False)
+  # Opt-out of "View Issue" button in Gmail inbox.
+  email_view_widget = messages.BooleanField(15, default=True)
+  # Opt-in to ping emails from issues that the user starred.
+  notify_starred_ping = messages.BooleanField(16, default=False)
+
+  # This user has been banned, and this string describes why. All access
+  # to Monorail pages should be disabled.
+  banned = messages.StringField(7, default='')
+
+  # Fields 8-13 are no longer used: they were User action counts and limits.
+
+  after_issue_update = messages.EnumField(
+      IssueUpdateNav, 29, default=IssueUpdateNav.STAY_SAME_ISSUE)
+
+  # Should we obfuscate the user's email address and require solving a captcha
+  # to reveal it entirely? The default value corresponds to requiring users to
+  # opt into publishing their identities, but our code ensures that the
+  # opposite takes place for Gmail accounts.
+  obscure_email = messages.BooleanField(26, default=True)
+
+  # The email address chosen by the user to reveal on the site.
+  email = messages.StringField(27)
+
+  # Sticky state for show/hide widget on people details page.
+  keep_people_perms_open = messages.BooleanField(33, default=False)
+
+  deleted = messages.BooleanField(39, default=False)
+  deleted_timestamp = messages.IntegerField(40, default=0)
+
+  preview_on_hover = messages.BooleanField(42, default=True)
+
+  last_visit_timestamp = messages.IntegerField(45, default=0)
+  email_bounce_timestamp = messages.IntegerField(46, default=0)
+  vacation_message = messages.StringField(47)
+
+  linked_parent_id = messages.IntegerField(48)
+  linked_child_ids = messages.IntegerField(49, repeated=True)
+
+
+class UserPrefValue(messages.Message):
+  """Holds a single non-default user pref."""
+  name = messages.StringField(1, required=True)
+  value = messages.StringField(2)
+
+
+class UserPrefs(messages.Message):
+  """In-memory business object for representing user preferences."""
+  user_id = messages.IntegerField(1, required=True)
+  prefs = messages.MessageField(UserPrefValue, 2, repeated=True)
+
+
+
+def MakeUser(user_id, email=None, obscure_email=False):
+  """Create and return a new user record in RAM."""
+  user = User(user_id=user_id, obscure_email=bool(obscure_email))
+  if email:
+    user.email = email
+  return user
diff --git a/mrproto/usergroup_pb2.py b/mrproto/usergroup_pb2.py
new file mode 100644
index 0000000..5b37640
--- /dev/null
+++ b/mrproto/usergroup_pb2.py
@@ -0,0 +1,54 @@
+# 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.
+
+"""Protocol buffers for Monorail usergroups."""
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+from protorpc import messages
+
+
+class MemberVisibility(messages.Enum):
+  """Enum controlling who can see the members of a user group."""
+  OWNERS = 0
+  MEMBERS = 1
+  ANYONE = 2
+
+
+class GroupType(messages.Enum):
+  """Type of external group to import."""
+  CHROME_INFRA_AUTH = 0
+  MDB = 1
+  BAGGINS = 3
+  COMPUTED = 4
+
+
+class UserGroupSettings(messages.Message):
+  """In-memory busines object for representing user group settings."""
+  who_can_view_members = messages.EnumField(
+      MemberVisibility, 1, default=MemberVisibility.MEMBERS)
+  ext_group_type = messages.EnumField(GroupType, 2)
+  last_sync_time = messages.IntegerField(
+      3, default=0, variant=messages.Variant.INT32)
+  friend_projects = messages.IntegerField(
+      4, repeated=True, variant=messages.Variant.INT32)
+  notify_members = messages.BooleanField(5, default=True)
+  notify_group = messages.BooleanField(6, default=False)
+# TODO(jrobbins): add settings to control who can join, etc.
+
+
+def MakeSettings(who_can_view_members_str, ext_group_type_str=None,
+                 last_sync_time=0, friend_projects=None, notify_members=True,
+                 notify_group=False):
+  """Create and return a new user record in RAM."""
+  settings = UserGroupSettings(
+      who_can_view_members=MemberVisibility(who_can_view_members_str.upper()),
+      notify_members=notify_members, notify_group=notify_group)
+  if ext_group_type_str:
+    settings.ext_group_type = GroupType(ext_group_type_str.upper())
+  settings.last_sync_time = last_sync_time
+  settings.friend_projects = friend_projects or []
+  return settings