Commit 8ab250ab authored by Valery Sizov's avatar Valery Sizov

Merge branch 'ce_upstream' into 'master'

CE upstream

Closes #1451 and gitlab-ce#25827

See merge request !1000
parents 7e115252 80b54eeb
...@@ -407,7 +407,7 @@ notify:slack: ...@@ -407,7 +407,7 @@ notify:slack:
SETUP_DB: "false" SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false" USE_BUNDLE_INSTALL: "false"
script: script:
- ./scripts/notify_slack.sh "#development" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>" - ./scripts/notify_slack.sh "#development" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/pipelines>"
when: on_failure when: on_failure
only: only:
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
......
...@@ -28,7 +28,7 @@ entry. ...@@ -28,7 +28,7 @@ entry.
- Fix Pipeline builds list blank on MR. !8255 - Fix Pipeline builds list blank on MR. !8255
- Do not show retried builds in pipeline stage dropdown. !8260 - Do not show retried builds in pipeline stage dropdown. !8260
## 8.15.0 (2017-01-22) ## 8.15.0 (2016-12-22)
- Whitelist next project names: notes, services. - Whitelist next project names: notes, services.
- Use Grape's new Route methods. - Use Grape's new Route methods.
......
...@@ -217,8 +217,8 @@ We welcome merge requests with fixes and improvements to GitLab code, tests, ...@@ -217,8 +217,8 @@ We welcome merge requests with fixes and improvements to GitLab code, tests,
and/or documentation. The features we would really like a merge request for are and/or documentation. The features we would really like a merge request for are
listed with the label [`Accepting Merge Requests` on our issue tracker for CE][accepting-mrs-ce] listed with the label [`Accepting Merge Requests` on our issue tracker for CE][accepting-mrs-ce]
and [EE][accepting-mrs-ee] but other improvements are also welcome. Please note and [EE][accepting-mrs-ee] but other improvements are also welcome. Please note
that if an issue is marked for the current milestone either before or while you that if an issue is marked for the current milestone either before or while you
are working on it, a team member may take over the merge request in order to are working on it, a team member may take over the merge request in order to
ensure the work is finished before the release date. ensure the work is finished before the release date.
If you want to add a new feature that is not labeled it is best to first create If you want to add a new feature that is not labeled it is best to first create
...@@ -300,6 +300,7 @@ you start with a very simple UI? Can you do part of the refactor? The increased ...@@ -300,6 +300,7 @@ you start with a very simple UI? Can you do part of the refactor? The increased
reviewability of small MRs that leads to higher code quality is more important reviewability of small MRs that leads to higher code quality is more important
to us than having a minimal commit log. The smaller an MR is the more likely it to us than having a minimal commit log. The smaller an MR is the more likely it
is it will be merged (quickly). After that you can send more MRs to enhance it. is it will be merged (quickly). After that you can send more MRs to enhance it.
The ['How to get faster PR reviews' document of Kubernetes](https://github.com/kubernetes/community/blob/master/contributors/devel/faster_reviews.md) also has some great points regarding this.
For examples of feedback on merge requests please look at already For examples of feedback on merge requests please look at already
[closed merge requests][closed-merge-requests]. If you would like quick feedback [closed merge requests][closed-merge-requests]. If you would like quick feedback
......
...@@ -45,10 +45,10 @@ To see how GitLab looks please see the [features page on our website](https://ab ...@@ -45,10 +45,10 @@ To see how GitLab looks please see the [features page on our website](https://ab
- Manage Git repositories with fine grained access controls that keep your code secure - Manage Git repositories with fine grained access controls that keep your code secure
- Perform code reviews and enhance collaboration with merge requests - Perform code reviews and enhance collaboration with merge requests
- Each project can also have an issue tracker and a wiki - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
- Each project can also have an issue tracker, issue board, and a wiki
- Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
- Completely free and open source (MIT Expat license) - Completely free and open source (MIT Expat license)
- Powered by [Ruby on Rails](https://github.com/rails/rails)
## Hiring ## Hiring
...@@ -104,11 +104,11 @@ Instructions on how to start GitLab and how to run the tests can be found in the ...@@ -104,11 +104,11 @@ Instructions on how to start GitLab and how to run the tests can be found in the
GitLab is a Ruby on Rails application that runs on the following software: GitLab is a Ruby on Rails application that runs on the following software:
- Ubuntu/Debian/CentOS/RHEL - Ubuntu/Debian/CentOS/RHEL/OpenSUSE
- Ruby (MRI) 2.3 - Ruby (MRI) 2.3
- Git 2.8.4+ - Git 2.8.4+
- Redis 2.8+ - Redis 2.8+
- MySQL or PostgreSQL - PostgreSQL (preferred) or MySQL
For more information please see the [architecture documentation](https://docs.gitlab.com/ce/development/architecture.html). For more information please see the [architecture documentation](https://docs.gitlab.com/ce/development/architecture.html).
......
...@@ -92,8 +92,8 @@ ...@@ -92,8 +92,8 @@
success: function(buildData) { success: function(buildData) {
$('.js-build-output').html(buildData.trace_html); $('.js-build-output').html(buildData.trace_html);
if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
this.initScrollMonitor(); this.$buildRefreshAnimation.remove();
return this.$buildRefreshAnimation.remove(); return this.initScrollMonitor();
} }
}.bind(this) }.bind(this)
}); });
......
...@@ -367,7 +367,7 @@ ...@@ -367,7 +367,7 @@
return $input.trigger('keyup'); return $input.trigger('keyup');
}, },
isLoading(data) { isLoading(data) {
if (!data) return false; if (!data || !data.length) return false;
if (Array.isArray(data)) data = data[0]; if (Array.isArray(data)) data = data[0];
return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0]; return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0];
}, },
......
...@@ -139,15 +139,12 @@ ...@@ -139,15 +139,12 @@
return; return;
} }
return $.getJSON($container.data('path')).error(function() { return $.getJSON($container.data('path')).error(function() {
$container.find('.checking').hide();
$container.find('.unavailable').show(); $container.find('.unavailable').show();
return new Flash('Failed to check if a new branch can be created.', 'alert'); return new Flash('Failed to check if a new branch can be created.', 'alert');
}).success(function(data) { }).success(function(data) {
if (data.can_create_branch) { if (data.can_create_branch) {
$container.find('.checking').hide();
$container.find('.available').show(); $container.find('.available').show();
} else { } else {
$container.find('.checking').hide();
return $container.find('.unavailable').show(); return $container.find('.unavailable').show();
} }
}); });
......
...@@ -245,7 +245,6 @@ ul.content-list { ...@@ -245,7 +245,6 @@ ul.content-list {
} }
ul.controls { ul.controls {
padding-top: 1px;
float: right; float: right;
list-style: none; list-style: none;
......
...@@ -23,12 +23,12 @@ ...@@ -23,12 +23,12 @@
} }
.stage-header { .stage-header {
width: 28%; width: 26%;
padding-left: $gl-padding; padding-left: $gl-padding;
} }
.median-header { .median-header {
width: 12%; width: 14%;
} }
.event-header { .event-header {
...@@ -141,7 +141,7 @@ ...@@ -141,7 +141,7 @@
.dismiss-icon { .dismiss-icon {
position: absolute; position: absolute;
right: $cycle-analytics-dismiss-icon-color; right: $cycle-analytics-box-padding;
cursor: pointer; cursor: pointer;
color: $cycle-analytics-dismiss-icon-color; color: $cycle-analytics-dismiss-icon-color;
} }
...@@ -215,7 +215,6 @@ ...@@ -215,7 +215,6 @@
border-bottom: 1px solid transparent; border-bottom: 1px solid transparent;
border-right: 1px solid $border-color; border-right: 1px solid $border-color;
background-color: $gray-light; background-color: $gray-light;
cursor: default;
&.active { &.active {
background-color: transparent; background-color: transparent;
...@@ -247,11 +246,11 @@ ...@@ -247,11 +246,11 @@
float: left; float: left;
&.stage-name { &.stage-name {
width: 70%; width: 65%;
} }
&.stage-median { &.stage-median {
width: 30%; width: 35%;
} }
} }
......
...@@ -109,7 +109,7 @@ ...@@ -109,7 +109,7 @@
margin: auto; margin: auto;
margin-top: 0; margin-top: 0;
text-align: center; text-align: center;
font-size: 13px; font-size: 12px;
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
// On smaller devices the warning becomes the fourth item in the list, // On smaller devices the warning becomes the fourth item in the list,
......
...@@ -43,7 +43,7 @@ ul.notes { ...@@ -43,7 +43,7 @@ ul.notes {
} }
.system-note-message { .system-note-message {
display: inline; display: inline-block;
&::first-letter { &::first-letter {
text-transform: lowercase; text-transform: lowercase;
...@@ -55,7 +55,7 @@ ul.notes { ...@@ -55,7 +55,7 @@ ul.notes {
} }
p { p {
display: inline; display: inline-block;
margin: 0; margin: 0;
&::first-letter { &::first-letter {
...@@ -151,10 +151,6 @@ ul.notes { ...@@ -151,10 +151,6 @@ ul.notes {
} }
} }
} }
.note-headline-light {
display: inline;
}
} }
.discussion-body { .discussion-body {
...@@ -452,11 +448,6 @@ ul.notes { ...@@ -452,11 +448,6 @@ ul.notes {
border-radius: $border-radius-base; border-radius: $border-radius-base;
} }
.diff-file .note .note-actions {
right: 0;
top: 0;
}
/** /**
* Line note button on the side of diffs * Line note button on the side of diffs
...@@ -590,3 +581,19 @@ ul.notes { ...@@ -590,3 +581,19 @@ ul.notes {
} }
} }
} }
// Merge request notes in diffs
.diff-file {
// Diff is side by side
.notes_content.parallel .note-header .note-headline-light {
display: block;
position: relative;
}
// Diff is inline
.notes_content .note-header .note-headline-light {
display: inline-block;
position: relative;
}
}
...@@ -201,7 +201,7 @@ ...@@ -201,7 +201,7 @@
width: 8px; width: 8px;
position: absolute; position: absolute;
right: -7px; right: -7px;
bottom: 10px; top: 10px;
border-bottom: 2px solid $border-color; border-bottom: 2px solid $border-color;
} }
} }
...@@ -335,7 +335,6 @@ ...@@ -335,7 +335,6 @@
width: 100%; width: 100%;
background-color: $gray-light; background-color: $gray-light;
padding: $gl-padding; padding: $gl-padding;
overflow: auto;
white-space: nowrap; white-space: nowrap;
transition: max-height 0.3s, padding 0.3s; transition: max-height 0.3s, padding 0.3s;
...@@ -621,14 +620,14 @@ ...@@ -621,14 +620,14 @@
} }
.dropdown-counter-badge { .dropdown-counter-badge {
float: right;
color: $border-color; color: $border-color;
font-weight: 100; font-weight: 100;
font-size: 15px; font-size: 15px;
margin-right: 2px; position: absolute;
right: 5px;
top: 8px;
} }
.grouped-pipeline-dropdown { .grouped-pipeline-dropdown {
padding: 0; padding: 0;
width: 191px; width: 191px;
...@@ -784,11 +783,72 @@ ...@@ -784,11 +783,72 @@
.mini-pipeline-graph { .mini-pipeline-graph {
.builds-dropdown { .builds-dropdown {
background-color: transparent; background-color: transparent;
border: none;
padding: 0; padding: 0;
color: $gl-text-color-light; color: $gl-text-color-light;
border: none; border: none;
margin: 0; margin: 0;
&:focus,
&:hover {
outline: none;
margin-right: -8px;
.ci-status-icon {
width: 32px;
padding: 0 8px 0 0;
transition: width 0.1s cubic-bezier(0.25, 0, 1, 1);
+ .dropdown-caret {
visibility: visible;
opacity: 1;
}
}
}
&:focus,
&:active {
.ci-status-icon-success {
background-color: rgba($gl-success, .1);
}
.ci-status-icon-failed {
background-color: rgba($gl-danger, .1);
}
.ci-status-icon-pending,
.ci-status-icon-success_with_warnings {
background-color: rgba($gl-warning, .1);
}
.ci-status-icon-running {
background-color: rgba($blue-normal, .1);
}
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found {
background-color: rgba($gl-gray, .1);
}
.ci-status-icon-created,
.ci-status-icon-skipped {
background-color: rgba($gray-darkest, .1);
}
}
.mini-pipeline-graph-icon-container {
.dropdown-caret {
font-size: 11px;
position: absolute;
top: 6px;
left: 20px;
margin-right: -6px;
z-index: 2;
visibility: hidden;
opacity: 0;
transition: visibility 0.1s, opacity 0.1s linear;
}
}
} }
.dropdown-build .build-content { .dropdown-build .build-content {
...@@ -849,7 +909,7 @@ ...@@ -849,7 +909,7 @@
height: 22px; height: 22px;
position: relative; position: relative;
z-index: 2; z-index: 2;
transition: all 0.2s cubic-bezier(0.25, 0, 1, 1); transition: all 0.1s cubic-bezier(0.25, 0, 1, 1);
svg { svg {
top: -1px; top: -1px;
...@@ -862,75 +922,6 @@ ...@@ -862,75 +922,6 @@
height: 22px; height: 22px;
} }
.builds-dropdown {
&:focus {
outline: none;
margin-right: -8px;
.ci-status-icon {
width: 32px;
padding: 0 8px 0 0;
transition: width 0.2s cubic-bezier(0.25, 0, 1, 1);
+ .dropdown-caret {
display: inline-block;
}
}
}
&:focus,
&:active {
.ci-status-icon-success {
background-color: rgba($gl-success, .1);
}
.ci-status-icon-failed {
background-color: rgba($gl-danger, .1);
}
.ci-status-icon-pending,
.ci-status-icon-success_with_warnings {
background-color: rgba($gl-warning, .1);
}
.ci-status-icon-running {
background-color: rgba($blue-normal, .1);
}
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found {
background-color: rgba($gl-gray, .1);
}
.ci-status-icon-created,
.ci-status-icon-skipped {
background-color: rgba($gray-darkest, .1);
}
}
.mini-pipeline-graph-icon-container {
.ci-status-icon:hover,
.ci-status-icon:focus {
width: 32px;
padding: 0 8px 0 0;
+ .dropdown-caret {
display: inline-block;
}
}
.dropdown-caret {
font-size: 11px;
position: relative;
top: 3px;
left: -14px;
margin-right: -6px;
display: none;
z-index: 2;
}
}
}
.terminal-icon { .terminal-icon {
margin-left: 3px; margin-left: 3px;
......
class Admin::GroupsController < Admin::ApplicationController class Admin::GroupsController < Admin::ApplicationController
before_action :group, only: [:edit, :show, :update, :destroy, :project_update, :members_update] before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update]
def index def index
@groups = Group.all @groups = Group.with_statistics
@groups = @groups.sort(@sort = params[:sort]) @groups = @groups.sort(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present? @groups = @groups.search(params[:name]) if params[:name].present?
@groups = @groups.page(params[:page]) @groups = @groups.page(params[:page])
end end
def show def show
@group = Group.with_statistics.find_by_full_path(params[:id])
@members = @group.members.order("access_level DESC").page(params[:members_page]) @members = @group.members.order("access_level DESC").page(params[:members_page])
@requesters = AccessRequestsFinder.new(@group).execute(current_user) @requesters = AccessRequestsFinder.new(@group).execute(current_user)
@projects = @group.projects.page(params[:projects_page]) @projects = @group.projects.with_statistics.page(params[:projects_page])
end end
def new def new
......
...@@ -3,7 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController ...@@ -3,7 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController
before_action :group, only: [:show, :transfer] before_action :group, only: [:show, :transfer]
def index def index
@projects = Project.all @projects = Project.with_statistics
@projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present? @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = @projects.with_push if params[:with_push].present? @projects = @projects.with_push if params[:with_push].present?
......
...@@ -75,7 +75,7 @@ class GroupsController < Groups::ApplicationController ...@@ -75,7 +75,7 @@ class GroupsController < Groups::ApplicationController
end end
def projects def projects
@projects = @group.projects.page(params[:page]) @projects = @group.projects.with_statistics.page(params[:page])
end end
def update def update
......
...@@ -256,15 +256,6 @@ module ProjectsHelper ...@@ -256,15 +256,6 @@ module ProjectsHelper
end end
end end
def repository_size(project = @project)
size_in_bytes = project.repository_and_lfs_size * 1.megabyte
limit_in_bytes = project.actual_size_limit * 1.megabyte
limit_text = limit_in_bytes.zero? ? '' : "/#{number_to_human_size(limit_in_bytes, delimiter: ',', precision: 2)}"
"#{number_to_human_size(size_in_bytes, delimiter: ',', precision: 2)}#{limit_text}"
end
def default_url_to_repo(project = @project) def default_url_to_repo(project = @project)
case default_clone_protocol case default_clone_protocol
when 'krb5' when 'krb5'
...@@ -429,20 +420,6 @@ module ProjectsHelper ...@@ -429,20 +420,6 @@ module ProjectsHelper
[@project.path_with_namespace, sha, "readme"].join('-') [@project.path_with_namespace, sha, "readme"].join('-')
end end
def round_commit_count(project)
count = project.commit_count
if count > 10000
'10000+'
elsif count > 5000
'5000+'
elsif count > 1000
'1000+'
else
count
end
end
def current_ref def current_ref
@ref || @repository.try(:root_ref) @ref || @repository.try(:root_ref)
end end
......
...@@ -11,6 +11,7 @@ module SortingHelper ...@@ -11,6 +11,7 @@ module SortingHelper
sort_value_due_date_soon => sort_title_due_date_soon, sort_value_due_date_soon => sort_title_due_date_soon,
sort_value_due_date_later => sort_title_due_date_later, sort_value_due_date_later => sort_title_due_date_later,
sort_value_largest_repo => sort_title_largest_repo, sort_value_largest_repo => sort_title_largest_repo,
sort_value_largest_group => sort_title_largest_group,
sort_value_recently_signin => sort_title_recently_signin, sort_value_recently_signin => sort_title_recently_signin,
sort_value_oldest_signin => sort_title_oldest_signin, sort_value_oldest_signin => sort_title_oldest_signin,
sort_value_downvotes => sort_title_downvotes, sort_value_downvotes => sort_title_downvotes,
...@@ -94,6 +95,10 @@ module SortingHelper ...@@ -94,6 +95,10 @@ module SortingHelper
'Largest repository' 'Largest repository'
end end
def sort_title_largest_group
'Largest group'
end
def sort_title_recently_signin def sort_title_recently_signin
'Recent sign in' 'Recent sign in'
end end
...@@ -203,7 +208,11 @@ module SortingHelper ...@@ -203,7 +208,11 @@ module SortingHelper
end end
def sort_value_largest_repo def sort_value_largest_repo
'repository_size_desc' 'storage_size_desc'
end
def sort_value_largest_group
'storage_size_desc'
end end
def sort_value_recently_signin def sort_value_recently_signin
......
module StorageHelper
def storage_counter(size_in_bytes)
precision = size_in_bytes < 1.megabyte ? 0 : 1
number_to_human_size(size_in_bytes, delimiter: ',', precision: precision, significant: false)
end
end
...@@ -43,6 +43,8 @@ module Ci ...@@ -43,6 +43,8 @@ module Ci
before_destroy { project } before_destroy { project }
after_create :execute_hooks after_create :execute_hooks
after_save :update_project_statistics, if: :artifacts_size_changed?
after_destroy :update_project_statistics
class << self class << self
def first_pending def first_pending
...@@ -585,5 +587,9 @@ module Ci ...@@ -585,5 +587,9 @@ module Ci
Ci::MaskSecret.mask!(trace, token) Ci::MaskSecret.mask!(trace, token)
trace trace
end end
def update_project_statistics
ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size])
end
end end
end end
...@@ -93,11 +93,8 @@ module Ci ...@@ -93,11 +93,8 @@ module Ci
.select("max(#{quoted_table_name}.id)") .select("max(#{quoted_table_name}.id)")
.group(:ref, :sha) .group(:ref, :sha)
if ref relation = ref ? where(ref: ref) : self
where(id: max_id, ref: ref) relation.where(id: max_id)
else
where(id: max_id)
end
end end
def self.latest_status(ref = nil) def self.latest_status(ref = nil)
...@@ -105,7 +102,7 @@ module Ci ...@@ -105,7 +102,7 @@ module Ci
end end
def self.latest_successful_for(ref) def self.latest_successful_for(ref)
success.latest(ref).first success.latest(ref).order(id: :desc).first
end end
def self.truncate_sha(sha) def self.truncate_sha(sha)
......
...@@ -64,7 +64,13 @@ class Group < Namespace ...@@ -64,7 +64,13 @@ class Group < Namespace
end end
def sort(method) def sort(method)
order_by(method) if method == 'storage_size_desc'
# storage_size is a virtual column so we need to
# pass a string to avoid AR adding the table name
reorder('storage_size DESC, namespaces.id DESC')
else
order_by(method)
end
end end
def reference_prefix def reference_prefix
......
...@@ -5,4 +5,13 @@ class LfsObjectsProject < ActiveRecord::Base ...@@ -5,4 +5,13 @@ class LfsObjectsProject < ActiveRecord::Base
validates :lfs_object_id, presence: true validates :lfs_object_id, presence: true
validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" } validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" }
validates :project_id, presence: true validates :project_id, presence: true
after_create :update_project_statistics
after_destroy :update_project_statistics
private
def update_project_statistics
ProjectCacheWorker.perform_async(project_id, [], [:lfs_objects_size])
end
end end
...@@ -205,7 +205,9 @@ class MergeRequest < ActiveRecord::Base ...@@ -205,7 +205,9 @@ class MergeRequest < ActiveRecord::Base
end end
def diff_size def diff_size
diffs(diff_options).size opts = diff_options || {}
raw_diffs(opts).size
end end
def diff_base_commit def diff_base_commit
......
...@@ -9,6 +9,7 @@ class Namespace < ActiveRecord::Base ...@@ -9,6 +9,7 @@ class Namespace < ActiveRecord::Base
cache_markdown_field :description, pipeline: :description cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy has_many :projects, dependent: :destroy
has_many :project_statistics
belongs_to :owner, class_name: "User" belongs_to :owner, class_name: "User"
belongs_to :parent, class_name: "Namespace" belongs_to :parent, class_name: "Namespace"
...@@ -38,6 +39,18 @@ class Namespace < ActiveRecord::Base ...@@ -38,6 +39,18 @@ class Namespace < ActiveRecord::Base
scope :root, -> { where('type IS NULL') } scope :root, -> { where('type IS NULL') }
scope :with_statistics, -> do
joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id')
.group('namespaces.id')
.select(
'namespaces.*',
'COALESCE(SUM(ps.storage_size), 0) AS storage_size',
'COALESCE(SUM(ps.repository_size), 0) AS repository_size',
'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size',
)
end
class << self class << self
def by_path(path) def by_path(path)
find_by('lower(path) = :value', value: path.downcase) find_by('lower(path) = :value', value: path.downcase)
......
...@@ -43,6 +43,7 @@ class Project < ActiveRecord::Base ...@@ -43,6 +43,7 @@ class Project < ActiveRecord::Base
after_create :ensure_dir_exist after_create :ensure_dir_exist
after_create :create_project_feature, unless: :project_feature after_create :create_project_feature, unless: :project_feature
after_save :ensure_dir_exist, if: :namespace_id_changed? after_save :ensure_dir_exist, if: :namespace_id_changed?
after_save :update_project_statistics, if: :namespace_id_changed?
# set last_activity_at to the same as created_at # set last_activity_at to the same as created_at
after_create :set_last_activity_at after_create :set_last_activity_at
...@@ -152,6 +153,7 @@ class Project < ActiveRecord::Base ...@@ -152,6 +153,7 @@ class Project < ActiveRecord::Base
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_one :project_feature, dependent: :destroy has_one :project_feature, dependent: :destroy
has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
...@@ -237,6 +239,7 @@ class Project < ActiveRecord::Base ...@@ -237,6 +239,7 @@ class Project < ActiveRecord::Base
scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct } scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') } scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
scope :with_statistics, -> { includes(:statistics) }
# "enabled" here means "not disabled". It includes private features! # "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) { scope :with_feature_enabled, ->(feature) {
...@@ -366,8 +369,10 @@ class Project < ActiveRecord::Base ...@@ -366,8 +369,10 @@ class Project < ActiveRecord::Base
end end
def sort(method) def sort(method)
if method == 'repository_size_desc' if method == 'storage_size_desc'
reorder(repository_size: :desc, id: :desc) # storage_size is a joined column so we need to
# pass a string to avoid AR adding the table name
reorder('project_statistics.storage_size DESC, projects.id DESC')
else else
order_by(method) order_by(method)
end end
...@@ -1147,14 +1152,6 @@ class Project < ActiveRecord::Base ...@@ -1147,14 +1152,6 @@ class Project < ActiveRecord::Base
forked? && project == forked_from_project forked? && project == forked_from_project
end end
def update_repository_size
update_attribute(:repository_size, repository.size)
end
def update_commit_count
update_attribute(:commit_count, repository.commit_count)
end
def forks_count def forks_count
forks.count forks.count
end end
...@@ -1510,7 +1507,7 @@ class Project < ActiveRecord::Base ...@@ -1510,7 +1507,7 @@ class Project < ActiveRecord::Base
end end
def repository_and_lfs_size def repository_and_lfs_size
repository_size + lfs_objects.sum(:size).to_i.to_mb statistics.storage_size + statistics.lfs_objects_size
end end
def above_size_limit? def above_size_limit?
...@@ -1586,4 +1583,9 @@ class Project < ActiveRecord::Base ...@@ -1586,4 +1583,9 @@ class Project < ActiveRecord::Base
def full_path_changed? def full_path_changed?
path_changed? || namespace_id_changed? path_changed? || namespace_id_changed?
end end
def update_project_statistics
stats = statistics || build_statistics
stats.update(namespace_id: namespace_id)
end
end end
...@@ -49,11 +49,13 @@ class ChatNotificationService < Service ...@@ -49,11 +49,13 @@ class ChatNotificationService < Service
return false unless message return false unless message
opt = {} channel_name = get_channel_field(object_kind).presence || channel
opt[:channel] = get_channel_field(object_kind).presence || channel || default_channel opts = {}
opt[:username] = username if username opts[:channel] = channel_name if channel_name
notifier = Slack::Notifier.new(webhook, opt) opts[:username] = username if username
notifier = Slack::Notifier.new(webhook, opts)
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback) notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
true true
...@@ -71,7 +73,7 @@ class ChatNotificationService < Service ...@@ -71,7 +73,7 @@ class ChatNotificationService < Service
fields.reject { |field| field[:name].end_with?('channel') } fields.reject { |field| field[:name].end_with?('channel') }
end end
def default_channel def default_channel_placeholder
raise NotImplementedError raise NotImplementedError
end end
...@@ -103,7 +105,7 @@ class ChatNotificationService < Service ...@@ -103,7 +105,7 @@ class ChatNotificationService < Service
def build_event_channels def build_event_channels
supported_events.reduce([]) do |channels, event| supported_events.reduce([]) do |channels, event|
channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel } channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel_placeholder }
end end
end end
......
...@@ -35,7 +35,7 @@ class MattermostService < ChatNotificationService ...@@ -35,7 +35,7 @@ class MattermostService < ChatNotificationService
] ]
end end
def default_channel def default_channel_placeholder
"#town-square" "#town-square"
end end
end end
...@@ -34,7 +34,7 @@ class SlackService < ChatNotificationService ...@@ -34,7 +34,7 @@ class SlackService < ChatNotificationService
] ]
end end
def default_channel def default_channel_placeholder
"#general" "#general"
end end
end end
class ProjectStatistics < ActiveRecord::Base
belongs_to :project
belongs_to :namespace
before_save :update_storage_size
STORAGE_COLUMNS = [:repository_size, :lfs_objects_size, :build_artifacts_size]
STATISTICS_COLUMNS = [:commit_count] + STORAGE_COLUMNS
def total_repository_size
repository_size + lfs_objects_size
end
def refresh!(only: nil)
STATISTICS_COLUMNS.each do |column, generator|
if only.blank? || only.include?(column)
public_send("update_#{column}")
end
end
save!
end
def update_commit_count
self.commit_count = project.repository.commit_count
end
def update_repository_size
self.repository_size = project.repository.size
end
def update_lfs_objects_size
self.lfs_objects_size = project.lfs_objects.sum(:size)
end
def update_build_artifacts_size
self.build_artifacts_size = project.builds.sum(:artifacts_size)
end
def update_storage_size
self.storage_size = STORAGE_COLUMNS.sum(&method(:read_attribute))
end
end
...@@ -3,6 +3,9 @@ class GitPushService < BaseService ...@@ -3,6 +3,9 @@ class GitPushService < BaseService
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
include Gitlab::Access include Gitlab::Access
# The N most recent commits to process in a single push payload.
PROCESS_COMMIT_LIMIT = 100
# This method will be called after each git update # This method will be called after each git update
# and only if the provided user and project are present in GitLab. # and only if the provided user and project are present in GitLab.
# #
...@@ -78,7 +81,17 @@ class GitPushService < BaseService ...@@ -78,7 +81,17 @@ class GitPushService < BaseService
types = [] types = []
end end
ProjectCacheWorker.perform_async(@project.id, types) ProjectCacheWorker.perform_async(@project.id, types, [:commit_count, :repository_size])
end
# Schedules processing of commit messages.
def process_commit_messages
default = is_default_branch?
push_commits.last(PROCESS_COMMIT_LIMIT).each do |commit|
ProcessCommitWorker.
perform_async(project.id, current_user.id, commit.to_hash, default)
end
end end
protected protected
...@@ -133,17 +146,6 @@ class GitPushService < BaseService ...@@ -133,17 +146,6 @@ class GitPushService < BaseService
end end
end end
# Extract any GFM references from the pushed commit messages. If the configured issue-closing regex is matched,
# close the referenced Issue. Create cross-reference Notes corresponding to any other referenced Mentionables.
def process_commit_messages
default = is_default_branch?
@push_commits.each do |commit|
ProcessCommitWorker.
perform_async(project.id, current_user.id, commit.to_hash, default)
end
end
def build_push_data def build_push_data
@push_data ||= Gitlab::DataBuilder::Push.build( @push_data ||= Gitlab::DataBuilder::Push.build(
@project, @project,
......
...@@ -12,7 +12,7 @@ class GitTagPushService < BaseService ...@@ -12,7 +12,7 @@ class GitTagPushService < BaseService
project.execute_hooks(@push_data.dup, :tag_push_hooks) project.execute_hooks(@push_data.dup, :tag_push_hooks)
project.execute_services(@push_data.dup, :tag_push_hooks) project.execute_services(@push_data.dup, :tag_push_hooks)
Ci::CreatePipelineService.new(project, current_user, @push_data).execute(mirror_update: params[:mirror_update]) Ci::CreatePipelineService.new(project, current_user, @push_data).execute(mirror_update: params[:mirror_update])
ProjectCacheWorker.perform_async(project.id) ProjectCacheWorker.perform_async(project.id, [], [:commit_count, :repository_size])
true true
end end
......
...@@ -41,7 +41,7 @@ module Notes ...@@ -41,7 +41,7 @@ module Notes
# We must add the error after we call #save because errors are reset # We must add the error after we call #save because errors are reset
# when #save is called # when #save is called
if only_commands if only_commands
note.errors.add(:commands_only, 'Your commands have been executed!') note.errors.add(:commands_only, 'Commands applied')
end end
note.commands_changes = command_params.keys note.commands_changes = command_params.keys
......
...@@ -5,6 +5,9 @@ ...@@ -5,6 +5,9 @@
= link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn' = link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
= link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove' = link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove'
.stats .stats
%span.badge
= storage_counter(group.storage_size)
%span %span
= icon('bookmark') = icon('bookmark')
= number_with_delimiter(group.projects.count) = number_with_delimiter(group.projects.count)
......
...@@ -27,6 +27,8 @@ ...@@ -27,6 +27,8 @@
= sort_title_recently_updated = sort_title_recently_updated
= link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do = link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do
= sort_title_oldest_updated = sort_title_oldest_updated
= link_to admin_groups_path(sort: sort_value_largest_group, name: project_name) do
= sort_title_largest_group
= link_to new_admin_group_path, class: "btn btn-new" do = link_to new_admin_group_path, class: "btn btn-new" do
New Group New Group
%ul.content-list %ul.content-list
......
...@@ -38,6 +38,18 @@ ...@@ -38,6 +38,18 @@
%strong %strong
= @group.created_at.to_s(:medium) = @group.created_at.to_s(:medium)
%li
%span.light Storage:
%strong= storage_counter(@group.storage_size)
(
= storage_counter(@group.repository_size)
repositories,
= storage_counter(@group.build_artifacts_size)
build artifacts,
= storage_counter(@group.lfs_objects_size)
LFS
)
%li %li
%span.light Group Git LFS status: %span.light Group Git LFS status:
%strong %strong
...@@ -66,8 +78,8 @@ ...@@ -66,8 +78,8 @@
%li %li
%strong %strong
= link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
%span.label.label-gray %span.badge
= repository_size(project) = storage_counter(project.statistics.storage_size)
%span.pull-right.light %span.pull-right.light
%span.monospace= project.path_with_namespace + ".git" %span.monospace= project.path_with_namespace + ".git"
.panel-footer .panel-footer
...@@ -84,8 +96,8 @@ ...@@ -84,8 +96,8 @@
%li %li
%strong %strong
= link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
%span.label.label-gray %span.badge
= repository_size(project) = storage_counter(project.statistics.storage_size)
%span.pull-right.light %span.pull-right.light
%span.monospace= project.path_with_namespace + ".git" %span.monospace= project.path_with_namespace + ".git"
......
...@@ -69,8 +69,8 @@ ...@@ -69,8 +69,8 @@
.controls .controls
- if project.archived - if project.archived
%span.label.label-warning archived %span.label.label-warning archived
%span.label.label-gray %span.badge
= repository_size(project) = storage_counter(project.statistics.storage_size)
= link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn" = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn"
= link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove" = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove"
.title .title
......
...@@ -65,9 +65,16 @@ ...@@ -65,9 +65,16 @@
= @project.repository.path_to_repo = @project.repository.path_to_repo
%li %li
%span.light Size %span.light Storage:
%strong %strong= storage_counter(@project.statistics.storage_size)
= repository_size(@project) (
= storage_counter(@project.statistics.repository_size)
repository,
= storage_counter(@project.statistics.build_artifacts_size)
build artifacts,
= storage_counter(@project.statistics.lfs_objects_size)
LFS
)
%li %li
%span.light last commit: %span.light last commit:
......
...@@ -18,8 +18,8 @@ ...@@ -18,8 +18,8 @@
.pull-right .pull-right
- if project.archived - if project.archived
%span.label.label-warning archived %span.label.label-warning archived
%span.label.label-gray %span.badge
= repository_size(project) = storage_counter(project.statistics.storage_size)
= link_to 'Members', namespace_project_project_members_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" = link_to 'Members', namespace_project_project_members_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
= link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
= link_to 'Remove', project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-sm btn-remove" = link_to 'Remove', project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-sm btn-remove"
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
- if diff_file.deleted_file - if diff_file.deleted_file
deleted deleted
= clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy filename to clipboard') = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
- if diff_file.mode_changed? - if diff_file.mode_changed?
%small %small
......
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
.pull-right .pull-right
#new-branch.new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)} #new-branch.new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)}
= link_to '#', class: 'checking btn btn-grouped', disabled: 'disabled' do
= icon('spinner spin')
Checking branches
= link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid),
method: :post, class: 'btn btn-new btn-inverted btn-grouped has-tooltip available hide', title: @issue.to_branch_name do method: :post, class: 'btn btn-new btn-inverted btn-grouped has-tooltip available hide', title: @issue.to_branch_name do
New branch New branch
......
...@@ -8,7 +8,9 @@ ...@@ -8,7 +8,9 @@
= @teams.one? ? 'The team' : 'Select the team' = @teams.one? ? 'The team' : 'Select the team'
where the slash commands will be used in where the slash commands will be used in
- selected_id = @teams.keys.first if @teams.one? - selected_id = @teams.keys.first if @teams.one?
= f.select(:team_id, mattermost_teams_options(@teams), {}, { class: 'form-control', selected: "#{selected_id}", disabled: @teams.one? }) - options = mattermost_teams_options(@teams)
- options = options_for_select(options, selected_id)
= f.select(:team_id, options, {}, { class: 'form-control', selected: "#{selected_id}" })
.help-block .help-block
- if @teams.one? - if @teams.one?
This is the only team where you are an administrator. This is the only team where you are an administrator.
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
.timeline-content .timeline-content
.note-header .note-header
= link_to_member(note.project, note.author, avatar: false) = link_to_member(note.project, note.author, avatar: false)
.inline.note-headline-light .note-headline-light
= note.author.to_reference = note.author.to_reference
- unless note.system - unless note.system
commented commented
......
...@@ -19,10 +19,10 @@ ...@@ -19,10 +19,10 @@
%ul.nav %ul.nav
%li %li
= link_to project_files_path(@project) do = link_to project_files_path(@project) do
Files (#{repository_size}) Files (#{storage_counter(@project.statistics.total_repository_size)})
%li %li
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
#{'Commit'.pluralize(@project.commit_count)} (#{number_with_delimiter(@project.commit_count)}) #{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)})
%li %li
= link_to namespace_project_branches_path(@project.namespace, @project) do = link_to namespace_project_branches_path(@project.namespace, @project) do
#{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)}) #{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
...@@ -72,8 +72,8 @@ ...@@ -72,8 +72,8 @@
= link_to 'Set up Koding', add_koding_stack_path(@project) = link_to 'Set up Koding', add_koding_stack_path(@project)
- if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present? - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
%li.missing %li.missing
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up autodeploy', target_branch: 'autodeploy', context: 'autodeploy') do = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do
Set up autodeploy Set up auto deploy
- if @repository.commit - if @repository.commit
.project-last-commit{ class: container_class } .project-last-commit{ class: container_class }
......
...@@ -7,26 +7,27 @@ class ProjectCacheWorker ...@@ -7,26 +7,27 @@ class ProjectCacheWorker
LEASE_TIMEOUT = 15.minutes.to_i LEASE_TIMEOUT = 15.minutes.to_i
# project_id - The ID of the project for which to flush the cache. # project_id - The ID of the project for which to flush the cache.
# refresh - An Array containing extra types of data to refresh such as # files - An Array containing extra types of files to refresh such as
# `:readme` to flush the README and `:changelog` to flush the # `:readme` to flush the README and `:changelog` to flush the
# CHANGELOG. # CHANGELOG.
def perform(project_id, refresh = []) # statistics - An Array containing columns from ProjectStatistics to
# refresh, if empty all columns will be refreshed
def perform(project_id, files = [], statistics = [])
project = Project.find_by(id: project_id) project = Project.find_by(id: project_id)
return unless project && project.repository.exists? return unless project && project.repository.exists?
update_repository_size(project) update_statistics(project, statistics.map(&:to_sym))
project.update_commit_count
project.repository.refresh_method_caches(refresh.map(&:to_sym)) project.repository.refresh_method_caches(files.map(&:to_sym))
end end
def update_repository_size(project) def update_statistics(project, statistics = [])
return unless try_obtain_lease_for(project.id, :update_repository_size) return unless try_obtain_lease_for(project.id, :update_statistics)
Rails.logger.info("Updating repository size for project #{project.id}") Rails.logger.info("Updating statistics for project #{project.id}")
project.update_repository_size project.statistics.refresh!(only: statistics)
end end
private private
......
---
title: Replace wording for slash command confirmation message
merge_request: 8123
---
title: Fix mr list timestamp alignment
merge_request: 8271
author:
---
title: Fix discussion overlap text in regular screens
merge_request: 8273
author:
---
title: Fix timeout when MR contains large files marked as binary by .gitattributes
merge_request:
author:
---
title: Fixes mini-pipeline-graph dropdown animation and stage position in chrome, firefox and safari
merge_request: 8282
author:
---
title: Fix line breaking in nodes of the pipeline graph in firefox
merge_request: 8292
author:
---
title: Fixes confendential warning text alignment
merge_request: 8293
author:
---
title: Hide Scroll Top button for failed build page
merge_request: 8295
author:
---
title: Rename "autodeploy" to "auto deploy"
merge_request:
author:
---
title: Disable PostgreSQL statement timeouts when removing unneeded services
merge_request: 8322
author:
---
title: Add more storage statistics
merge_request: 7754
author: Markus Koller
---
title: Rename filename to file path in tooltip of file header in merge request diff
merge_request: 8314
\ No newline at end of file
---
title: Fix finding the latest pipeline
merge_request: 8301
author:
---
title: Fixed GFM autocomplete error when no data exists
merge_request:
author:
---
title: Remove checking branches state in issue new branch button
merge_request: 8023
...@@ -10,6 +10,6 @@ ...@@ -10,6 +10,6 @@
# end # end
# #
ActiveSupport::Inflector.inflections do |inflect| ActiveSupport::Inflector.inflections do |inflect|
inflect.uncountable %w(award_emoji) inflect.uncountable %w(award_emoji project_statistics)
inflect.acronym 'EE' inflect.acronym 'EE'
end end
Rails.application.configure do |config| Rails.application.configure do |config|
config.middleware.use(Gitlab::Middleware::Multipart) config.middleware.use(Gitlab::Middleware::Multipart)
end end
module Gitlab
module StrongParameterScalars
GITLAB_PERMITTED_SCALAR_TYPES = [::UploadedFile]
def permitted_scalar?(value)
super || GITLAB_PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) }
end
end
end
module ActionController
class Parameters
prepend Gitlab::StrongParameterScalars
end
end
class CreateProjectStatistics < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
# use bigint columns to support values >2GB
counter_column = { limit: 8, null: false, default: 0 }
create_table :project_statistics do |t|
t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
t.references :namespace, null: false, index: true
t.integer :commit_count, counter_column
t.integer :storage_size, counter_column
t.integer :repository_size, counter_column
t.integer :lfs_objects_size, counter_column
t.integer :build_artifacts_size, counter_column
end
end
end
class MigrateProjectStatistics < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = true
DOWNTIME_REASON = 'Removes two columns from the projects table'
def up
# convert repository_size in float (megabytes) to integer (bytes),
# initialize total storage_size with repository_size
execute <<-EOF
INSERT INTO project_statistics (project_id, namespace_id, commit_count, storage_size, repository_size)
SELECT id, namespace_id, commit_count, (repository_size * 1024 * 1024), (repository_size * 1024 * 1024) FROM projects
EOF
remove_column :projects, :repository_size
remove_column :projects, :commit_count
end
def down
add_column_with_default :projects, :repository_size, :float, default: 0.0
add_column_with_default :projects, :commit_count, :integer, default: 0
end
end
...@@ -4,6 +4,8 @@ class RemoveUnneededServices < ActiveRecord::Migration ...@@ -4,6 +4,8 @@ class RemoveUnneededServices < ActiveRecord::Migration
DOWNTIME = false DOWNTIME = false
def up def up
disable_statement_timeout
execute("DELETE FROM services WHERE active = false AND properties = '{}';") execute("DELETE FROM services WHERE active = false AND properties = '{}';")
end end
......
...@@ -1028,6 +1028,19 @@ ActiveRecord::Schema.define(version: 20161221140236) do ...@@ -1028,6 +1028,19 @@ ActiveRecord::Schema.define(version: 20161221140236) do
add_index "project_import_data", ["project_id"], name: "index_project_import_data_on_project_id", using: :btree add_index "project_import_data", ["project_id"], name: "index_project_import_data_on_project_id", using: :btree
create_table "project_statistics", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "namespace_id", null: false
t.integer "commit_count", limit: 8, default: 0, null: false
t.integer "storage_size", limit: 8, default: 0, null: false
t.integer "repository_size", limit: 8, default: 0, null: false
t.integer "lfs_objects_size", limit: 8, default: 0, null: false
t.integer "build_artifacts_size", limit: 8, default: 0, null: false
end
add_index "project_statistics", ["namespace_id"], name: "index_project_statistics_on_namespace_id", using: :btree
add_index "project_statistics", ["project_id"], name: "index_project_statistics_on_project_id", unique: true, using: :btree
create_table "projects", force: :cascade do |t| create_table "projects", force: :cascade do |t|
t.string "name" t.string "name"
t.string "path" t.string "path"
...@@ -1042,7 +1055,6 @@ ActiveRecord::Schema.define(version: 20161221140236) do ...@@ -1042,7 +1055,6 @@ ActiveRecord::Schema.define(version: 20161221140236) do
t.boolean "archived", default: false, null: false t.boolean "archived", default: false, null: false
t.string "avatar" t.string "avatar"
t.string "import_status" t.string "import_status"
t.float "repository_size", default: 0.0
t.text "merge_requests_template" t.text "merge_requests_template"
t.integer "star_count", default: 0, null: false t.integer "star_count", default: 0, null: false
t.boolean "merge_requests_rebase_enabled", default: false t.boolean "merge_requests_rebase_enabled", default: false
...@@ -1050,7 +1062,6 @@ ActiveRecord::Schema.define(version: 20161221140236) do ...@@ -1050,7 +1062,6 @@ ActiveRecord::Schema.define(version: 20161221140236) do
t.string "import_source" t.string "import_source"
t.integer "approvals_before_merge", default: 0, null: false t.integer "approvals_before_merge", default: 0, null: false
t.boolean "reset_approvals_on_push", default: true t.boolean "reset_approvals_on_push", default: true
t.integer "commit_count", default: 0
t.boolean "merge_requests_ff_only_enabled", default: false t.boolean "merge_requests_ff_only_enabled", default: false
t.text "issues_template" t.text "issues_template"
t.boolean "mirror", default: false, null: false t.boolean "mirror", default: false, null: false
...@@ -1488,6 +1499,7 @@ ActiveRecord::Schema.define(version: 20161221140236) do ...@@ -1488,6 +1499,7 @@ ActiveRecord::Schema.define(version: 20161221140236) do
add_foreign_key "project_authorizations", "projects", on_delete: :cascade add_foreign_key "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade add_foreign_key "project_authorizations", "users", on_delete: :cascade
add_foreign_key "protected_branch_merge_access_levels", "namespaces", column: "group_id" add_foreign_key "protected_branch_merge_access_levels", "namespaces", column: "group_id"
add_foreign_key "project_statistics", "projects", on_delete: :cascade
add_foreign_key "protected_branch_merge_access_levels", "protected_branches" add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_merge_access_levels", "users" add_foreign_key "protected_branch_merge_access_levels", "users"
add_foreign_key "protected_branch_push_access_levels", "namespaces", column: "group_id" add_foreign_key "protected_branch_push_access_levels", "namespaces", column: "group_id"
......
...@@ -88,3 +88,9 @@ artifacts through the [Admin area settings](../user/admin_area/settings/continuo ...@@ -88,3 +88,9 @@ artifacts through the [Admin area settings](../user/admin_area/settings/continuo
[reconfigure gitlab]: restart_gitlab.md "How to restart GitLab" [reconfigure gitlab]: restart_gitlab.md "How to restart GitLab"
[restart gitlab]: restart_gitlab.md "How to restart GitLab" [restart gitlab]: restart_gitlab.md "How to restart GitLab"
## Storage statistics
You can see the total storage used for build artifacts on groups and projects
in the administration area, as well as through the [groups](../api/groups.md)
and [projects APIs](../api/projects.md).
...@@ -13,6 +13,7 @@ Parameters: ...@@ -13,6 +13,7 @@ Parameters:
| `search` | string | no | Return list of authorized groups matching the search criteria | | `search` | string | no | Return list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` | | `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` | | `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) |
``` ```
GET /groups GET /groups
...@@ -31,7 +32,6 @@ GET /groups ...@@ -31,7 +32,6 @@ GET /groups
You can search for groups by name or path, see below. You can search for groups by name or path, see below.
=======
## List owned groups ## List owned groups
Get a list of groups which are owned by the authenticated user. Get a list of groups which are owned by the authenticated user.
...@@ -40,6 +40,12 @@ Get a list of groups which are owned by the authenticated user. ...@@ -40,6 +40,12 @@ Get a list of groups which are owned by the authenticated user.
GET /groups/owned GET /groups/owned
``` ```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `statistics` | boolean | no | Include group statistics |
## List a group's projects ## List a group's projects
Get a list of projects in this group. Get a list of projects in this group.
......
...@@ -307,6 +307,8 @@ Parameters: ...@@ -307,6 +307,8 @@ Parameters:
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | | `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of authorized projects matching the search criteria | | `search` | string | no | Return list of authorized projects matching the search criteria |
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
| `statistics` | boolean | no | Include project statistics |
### List starred projects ### List starred projects
...@@ -325,6 +327,7 @@ Parameters: ...@@ -325,6 +327,7 @@ Parameters:
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | | `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of authorized projects matching the search criteria | | `search` | string | no | Return list of authorized projects matching the search criteria |
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
### List ALL projects ### List ALL projects
...@@ -343,6 +346,7 @@ Parameters: ...@@ -343,6 +346,7 @@ Parameters:
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` | | `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of authorized projects matching the search criteria | | `search` | string | no | Return list of authorized projects matching the search criteria |
| `statistics` | boolean | no | Include project statistics |
### Get single project ### Get single project
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
- [CI/CD pipelines settings](../user/project/pipelines/settings.md) - [CI/CD pipelines settings](../user/project/pipelines/settings.md)
- [Review Apps](review_apps/index.md) - [Review Apps](review_apps/index.md)
- [Git submodules](git_submodules.md) Using Git submodules in your CI jobs - [Git submodules](git_submodules.md) Using Git submodules in your CI jobs
- [Autodeploy](autodeploy/index.md) - [Auto deploy](autodeploy/index.md)
## Breaking changes ## Breaking changes
......
# Autodeploy # Auto deploy
> [Introduced][mr-8135] in GitLab 8.15. > [Introduced][mr-8135] in GitLab 8.15.
Autodeploy is an easy way to configure GitLab CI for the deployment of your Auto deploy is an easy way to configure GitLab CI for the deployment of your
application. GitLab Community maintains a list of `.gitlab-ci.yml` application. GitLab Community maintains a list of `.gitlab-ci.yml`
templates for various infrastructure providers and deployment scripts templates for various infrastructure providers and deployment scripts
powering them. These scripts are responsible for packaging your application, powering them. These scripts are responsible for packaging your application,
...@@ -15,7 +15,7 @@ deployment. ...@@ -15,7 +15,7 @@ deployment.
## Supported templates ## Supported templates
The list of supported autodeploy templates is available [here][autodeploy-templates]. The list of supported auto deploy templates is available [here][auto-deploy-templates].
## Configuration ## Configuration
...@@ -24,17 +24,17 @@ credentials. For example, if you want to deploy to OpenShift you have to ...@@ -24,17 +24,17 @@ credentials. For example, if you want to deploy to OpenShift you have to
enable [Kubernetes service][kubernetes-service]. enable [Kubernetes service][kubernetes-service].
1. Configure GitLab Runner to use Docker or Kubernetes executor with 1. Configure GitLab Runner to use Docker or Kubernetes executor with
[privileged mode enabled][docker-in-docker]. [privileged mode enabled][docker-in-docker].
1. Navigate to the "Project" tab and click "Set up autodeploy" button. 1. Navigate to the "Project" tab and click "Set up auto deploy" button.
![Autodeploy button](img/autodeploy_button.png) ![Auto deploy button](img/auto_deploy_button.png)
1. Select a template. 1. Select a template.
![Dropdown with autodeploy templates](img/autodeploy_dropdown.png) ![Dropdown with auto deploy templates](img/auto_deploy_dropdown.png)
1. Commit your changes and create a merge request. 1. Commit your changes and create a merge request.
1. Test your deployment configuration using a [Review App][review-app] that was 1. Test your deployment configuration using a [Review App][review-app] that was
created automatically for you. created automatically for you.
[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135 [mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135
[project-services]: ../../project_services/project_services.md [project-services]: ../../project_services/project_services.md
[autodeploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy [auto-deploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy
[kubernetes-service]: ../../project_services/kubernetes.md [kubernetes-service]: ../../project_services/kubernetes.md
[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor [docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor
[review-app]: ../review_apps/index.md [review-app]: ../review_apps/index.md
...@@ -101,3 +101,11 @@ The form should be titled `Edit issue`. The submit button should be labeled `Sav ...@@ -101,3 +101,11 @@ The form should be titled `Edit issue`. The submit button should be labeled `Sav
| Approve | Approve an open merge request || | Approve | Approve an open merge request ||
| Remove approval, unapproved | Remove approval of an open merge request | Do not use `unapprove` as that is not an English word| | Remove approval, unapproved | Remove approval of an open merge request | Do not use `unapprove` as that is not an English word|
| Merge | Merge an open merge request || | Merge | Merge an open merge request ||
### Comments & Discussions
#### Comment
A **comment** is a written piece of text that users of GitLab can create. Comments have the meta data of author and time stamp. Comments can be added in a variety of contexts, such as issues, merge requests, and discussions.
#### Dicussion
A **discussion** is a group of 1 or more comments. A discussion can include sub discussions. Some discussions have the special capability of being able to be **resolved**. Both the comments in the discussion and the discussion itself can be resolved.
\ No newline at end of file
...@@ -40,6 +40,12 @@ In `config/gitlab.yml`: ...@@ -40,6 +40,12 @@ In `config/gitlab.yml`:
storage_path: /mnt/storage/lfs-objects storage_path: /mnt/storage/lfs-objects
``` ```
## Storage statistics
You can see the total storage used for LFS objects on groups and projects
in the administration area, as well as through the [groups](../api/groups.md)
and [projects APIs](../api/projects.md).
## Known limitations ## Known limitations
* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets) * Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets)
...@@ -47,3 +53,5 @@ In `config/gitlab.yml`: ...@@ -47,3 +53,5 @@ In `config/gitlab.yml`:
* Currently, removing LFS objects from GitLab Git LFS storage is not supported * Currently, removing LFS objects from GitLab Git LFS storage is not supported
* LFS authentications via SSH was added with GitLab 8.12 * LFS authentications via SSH was added with GitLab 8.12
* Only compatible with the GitLFS client versions 1.1.0 and up, or 1.0.2. * Only compatible with the GitLFS client versions 1.1.0 and up, or 1.0.2.
* The storage statistics currently count each LFS object multiple times for
every project linking to it
...@@ -134,7 +134,6 @@ This behaviour is caused by Git LFS using HTTPS connections by default when a ...@@ -134,7 +134,6 @@ This behaviour is caused by Git LFS using HTTPS connections by default when a
To prevent this from happening, set the lfs url in project Git config: To prevent this from happening, set the lfs url in project Git config:
```bash ```bash
git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs" git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs"
``` ```
......
...@@ -88,21 +88,21 @@ module API ...@@ -88,21 +88,21 @@ module API
expose :container_registry_enabled expose :container_registry_enabled
# Expose old field names with the new permissions methods to keep API compatible # Expose old field names with the new permissions methods to keep API compatible
expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:user]) } expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) }
expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:user]) } expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) }
expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:user]) } expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:user]) } expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:user]) } expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
expose :created_at, :last_activity_at expose :created_at, :last_activity_at
expose :shared_runners_enabled expose :shared_runners_enabled
expose :lfs_enabled?, as: :lfs_enabled expose :lfs_enabled?, as: :lfs_enabled
expose :creator_id expose :creator_id
expose :namespace expose :namespace, using: 'API::Entities::Namespace'
expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? } expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? }
expose :avatar_url expose :avatar_url
expose :star_count, :forks_count expose :star_count, :forks_count
expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:user]) && project.default_issues_tracker? } expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? }
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
expose :public_builds expose :public_builds
expose :shared_with_groups do |project, options| expose :shared_with_groups do |project, options|
...@@ -113,6 +113,16 @@ module API ...@@ -113,6 +113,16 @@ module API
expose :request_access_enabled expose :request_access_enabled
expose :only_allow_merge_if_all_discussions_are_resolved expose :only_allow_merge_if_all_discussions_are_resolved
expose :approvals_before_merge expose :approvals_before_merge
expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics
end
class ProjectStatistics < Grape::Entity
expose :commit_count
expose :storage_size
expose :repository_size
expose :lfs_objects_size
expose :build_artifacts_size
end end
class Member < UserBasic class Member < UserBasic
...@@ -149,6 +159,15 @@ module API ...@@ -149,6 +159,15 @@ module API
expose :avatar_url expose :avatar_url
expose :web_url expose :web_url
expose :request_access_enabled expose :request_access_enabled
expose :statistics, if: :statistics do
with_options format_with: -> (value) { value.to_i } do
expose :storage_size
expose :repository_size
expose :lfs_objects_size
expose :build_artifacts_size
end
end
end end
class GroupDetail < Group class GroupDetail < Group
...@@ -431,7 +450,7 @@ module API ...@@ -431,7 +450,7 @@ module API
end end
class Namespace < Grape::Entity class Namespace < Grape::Entity
expose :id, :path, :kind expose :id, :name, :path, :kind
end end
class MemberAccess < Grape::Entity class MemberAccess < Grape::Entity
...@@ -480,12 +499,12 @@ module API ...@@ -480,12 +499,12 @@ module API
class ProjectWithAccess < Project class ProjectWithAccess < Project
expose :permissions do expose :permissions do
expose :project_access, using: Entities::ProjectAccess do |project, options| expose :project_access, using: Entities::ProjectAccess do |project, options|
project.project_members.find_by(user_id: options[:user].id) project.project_members.find_by(user_id: options[:current_user].id)
end end
expose :group_access, using: Entities::GroupAccess do |project, options| expose :group_access, using: Entities::GroupAccess do |project, options|
if project.group if project.group
project.group.group_members.find_by(user_id: options[:user].id) project.group.group_members.find_by(user_id: options[:current_user].id)
end end
end end
end end
......
...@@ -19,6 +19,20 @@ module API ...@@ -19,6 +19,20 @@ module API
optional :ldap_access, type: Integer, desc: 'A valid access level' optional :ldap_access, type: Integer, desc: 'A valid access level'
all_or_none_of :ldap_cn, :ldap_access all_or_none_of :ldap_cn, :ldap_access
end end
params :statistics_params do
optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
end
def present_groups(groups, options = {})
options = options.reverse_merge(
with: Entities::Group,
current_user: current_user,
)
groups = groups.with_statistics if options[:statistics]
present paginate(groups), options
end
end end
resource :groups do resource :groups do
...@@ -26,6 +40,7 @@ module API ...@@ -26,6 +40,7 @@ module API
success Entities::Group success Entities::Group
end end
params do params do
use :statistics_params
optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list' optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list'
optional :all_available, type: Boolean, desc: 'Show all group that you have access to' optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
optional :search, type: String, desc: 'Search for a specific group' optional :search, type: String, desc: 'Search for a specific group'
...@@ -46,7 +61,7 @@ module API ...@@ -46,7 +61,7 @@ module API
groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present? groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
groups = groups.reorder(params[:order_by] => params[:sort]) groups = groups.reorder(params[:order_by] => params[:sort])
present paginate(groups), with: Entities::Group present_groups groups, statistics: params[:statistics] && current_user.is_admin?
end end
desc 'Get list of owned groups for authenticated user' do desc 'Get list of owned groups for authenticated user' do
...@@ -54,10 +69,10 @@ module API ...@@ -54,10 +69,10 @@ module API
end end
params do params do
use :pagination use :pagination
use :statistics_params
end end
get '/owned' do get '/owned' do
groups = current_user.owned_groups present_groups current_user.owned_groups, statistics: params[:statistics]
present paginate(groups), with: Entities::Group, user: current_user
end end
desc 'Create a group. Available only for users who can create groups.' do desc 'Create a group. Available only for users who can create groups.' do
...@@ -88,7 +103,7 @@ module API ...@@ -88,7 +103,7 @@ module API
) )
end end
present group, with: Entities::Group present group, with: Entities::Group, current_user: current_user
else else
render_api_error!("Failed to save group #{group.errors.messages}", 400) render_api_error!("Failed to save group #{group.errors.messages}", 400)
end end
...@@ -114,7 +129,7 @@ module API ...@@ -114,7 +129,7 @@ module API
authorize! :admin_group, group authorize! :admin_group, group
if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute if ::Groups::UpdateService.new(group, current_user, declared_params(include_missing: false)).execute
present group, with: Entities::GroupDetail present group, with: Entities::GroupDetail, current_user: current_user
else else
render_validation_error!(group) render_validation_error!(group)
end end
...@@ -125,7 +140,7 @@ module API ...@@ -125,7 +140,7 @@ module API
end end
get ":id" do get ":id" do
group = find_group!(params[:id]) group = find_group!(params[:id])
present group, with: Entities::GroupDetail present group, with: Entities::GroupDetail, current_user: current_user
end end
desc 'Remove a group.' desc 'Remove a group.'
...@@ -156,7 +171,7 @@ module API ...@@ -156,7 +171,7 @@ module API
projects = GroupProjectsFinder.new(group).execute(current_user) projects = GroupProjectsFinder.new(group).execute(current_user)
projects = filter_projects(projects) projects = filter_projects(projects)
entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project
present paginate(projects), with: entity, user: current_user present paginate(projects), with: entity, current_user: current_user
end end
desc 'Transfer a project to the group namespace. Available only for admin.' do desc 'Transfer a project to the group namespace. Available only for admin.' do
...@@ -172,7 +187,7 @@ module API ...@@ -172,7 +187,7 @@ module API
result = ::Projects::TransferService.new(project, current_user).execute(group) result = ::Projects::TransferService.new(project, current_user).execute(group)
if result if result
present group, with: Entities::GroupDetail present group, with: Entities::GroupDetail, current_user: current_user
else else
render_api_error!("Failed to transfer project #{project.errors.messages}", 400) render_api_error!("Failed to transfer project #{project.errors.messages}", 400)
end end
......
...@@ -259,7 +259,7 @@ module API ...@@ -259,7 +259,7 @@ module API
rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500) rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500)
end end
# Projects helpers # project helpers
def filter_projects(projects) def filter_projects(projects)
if params[:search].present? if params[:search].present?
......
...@@ -44,6 +44,15 @@ module API ...@@ -44,6 +44,15 @@ module API
resource :projects do resource :projects do
helpers do helpers do
params :collection_params do
use :sort_params
use :filter_params
use :pagination
optional :simple, type: Boolean, default: false,
desc: 'Return only the ID, URL, name, and path of each project'
end
params :sort_params do params :sort_params do
optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at], optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
default: 'created_at', desc: 'Return projects ordered by field' default: 'created_at', desc: 'Return projects ordered by field'
...@@ -56,97 +65,94 @@ module API ...@@ -56,97 +65,94 @@ module API
optional :visibility, type: String, values: %w[public internal private], optional :visibility, type: String, values: %w[public internal private],
desc: 'Limit by visibility' desc: 'Limit by visibility'
optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria' optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
use :sort_params end
params :statistics_params do
optional :statistics, type: Boolean, default: false, desc: 'Include project statistics'
end end
params :create_params do params :create_params do
optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.' optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.'
optional :import_url, type: String, desc: 'URL from which the project is imported' optional :import_url, type: String, desc: 'URL from which the project is imported'
end end
def present_projects(projects, options = {})
options = options.reverse_merge(
with: Entities::Project,
current_user: current_user,
simple: params[:simple],
)
projects = filter_projects(projects)
projects = projects.with_statistics if options[:statistics]
options[:with] = Entities::BasicProjectDetails if options[:simple]
present paginate(projects), options
end
end end
desc 'Get a list of visible projects for authenticated user' do desc 'Get a list of visible projects for authenticated user' do
success Entities::BasicProjectDetails success Entities::BasicProjectDetails
end end
params do params do
optional :simple, type: Boolean, default: false, use :collection_params
desc: 'Return only the ID, URL, name, and path of each project'
use :filter_params
use :pagination
end end
get '/visible' do get '/visible' do
projects = ProjectsFinder.new.execute(current_user) entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
projects = filter_projects(projects) present_projects ProjectsFinder.new.execute(current_user), with: entity
entity = params[:simple] || !current_user ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
present paginate(projects), with: entity, user: current_user
end end
desc 'Get a projects list for authenticated user' do desc 'Get a projects list for authenticated user' do
success Entities::BasicProjectDetails success Entities::BasicProjectDetails
end end
params do params do
optional :simple, type: Boolean, default: false, use :collection_params
desc: 'Return only the ID, URL, name, and path of each project'
use :filter_params
use :pagination
end end
get do get do
authenticate! authenticate!
projects = current_user.authorized_projects present_projects current_user.authorized_projects,
projects = filter_projects(projects) with: Entities::ProjectWithAccess
entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
present paginate(projects), with: entity, user: current_user
end end
desc 'Get an owned projects list for authenticated user' do desc 'Get an owned projects list for authenticated user' do
success Entities::BasicProjectDetails success Entities::BasicProjectDetails
end end
params do params do
use :filter_params use :collection_params
use :pagination use :statistics_params
end end
get '/owned' do get '/owned' do
authenticate! authenticate!
projects = current_user.owned_projects present_projects current_user.owned_projects,
projects = filter_projects(projects) with: Entities::ProjectWithAccess,
statistics: params[:statistics]
present paginate(projects), with: Entities::ProjectWithAccess, user: current_user
end end
desc 'Gets starred project for the authenticated user' do desc 'Gets starred project for the authenticated user' do
success Entities::BasicProjectDetails success Entities::BasicProjectDetails
end end
params do params do
use :filter_params use :collection_params
use :pagination
end end
get '/starred' do get '/starred' do
authenticate! authenticate!
projects = current_user.viewable_starred_projects present_projects current_user.viewable_starred_projects
projects = filter_projects(projects)
present paginate(projects), with: Entities::Project, user: current_user
end end
desc 'Get all projects for admin user' do desc 'Get all projects for admin user' do
success Entities::BasicProjectDetails success Entities::BasicProjectDetails
end end
params do params do
use :filter_params use :collection_params
use :pagination use :statistics_params
end end
get '/all' do get '/all' do
authenticated_as_admin! authenticated_as_admin!
projects = Project.all present_projects Project.all, with: Entities::ProjectWithAccess, statistics: params[:statistics]
projects = filter_projects(projects)
present paginate(projects), with: Entities::ProjectWithAccess, user: current_user
end end
desc 'Search for projects the current user has access to' do desc 'Search for projects the current user has access to' do
...@@ -225,7 +231,7 @@ module API ...@@ -225,7 +231,7 @@ module API
end end
get ":id" do get ":id" do
entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
present user_project, with: entity, user: current_user, present user_project, with: entity, current_user: current_user,
user_can_admin_project: can?(current_user, :admin_project, user_project) user_can_admin_project: can?(current_user, :admin_project, user_project)
end end
......
...@@ -49,8 +49,9 @@ module Gitlab ...@@ -49,8 +49,9 @@ module Gitlab
end end
def url(subject) def url(subject)
polymorphic_url( project = subject.project
[ subject.project.namespace.becomes(Namespace), subject.project, subject ])
namespace_project_build_url(project.namespace.becomes(Namespace), project, subject)
end end
end end
end end
......
...@@ -30,12 +30,12 @@ module Gitlab ...@@ -30,12 +30,12 @@ module Gitlab
if subject.is_a?(Gitlab::ChatCommands::Result) if subject.is_a?(Gitlab::ChatCommands::Result)
show_result(subject) show_result(subject)
elsif subject.respond_to?(:count) elsif subject.respond_to?(:count)
if subject.many? if subject.none?
multiple_resources(subject)
elsif subject.none?
not_found not_found
elsif subject.one?
single_resource(subject.first)
else else
single_resource(subject) multiple_resources(subject)
end end
else else
single_resource(subject) single_resource(subject)
...@@ -71,9 +71,9 @@ module Gitlab ...@@ -71,9 +71,9 @@ module Gitlab
end end
def multiple_resources(resources) def multiple_resources(resources)
resources.map! { |resource| title(resource) } titles = resources.map { |resource| title(resource) }
message = header_with_list("Multiple results were found:", resources) message = header_with_list("Multiple results were found:", titles)
ephemeral_response(message) ephemeral_response(message)
end end
......
...@@ -61,7 +61,10 @@ module Gitlab ...@@ -61,7 +61,10 @@ module Gitlab
end end
def cacheable?(diff_file) def cacheable?(diff_file)
@merge_request_diff.present? && diff_file.blob && diff_file.blob.text? @merge_request_diff.present? &&
diff_file.blob &&
diff_file.blob.text? &&
@project.repository.diffable?(diff_file.blob)
end end
def cache_key def cache_key
......
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
{ {
'General' => '', 'General' => '',
'Pages' => 'Pages', 'Pages' => 'Pages',
'Autodeploy' => 'autodeploy' 'Auto deploy' => 'autodeploy'
} }
end end
...@@ -28,7 +28,7 @@ module Gitlab ...@@ -28,7 +28,7 @@ module Gitlab
end end
def dropdown_names(context) def dropdown_names(context)
categories = context == 'autodeploy' ? ['Autodeploy'] : ['General', 'Pages'] categories = context == 'autodeploy' ? ['Auto deploy'] : ['General', 'Pages']
super().slice(*categories) super().slice(*categories)
end end
end end
......
...@@ -63,8 +63,7 @@ namespace :gitlab do ...@@ -63,8 +63,7 @@ namespace :gitlab do
if project.persisted? if project.persisted?
puts " * Created #{project.name} (#{repo_path})".color(:green) puts " * Created #{project.name} (#{repo_path})".color(:green)
project.update_repository_size ProjectCacheWorker.perform(project.id)
project.update_commit_count
else else
puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red) puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red)
puts " Errors: #{project.errors.messages}".color(:red) puts " Errors: #{project.errors.messages}".color(:red)
......
namespace :gitlab do
desc "GitLab | Update commit count for projects"
task update_commit_count: :environment do
projects = Project.where(commit_count: 0)
puts "#{projects.size} projects need to be updated. This might take a while."
ask_to_continue unless ENV['force'] == 'yes'
projects.find_each(batch_size: 100) do |project|
print "#{project.name_with_namespace.color(:yellow)} ... "
unless project.repo_exists?
puts "skipping, because the repo is empty".color(:magenta)
next
end
project.update_commit_count
puts project.commit_count.to_s.color(:green)
end
end
end
...@@ -2,7 +2,7 @@ include ActionDispatch::TestProcess ...@@ -2,7 +2,7 @@ include ActionDispatch::TestProcess
FactoryGirl.define do FactoryGirl.define do
factory :lfs_object do factory :lfs_object do
oid "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" sequence(:oid) { |n| "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a%05x" % n }
size 499013 size 499013
end end
......
FactoryGirl.define do
factory :project_statistics do
project { create :project }
namespace { project.namespace }
end
end
...@@ -26,7 +26,7 @@ describe 'Auto deploy' do ...@@ -26,7 +26,7 @@ describe 'Auto deploy' do
it 'does not show a button to set up auto deploy' do it 'does not show a button to set up auto deploy' do
visit namespace_project_path(project.namespace, project) visit namespace_project_path(project.namespace, project)
expect(page).to have_no_content('Set up autodeploy') expect(page).to have_no_content('Set up auto deploy')
end end
end end
...@@ -37,11 +37,11 @@ describe 'Auto deploy' do ...@@ -37,11 +37,11 @@ describe 'Auto deploy' do
end end
it 'shows a button to set up auto deploy' do it 'shows a button to set up auto deploy' do
expect(page).to have_link('Set up autodeploy') expect(page).to have_link('Set up auto deploy')
end end
it 'includes Kubernetes as an available template', js: true do it 'includes OpenShift as an available template', js: true do
click_link 'Set up autodeploy' click_link 'Set up auto deploy'
click_button 'Choose a GitLab CI Yaml template' click_button 'Choose a GitLab CI Yaml template'
within '.gitlab-ci-yml-selector' do within '.gitlab-ci-yml-selector' do
...@@ -49,8 +49,8 @@ describe 'Auto deploy' do ...@@ -49,8 +49,8 @@ describe 'Auto deploy' do
end end
end end
it 'creates a merge request using "autodeploy" branch', js: true do it 'creates a merge request using "auto-deploy" branch', js: true do
click_link 'Set up autodeploy' click_link 'Set up auto deploy'
click_button 'Choose a GitLab CI Yaml template' click_button 'Choose a GitLab CI Yaml template'
within '.gitlab-ci-yml-selector' do within '.gitlab-ci-yml-selector' do
click_on 'OpenShift' click_on 'OpenShift'
...@@ -58,7 +58,7 @@ describe 'Auto deploy' do ...@@ -58,7 +58,7 @@ describe 'Auto deploy' do
wait_for_ajax wait_for_ajax
click_button 'Commit Changes' click_button 'Commit Changes'
expect(page).to have_content('New Merge Request From autodeploy into master') expect(page).to have_content('New Merge Request From auto-deploy into master')
end end
end end
end end
...@@ -47,7 +47,7 @@ feature 'GFM autocomplete', feature: true, js: true do ...@@ -47,7 +47,7 @@ feature 'GFM autocomplete', feature: true, js: true do
expect_to_wrap(true, label_item, note, label.title) expect_to_wrap(true, label_item, note, label.title)
end end
it "does not show drpdown when preceded with a special character" do it "does not show dropdown when preceded with a special character" do
note = find('#note_note') note = find('#note_note')
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
note.native.send_keys('') note.native.send_keys('')
...@@ -65,6 +65,17 @@ feature 'GFM autocomplete', feature: true, js: true do ...@@ -65,6 +65,17 @@ feature 'GFM autocomplete', feature: true, js: true do
expect(page).to have_selector('.atwho-container', visible: false) expect(page).to have_selector('.atwho-container', visible: false)
end end
it "does not throw an error if no labels exist" do
note = find('#note_note')
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys('~')
note.click
end
expect(page).to have_selector('.atwho-container', visible: false)
end
it 'doesn\'t wrap for assignee values' do it 'doesn\'t wrap for assignee values' do
note = find('#note_note') note = find('#note_note')
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
......
...@@ -30,7 +30,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do ...@@ -30,7 +30,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
write_note("/due 2016-08-28") write_note("/due 2016-08-28")
expect(page).not_to have_content '/due 2016-08-28' expect(page).not_to have_content '/due 2016-08-28'
expect(page).to have_content 'Your commands have been executed!' expect(page).to have_content 'Commands applied'
issue.reload issue.reload
...@@ -51,7 +51,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do ...@@ -51,7 +51,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
write_note("/due 2016-08-28") write_note("/due 2016-08-28")
expect(page).to have_content '/due 2016-08-28' expect(page).to have_content '/due 2016-08-28'
expect(page).not_to have_content 'Your commands have been executed!' expect(page).not_to have_content 'Commands applied'
issue.reload issue.reload
...@@ -70,7 +70,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do ...@@ -70,7 +70,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
write_note("/remove_due_date") write_note("/remove_due_date")
expect(page).not_to have_content '/remove_due_date' expect(page).not_to have_content '/remove_due_date'
expect(page).to have_content 'Your commands have been executed!' expect(page).to have_content 'Commands applied'
issue.reload issue.reload
...@@ -91,7 +91,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do ...@@ -91,7 +91,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
write_note("/remove_due_date") write_note("/remove_due_date")
expect(page).to have_content '/remove_due_date' expect(page).to have_content '/remove_due_date'
expect(page).not_to have_content 'Your commands have been executed!' expect(page).not_to have_content 'Commands applied'
issue.reload issue.reload
......
...@@ -371,23 +371,25 @@ describe 'Issues', feature: true do ...@@ -371,23 +371,25 @@ describe 'Issues', feature: true do
describe 'when I want to reset my incoming email token' do describe 'when I want to reset my incoming email token' do
let(:project1) { create(:project, namespace: @user.namespace) } let(:project1) { create(:project, namespace: @user.namespace) }
let(:issue) { create(:issue, project: project1) } let!(:issue) { create(:issue, project: project1) }
before do before do
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true) stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
project1.team << [@user, :master] project1.team << [@user, :master]
project1.issues << issue
visit namespace_project_issues_path(@user.namespace, project1) visit namespace_project_issues_path(@user.namespace, project1)
end end
it 'changes incoming email address token', js: true do it 'changes incoming email address token', js: true do
find('.issue-email-modal-btn').click find('.issue-email-modal-btn').click
previous_token = find('input#issue_email').value previous_token = find('input#issue_email').value
find('.incoming-email-token-reset').click find('.incoming-email-token-reset').click
wait_for_ajax
expect(find('input#issue_email').value).not_to eq(previous_token) expect(page).to have_no_field('issue_email', with: previous_token)
new_token = project1.new_issue_address(@user.reload)
expect(page).to have_field(
'issue_email',
with: new_token
)
end end
end end
......
...@@ -31,7 +31,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do ...@@ -31,7 +31,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
write_note("/wip") write_note("/wip")
expect(page).not_to have_content '/wip' expect(page).not_to have_content '/wip'
expect(page).to have_content 'Your commands have been executed!' expect(page).to have_content 'Commands applied'
expect(merge_request.reload.work_in_progress?).to eq true expect(merge_request.reload.work_in_progress?).to eq true
end end
...@@ -42,7 +42,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do ...@@ -42,7 +42,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
write_note("/wip") write_note("/wip")
expect(page).not_to have_content '/wip' expect(page).not_to have_content '/wip'
expect(page).to have_content 'Your commands have been executed!' expect(page).to have_content 'Commands applied'
expect(merge_request.reload.work_in_progress?).to eq false expect(merge_request.reload.work_in_progress?).to eq false
end end
...@@ -61,7 +61,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do ...@@ -61,7 +61,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
write_note("/wip") write_note("/wip")
expect(page).not_to have_content '/wip' expect(page).not_to have_content '/wip'
expect(page).not_to have_content 'Your commands have been executed!' expect(page).not_to have_content 'Commands applied'
expect(merge_request.reload.work_in_progress?).to eq false expect(merge_request.reload.work_in_progress?).to eq false
end end
......
require 'spec_helper'
describe StorageHelper do
describe '#storage_counter' do
it 'formats bytes to one decimal place' do
expect(helper.storage_counter(1.23.megabytes)).to eq '1.2 MB'
end
it 'does not add decimals for sizes < 1 MB' do
expect(helper.storage_counter(23.5.kilobytes)).to eq '24 KB'
end
it 'does not add decimals for zeroes' do
expect(helper.storage_counter(2.megabytes)).to eq '2 MB'
end
it 'uses commas as thousands separator' do
expect(helper.storage_counter(100_000_000_000_000_000)).to eq '90,949.5 TB'
end
end
end
...@@ -54,6 +54,30 @@ describe Gitlab::ChatCommands::Command, service: true do ...@@ -54,6 +54,30 @@ describe Gitlab::ChatCommands::Command, service: true do
end end
end end
context 'searching for an issue' do
let(:params) { { text: 'issue search find me' } }
let!(:issue) { create(:issue, project: project, title: 'find me') }
before do
project.team << [user, :master]
end
context 'a single issue is found' do
it 'presents the issue' do
expect(subject[:text]).to match(issue.title)
end
end
context 'multiple issues found' do
let!(:issue2) { create(:issue, project: project, title: "someone find me") }
it 'shows a link to the new issue' do
expect(subject[:text]).to match(issue.title)
expect(subject[:text]).to match(issue2.title)
end
end
end
context 'when trying to do deployment' do context 'when trying to do deployment' do
let(:params) { { text: 'deploy staging to production' } } let(:params) { { text: 'deploy staging to production' } }
let!(:build) { create(:ci_build, project: project) } let!(:build) { create(:ci_build, project: project) }
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Diff::FileCollection::MergeRequestDiff do describe Gitlab::Diff::FileCollection::MergeRequestDiff do
let(:merge_request) { create :merge_request } let(:merge_request) { create(:merge_request) }
let(:diff_files) { described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files }
it 'does not hightlight binary files' do it 'does not highlight binary files' do
allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => false)) allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => false))
expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines) expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines)
described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files diff_files
end end
it 'does not hightlight file if blob is not accessable' do it 'does not highlight file if blob is not accessable' do
allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(nil) allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(nil)
expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines) expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines)
described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files diff_files
end
it 'does not files marked as undiffable in .gitattributes' do
allow_any_instance_of(Repository).to receive(:diffable?).and_return(false)
expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines)
diff_files
end end
end end
...@@ -212,6 +212,7 @@ project: ...@@ -212,6 +212,7 @@ project:
- path_locks - path_locks
- approver_groups - approver_groups
- route - route
- statistics
award_emoji: award_emoji:
- awardable - awardable
- user - user
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::RepositorySizeError, lib: true do describe Gitlab::RepositorySizeError, lib: true do
let(:project) { create(:empty_project, repository_size: 15) } let(:project) do
create(:empty_project, statistics: build(:project_statistics, repository_size: 15))
end
let(:message) { Gitlab::RepositorySizeError.new(project) } let(:message) { Gitlab::RepositorySizeError.new(project) }
let(:base_message) { 'because this repository has exceeded its size limit of 10 MB by 5 MB' } let(:base_message) { 'because this repository has exceeded its size limit of 10 MB by 5 MB' }
......
...@@ -85,4 +85,30 @@ describe Ci::Build, models: true do ...@@ -85,4 +85,30 @@ describe Ci::Build, models: true do
it { expect(build.trace_file_path).to eq(build.old_path_to_trace) } it { expect(build.trace_file_path).to eq(build.old_path_to_trace) }
end end
end end
describe '#update_project_statistics' do
let!(:build) { create(:ci_build, artifacts_size: 23) }
it 'updates project statistics when the artifact size changes' do
expect(ProjectCacheWorker).to receive(:perform_async)
.with(build.project_id, [], [:build_artifacts_size])
build.artifacts_size = 42
build.save!
end
it 'does not update project statistics when the artifact size stays the same' do
expect(ProjectCacheWorker).not_to receive(:perform_async)
build.name = 'changed'
build.save!
end
it 'updates project statistics when the build is destroyed' do
expect(ProjectCacheWorker).to receive(:perform_async)
.with(build.project_id, [], [:build_artifacts_size])
build.destroy
end
end
end end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment