summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue59
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.vue2
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue109
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js6
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/responsive-tables.scss86
-rw-r--r--app/assets/stylesheets/pages/environments.scss72
-rw-r--r--app/finders/events_finder.rb62
-rw-r--r--app/models/event.rb32
-rw-r--r--app/models/pages_domain.rb4
-rw-r--r--changelogs/unreleased/14707-allow-activity-feed-to-be-accessible-through-api.yml4
-rw-r--r--changelogs/unreleased/32642_last_commit_id_in_file_api.yml4
-rw-r--r--changelogs/unreleased/allow_numeric_pages_domain.yml4
-rw-r--r--doc/administration/container_registry.md30
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/events.md347
-rw-r--r--doc/api/projects.md138
-rw-r--r--doc/api/repository_files.md1
-rw-r--r--doc/api/users.md141
-rw-r--r--doc/ci/variables/README.md36
-rw-r--r--doc/ci/yaml/README.md21
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/events.rb86
-rw-r--r--lib/api/files.rb11
-rw-r--r--lib/api/projects.rb10
-rw-r--r--lib/api/users.rb21
-rw-r--r--spec/features/projects/environments/environments_spec.rb4
-rw-r--r--spec/finders/events_finder_spec.rb44
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js4
-rw-r--r--spec/javascripts/environments/environment_spec.js2
-rw-r--r--spec/javascripts/environments/environment_table_spec.js2
-rw-r--r--spec/models/pages_domain_spec.rb51
-rw-r--r--spec/requests/api/events_spec.rb142
-rw-r--r--spec/requests/api/files_spec.rb19
-rw-r--r--spec/requests/api/projects_spec.rb58
-rw-r--r--spec/requests/api/users_spec.rb77
40 files changed, 1083 insertions, 617 deletions
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index a2448520a5f..41d5453f1b2 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -70,7 +70,7 @@ export default {
</span>
</button>
- <ul class="dropdown-menu dropdown-menu-align-right">
+ <ul class="dropdown-menu">
<li v-for="action in actions">
<button
type="button"
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 012ff1f975b..03eb51ba1b2 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -421,14 +421,19 @@ export default {
};
</script>
<template>
- <tr :class="{ 'js-child-row': model.isChildren }">
- <td>
+ <div
+ :class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }">
+ <div class="table-section section-10" role="gridcell">
+ <div
+ v-if="!model.isFolder"
+ class="table-mobile-header">
+ Environment
+ </div>
<a
v-if="!model.isFolder"
- class="environment-name"
- :class="{ 'prepend-left-default': model.isChildren }"
+ class="environment-name flex-truncate-parent table-mobile-content"
:href="environmentPath">
- {{model.name}}
+ <span class="flex-truncate-child">{{model.name}}</span>
</a>
<span
v-else
@@ -461,9 +466,9 @@ export default {
{{model.size}}
</span>
</span>
- </td>
+ </div>
- <td class="deployment-column">
+ <div class="table-section section-10 deployment-column hidden-xs hidden-sm" role="gridcell">
<span v-if="shouldRenderDeploymentID">
{{deploymentInternalId}}
</span>
@@ -478,21 +483,26 @@ export default {
:tooltip-text="deploymentUser.username"
/>
</span>
- </td>
+ </div>
- <td class="environments-build-cell">
+ <div class="table-section section-15 hidden-xs hidden-sm" role="gridcell">
<a
v-if="shouldRenderBuildName"
class="build-link"
:href="buildPath">
{{buildName}}
</a>
- </td>
+ </div>
- <td>
+ <div class="table-section section-25" role="gridcell">
+ <div
+ v-if="!model.isFolder"
+ class="table-mobile-header">
+ Commit
+ </div>
<div
v-if="!model.isFolder && hasLastDeploymentKey"
- class="js-commit-component">
+ class="js-commit-component table-mobile-content">
<commit-component
:tag="commitTag"
:commit-ref="commitRef"
@@ -501,25 +511,30 @@ export default {
:title="commitTitle"
:author="commitAuthor"/>
</div>
- <p
+ <div
v-if="!model.isFolder && !hasLastDeploymentKey"
class="commit-title">
No deployments yet
- </p>
- </td>
+ </div>
+ </div>
- <td>
+ <div class="table-section section-10" role="gridcell">
+ <div
+ v-if="!model.isFolder"
+ class="table-mobile-header">
+ Updated
+ </div>
<span
v-if="!model.isFolder && canShowDate"
- class="environment-created-date-timeago">
+ class="environment-created-date-timeago table-mobile-content">
{{createdDate}}
</span>
- </td>
+ </div>
- <td class="environments-actions">
+ <div class="table-section section-30 environments-actions table-button-footer" role="gridcell">
<div
v-if="!model.isFolder"
- class="btn-group pull-right"
+ class="btn-group environment-action-buttons"
role="group">
<actions-component
@@ -553,6 +568,6 @@ export default {
:retry-url="retryUrl"
/>
</div>
- </td>
- </tr>
+ </div>
+ </div>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue
index 79c019b3491..07cf92281a0 100644
--- a/app/assets/javascripts/environments/components/environment_monitoring.vue
+++ b/app/assets/javascripts/environments/components/environment_monitoring.vue
@@ -19,7 +19,7 @@ export default {
</script>
<template>
<a
- class="btn monitoring-url has-tooltip"
+ class="btn monitoring-url has-tooltip hidden-xs hidden-sm"
data-container="body"
rel="noopener noreferrer nofollow"
:href="monitoringUrl"
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index 2ba985bfe3e..49dba38edfb 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -43,7 +43,7 @@ export default {
<template>
<button
type="button"
- class="btn"
+ class="btn hidden-xs hidden-sm"
@click="onClick"
:disabled="isLoading">
diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue
index a904453ffa9..091c543860b 100644
--- a/app/assets/javascripts/environments/components/environment_stop.vue
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -47,7 +47,7 @@ export default {
<template>
<button
type="button"
- class="btn stop-env-link has-tooltip"
+ class="btn stop-env-link has-tooltip hidden-xs hidden-sm"
data-container="body"
@click="onClick"
:disabled="isLoading"
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue
index c8c1f17d4d8..1ca65a79951 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.vue
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue
@@ -29,7 +29,7 @@ export default {
</script>
<template>
<a
- class="btn terminal-button has-tooltip"
+ class="btn terminal-button has-tooltip hidden-xs hidden-sm"
data-container="body"
:title="title"
:aria-label="title"
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 5148a2ae79b..f9262ab85c5 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -45,68 +45,59 @@ export default {
};
</script>
<template>
- <table class="table ci-table">
- <thead>
- <tr>
- <th class="environments-name">
- Environment
- </th>
- <th class="environments-deploy">
- Last deployment
- </th>
- <th class="environments-build">
- Job
- </th>
- <th class="environments-commit">
- Commit
- </th>
- <th class="environments-date">
- Updated
- </th>
- <th class="environments-actions"></th>
- </tr>
- </thead>
- <tbody>
- <template
- v-for="model in environments"
- v-bind:model="model">
- <tr
- is="environment-item"
- :model="model"
- :can-create-deployment="canCreateDeployment"
- :can-read-environment="canReadEnvironment"
- />
+ <div class="ci-table" role="grid">
+ <div class="gl-responsive-table-row table-row-header" role="row">
+ <div class="table-section section-10 environments-name" role="rowheader">
+ Environment
+ </div>
+ <div class="table-section section-10 environments-deploy" role="rowheader">
+ Deployment
+ </div>
+ <div class="table-section section-15 environments-build" role="rowheader">
+ Job
+ </div>
+ <div class="table-section section-25 environments-commit" role="rowheader">
+ Commit
+ </div>
+ <div class="table-section section-10 environments-date" role="rowheader">
+ Updated
+ </div>
+ </div>
+ <template
+ v-for="model in environments"
+ v-bind:model="model">
+ <div
+ is="environment-item"
+ :model="model"
+ :can-create-deployment="canCreateDeployment"
+ :can-read-environment="canReadEnvironment"
+ />
- <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
- <tr v-if="isLoadingFolderContent">
- <td colspan="6">
- <loading-icon size="2" />
- </td>
- </tr>
+ <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
+ <div v-if="isLoadingFolderContent">
+ <loading-icon size="2" />
+ </div>
- <template v-else>
- <tr
- is="environment-item"
- v-for="children in model.children"
- :model="children"
- :can-create-deployment="canCreateDeployment"
- :can-read-environment="canReadEnvironment"
- />
+ <template v-else>
+ <div
+ is="environment-item"
+ v-for="children in model.children"
+ :model="children"
+ :can-create-deployment="canCreateDeployment"
+ :can-read-environment="canReadEnvironment"
+ />
- <tr>
- <td
- colspan="6"
- class="text-center">
- <a
- :href="folderUrl(model)"
- class="btn btn-default">
- Show all
- </a>
- </td>
- </tr>
- </template>
+ <div>
+ <div class="text-center prepend-top-10">
+ <a
+ :href="folderUrl(model)"
+ class="btn btn-default">
+ Show all
+ </a>
+ </div>
+ </div>
</template>
</template>
- </tbody>
- </table>
+ </template>
+ </div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js
index 8e22057e2e9..ff5ae28e062 100644
--- a/app/assets/javascripts/vue_shared/components/commit.js
+++ b/app/assets/javascripts/vue_shared/components/commit.js
@@ -135,8 +135,8 @@ export default {
{{shortSha}}
</a>
- <p class="commit-title">
- <span v-if="title">
+ <div class="commit-title flex-truncate-parent">
+ <span v-if="title" class="flex-truncate-child">
<user-avatar-link
v-if="hasAuthor"
class="avatar-image-container"
@@ -153,7 +153,7 @@ export default {
<span v-else>
Cant find HEAD commit for this branch
</span>
- </p>
+ </div>
</div>
`,
};
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index b8ba77f4513..9dc9f9a9068 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -49,3 +49,4 @@
@import "framework/icons.scss";
@import "framework/snippets.scss";
@import "framework/memory_graph.scss";
+@import "framework/responsive-tables.scss";
diff --git a/app/assets/stylesheets/framework/responsive-tables.scss b/app/assets/stylesheets/framework/responsive-tables.scss
new file mode 100644
index 00000000000..94528c7a222
--- /dev/null
+++ b/app/assets/stylesheets/framework/responsive-tables.scss
@@ -0,0 +1,86 @@
+@mixin flex-max-width($max) {
+ flex: 0 0 #{$max + '%'};
+ max-width: #{$max + '%'};
+}
+
+.gl-responsive-table-row {
+ margin-top: 10px;
+ border: 1px solid $border-color;
+
+ @media (min-width: $screen-md-min) {
+ padding: 15px 0;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ border: none;
+ border-bottom: 1px solid $white-normal;
+ }
+
+ .table-section {
+ white-space: nowrap;
+
+ $section-widths: 10 15 25 30;
+ @each $width in $section-widths {
+ &.section-#{$width} {
+ flex: 0 0 #{$width + '%'};
+
+ @media (min-width: $screen-md-min) {
+ max-width: #{$width + '%'};
+ }
+ }
+ }
+
+ &:not(.table-button-footer) {
+ @media (max-width: $screen-sm-max) {
+ display: flex;
+ align-self: stretch;
+ padding: 10px;
+ align-items: center;
+ height: 62px;
+
+ &:not(:first-of-type) {
+ border-top: 1px solid $white-normal;
+ }
+ }
+ }
+ }
+}
+
+.table-row-header {
+ font-size: 13px;
+
+ @media (max-width: $screen-sm-max) {
+ display: none;
+ }
+}
+
+.table-mobile-header {
+ color: $gl-text-color-secondary;
+ @include flex-max-width(40);
+
+ @media (min-width: $screen-md-min) {
+ display: none;
+ }
+}
+
+.table-mobile-content {
+ @media (max-width: $screen-sm-max) {
+ @include flex-max-width(60);
+ text-align: right;
+ }
+}
+
+.flex-truncate-parent {
+ display: flex;
+}
+
+.flex-truncate-child {
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ @media (min-width: $screen-md-min) {
+ flex: 0 0 90%;
+ }
+}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index f269d53093d..bc151d2358f 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -11,34 +11,7 @@
}
.environments-container {
- .table-holder {
- width: 100%;
-
- @media (max-width: $screen-sm-max) {
- overflow: auto;
- }
- }
-
- .table.ci-table {
- .environments-actions {
- min-width: 300px;
- }
-
- .environments-commit,
- .environments-actions {
- width: 20%;
- }
-
- .environments-date {
- width: 10%;
- }
-
- .environments-name,
- .environments-deploy,
- .environments-build {
- width: 15%;
- }
-
+ .ci-table {
.deployment-column {
> span {
word-break: break-all;
@@ -150,6 +123,49 @@
}
}
+.gl-responsive-table-row {
+ .environments-actions {
+ @media (min-width: $screen-md-min) {
+ text-align: right;
+ }
+
+ @media (max-width: $screen-sm-max) {
+ background-color: $gray-normal;
+ align-self: stretch;
+ border-top: 1px solid $border-color;
+
+ .environment-action-buttons {
+ padding: 10px;
+ display: flex;
+
+ .btn {
+ border-radius: 3px;
+ }
+
+ > .btn-group,
+ .external-url {
+ flex: 1;
+ flex-basis: 28px;
+ }
+
+ .dropdown-new {
+ width: 100%;
+ }
+ }
+ }
+ }
+}
+
+.folder-row {
+ padding: 15px 0;
+ border-bottom: 1px solid $white-normal;
+
+ @media (max-width: $screen-sm-max) {
+ border-top: 1px solid $white-normal;
+ margin-top: 10px;
+ }
+}
+
.prometheus-graph {
text {
fill: $gl-text-color;
diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb
new file mode 100644
index 00000000000..b0450ddc1fd
--- /dev/null
+++ b/app/finders/events_finder.rb
@@ -0,0 +1,62 @@
+class EventsFinder
+ attr_reader :source, :params, :current_user
+
+ # Used to filter Events
+ #
+ # Arguments:
+ # source - which user or project to looks for events on
+ # current_user - only return events for projects visible to this user
+ # params:
+ # action: string
+ # target_type: string
+ # before: datetime
+ # after: datetime
+ #
+ def initialize(params = {})
+ @source = params.delete(:source)
+ @current_user = params.delete(:current_user)
+ @params = params
+ end
+
+ def execute
+ events = source.events
+
+ events = by_current_user_access(events)
+ events = by_action(events)
+ events = by_target_type(events)
+ events = by_created_at_before(events)
+ events = by_created_at_after(events)
+
+ events
+ end
+
+ private
+
+ def by_current_user_access(events)
+ events.merge(ProjectsFinder.new(current_user: current_user).execute).references(:project)
+ end
+
+ def by_action(events)
+ return events unless Event::ACTIONS[params[:action]]
+
+ events.where(action: Event::ACTIONS[params[:action]])
+ end
+
+ def by_target_type(events)
+ return events unless Event::TARGET_TYPES[params[:target_type]]
+
+ events.where(target_type: Event::TARGET_TYPES[params[:target_type]])
+ end
+
+ def by_created_at_before(events)
+ return events unless params[:before]
+
+ events.where('events.created_at < ?', params[:before].beginning_of_day)
+ end
+
+ def by_created_at_after(events)
+ return events unless params[:after]
+
+ events.where('events.created_at > ?', params[:after].end_of_day)
+ end
+end
diff --git a/app/models/event.rb b/app/models/event.rb
index 46e89388bc1..d6d39473774 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -14,6 +14,30 @@ class Event < ActiveRecord::Base
DESTROYED = 10
EXPIRED = 11 # User left project due to expiry
+ ACTIONS = HashWithIndifferentAccess.new(
+ created: CREATED,
+ updated: UPDATED,
+ closed: CLOSED,
+ reopened: REOPENED,
+ pushed: PUSHED,
+ commented: COMMENTED,
+ merged: MERGED,
+ joined: JOINED,
+ left: LEFT,
+ destroyed: DESTROYED,
+ expired: EXPIRED
+ ).freeze
+
+ TARGET_TYPES = HashWithIndifferentAccess.new(
+ issue: Issue,
+ milestone: Milestone,
+ merge_request: MergeRequest,
+ note: Note,
+ project: Project,
+ snippet: Snippet,
+ user: User
+ ).freeze
+
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
@@ -55,6 +79,14 @@ class Event < ActiveRecord::Base
def limit_recent(limit = 20, offset = nil)
recent.limit(limit).offset(offset)
end
+
+ def actions
+ ACTIONS.keys
+ end
+
+ def target_types
+ TARGET_TYPES.keys
+ end
end
def visible_to_user?(user = nil)
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index f2f2fc1e32a..5d798247863 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -1,7 +1,7 @@
class PagesDomain < ActiveRecord::Base
belongs_to :project
- validates :domain, hostname: true
+ validates :domain, hostname: { allow_numeric_hostname: true }
validates :domain, uniqueness: { case_sensitive: false }
validates :certificate, certificate: true, allow_nil: true, allow_blank: true
validates :key, certificate_key: true, allow_nil: true, allow_blank: true
@@ -98,7 +98,7 @@ class PagesDomain < ActiveRecord::Base
def validate_pages_domain
return unless domain
- if domain.downcase.ends_with?(".#{Settings.pages.host}".downcase)
+ if domain.downcase.ends_with?(Settings.pages.host.downcase)
self.errors.add(:domain, "*.#{Settings.pages.host} is restricted")
end
end
diff --git a/changelogs/unreleased/14707-allow-activity-feed-to-be-accessible-through-api.yml b/changelogs/unreleased/14707-allow-activity-feed-to-be-accessible-through-api.yml
new file mode 100644
index 00000000000..9c17c3b949c
--- /dev/null
+++ b/changelogs/unreleased/14707-allow-activity-feed-to-be-accessible-through-api.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce an Events API
+merge_request: 11755
+author:
diff --git a/changelogs/unreleased/32642_last_commit_id_in_file_api.yml b/changelogs/unreleased/32642_last_commit_id_in_file_api.yml
new file mode 100644
index 00000000000..80435352e10
--- /dev/null
+++ b/changelogs/unreleased/32642_last_commit_id_in_file_api.yml
@@ -0,0 +1,4 @@
+---
+title: 'Introduce optimistic locking support via optional parameter last_commit_sha on File Update API'
+merge_request: 11694
+author: electroma
diff --git a/changelogs/unreleased/allow_numeric_pages_domain.yml b/changelogs/unreleased/allow_numeric_pages_domain.yml
new file mode 100644
index 00000000000..10d9f26f88d
--- /dev/null
+++ b/changelogs/unreleased/allow_numeric_pages_domain.yml
@@ -0,0 +1,4 @@
+---
+title: Allow numeric pages domain
+merge_request: 11550
+author:
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index f707039827b..afafb6bf1f5 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -1,10 +1,7 @@
# GitLab Container Registry administration
-> [Introduced][ce-4040] in GitLab 8.8.
-
----
-
> **Notes:**
+- [Introduced][ce-4040] in GitLab 8.8.
- Container Registry manifest `v1` support was added in GitLab 8.9 to support
Docker versions earlier than 1.10.
- This document is about the admin guide. To learn how to use GitLab Container
@@ -514,8 +511,8 @@ configurable in future releases.
## Configure Container Registry notifications
-You can configure the Container Registry to send webhook notifications in
-response to events happening within the registry.
+You can configure the Container Registry to send webhook notifications in
+response to events happening within the registry.
Read more about the Container Registry notifications config options in the
[Docker Registry notifications documentation][notifications-config].
@@ -568,12 +565,25 @@ notifications:
backoff: 1000
```
-## Changelog
+## Using self-signed certificates with Container Registry
+
+If you're using a self-signed certificate with your Container Registry, you
+might encounter issues during the CI jobs like the following:
+
+```
+Error response from daemon: Get registry.example.com/v1/users/: x509: certificate signed by unknown authority
+```
-**GitLab 8.8 ([source docs][8-8-docs])**
+The Docker daemon running the command expects a cert signed by a recognized CA,
+thus the error above.
-- GitLab Container Registry feature was introduced.
+While GitLab doesn't support using self-signed certificates with Container
+Registry out of the box, it is possible to make it work if you follow
+[Docker's documentation][docker-insecure]. You may find some additional
+information in [issue 18239][ce-18239].
+[ce-18239]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18239
+[docker-insecure]: https://docs.docker.com/registry/insecure/#using-self-signed-certificates
[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure
[restart gitlab]: restart_gitlab.md#installations-from-source
[wildcard certificate]: https://en.wikipedia.org/wiki/Wildcard_certificate
@@ -589,4 +599,4 @@ notifications:
[existing-domain]: #configure-container-registry-under-an-existing-gitlab-domain
[new-domain]: #configure-container-registry-under-its-own-domain
[notifications-config]: https://docs.docker.com/registry/notifications/
-[registry-notifications-config]: https://docs.docker.com/registry/configuration/#notifications \ No newline at end of file
+[registry-notifications-config]: https://docs.docker.com/registry/configuration/#notifications
diff --git a/doc/api/README.md b/doc/api/README.md
index 44e345b1cf6..e1d4009dedc 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -16,6 +16,7 @@ following locations:
- [Deployments](deployments.md)
- [Deploy Keys](deploy_keys.md)
- [Environments](environments.md)
+- [Events](events.md)
- [Gitignores templates](templates/gitignores.md)
- [GitLab CI Config templates](templates/gitlab_ci_ymls.md)
- [Groups](groups.md)
diff --git a/doc/api/events.md b/doc/api/events.md
new file mode 100644
index 00000000000..e7829c9f479
--- /dev/null
+++ b/doc/api/events.md
@@ -0,0 +1,347 @@
+# Events
+
+## Filter parameters
+
+### Action Types
+
+Available action types for the `action` parameter are:
+
+- `created`
+- `updated`
+- `closed`
+- `reopened`
+- `pushed`
+- `commented`
+- `merged`
+- `joined`
+- `left`
+- `destroyed`
+- `expired`
+
+Note that these options are downcased.
+
+### Target Types
+
+Available target types for the `target_type` parameter are:
+
+- `issue`
+- `milestone`
+- `merge_request`
+- `note`
+- `project`
+- `snippet`
+- `user`
+
+Note that these options are downcased.
+
+### Date formatting
+
+Dates for the `before` and `after` parameters should be supplied in the following format:
+
+```
+YYYY-MM-DD
+```
+
+## List currently authenticated user's events
+
+>**Note:** This endpoint was introduced in GitLab 9.3.
+
+Get a list of events for the authenticated user.
+
+```
+GET /events
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `action` | string | no | Include only events of a particular [action type][action-types] |
+| `target_type` | string | no | Include only events of a particular [target type][target-types] |
+| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
+| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
+| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
+
+Example request:
+
+```
+curl --header "PRIVATE-TOKEN 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01
+```
+
+Example response:
+
+```json
+[
+ {
+ "title":null,
+ "project_id":1,
+ "action_name":"opened",
+ "target_id":160,
+ "target_type":"Issue",
+ "author_id":25,
+ "data":null,
+ "target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.",
+ "created_at":"2017-02-09T10:43:19.667Z",
+ "author":{
+ "name":"User 3",
+ "username":"user3",
+ "id":25,
+ "state":"active",
+ "avatar_url":"http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80\u0026d=identicon",
+ "web_url":"https://gitlab.example.com/user3"
+ },
+ "author_username":"user3"
+ },
+ {
+ "title":null,
+ "project_id":1,
+ "action_name":"opened",
+ "target_id":159,
+ "target_type":"Issue",
+ "author_id":21,
+ "data":null,
+ "target_title":"Nostrum enim non et sed optio illo deleniti non.",
+ "created_at":"2017-02-09T10:43:19.426Z",
+ "author":{
+ "name":"Test User",
+ "username":"ted",
+ "id":21,
+ "state":"active",
+ "avatar_url":"http://www.gravatar.com/avatar/80fb888c9a48b9a3f87477214acaa63f?s=80\u0026d=identicon",
+ "web_url":"https://gitlab.example.com/ted"
+ },
+ "author_username":"ted"
+ }
+]
+```
+
+### Get user contribution events
+
+>**Note:** Documentation was formerly located in the [Users API pages][users-api].
+
+Get the contribution events for the specified user, sorted from newest to oldest.
+
+```
+GET /users/:id/events
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID or Username of the user |
+| `action` | string | no | Include only events of a particular [action type][action-types] |
+| `target_type` | string | no | Include only events of a particular [target type][target-types] |
+| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
+| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
+| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/:id/events
+```
+
+Example response:
+
+```json
+[
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "closed",
+ "target_id": 830,
+ "target_type": "Issue",
+ "author_id": 1,
+ "data": null,
+ "target_title": "Public project search field",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_username": "root"
+ },
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "opened",
+ "target_id": null,
+ "target_type": null,
+ "author_id": 1,
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_username": "john",
+ "data": {
+ "before": "50d4420237a9de7be1304607147aec22e4a14af7",
+ "after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ "ref": "refs/heads/master",
+ "user_id": 1,
+ "user_name": "Dmitriy Zaporozhets",
+ "repository": {
+ "name": "gitlabhq",
+ "url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
+ "description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
+ "homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
+ },
+ "commits": [
+ {
+ "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ "message": "Add simple search to projects in public area",
+ "timestamp": "2013-05-13T18:18:08+00:00",
+ "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "email": "dmitriy.zaporozhets@gmail.com"
+ }
+ }
+ ],
+ "total_commits_count": 1
+ },
+ "target_title": null
+ },
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "closed",
+ "target_id": 840,
+ "target_type": "Issue",
+ "author_id": 1,
+ "data": null,
+ "target_title": "Finish & merge Code search PR",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_username": "root"
+ },
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "commented on",
+ "target_id": 1312,
+ "target_type": "Note",
+ "author_id": 1,
+ "data": null,
+ "target_title": null,
+ "created_at": "2015-12-04T10:33:58.089Z",
+ "note": {
+ "id": 1312,
+ "body": "What an awesome day!",
+ "attachment": null,
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2015-12-04T10:33:56.698Z",
+ "system": false,
+ "noteable_id": 377,
+ "noteable_type": "Issue"
+ },
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_username": "root"
+ }
+]
+```
+
+## List a Project's visible events
+
+>**Note:** This endpoint has been around longer than the others. Documentation was formerly located in the [Projects API pages][projects-api].
+
+Get a list of visible events for a particular project.
+
+```
+GET /:project_id/events
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `action` | string | no | Include only events of a particular [action type][action-types] |
+| `target_type` | string | no | Include only events of a particular [target type][target-types] |
+| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
+| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
+| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
+
+Example request:
+
+```
+curl --header "PRIVATE-TOKEN 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:project_id/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01
+```
+
+Example response:
+
+```json
+[
+ {
+ "title":null,
+ "project_id":1,
+ "action_name":"opened",
+ "target_id":160,
+ "target_type":"Issue",
+ "author_id":25,
+ "data":null,
+ "target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.",
+ "created_at":"2017-02-09T10:43:19.667Z",
+ "author":{
+ "name":"User 3",
+ "username":"user3",
+ "id":25,
+ "state":"active",
+ "avatar_url":"http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80\u0026d=identicon",
+ "web_url":"https://gitlab.example.com/user3"
+ },
+ "author_username":"user3"
+ },
+ {
+ "title":null,
+ "project_id":1,
+ "action_name":"opened",
+ "target_id":159,
+ "target_type":"Issue",
+ "author_id":21,
+ "data":null,
+ "target_title":"Nostrum enim non et sed optio illo deleniti non.",
+ "created_at":"2017-02-09T10:43:19.426Z",
+ "author":{
+ "name":"Test User",
+ "username":"ted",
+ "id":21,
+ "state":"active",
+ "avatar_url":"http://www.gravatar.com/avatar/80fb888c9a48b9a3f87477214acaa63f?s=80\u0026d=identicon",
+ "web_url":"https://gitlab.example.com/ted"
+ },
+ "author_username":"ted"
+ }
+]
+```
+
+[target-types]: #target-types "Target Type parameter"
+[action-types]: #action-types "Action Type parameter"
+[date-formatting]: #date-formatting "Date Formatting guidance"
+[projects-api]: projects.md "Projects API pages"
+[users-api]: users.md "Users API pages"
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 70cad8a6025..0debdcfae89 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -310,143 +310,7 @@ GET /projects/:id/users
### Get project events
-Get the events for the specified project sorted from newest to oldest. This
-endpoint can be accessed without authentication if the project is publicly
-accessible.
-
-```
-GET /projects/:id/events
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
-
-```json
-[
- {
- "title": null,
- "project_id": 15,
- "action_name": "closed",
- "target_id": 830,
- "target_type": "Issue",
- "author_id": 1,
- "data": null,
- "target_title": "Public project search field",
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "author_username": "root"
- },
- {
- "title": null,
- "project_id": 15,
- "action_name": "opened",
- "target_id": null,
- "target_type": null,
- "author_id": 1,
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "author_username": "john",
- "data": {
- "before": "50d4420237a9de7be1304607147aec22e4a14af7",
- "after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
- "ref": "refs/heads/master",
- "user_id": 1,
- "user_name": "Dmitriy Zaporozhets",
- "repository": {
- "name": "gitlabhq",
- "url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
- "description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
- "homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
- },
- "commits": [
- {
- "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
- "message": "Add simple search to projects in public area",
- "timestamp": "2013-05-13T18:18:08+00:00",
- "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
- "author": {
- "name": "Dmitriy Zaporozhets",
- "email": "dmitriy.zaporozhets@gmail.com"
- }
- }
- ],
- "total_commits_count": 1
- },
- "target_title": null
- },
- {
- "title": null,
- "project_id": 15,
- "action_name": "closed",
- "target_id": 840,
- "target_type": "Issue",
- "author_id": 1,
- "data": null,
- "target_title": "Finish & merge Code search PR",
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "author_username": "root"
- },
- {
- "title": null,
- "project_id": 15,
- "action_name": "commented on",
- "target_id": 1312,
- "target_type": "Note",
- "author_id": 1,
- "data": null,
- "target_title": null,
- "created_at": "2015-12-04T10:33:58.089Z",
- "note": {
- "id": 1312,
- "body": "What an awesome day!",
- "attachment": null,
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "created_at": "2015-12-04T10:33:56.698Z",
- "system": false,
- "noteable_id": 377,
- "noteable_type": "Issue"
- },
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "author_username": "root"
- }
-]
-```
+Please refer to the [Events API documentation](events.md#list-a-projects-visible-events)
### Create project
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index 0b5782a8cc4..18ceb8f779e 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -111,6 +111,7 @@ Parameters:
- `author_name` (optional) - Specify the commit author's name
- `content` (required) - New file content
- `commit_message` (required) - Commit message
+- `last_commit_id` (optional) - Last known file commit id
If the commit fails for any reason we return a 400 error with a non-specific
error message. Possible causes for a failed commit include:
diff --git a/doc/api/users.md b/doc/api/users.md
index 7e118dcf4a9..f4167ba2605 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -701,147 +701,8 @@ Will return `201 OK` on success, `404 User Not Found` is user cannot be found or
### Get user contribution events
-Get the contribution events for the specified user, sorted from newest to oldest.
+Please refer to the [Events API documentation](events.md#get-user-contribution-events)
-```
-GET /users/:id/events
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the user |
-
-```bash
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/:id/events
-```
-
-Example response:
-
-```json
-[
- {
- "title": null,
- "project_id": 15,
- "action_name": "closed",
- "target_id": 830,
- "target_type": "Issue",
- "author_id": 1,
- "data": null,
- "target_title": "Public project search field",
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "author_username": "root"
- },
- {
- "title": null,
- "project_id": 15,
- "action_name": "opened",
- "target_id": null,
- "target_type": null,
- "author_id": 1,
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "author_username": "john",
- "data": {
- "before": "50d4420237a9de7be1304607147aec22e4a14af7",
- "after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
- "ref": "refs/heads/master",
- "user_id": 1,
- "user_name": "Dmitriy Zaporozhets",
- "repository": {
- "name": "gitlabhq",
- "url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
- "description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
- "homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
- },
- "commits": [
- {
- "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
- "message": "Add simple search to projects in public area",
- "timestamp": "2013-05-13T18:18:08+00:00",
- "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
- "author": {
- "name": "Dmitriy Zaporozhets",
- "email": "dmitriy.zaporozhets@gmail.com"
- }
- }
- ],
- "total_commits_count": 1
- },
- "target_title": null
- },
- {
- "title": null,
- "project_id": 15,
- "action_name": "closed",
- "target_id": 840,
- "target_type": "Issue",
- "author_id": 1,
- "data": null,
- "target_title": "Finish & merge Code search PR",
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "author_username": "root"
- },
- {
- "title": null,
- "project_id": 15,
- "action_name": "commented on",
- "target_id": 1312,
- "target_type": "Note",
- "author_id": 1,
- "data": null,
- "target_title": null,
- "created_at": "2015-12-04T10:33:58.089Z",
- "note": {
- "id": 1312,
- "body": "What an awesome day!",
- "attachment": null,
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "created_at": "2015-12-04T10:33:56.698Z",
- "system": false,
- "noteable_id": 377,
- "noteable_type": "Issue"
- },
- "author": {
- "name": "Dmitriy Zaporozhets",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
- "web_url": "http://localhost:3000/root"
- },
- "author_username": "root"
- }
-]
-```
## Get all impersonation tokens of a user
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 56ff245f9f9..76ba78ea7be 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -120,7 +120,7 @@ The YAML-defined variables are also set to all created
tune them.
Variables can be defined at a global level, but also at a job level. To turn off
-global defined variables in your job, define an empty array:
+global defined variables in your job, define an empty hash:
```yaml
job_name:
@@ -345,20 +345,45 @@ All variables are set as environment variables in the build environment, and
they are accessible with normal methods that are used to access such variables.
In most cases `bash` or `sh` is used to execute the job script.
-To access the variables (predefined and user-defined) in a `bash`/`sh` environment,
-prefix the variable name with the dollar sign (`$`):
+To access environment variables, use the syntax for your Runner's [shell][shellexecutors].
-```
+| Shell | Usage |
+|----------------------|-----------------|
+| bash/sh | `$variable` |
+| windows batch | `%variable%` |
+| PowerShell | `$env:variable` |
+
+To access environment variables in bash, prefix the variable name with (`$`):
+
+```yaml
job_name:
script:
- echo $CI_JOB_ID
```
+To access environment variables in **Windows Batch**, surround the variable
+with (`%`):
+
+```yaml
+job_name:
+ script:
+ - echo %CI_JOB_ID%
+```
+
+To access environment variables in a **Windows PowerShell** environment, prefix
+the variable name with (`$env:`):
+
+```yaml
+job_name:
+ script:
+ - echo $env:CI_JOB_ID
+```
+
You can also list all environment variables with the `export` command,
but be aware that this will also expose the values of all the secret variables
you set, in the job log:
-```
+```yaml
job_name:
script:
- export
@@ -405,3 +430,4 @@ export CI_REGISTRY_PASSWORD="longalfanumstring"
[triggers]: ../triggers/README.md#pass-job-variables-to-a-trigger
[protected branches]: ../../user/project/protected_branches.md
[protected tags]: ../../user/project/protected_tags.md
+[shellexecutors]: https://docs.gitlab.com/runner/executors/
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 2c9aa437932..fc813694ff2 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -297,6 +297,15 @@ cache:
untracked: true
```
+If you use **Windows PowerShell** to run your shell scripts you need to replace
+`$` with `$env:`:
+
+```yaml
+cache:
+ key: "$env:CI_JOB_STAGE/$env:CI_COMMIT_REF_NAME"
+ untracked: true
+```
+
## Jobs
`.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job
@@ -434,7 +443,7 @@ but allows you to define job-specific variables.
When the `variables` keyword is used on a job level, it overrides the global YAML
job variables and predefined ones. To turn off global defined variables
-in your job, define an empty array:
+in your job, define an empty hash:
```yaml
job_name:
@@ -909,6 +918,16 @@ job:
untracked: true
```
+If you use **Windows PowerShell** to run your shell scripts you need to replace
+`$` with `$env:`:
+
+```yaml
+job:
+ artifacts:
+ name: "$env:CI_JOB_STAGE_$env:CI_COMMIT_REF_NAME"
+ untracked: true
+```
+
#### artifacts:when
> Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 7ae2f3cad40..88f91c07194 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -94,6 +94,7 @@ module API
mount ::API::DeployKeys
mount ::API::Deployments
mount ::API::Environments
+ mount ::API::Events
mount ::API::Features
mount ::API::Files
mount ::API::Groups
diff --git a/lib/api/events.rb b/lib/api/events.rb
new file mode 100644
index 00000000000..dabdf579119
--- /dev/null
+++ b/lib/api/events.rb
@@ -0,0 +1,86 @@
+module API
+ class Events < Grape::API
+ include PaginationParams
+
+ helpers do
+ params :event_filter_params do
+ optional :action, type: String, values: Event.actions, desc: 'Event action to filter on'
+ optional :target_type, type: String, values: Event.target_types, desc: 'Event target type to filter on'
+ optional :before, type: Date, desc: 'Include only events created before this date'
+ optional :after, type: Date, desc: 'Include only events created after this date'
+ end
+
+ params :sort_params do
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return events sorted in ascending and descending order'
+ end
+
+ def present_events(events)
+ events = events.reorder(created_at: params[:sort])
+
+ present paginate(events), with: Entities::Event
+ end
+ end
+
+ resource :events do
+ desc "List currently authenticated user's events" do
+ detail 'This feature was introduced in GitLab 9.3.'
+ success Entities::Event
+ end
+ params do
+ use :pagination
+ use :event_filter_params
+ use :sort_params
+ end
+ get do
+ authenticate!
+
+ events = EventsFinder.new(params.merge(source: current_user, current_user: current_user)).execute.preload(:author, :target)
+
+ present_events(events)
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID or Username of the user'
+ end
+ resource :users do
+ desc 'Get the contribution events of a specified user' do
+ detail 'This feature was introduced in GitLab 8.13.'
+ success Entities::Event
+ end
+ params do
+ use :pagination
+ use :event_filter_params
+ use :sort_params
+ end
+ get ':id/events' do
+ user = find_user(params[:id])
+ not_found!('User') unless user
+
+ events = EventsFinder.new(params.merge(source: user, current_user: current_user)).execute.preload(:author, :target)
+
+ present_events(events)
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc "List a Project's visible events" do
+ success Entities::Event
+ end
+ params do
+ use :pagination
+ use :event_filter_params
+ use :sort_params
+ end
+ get ":id/events" do
+ events = EventsFinder.new(params.merge(source: user_project, current_user: current_user)).execute.preload(:author, :target)
+
+ present_events(events)
+ end
+ end
+ end
+end
diff --git a/lib/api/files.rb b/lib/api/files.rb
index e6ea12c5ab7..25b0968a271 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -10,7 +10,8 @@ module API
file_content: attrs[:content],
file_content_encoding: attrs[:encoding],
author_email: attrs[:author_email],
- author_name: attrs[:author_name]
+ author_name: attrs[:author_name],
+ last_commit_sha: attrs[:last_commit_id]
}
end
@@ -46,6 +47,7 @@ module API
use :simple_file_params
requires :content, type: String, desc: 'File content'
optional :encoding, type: String, values: %w[base64], desc: 'File encoding'
+ optional :last_commit_id, type: String, desc: 'Last known commit id for this file'
end
end
@@ -111,7 +113,12 @@ module API
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
- result = ::Files::UpdateService.new(user_project, current_user, commit_params(file_params)).execute
+
+ begin
+ result = ::Files::UpdateService.new(user_project, current_user, commit_params(file_params)).execute
+ rescue ::Files::UpdateService::FileChangedError => e
+ render_api_error!(e.message, 400)
+ end
if result[:status] == :success
status(200)
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index deac3934d57..56046742e08 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -167,16 +167,6 @@ module API
user_can_admin_project: can?(current_user, :admin_project, user_project), statistics: params[:statistics]
end
- desc 'Get events for a single project' do
- success Entities::Event
- end
- params do
- use :pagination
- end
- get ":id/events" do
- present paginate(user_project.events.recent), with: Entities::Event
- end
-
desc 'Fork new project for the current user or provided namespace.' do
success Entities::Project
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index e8694e90cf2..3f87a403a09 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -328,27 +328,6 @@ module API
end
end
- desc 'Get the contribution events of a specified user' do
- detail 'This feature was introduced in GitLab 8.13.'
- success Entities::Event
- end
- params do
- requires :id, type: Integer, desc: 'The ID of the user'
- use :pagination
- end
- get ':id/events' do
- user = User.find_by(id: params[:id])
- not_found!('User') unless user
-
- events = user.events.
- merge(ProjectsFinder.new(current_user: current_user).execute).
- references(:project).
- with_associations.
- recent
-
- present paginate(events), with: Entities::Event
- end
-
params do
requires :user_id, type: Integer, desc: 'The ID of the user'
end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 31345403702..613b1edba36 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -31,7 +31,7 @@ feature 'Environments page', :feature, :js do
it 'should show one environment' do
visit namespace_project_environments_path(project.namespace, project, scope: 'available')
expect(page).to have_css('.environments-container')
- expect(page.all('tbody > tr').length).to eq(1)
+ expect(page.all('.environment-name').length).to eq(1)
end
end
@@ -59,7 +59,7 @@ feature 'Environments page', :feature, :js do
it 'should show one environment' do
visit namespace_project_environments_path(project.namespace, project, scope: 'stopped')
expect(page).to have_css('.environments-container')
- expect(page.all('tbody > tr').length).to eq(1)
+ expect(page.all('.environment-name').length).to eq(1)
end
end
end
diff --git a/spec/finders/events_finder_spec.rb b/spec/finders/events_finder_spec.rb
new file mode 100644
index 00000000000..30a2bd14f10
--- /dev/null
+++ b/spec/finders/events_finder_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe EventsFinder do
+ let(:user) { create(:user) }
+ let(:other_user) { create(:user) }
+ let(:project1) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) }
+ let(:project2) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) }
+ let(:closed_issue) { create(:closed_issue, project: project1, author: user) }
+ let(:opened_merge_request) { create(:merge_request, source_project: project2, author: user) }
+ let!(:closed_issue_event) { create(:event, project: project1, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 12, 30)) }
+ let!(:opened_merge_request_event) { create(:event, project: project2, author: user, target: opened_merge_request, action: Event::CREATED, created_at: Date.new(2017, 1, 31)) }
+ let(:closed_issue2) { create(:closed_issue, project: project1, author: user) }
+ let(:opened_merge_request2) { create(:merge_request, source_project: project2, author: user) }
+ let!(:closed_issue_event2) { create(:event, project: project1, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 2, 2)) }
+ let!(:opened_merge_request_event2) { create(:event, project: project2, author: user, target: opened_merge_request, action: Event::CREATED, created_at: Date.new(2017, 2, 2)) }
+
+ context 'when targeting a user' do
+ it 'returns events between specified dates filtered on action and type' do
+ events = described_class.new(source: user, current_user: user, action: 'created', target_type: 'merge_request', after: Date.new(2017, 1, 1), before: Date.new(2017, 2, 1)).execute
+
+ expect(events).to eq([opened_merge_request_event])
+ end
+
+ it 'does not return events the current_user does not have access to' do
+ events = described_class.new(source: user, current_user: other_user).execute
+
+ expect(events).not_to include(opened_merge_request_event)
+ end
+ end
+
+ context 'when targeting a project' do
+ it 'returns project events between specified dates filtered on action and type' do
+ events = described_class.new(source: project1, current_user: user, action: 'closed', target_type: 'issue', after: Date.new(2016, 12, 1), before: Date.new(2017, 1, 1)).execute
+
+ expect(events).to eq([closed_issue_event])
+ end
+
+ it 'does not return events the current_user does not have access to' do
+ events = described_class.new(source: project2, current_user: other_user).execute
+
+ expect(events).to be_empty
+ end
+ end
+end
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index 398c593eec2..ebfd60198b2 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -71,7 +71,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
it('should render a table with the received pipelines', (done) => {
setTimeout(() => {
- expect(this.component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1);
+ expect(this.component.$el.querySelectorAll('.ci-table .commit').length).toEqual(1);
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
expect(this.component.$el.querySelector('.empty-state')).toBe(null);
expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBe(null);
@@ -108,7 +108,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
expect(this.component.$el.querySelector('.js-empty-state')).toBe(null);
- expect(this.component.$el.querySelector('table')).toBe(null);
+ expect(this.component.$el.querySelector('.ci-table')).toBe(null);
done();
}, 0);
});
diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environment_spec.js
index c31642ac788..6639a6b5e7b 100644
--- a/spec/javascripts/environments/environment_spec.js
+++ b/spec/javascripts/environments/environment_spec.js
@@ -271,7 +271,7 @@ describe('Environment', () => {
// wait for next async request
setTimeout(() => {
expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1);
- expect(component.$el.querySelector('td.text-center > a.btn').textContent).toContain('Show all');
+ expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain('Show all');
Vue.http.interceptors = _.without(Vue.http.interceptors, folderInterceptor);
done();
diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js
index effbc6c3ee1..2862971bec4 100644
--- a/spec/javascripts/environments/environment_table_spec.js
+++ b/spec/javascripts/environments/environment_table_spec.js
@@ -29,6 +29,6 @@ describe('Environment item', () => {
},
}).$mount();
- expect(component.$el.tagName).toEqual('TABLE');
+ expect(component.$el.getAttribute('class')).toContain('ci-table');
});
});
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index c6c45d78990..f9d060d4e0e 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -6,7 +6,7 @@ describe PagesDomain, models: true do
end
describe 'validate domain' do
- subject { build(:pages_domain, domain: domain) }
+ subject(:pages_domain) { build(:pages_domain, domain: domain) }
context 'is unique' do
let(:domain) { 'my.domain.com' }
@@ -14,36 +14,25 @@ describe PagesDomain, models: true do
it { is_expected.to validate_uniqueness_of(:domain) }
end
- context 'valid domain' do
- let(:domain) { 'my.domain.com' }
-
- it { is_expected.to be_valid }
- end
-
- context 'valid hexadecimal-looking domain' do
- let(:domain) { '0x12345.com'}
-
- it { is_expected.to be_valid }
- end
-
- context 'no domain' do
- let(:domain) { nil }
-
- it { is_expected.not_to be_valid }
- end
-
- context 'invalid domain' do
- let(:domain) { '0123123' }
-
- it { is_expected.not_to be_valid }
- end
-
- context 'domain from .example.com' do
- let(:domain) { 'my.domain.com' }
-
- before { allow(Settings.pages).to receive(:host).and_return('domain.com') }
-
- it { is_expected.not_to be_valid }
+ {
+ 'my.domain.com' => true,
+ '123.456.789' => true,
+ '0x12345.com' => true,
+ '0123123' => true,
+ '_foo.com' => false,
+ 'reserved.com' => false,
+ 'a.reserved.com' => false,
+ nil => false
+ }.each do |value, validity|
+ context "domain #{value.inspect} validity" do
+ before do
+ allow(Settings.pages).to receive(:host).and_return('reserved.com')
+ end
+
+ let(:domain) { value }
+
+ it { expect(pages_domain.valid?).to eq(validity) }
+ end
end
end
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
new file mode 100644
index 00000000000..a19870a95e8
--- /dev/null
+++ b/spec/requests/api/events_spec.rb
@@ -0,0 +1,142 @@
+require 'spec_helper'
+
+describe API::Events, api: true do
+ include ApiHelpers
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:other_user) { create(:user, username: 'otheruser') }
+ let(:private_project) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) }
+ let(:closed_issue) { create(:closed_issue, project: private_project, author: user) }
+ let!(:closed_issue_event) { create(:event, project: private_project, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 12, 30)) }
+
+ describe 'GET /events' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api('/events')
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns users events' do
+ get api('/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31', user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ end
+ end
+ end
+
+ describe 'GET /users/:id/events' do
+ context "as a user that cannot see the event's project" do
+ it 'returns no events' do
+ get api("/users/#{user.id}/events", other_user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_empty
+ end
+ end
+
+ context "as a user that can see the event's project" do
+ it 'accepts a username' do
+ get api("/users/#{user.username}/events", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ end
+
+ it 'returns the events' do
+ get api("/users/#{user.id}/events", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ end
+
+ context 'when there are multiple events from different projects' do
+ let(:second_note) { create(:note_on_issue, project: create(:empty_project)) }
+
+ before do
+ second_note.project.add_user(user, :developer)
+
+ [second_note].each do |note|
+ EventCreateService.new.leave_note(note, user)
+ end
+ end
+
+ it 'returns events in the correct order (from newest to oldest)' do
+ get api("/users/#{user.id}/events", user)
+
+ comment_events = json_response.select { |e| e['action_name'] == 'commented on' }
+ close_events = json_response.select { |e| e['action_name'] == 'closed' }
+
+ expect(comment_events[0]['target_id']).to eq(second_note.id)
+ expect(close_events[0]['target_id']).to eq(closed_issue.id)
+ end
+
+ it 'accepts filter parameters' do
+ get api("/users/#{user.id}/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31", user)
+
+ expect(json_response.size).to eq(1)
+ expect(json_response[0]['target_id']).to eq(closed_issue.id)
+ end
+ end
+ end
+
+ it 'returns a 404 error if not found' do
+ get api('/users/42/events', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+ end
+
+ describe 'GET /projects/:id/events' do
+ context 'when unauthenticated ' do
+ it 'returns 404 for private project' do
+ get api("/projects/#{private_project.id}/events")
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 200 status for a public project' do
+ public_project = create(:empty_project, :public)
+
+ get api("/projects/#{public_project.id}/events")
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ context 'when not permitted to read' do
+ it 'returns 404' do
+ get api("/projects/#{private_project.id}/events", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns project events' do
+ get api("/projects/#{private_project.id}/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31", user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ end
+
+ it 'returns 404 if project does not exist' do
+ get api("/projects/1234/events", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index deb2cac6869..d325c6eff9d 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -258,6 +258,25 @@ describe API::Files do
expect(last_commit.author_name).to eq(user.name)
end
+ it "returns a 400 bad request if update existing file with stale last commit id" do
+ params_with_stale_id = valid_params.merge(last_commit_id: 'stale')
+
+ put api(route(file_path), user), params_with_stale_id
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('You are attempting to update a file that has changed since you started editing it.')
+ end
+
+ it "updates existing file in project repo with accepts correct last commit id" do
+ last_commit = Gitlab::Git::Commit
+ .last_for_path(project.repository, 'master', URI.unescape(file_path))
+ params_with_correct_id = valid_params.merge(last_commit_id: last_commit.id)
+
+ put api(route(file_path), user), params_with_correct_id
+
+ expect(response).to have_http_status(200)
+ end
+
it "returns a 400 bad request if no params given" do
put api(route(file_path), user)
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index c0ecb4d2aaa..86c57204971 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -762,64 +762,6 @@ describe API::Projects do
end
end
- describe 'GET /projects/:id/events' do
- shared_examples_for 'project events response' do
- it 'returns the project events' do
- member = create(:user)
- create(:project_member, :developer, user: member, project: project)
- note = create(:note_on_issue, note: 'What an awesome day!', project: project)
- EventCreateService.new.leave_note(note, note.author)
-
- get api("/projects/#{project.id}/events", current_user)
-
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
-
- first_event = json_response.first
- expect(first_event['action_name']).to eq('commented on')
- expect(first_event['note']['body']).to eq('What an awesome day!')
-
- last_event = json_response.last
-
- expect(last_event['action_name']).to eq('joined')
- expect(last_event['project_id'].to_i).to eq(project.id)
- expect(last_event['author_username']).to eq(member.username)
- expect(last_event['author']['name']).to eq(member.name)
- end
- end
-
- context 'when unauthenticated' do
- it_behaves_like 'project events response' do
- let(:project) { create(:empty_project, :public) }
- let(:current_user) { nil }
- end
- end
-
- context 'when authenticated' do
- context 'valid request' do
- it_behaves_like 'project events response' do
- let(:current_user) { user }
- end
- end
-
- it 'returns a 404 error if not found' do
- get api('/projects/42/events', user)
-
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Project Not Found')
- end
-
- it 'returns a 404 error if user is not a member' do
- other_user = create(:user)
-
- get api("/projects/#{project.id}/events", other_user)
-
- expect(response).to have_http_status(404)
- end
- end
- end
-
describe 'GET /projects/:id/users' do
shared_examples_for 'project users response' do
it 'returns the project users' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 1c33b8f9502..4efc3e1a1e2 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -1130,83 +1130,6 @@ describe API::Users do
end
end
- describe 'GET /users/:id/events' do
- let(:user) { create(:user) }
- let(:project) { create(:empty_project) }
- let(:note) { create(:note_on_issue, note: 'What an awesome day!', project: project) }
-
- before do
- project.add_user(user, :developer)
- EventCreateService.new.leave_note(note, user)
- end
-
- context "as a user than cannot see the event's project" do
- it 'returns no events' do
- other_user = create(:user)
-
- get api("/users/#{user.id}/events", other_user)
-
- expect(response).to have_http_status(200)
- expect(json_response).to be_empty
- end
- end
-
- context "as a user than can see the event's project" do
- context 'joined event' do
- it 'returns the "joined" event' do
- get api("/users/#{user.id}/events", user)
-
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
-
- comment_event = json_response.find { |e| e['action_name'] == 'commented on' }
-
- expect(comment_event['project_id'].to_i).to eq(project.id)
- expect(comment_event['author_username']).to eq(user.username)
- expect(comment_event['note']['id']).to eq(note.id)
- expect(comment_event['note']['body']).to eq('What an awesome day!')
-
- joined_event = json_response.find { |e| e['action_name'] == 'joined' }
-
- expect(joined_event['project_id'].to_i).to eq(project.id)
- expect(joined_event['author_username']).to eq(user.username)
- expect(joined_event['author']['name']).to eq(user.name)
- end
- end
-
- context 'when there are multiple events from different projects' do
- let(:second_note) { create(:note_on_issue, project: create(:empty_project)) }
- let(:third_note) { create(:note_on_issue, project: project) }
-
- before do
- second_note.project.add_user(user, :developer)
-
- [second_note, third_note].each do |note|
- EventCreateService.new.leave_note(note, user)
- end
- end
-
- it 'returns events in the correct order (from newest to oldest)' do
- get api("/users/#{user.id}/events", user)
-
- comment_events = json_response.select { |e| e['action_name'] == 'commented on' }
-
- expect(comment_events[0]['target_id']).to eq(third_note.id)
- expect(comment_events[1]['target_id']).to eq(second_note.id)
- expect(comment_events[2]['target_id']).to eq(note.id)
- end
- end
- end
-
- it 'returns a 404 error if not found' do
- get api('/users/42/events', user)
-
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 User Not Found')
- end
- end
-
context "user activities", :redis do
let!(:old_active_user) { create(:user, last_activity_on: Time.utc(2000, 1, 1)) }
let!(:newly_active_user) { create(:user, last_activity_on: 2.days.ago.midday) }