Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-16 12:10:23 +00:00
parent 2c90b9b579
commit dc9ff5fda1
57 changed files with 798 additions and 152 deletions

View File

@ -11,7 +11,6 @@ import {
GlIntersectionObserver,
} from '@gitlab/ui';
import { escapeRegExp } from 'lodash';
import filesQuery from 'shared_queries/repository/files.query.graphql';
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import { escapeFileUrl } from '~/lib/utils/url_utility';
import { TREE_PAGE_SIZE } from '~/repository/constants';
@ -178,8 +177,7 @@ export default {
return this.isFolder ? this.loadFolder() : this.loadBlob();
},
loadFolder() {
const query = this.glFeatures.paginatedTreeGraphqlQuery ? paginatedTreeQuery : filesQuery;
this.apolloQuery(query, {
this.apolloQuery(paginatedTreeQuery, {
projectPath: this.projectPath,
ref: this.ref,
path: this.path,

View File

@ -1,5 +1,4 @@
<script>
import filesQuery from 'shared_queries/repository/files.query.graphql';
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import createFlash from '~/flash';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@ -72,9 +71,6 @@ export default {
hasShowMore() {
return !this.clickedShowMore && this.pageLimitReached;
},
paginatedTreeEnabled() {
return this.glFeatures.paginatedTreeGraphqlQuery;
},
},
watch: {
@ -101,7 +97,7 @@ export default {
return this.$apollo
.query({
query: this.paginatedTreeEnabled ? paginatedTreeQuery : filesQuery,
query: paginatedTreeQuery,
variables: {
projectPath: this.projectPath,
ref: this.ref,
@ -114,20 +110,19 @@ export default {
if (data.errors) throw data.errors;
if (!data?.project?.repository || originalPath !== (this.path || '/')) return;
const pageInfo = this.paginatedTreeEnabled
? data.project.repository.paginatedTree.pageInfo
: this.hasNextPage(data.project.repository.tree);
const {
project: {
repository: {
paginatedTree: { pageInfo },
},
},
} = data;
this.isLoadingFiles = false;
this.entries = Object.keys(this.entries).reduce(
(acc, key) => ({
...acc,
[key]: this.normalizeData(
key,
this.paginatedTreeEnabled
? data.project.repository.paginatedTree.nodes[0][key]
: data.project.repository.tree[key].edges,
),
[key]: this.normalizeData(key, data.project.repository.paginatedTree.nodes[0][key]),
}),
{},
);
@ -149,9 +144,7 @@ export default {
});
},
normalizeData(key, data) {
return this.entries[key].concat(
this.paginatedTreeEnabled ? data.nodes : data.map(({ node }) => node),
);
return this.entries[key].concat(data.nodes);
},
hasNextPage(data) {
return []

View File

@ -1,4 +1,3 @@
import filesQuery from 'shared_queries/repository/files.query.graphql';
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import projectPathQuery from '../queries/project_path.query.graphql';
import getRefMixin from './get_ref';
@ -22,7 +21,7 @@ export default {
return this.$apollo
.query({
query: gon.features.paginatedTreeGraphqlQuery ? paginatedTreeQuery : filesQuery,
query: paginatedTreeQuery,
variables: {
projectPath: this.projectPath,
ref: this.ref,

View File

@ -1,5 +1,5 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { HIGHLIGHT_CLASS_NAME } from './constants';
import ViewerMixin from './mixins';
@ -9,6 +9,9 @@ export default {
components: {
GlIcon,
},
directives: {
SafeHtml: GlSafeHtmlDirective,
},
mixins: [ViewerMixin, glFeatureFlagsMixin()],
inject: ['blobHash'],
data() {
@ -65,7 +68,7 @@ export default {
<div class="blob-content">
<pre
class="code highlight"
><code :data-blob-hash="blobHash" v-html="content /* eslint-disable-line vue/no-v-html */"></code></pre>
><code v-safe-html="content" :data-blob-hash="blobHash"></code></pre>
</div>
</div>
</div>

View File

@ -5,9 +5,6 @@ class Projects::ReleasesController < Projects::ApplicationController
before_action :require_non_empty_project, except: [:index]
before_action :release, only: %i[edit show update downloads]
before_action :authorize_read_release!
# We have to check `download_code` permission because detail URL path
# contains git-tag name.
before_action :authorize_download_code!, except: [:index]
before_action :authorize_update_release!, only: %i[edit update]
before_action :authorize_create_release!, only: :new
before_action only: :index do

View File

@ -17,7 +17,6 @@ class Projects::TreeController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:lazy_load_commits, @project, default_enabled: :yaml)
push_frontend_feature_flag(:paginated_tree_graphql_query, @project, default_enabled: :yaml)
push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml)
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
end

View File

@ -37,7 +37,6 @@ class ProjectsController < Projects::ApplicationController
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:refactor_text_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml)
push_frontend_feature_flag(:paginated_tree_graphql_query, @project, default_enabled: :yaml)
push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml)
end

View File

@ -135,11 +135,7 @@ module Issuables
# rubocop: disable CodeReuse/ActiveRecord
def label_link_query(target_model, label_ids: nil, label_names: nil)
relation = LabelLink
.where(target_type: target_model.name)
.where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id']))
relation = relation.where(label_id: label_ids) if label_ids
relation = LabelLink.by_target_for_exists_query(target_model.name, target_model.arel_table['id'], label_ids)
relation = relation.joins(:label).where(labels: { name: label_names }) if label_names
relation

View File

@ -306,7 +306,7 @@ module Types
null: true,
description: 'A single release of the project.',
resolver: Resolvers::ReleasesResolver.single,
authorize: :download_code
authorize: :read_release
field :container_expiration_policy,
Types::ContainerExpirationPolicyType,

View File

@ -4,7 +4,7 @@ module Types
class ReleaseLinksType < BaseObject
graphql_name 'ReleaseLinks'
authorize :download_code
authorize :read_release
alias_method :release, :object
@ -16,14 +16,19 @@ module Types
description: "HTTP URL of the release's edit page.",
authorize: :update_release
field :opened_merge_requests_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the merge request page, filtered by this release and `state=open`.'
description: 'HTTP URL of the merge request page, filtered by this release and `state=open`.',
authorize: :download_code
field :merged_merge_requests_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the merge request page , filtered by this release and `state=merged`.'
description: 'HTTP URL of the merge request page , filtered by this release and `state=merged`.',
authorize: :download_code
field :closed_merge_requests_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the merge request page , filtered by this release and `state=closed`.'
description: 'HTTP URL of the merge request page , filtered by this release and `state=closed`.',
authorize: :download_code
field :opened_issues_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the issues page, filtered by this release and `state=open`.'
description: 'HTTP URL of the issues page, filtered by this release and `state=open`.',
authorize: :download_code
field :closed_issues_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the issues page, filtered by this release and `state=closed`.'
description: 'HTTP URL of the issues page, filtered by this release and `state=closed`.',
authorize: :download_code
end
end

View File

@ -14,8 +14,7 @@ module Types
present_using ReleasePresenter
field :tag_name, GraphQL::Types::String, null: true, method: :tag,
description: 'Name of the tag associated with the release.',
authorize: :download_code
description: 'Name of the tag associated with the release.'
field :tag_path, GraphQL::Types::String, null: true,
description: 'Relative web path to the tag associated with the release.',
authorize: :download_code

View File

@ -16,8 +16,7 @@ module Types
description: 'Tree of the repository.'
field :paginated_tree, Types::Tree::TreeType.connection_type, null: true, resolver: Resolvers::PaginatedTreeResolver, calls_gitaly: true,
max_page_size: 100,
description: 'Paginated tree of the repository.',
feature_flag: :paginated_tree_graphql_query
description: 'Paginated tree of the repository.'
field :blobs, Types::Repository::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true,
description: 'Blobs contained within the repository'
field :branch_names, [GraphQL::Types::String], null: true, calls_gitaly: true,

View File

@ -20,6 +20,10 @@ module Analytics
def self.issuable_id_column
:issue_id
end
def self.issuable_model
::Issue
end
end
end
end

View File

@ -20,6 +20,10 @@ module Analytics
def self.issuable_id_column
:merge_request_id
end
def self.issuable_model
::MergeRequest
end
end
end
end

View File

@ -18,6 +18,10 @@ module Analytics
scope :end_event_is_not_happened_yet, -> { where(end_event_timestamp: nil) }
end
def issuable_id
attributes[self.class.issuable_id_column.to_s]
end
class_methods do
def upsert_data(data)
upsert_values = data.map do |row|
@ -26,8 +30,8 @@ module Analytics
:issuable_id,
:group_id,
:project_id,
:author_id,
:milestone_id,
:author_id,
:state_id,
:start_event_timestamp,
:end_event_timestamp

View File

@ -11,4 +11,16 @@ class LabelLink < ApplicationRecord
validates :label, presence: true, unless: :importing?
scope :for_target, -> (target_id, target_type) { where(target_id: target_id, target_type: target_type) }
# Example: Issues has at least one label within a project
# > Issue.where(project_id: 100) # or any scope on issues
# > .where(LabelLink.by_target_for_exists_query('Issue', Issue.arel_table[:id]).arel.exists)
scope :by_target_for_exists_query, -> (target_type, arel_join_column, label_ids = nil) do
relation = LabelLink
.where(target_type: target_type)
.where(arel_table['target_id'].eq(arel_join_column))
relation = relation.where(label_id: label_ids) if label_ids
relation
end
end

View File

@ -5,7 +5,8 @@
class WebauthnRegistration < ApplicationRecord
belongs_to :user
validates :credential_xid, :public_key, :name, :counter, presence: true
validates :credential_xid, :public_key, :counter, presence: true
validates :name, length: { minimum: 0, allow_nil: false }
validates :counter,
numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 2**32 - 1 }
end

View File

@ -22,8 +22,6 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
end
def self_url
return unless can_download_code?
project_release_url(project, release)
end
@ -64,7 +62,7 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
delegator_override :name
def name
can_download_code? ? release.name : "Release-#{release.id}"
release.name
end
def download_url(filepath)

View File

@ -4,6 +4,8 @@ module Packages
class CreatePackageService < ::Packages::CreatePackageService
include Gitlab::Utils::StrongMemoize
PACKAGE_JSON_NOT_ALLOWED_FIELDS = %w[readme readmeFilename].freeze
def execute
return error('Version is empty.', 400) if version.blank?
return error('Package already exists.', 403) if current_package_exists?
@ -22,7 +24,7 @@ module Packages
::Packages::Npm::CreateTagService.new(package, dist_tag).execute
if Feature.enabled?(:packages_npm_abbreviated_metadata, project, default_enabled: :yaml)
package.create_npm_metadatum!(package_json: version_data)
package.create_npm_metadatum!(package_json: package_json)
end
package
@ -50,6 +52,10 @@ module Packages
params[:versions][version]
end
def package_json
version_data.except(*PACKAGE_JSON_NOT_ALLOWED_FIELDS)
end
def dist_tag
params['dist-tags'].each_key.first
end

View File

@ -224,7 +224,7 @@ module Gitlab
config.assets.precompile << "page_bundles/build.css"
config.assets.precompile << "page_bundles/ci_status.css"
config.assets.precompile << "page_bundles/cycle_analytics.css"
config.assets.precompile << "page_bundles/dev_ops_report.css"
config.assets.precompile << "page_bundles/dev_ops_reports.css"
config.assets.precompile << "page_bundles/environments.css"
config.assets.precompile << "page_bundles/epics.css"
config.assets.precompile << "page_bundles/error_tracking_details.css"

View File

@ -1,8 +0,0 @@
---
name: paginated_tree_graphql_query
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66751
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337214
milestone: '14.2'
type: development
group: group::source code
default_enabled: true

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class RemoveCiPipelineChatDataFkOnChatNames < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
with_lock_retries do
remove_foreign_key_if_exists(:ci_pipeline_chat_data, :chat_names, name: "fk_rails_f300456b63")
end
end
def down
# Remove orphaned rows
execute <<~SQL
DELETE FROM ci_pipeline_chat_data
WHERE
NOT EXISTS (SELECT 1 FROM chat_names WHERE chat_names.id=ci_pipeline_chat_data.chat_name_id)
SQL
add_concurrent_foreign_key(:ci_pipeline_chat_data, :chat_names, name: "fk_rails_f300456b63", column: :chat_name_id, target_column: :id, on_delete: "cascade")
end
end

View File

@ -0,0 +1 @@
be11c0b1c7b9c99c28d44c164742815da57bfc4a32afd54df9135e3ce6edeff9

View File

@ -31078,9 +31078,6 @@ ALTER TABLE ONLY snippet_repositories
ALTER TABLE ONLY elastic_reindexing_subtasks
ADD CONSTRAINT fk_rails_f2cc190164 FOREIGN KEY (elastic_reindexing_task_id) REFERENCES elastic_reindexing_tasks(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_pipeline_chat_data
ADD CONSTRAINT fk_rails_f300456b63 FOREIGN KEY (chat_name_id) REFERENCES chat_names(id) ON DELETE CASCADE;
ALTER TABLE ONLY approval_project_rules_users
ADD CONSTRAINT fk_rails_f365da8250 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;

View File

@ -14030,7 +14030,7 @@ Returns [`[String!]`](#string).
##### `Repository.paginatedTree`
Paginated tree of the repository. Available only when feature flag `paginated_tree_graphql_query` is enabled. This flag is enabled by default.
Paginated tree of the repository.
Returns [`TreeConnection`](#treeconnection).

View File

@ -98,7 +98,7 @@ The following are example projects that demonstrate Review App configuration:
- [NGINX](https://gitlab.com/gitlab-examples/review-apps-nginx).
- [OpenShift](https://gitlab.com/gitlab-examples/review-apps-openshift).
- [HashiCorp Nomad](https://gitlab.com/gitlab-examples/review-apps-nomad)
- [HashiCorp Nomad](https://gitlab.com/gitlab-examples/review-apps-nomad).
Other examples of Review Apps:

View File

@ -18,7 +18,9 @@ use external test planning tools, which require additional overhead, context swi
## Create a test case
Users with Reporter or higher [permissions](../../user/permissions.md) can create test cases.
Prerequisite:
- You must have at least the Reporter [role](../../user/permissions.md).
To create a test case in a GitLab project:
@ -32,7 +34,9 @@ To create a test case in a GitLab project:
You can view all test cases in the project in the Test Cases list. Filter the
issue list with a search query, including labels or the test case's title.
Users with Guest or higher [permissions](../../user/permissions.md) can view test cases.
Prerequisite:
- You must have at least the Guest [role](../../user/permissions.md).
To view a test case:
@ -45,8 +49,10 @@ To view a test case:
You can edit a test case's title and description.
Users with Reporter or higher [permissions](../../user/permissions.md) can edit test cases.
Users demoted to the Guest role can continue to edit the test cases they created
Prerequisite:
- You must have at least the Reporter [role](../../user/permissions.md).
- Users demoted to the Guest role can continue to edit the test cases they created
when they were in the higher role.
To edit a test case:
@ -60,7 +66,9 @@ To edit a test case:
When you want to stop using a test case, you can archive it. You can [reopen an archived test case](#reopen-an-archived-test-case) later.
Users with Reporter or higher [permissions](../../user/permissions.md) can archive test cases.
Prerequisite:
- You must have at least the Reporter [role](../../user/permissions.md).
To archive a test case, on the test case's page, select the **Archive test case** button.
@ -73,6 +81,6 @@ To view archived test cases:
If you decide to start using an archived test case again, you can reopen it.
Users with Reporter or higher [permissions](../../user/permissions.md) can reopen test cases.
You must have at least the Reporter [role](../../user/permissions.md).
To reopen an archived test case, on the test case's page, select **Reopen test case**.

View File

@ -90,7 +90,7 @@ The following examples show the progression of a feature flag.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available,
ask an administrator to [enable the featured flag](../administration/feature_flags.md) named `forti_token_cloud`.
ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `forti_token_cloud`.
The feature is not ready for production use.
```

View File

@ -23,6 +23,17 @@ they found that "ARIA correlated to higher detectable errors".
It is likely that *misuse* of ARIA is a big cause of increased errors,
so when in doubt don't use `aria-*`, `role`, and `tabindex` and stick with semantic HTML.
## Enable keyboard navigation on macOS
By default, macOS limits the <kbd>tab</kbd> key to **Text boxes and lists only**. To enable full keyboard navigation:
1. Open **System Preferences**.
1. Select **Keyboard**.
1. Open the **Shortcuts** tab.
1. Enable the setting **Use keyboard navigation to move focus between controls**.
You can read more about enabling browser-specific keyboard navigation on [a11yproject](https://www.a11yproject.com/posts/2017-12-29-macos-browser-keyboard-navigation/).
## Quick checklist
- [Text](#text-inputs-with-accessible-names),

View File

@ -338,6 +338,23 @@ or [init scripts](upgrading_from_source.md#configure-sysv-init-script) by [follo
Workhorse can no longer connect. As a workaround, [disable the temporary `workhorse_use_sidechannel`](../administration/feature_flags.md#enable-or-disable-the-feature)
feature flag. If you need a proxy between Workhorse and Gitaly, use a TCP proxy. If you have feedback about this change, please go to [this issue](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1301).
- In 14.1 we introduced a background migration that changes how we store merge request diff commits
in order to significantly reduce the amount of storage needed.
In 14.5 we introduce a set of migrations that wrap up this process by making sure
that all remaining jobs over the `merge_request_diff_commits` table are completed.
These jobs will have already been processed in most cases so that no extra time is necessary during an upgrade to 14.5.
But if there are remaining jobs, the deployment may take a few extra minutes to complete.
All merge request diff commits will automatically incorporate these changes, and there are no
additional requirements to perform the upgrade.
Existing data in the `merge_request_diff_commits` table remains unpacked until you run `VACUUM FULL merge_request_diff_commits`.
But note that the `VACUUM FULL` operation locks and rewrites the entire `merge_request_diff_commits` table,
so the operation takes some time to complete and it blocks access to this table until the end of the process.
We advise you to only run this command while GitLab is not actively used or it is taken offline for the duration of the process.
The time it takes to complete depends on the size of the table, which can be obtained by using `select pg_size_pretty(pg_total_relation_size('merge_request_diff_commits'));`.
For more information, refer to [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/331823).
### 14.4.0
Git 2.33.x and later is required. We recommend you use the

View File

@ -70,7 +70,8 @@ synchronizations is:
```yaml
gitops:
manifest_projects:
- id: "path-to/your-manifest-project-1"
# The `id` is the path to the Git repository holding your manifest files
- id: "path/to/your-manifest-project-1"
paths:
- glob: '/**/*.{yaml,yml,json}'
```

View File

@ -298,6 +298,7 @@ after the limits change in January, 2021:
| **All** traffic (from a given **IP address**) | **600** requests per minute | **2,000** requests per minute | **2,000** requests per minute |
| **Issue creation** | | **300** requests per minute | **300** requests per minute |
| **Note creation** (on issues and merge requests) | | **300** requests per minute | **60** requests per minute |
| **Advanced, project, and group search** API (for a given **IP address**) | | | **10** requests per minute |
More details are available on the rate limits for [protected
paths](#protected-paths-throttle) and [raw

View File

@ -216,7 +216,7 @@ The following table lists project permissions available for each role:
1. If **Public pipelines** is enabled in **Project Settings > CI/CD**.
1. Not allowed for Guest, Reporter, Developer, Maintainer, or Owner. See [protected branches](project/protected_branches.md).
1. If the [branch is protected](project/protected_branches.md), this depends on the access Developers and Maintainers are given.
1. Guest users can access GitLab [**Releases**](project/releases/index.md) for downloading assets but are not allowed to download the source code nor see repository information like tags and commits.
1. Guest users can access GitLab [**Releases**](project/releases/index.md) for downloading assets but are not allowed to download the source code nor see [repository information like commits and release evidence](project/releases/index.md#view-a-release-and-download-assets).
1. Actions are limited only to records owned (referenced) by user.
1. When [Share Group Lock](group/index.md#prevent-a-project-from-being-shared-with-groups) is enabled the project can't be shared with other groups. It does not affect group with group sharing.
1. For information on eligible approvers for merge requests, see

View File

@ -393,14 +393,6 @@ The release title can be customized using the **Release title** field when
creating or editing a release. If no title is provided, the release's tag name
is used instead.
Guest users of private projects are allowed to view the **Releases** page
but are _not_ allowed to view details about the Git repository (in particular,
tag names). Because of this, release titles are replaced with a generic
title like "Release-1234" for Guest users to avoid leaking tag name information.
See the [Release permissions](#release-permissions) section for
more information about permissions.
### Tag name
The release tag name should include the release version. GitLab uses [Semantic Versioning](https://semver.org/)
@ -724,11 +716,14 @@ In the API:
### View a release and download assets
> [The Guest permission for read action was adjusted](https://gitlab.com/gitlab-org/gitlab/-/issues/335209) in GitLab 14.5.
- Users with [Reporter role or above](../../../user/permissions.md#project-members-permissions)
have read and download access to the project releases.
- Users with [Guest role](../../../user/permissions.md#project-members-permissions)
have read and download access to the project releases, however,
repository-related information are redacted (for example the Git tag name).
have read and download access to the project releases.
This includes associated Git-tag-names, release description, author information of the releases.
However, other repository-related information, such as [source code](#source-code), [release evidence](#release-evidence) are redacted.
### Create, update, and delete a release and its assets

View File

@ -14,7 +14,7 @@ For the latest updates, check the [Tasks Roadmap](https://gitlab.com/groups/gitl
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available,
ask an administrator to [enable the featured flag](../administration/feature_flags.md) named `work_items`.
ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `work_items`.
The feature is not ready for production use.
Use tasks to track steps needed for the [issue](project/issues/index.md) to be closed.

View File

@ -63,23 +63,12 @@ module Gitlab
def filter_label_names(query)
return query if params[:label_name].blank?
all_label_ids = Issuables::LabelFilter
.new(group: root_ancestor, project: nil, params: { label_name: params[:label_name] })
.find_label_ids(params[:label_name])
return query.none if params[:label_name].size != all_label_ids.size
all_label_ids.each do |label_ids|
relation = LabelLink
.where(target_type: stage.subject_class.name)
.where(LabelLink.arel_table['target_id'].eq(query.model.arel_table[query.model.issuable_id_column]))
relation = relation.where(label_id: label_ids)
query = query.where(relation.arel.exists)
end
query
LabelFilter.new(
stage: stage,
params: params,
project: nil,
group: root_ancestor
).filter(query)
end
def filter_assignees(query)

View File

@ -30,6 +30,12 @@ module Gitlab
strong_memoize(:count) { limit_count }
end
def records_fetcher
strong_memoize(:records_fetcher) do
RecordsFetcher.new(stage: stage, query: query, params: params)
end
end
private
attr_reader :stage, :params

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Gitlab
module Analytics
module CycleAnalytics
module Aggregated
# This class makes it possible to add label filters to stage event tables
class LabelFilter < Issuables::LabelFilter
extend ::Gitlab::Utils::Override
def initialize(stage:, project:, group:, **kwargs)
@stage = stage
super(project: project, group: group, **kwargs)
end
private
attr_reader :stage
override :label_link_query
def label_link_query(target_model, label_ids: nil)
join_column = target_model.arel_table[target_model.issuable_id_column]
LabelLink.by_target_for_exists_query(stage.subject_class.name, join_column, label_ids)
end
end
end
end
end
end

View File

@ -0,0 +1,116 @@
# frozen_string_literal: true
module Gitlab
module Analytics
module CycleAnalytics
module Aggregated
class RecordsFetcher
include Gitlab::Utils::StrongMemoize
include StageQueryHelpers
MAX_RECORDS = 20
MAPPINGS = {
Issue => {
serializer_class: AnalyticsIssueSerializer,
includes_for_query: { project: { namespace: [:route] }, author: [] },
columns_for_select: %I[title iid id created_at author_id project_id]
},
MergeRequest => {
serializer_class: AnalyticsMergeRequestSerializer,
includes_for_query: { target_project: [:namespace], author: [] },
columns_for_select: %I[title iid id created_at author_id state_id target_project_id]
}
}.freeze
def initialize(stage:, query:, params: {})
@stage = stage
@query = query
@params = params
@sort = params[:sort] || :end_event
@direction = params[:direction] || :desc
@page = params[:page] || 1
@per_page = MAX_RECORDS
@stage_event_model = query.model
end
def serialized_records
strong_memoize(:serialized_records) do
records = ordered_and_limited_query.select(stage_event_model.arel_table[Arel.star], duration.as('total_time'))
yield records if block_given?
issuables_and_records = load_issuables(records)
preload_associations(issuables_and_records.map(&:first))
issuables_and_records.map do |issuable, record|
project = issuable.project
attributes = issuable.attributes.merge({
project_path: project.path,
namespace_path: project.namespace.route.path,
author: issuable.author,
total_time: record.total_time
})
serializer.represent(attributes)
end
end
end
# rubocop: disable CodeReuse/ActiveRecord
def ordered_and_limited_query
sorting_options = {
end_event: {
asc: -> { query.order(end_event_timestamp: :asc) },
desc: -> { query.order(end_event_timestamp: :desc) }
},
duration: {
asc: -> { query.order(duration.asc) },
desc: -> { query.order(duration.desc) }
}
}
sort_lambda = sorting_options.dig(sort, direction) || sorting_options.dig(:end_event, :desc)
sort_lambda.call
.page(page)
.per(per_page)
.without_count
end
# rubocop: enable CodeReuse/ActiveRecord
private
attr_reader :stage, :query, :sort, :direction, :params, :page, :per_page, :stage_event_model
delegate :subject_class, to: :stage
def load_issuables(stage_event_records)
stage_event_records_by_issuable_id = stage_event_records.index_by(&:issuable_id)
issuable_model = stage_event_model.issuable_model
issuables_by_id = issuable_model.id_in(stage_event_records_by_issuable_id.keys).index_by(&:id)
stage_event_records_by_issuable_id.map do |issuable_id, record|
[issuables_by_id[issuable_id], record] if issuables_by_id[issuable_id]
end.compact
end
def serializer
MAPPINGS.fetch(subject_class).fetch(:serializer_class).new
end
# rubocop: disable CodeReuse/ActiveRecord
def preload_associations(records)
ActiveRecord::Associations::Preloader.new.preload(
records,
MAPPINGS.fetch(subject_class).fetch(:includes_for_query)
)
records
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
end
end
end

View File

@ -23,7 +23,11 @@ module Gitlab
def records_fetcher
strong_memoize(:records_fetcher) do
RecordsFetcher.new(stage: stage, query: query, params: params)
if use_aggregated_data_collector?
aggregated_data_collector.records_fetcher
else
RecordsFetcher.new(stage: stage, query: query, params: params)
end
end
end

View File

@ -8,23 +8,11 @@ module Gitlab
include StageQueryHelpers
include Gitlab::CycleAnalytics::MetricsTables
MAX_RECORDS = 20
MAPPINGS = {
Issue => {
serializer_class: AnalyticsIssueSerializer,
includes_for_query: { project: { namespace: [:route] }, author: [] },
columns_for_select: %I[title iid id created_at author_id project_id]
},
MergeRequest => {
serializer_class: AnalyticsMergeRequestSerializer,
includes_for_query: { target_project: [:namespace], author: [] },
columns_for_select: %I[title iid id created_at author_id state_id target_project_id]
}
}.freeze
delegate :subject_class, to: :stage
MAX_RECORDS = Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher::MAX_RECORDS
MAPPINGS = Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher::MAPPINGS
def initialize(stage:, query:, params: {})
@stage = stage
@query = query

View File

@ -114,7 +114,7 @@ module Gitlab
flags: flags,
links: links,
remediations: remediations,
raw_metadata: data.to_json,
original_data: data,
metadata_version: report_version,
details: data['details'] || {},
signatures: signatures,

View File

@ -17,7 +17,6 @@ module Gitlab
attr_reader :name
attr_reader :old_location
attr_reader :project_fingerprint
attr_reader :raw_metadata
attr_reader :report_type
attr_reader :scanner
attr_reader :scan
@ -28,10 +27,13 @@ module Gitlab
attr_reader :details
attr_reader :signatures
attr_reader :project_id
attr_reader :original_data
delegate :file_path, :start_line, :end_line, to: :location
def initialize(compare_key:, identifiers:, flags: [], links: [], remediations: [], location:, metadata_version:, name:, raw_metadata:, report_type:, scanner:, scan:, uuid:, confidence: nil, severity: nil, details: {}, signatures: [], project_id: nil, vulnerability_finding_signatures_enabled: false) # rubocop:disable Metrics/ParameterLists
alias_method :cve, :compare_key
def initialize(compare_key:, identifiers:, flags: [], links: [], remediations: [], location:, metadata_version:, name:, original_data:, report_type:, scanner:, scan:, uuid:, confidence: nil, severity: nil, details: {}, signatures: [], project_id: nil, vulnerability_finding_signatures_enabled: false) # rubocop:disable Metrics/ParameterLists
@compare_key = compare_key
@confidence = confidence
@identifiers = identifiers
@ -40,7 +42,7 @@ module Gitlab
@location = location
@metadata_version = metadata_version
@name = name
@raw_metadata = raw_metadata
@original_data = original_data
@report_type = report_type
@scanner = scanner
@scan = scan
@ -74,6 +76,10 @@ module Gitlab
uuid
details
signatures
description
message
cve
solution
].each_with_object({}) do |key, hash|
hash[key] = public_send(key) # rubocop:disable GitlabSecurity/PublicSend
end
@ -145,6 +151,26 @@ module Gitlab
signatures.present?
end
def raw_metadata
@raw_metadata ||= original_data.to_json
end
def description
original_data['description']
end
def message
original_data['message']
end
def solution
original_data['solution']
end
def location_data
original_data['location']
end
private
def generate_project_fingerprint

View File

@ -207,7 +207,18 @@ RSpec.describe Projects::ReleasesController do
let(:project) { private_project }
let(:user) { guest }
it_behaves_like 'not found'
it_behaves_like 'successful request'
end
context 'when user is an external user for the project' do
let(:project) { private_project }
let(:user) { create(:user) }
it 'behaves like not found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end

View File

@ -29,6 +29,7 @@ RSpec.describe 'Database schema' do
ci_builds: %w[erased_by_id runner_id trigger_request_id user_id],
ci_namespace_monthly_usages: %w[namespace_id],
ci_pipelines: %w[user_id],
ci_pipeline_chat_data: %w[chat_name_id], # it uses the loose foreign key featue
ci_runner_projects: %w[runner_id],
ci_trigger_requests: %w[commit_id],
cluster_providers_aws: %w[security_group_id vpc_id access_key_id],

View File

@ -9,7 +9,7 @@ FactoryBot.define do
metadata_version { 'sast:1.0' }
name { 'Cipher with no integrity' }
report_type { :sast }
raw_metadata do
original_data do
{
description: "The cipher does not provide data integrity update 1",
solution: "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.",
@ -26,7 +26,7 @@ FactoryBot.define do
url: "https://crypto.stackexchange.com/questions/31428/pbewithmd5anddes-cipher-does-not-check-for-integrity-first"
}
]
}.to_json
}.deep_stringify_keys
end
scanner factory: :ci_reports_security_scanner
severity { :high }

View File

@ -34,6 +34,10 @@ RSpec.describe 'Value Stream Analytics', :js do
wait_for_all_requests
end
before do
stub_feature_flags(use_vsa_aggregated_tables: false)
end
context 'as an allowed user' do
context 'when project is new' do
before do

View File

@ -123,11 +123,11 @@ RSpec.describe 'User views releases', :js do
within('.release-block', match: :first) do
expect(page).to have_content(release_v3.description)
expect(page).to have_content(release_v3.tag)
expect(page).to have_content(release_v3.name)
# The following properties (sometimes) include Git info,
# so they are not rendered for Guest users
expect(page).not_to have_content(release_v3.name)
expect(page).not_to have_content(release_v3.tag)
expect(page).not_to have_content(release_v3.commit.short_id)
end
end

View File

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['ReleaseLinks'] do
it { expect(described_class).to require_graphql_authorizations(:download_code) }
it { expect(described_class).to require_graphql_authorizations(:read_release) }
it 'has the expected fields' do
expected_fields = %w[
@ -18,4 +18,46 @@ RSpec.describe GitlabSchema.types['ReleaseLinks'] do
expect(described_class).to include_graphql_fields(*expected_fields)
end
context 'individual field authorization' do
def fetch_authorizations(field_name)
described_class.fields.dig(field_name).instance_variable_get(:@authorize)
end
describe 'openedMergeRequestsUrl' do
it 'has valid authorization' do
expect(fetch_authorizations('openedMergeRequestsUrl')).to include(:download_code)
end
end
describe 'mergedMergeRequestsUrl' do
it 'has valid authorization' do
expect(fetch_authorizations('mergedMergeRequestsUrl')).to include(:download_code)
end
end
describe 'closedMergeRequestsUrl' do
it 'has valid authorization' do
expect(fetch_authorizations('closedMergeRequestsUrl')).to include(:download_code)
end
end
describe 'openedIssuesUrl' do
it 'has valid authorization' do
expect(fetch_authorizations('openedIssuesUrl')).to include(:download_code)
end
end
describe 'closedIssuesUrl' do
it 'has valid authorization' do
expect(fetch_authorizations('closedIssuesUrl')).to include(:download_code)
end
end
describe 'editUrl' do
it 'has valid authorization' do
expect(fetch_authorizations('editUrl')).to include(:update_release)
end
end
end
end

View File

@ -0,0 +1,130 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher do
let_it_be(:project) { create(:project) }
let_it_be(:issue_1) { create(:issue, project: project) }
let_it_be(:issue_2) { create(:issue, project: project) }
let_it_be(:issue_3) { create(:issue, project: project) }
let_it_be(:stage_event_1) { create(:cycle_analytics_issue_stage_event, issue_id: issue_1.id, start_event_timestamp: 2.years.ago, end_event_timestamp: 1.year.ago) } # duration: 1 year
let_it_be(:stage_event_2) { create(:cycle_analytics_issue_stage_event, issue_id: issue_2.id, start_event_timestamp: 5.years.ago, end_event_timestamp: 2.years.ago) } # duration: 3 years
let_it_be(:stage_event_3) { create(:cycle_analytics_issue_stage_event, issue_id: issue_3.id, start_event_timestamp: 6.years.ago, end_event_timestamp: 3.months.ago) } # duration: 5+ years
let_it_be(:stage) { create(:cycle_analytics_project_stage, start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production, project: project) }
let(:params) { {} }
subject(:records_fetcher) do
described_class.new(stage: stage, query: Analytics::CycleAnalytics::IssueStageEvent.all, params: params)
end
shared_examples 'match returned records' do
it 'returns issues in the correct order' do
returned_iids = records_fetcher.serialized_records.pluck(:iid).map(&:to_i)
expect(returned_iids).to eq(expected_issue_ids)
end
end
describe '#serialized_records' do
describe 'sorting' do
context 'when sorting by end event DESC' do
let(:expected_issue_ids) { [issue_3.iid, issue_1.iid, issue_2.iid] }
before do
params[:sort] = :end_event
params[:direction] = :desc
end
it_behaves_like 'match returned records'
end
context 'when sorting by end event ASC' do
let(:expected_issue_ids) { [issue_2.iid, issue_1.iid, issue_3.iid] }
before do
params[:sort] = :end_event
params[:direction] = :asc
end
it_behaves_like 'match returned records'
end
context 'when sorting by duration DESC' do
let(:expected_issue_ids) { [issue_3.iid, issue_2.iid, issue_1.iid] }
before do
params[:sort] = :duration
params[:direction] = :desc
end
it_behaves_like 'match returned records'
end
context 'when sorting by duration ASC' do
let(:expected_issue_ids) { [issue_1.iid, issue_2.iid, issue_3.iid] }
before do
params[:sort] = :duration
params[:direction] = :asc
end
it_behaves_like 'match returned records'
end
end
describe 'pagination' do
let(:expected_issue_ids) { [issue_3.iid] }
before do
params[:sort] = :duration
params[:direction] = :asc
params[:page] = 2
stub_const('Gitlab::Analytics::CycleAnalytics::Aggregated::RecordsFetcher::MAX_RECORDS', 2)
end
it_behaves_like 'match returned records'
end
context 'when passing a block to serialized_records method' do
before do
params[:sort] = :duration
params[:direction] = :asc
end
it 'yields the underlying stage event scope' do
stage_event_records = []
records_fetcher.serialized_records do |scope|
stage_event_records.concat(scope.to_a)
end
expect(stage_event_records.map(&:issue_id)).to eq([issue_1.id, issue_2.id, issue_3.id])
end
end
context 'when the issue record no longer exists' do
it 'skips non-existing issue records' do
create(:cycle_analytics_issue_stage_event, {
issue_id: 0, # non-existing id
start_event_timestamp: 5.months.ago,
end_event_timestamp: 3.months.ago
})
stage_event_count = nil
records_fetcher.serialized_records do |scope|
stage_event_count = scope.to_a.size
end
issue_count = records_fetcher.serialized_records.to_a.size
expect(stage_event_count).to eq(4)
expect(issue_count).to eq(3)
end
end
end
end

View File

@ -5,9 +5,11 @@ require 'spec_helper'
RSpec.describe U2fRegistration do
let_it_be(:user) { create(:user) }
let(:u2f_registration_name) { 'u2f_device' }
let(:u2f_registration) do
device = U2F::FakeU2F.new(FFaker::BaconIpsum.characters(5))
create(:u2f_registration, name: 'u2f_device',
create(:u2f_registration, name: u2f_registration_name,
user: user,
certificate: Base64.strict_encode64(device.cert_raw),
key_handle: U2F.urlsafe_encode64(device.key_handle_raw),
@ -16,11 +18,27 @@ RSpec.describe U2fRegistration do
describe 'callbacks' do
describe '#create_webauthn_registration' do
it 'creates webauthn registration' do
u2f_registration.save!
shared_examples_for 'creates webauthn registration' do
it 'creates webauthn registration' do
u2f_registration.save!
webauthn_registration = WebauthnRegistration.where(u2f_registration_id: u2f_registration.id)
expect(webauthn_registration).to exist
webauthn_registration = WebauthnRegistration.where(u2f_registration_id: u2f_registration.id)
expect(webauthn_registration).to exist
end
end
it_behaves_like 'creates webauthn registration'
context 'when the u2f_registration has a blank name' do
let(:u2f_registration_name) { '' }
it_behaves_like 'creates webauthn registration'
end
context 'when the u2f_registration has the name as `nil`' do
let(:u2f_registration_name) { nil }
it_behaves_like 'creates webauthn registration'
end
it 'logs error' do

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WebauthnRegistration do
describe 'relations' do
it { is_expected.to belong_to(:user) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:credential_xid) }
it { is_expected.to validate_presence_of(:public_key) }
it { is_expected.to validate_presence_of(:counter) }
it { is_expected.to validate_length_of(:name).is_at_least(0) }
it { is_expected.not_to allow_value(nil).for(:name) }
it do
is_expected.to validate_numericality_of(:counter)
.only_integer
.is_greater_than_or_equal_to(0)
.is_less_than_or_equal_to(4294967295)
end
end
end

View File

@ -63,12 +63,6 @@ RSpec.describe ReleasePresenter do
it 'returns its own url' do
is_expected.to eq(project_release_url(project, release))
end
context 'when user is guest' do
let(:user) { guest }
it { is_expected.to be_nil }
end
end
describe '#opened_merge_requests_url' do
@ -147,13 +141,5 @@ RSpec.describe ReleasePresenter do
it 'returns the release name' do
is_expected.to eq release.name
end
context "when a user is not allowed to access any repository information" do
let(:presenter) { described_class.new(release, current_user: guest) }
it 'returns a replacement name to avoid potentially leaking tag information' do
is_expected.to eq "Release-#{release.id}"
end
end
end
end

View File

@ -228,6 +228,189 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
end
end
shared_examples 'restricted access to release fields' do
describe 'scalar fields' do
let(:path) { path_prefix }
let(:release_fields) do
%{
tagName
tagPath
description
descriptionHtml
name
createdAt
releasedAt
upcomingRelease
}
end
before do
post_query
end
it 'finds all release data' do
expect(data).to eq({
'tagName' => release.tag,
'tagPath' => nil,
'description' => release.description,
'descriptionHtml' => release.description_html,
'name' => release.name,
'createdAt' => release.created_at.iso8601,
'releasedAt' => release.released_at.iso8601,
'upcomingRelease' => false
})
end
end
describe 'milestones' do
let(:path) { path_prefix + %w[milestones nodes] }
let(:release_fields) do
query_graphql_field(:milestones, nil, 'nodes { id title }')
end
it 'finds milestones associated to a release' do
post_query
expected = release.milestones.order_by_dates_and_title.map do |milestone|
{ 'id' => global_id_of(milestone), 'title' => milestone.title }
end
expect(data).to eq(expected)
end
end
describe 'author' do
let(:path) { path_prefix + %w[author] }
let(:release_fields) do
query_graphql_field(:author, nil, 'id username')
end
it 'finds the author of the release' do
post_query
expect(data).to eq(
'id' => global_id_of(release.author),
'username' => release.author.username
)
end
end
describe 'commit' do
let(:path) { path_prefix + %w[commit] }
let(:release_fields) do
query_graphql_field(:commit, nil, 'sha')
end
it 'restricts commit associated with the release' do
post_query
expect(data).to eq(nil)
end
end
describe 'assets' do
describe 'count' do
let(:path) { path_prefix + %w[assets] }
let(:release_fields) do
query_graphql_field(:assets, nil, 'count')
end
it 'returns non source release links count' do
post_query
expect(data).to eq('count' => release.assets_count(except: [:sources]))
end
end
describe 'links' do
let(:path) { path_prefix + %w[assets links nodes] }
let(:release_fields) do
query_graphql_field(:assets, nil,
query_graphql_field(:links, nil, 'nodes { id name url external, directAssetUrl }'))
end
it 'finds all non source external release links' do
post_query
expected = release.links.map do |link|
{
'id' => global_id_of(link),
'name' => link.name,
'url' => link.url,
'external' => true,
'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url
}
end
expect(data).to match_array(expected)
end
end
describe 'sources' do
let(:path) { path_prefix + %w[assets sources nodes] }
let(:release_fields) do
query_graphql_field(:assets, nil,
query_graphql_field(:sources, nil, 'nodes { format url }'))
end
it 'restricts release sources' do
post_query
expect(data).to match_array([])
end
end
end
describe 'links' do
let(:path) { path_prefix + %w[links] }
let(:release_fields) do
query_graphql_field(:links, nil, %{
selfUrl
openedMergeRequestsUrl
mergedMergeRequestsUrl
closedMergeRequestsUrl
openedIssuesUrl
closedIssuesUrl
})
end
it 'finds only selfUrl' do
post_query
expect(data).to eq(
'selfUrl' => project_release_url(project, release),
'openedMergeRequestsUrl' => nil,
'mergedMergeRequestsUrl' => nil,
'closedMergeRequestsUrl' => nil,
'openedIssuesUrl' => nil,
'closedIssuesUrl' => nil
)
end
end
describe 'evidences' do
let(:path) { path_prefix + %w[evidences] }
let(:release_fields) do
query_graphql_field(:evidences, nil, 'nodes { id sha filepath collectedAt }')
end
it 'restricts all evidence fields' do
post_query
expect(data).to eq('nodes' => [])
end
end
end
shared_examples 'no access to the release field' do
describe 'repository-related fields' do
let(:path) { path_prefix }
@ -302,7 +485,8 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do
context 'when the user has Guest permissions' do
let(:current_user) { guest }
it_behaves_like 'no access to the release field'
it_behaves_like 'restricted access to release fields'
it_behaves_like 'no access to editUrl'
end
context 'when the user has Reporter permissions' do

View File

@ -129,10 +129,12 @@ RSpec.describe 'Query.project(fullPath).releases()' do
end
it 'does not return data for fields that expose repository information' do
tag_name = release.tag
release_name = release.name
expect(data).to eq(
'tagName' => nil,
'tagName' => tag_name,
'tagPath' => nil,
'name' => "Release-#{release.id}",
'name' => release_name,
'commit' => nil,
'assets' => {
'count' => release.assets_count(except: [:sources]),
@ -143,7 +145,14 @@ RSpec.describe 'Query.project(fullPath).releases()' do
'evidences' => {
'nodes' => []
},
'links' => nil
'links' => {
'closedIssuesUrl' => nil,
'closedMergeRequestsUrl' => nil,
'mergedMergeRequestsUrl' => nil,
'openedIssuesUrl' => nil,
'openedMergeRequestsUrl' => nil,
'selfUrl' => project_release_url(project, release)
}
)
end
end

View File

@ -73,6 +73,23 @@ RSpec.describe Packages::Npm::CreatePackageService do
end
end
described_class::PACKAGE_JSON_NOT_ALLOWED_FIELDS.each do |field|
context "with not allowed #{field} field" do
before do
params[:versions][version][field] = 'test'
end
it 'is persisted without the field' do
expect { subject }
.to change { Packages::Package.count }.by(1)
.and change { Packages::Package.npm.count }.by(1)
.and change { Packages::Tag.count }.by(1)
.and change { Packages::Npm::Metadatum.count }.by(1)
expect(subject.npm_metadatum.package_json[field]).to be_blank
end
end
end
context 'with packages_npm_abbreviated_metadata disabled' do
before do
stub_feature_flags(packages_npm_abbreviated_metadata: false)

View File

@ -36,8 +36,8 @@ RSpec.shared_examples 'StageEventModel' do
described_class.issuable_id_column,
:group_id,
:project_id,
:milestone_id,
:author_id,
:milestone_id,
:state_id,
:start_event_timestamp,
:end_event_timestamp