Commit 832ea8a8 authored by Valery Sizov's avatar Valery Sizov

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into ce_upstream

parents 4d9d9d59 47b35dde
......@@ -407,7 +407,7 @@ notify:slack:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
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
only:
- master@gitlab-org/gitlab-ce
......
......@@ -12,7 +12,7 @@ entry.
- Fix Pipeline builds list blank on MR. !8255
- 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.
- Use Grape's new Route methods.
......
......@@ -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
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.
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
[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
- Manage Git repositories with fine grained access controls that keep your code secure
- 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
- Completely free and open source (MIT Expat license)
- Powered by [Ruby on Rails](https://github.com/rails/rails)
## Hiring
......@@ -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:
- Ubuntu/Debian/CentOS/RHEL
- Ubuntu/Debian/CentOS/RHEL/OpenSUSE
- Ruby (MRI) 2.3
- Git 2.8.4+
- 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).
......
......@@ -92,8 +92,8 @@
success: function(buildData) {
$('.js-build-output').html(buildData.trace_html);
if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
this.initScrollMonitor();
return this.$buildRefreshAnimation.remove();
this.$buildRefreshAnimation.remove();
return this.initScrollMonitor();
}
}.bind(this)
});
......
......@@ -367,7 +367,7 @@
return $input.trigger('keyup');
},
isLoading(data) {
if (!data) return false;
if (!data || !data.length) return false;
if (Array.isArray(data)) data = data[0];
return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0];
},
......
......@@ -139,15 +139,12 @@
return;
}
return $.getJSON($container.data('path')).error(function() {
$container.find('.checking').hide();
$container.find('.unavailable').show();
return new Flash('Failed to check if a new branch can be created.', 'alert');
}).success(function(data) {
if (data.can_create_branch) {
$container.find('.checking').hide();
$container.find('.available').show();
} else {
$container.find('.checking').hide();
return $container.find('.unavailable').show();
}
});
......
......@@ -245,7 +245,6 @@ ul.content-list {
}
ul.controls {
padding-top: 1px;
float: right;
list-style: none;
......
......@@ -23,12 +23,12 @@
}
.stage-header {
width: 28%;
width: 26%;
padding-left: $gl-padding;
}
.median-header {
width: 12%;
width: 14%;
}
.event-header {
......@@ -141,7 +141,7 @@
.dismiss-icon {
position: absolute;
right: $cycle-analytics-dismiss-icon-color;
right: $cycle-analytics-box-padding;
cursor: pointer;
color: $cycle-analytics-dismiss-icon-color;
}
......@@ -215,7 +215,6 @@
border-bottom: 1px solid transparent;
border-right: 1px solid $border-color;
background-color: $gray-light;
cursor: default;
&.active {
background-color: transparent;
......@@ -247,11 +246,11 @@
float: left;
&.stage-name {
width: 70%;
width: 65%;
}
&.stage-median {
width: 30%;
width: 35%;
}
}
......
......@@ -109,7 +109,7 @@
margin: auto;
margin-top: 0;
text-align: center;
font-size: 13px;
font-size: 12px;
@media (max-width: $screen-sm-max) {
// On smaller devices the warning becomes the fourth item in the list,
......
......@@ -43,7 +43,7 @@ ul.notes {
}
.system-note-message {
display: inline;
display: inline-block;
&::first-letter {
text-transform: lowercase;
......@@ -55,7 +55,7 @@ ul.notes {
}
p {
display: inline;
display: inline-block;
margin: 0;
&::first-letter {
......@@ -151,10 +151,6 @@ ul.notes {
}
}
}
.note-headline-light {
display: inline;
}
}
.discussion-body {
......@@ -452,11 +448,6 @@ ul.notes {
border-radius: $border-radius-base;
}
.diff-file .note .note-actions {
right: 0;
top: 0;
}
/**
* Line note button on the side of diffs
......@@ -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 @@
width: 8px;
position: absolute;
right: -7px;
bottom: 10px;
top: 10px;
border-bottom: 2px solid $border-color;
}
}
......@@ -335,7 +335,6 @@
width: 100%;
background-color: $gray-light;
padding: $gl-padding;
overflow: auto;
white-space: nowrap;
transition: max-height 0.3s, padding 0.3s;
......@@ -621,14 +620,14 @@
}
.dropdown-counter-badge {
float: right;
color: $border-color;
font-weight: 100;
font-size: 15px;
margin-right: 2px;
position: absolute;
right: 5px;
top: 8px;
}
.grouped-pipeline-dropdown {
padding: 0;
width: 191px;
......@@ -784,11 +783,72 @@
.mini-pipeline-graph {
.builds-dropdown {
background-color: transparent;
border: none;
padding: 0;
color: $gl-text-color-light;
border: none;
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 {
......@@ -849,7 +909,7 @@
height: 22px;
position: relative;
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 {
top: -1px;
......@@ -862,75 +922,6 @@
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 {
margin-left: 3px;
......
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
@groups = Group.all
@groups = Group.with_statistics
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present?
@groups = @groups.page(params[:page])
end
def show
@group = Group.with_statistics.find_by_full_path(params[:id])
@members = @group.members.order("access_level DESC").page(params[:members_page])
@requesters = AccessRequestsFinder.new(@group).execute(current_user)
@projects = @group.projects.page(params[:projects_page])
@projects = @group.projects.with_statistics.page(params[:projects_page])
end
def new
......
......@@ -3,7 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController
before_action :group, only: [:show, :transfer]
def index
@projects = Project.all
@projects = Project.with_statistics
@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.with_push if params[:with_push].present?
......
......@@ -75,7 +75,7 @@ class GroupsController < Groups::ApplicationController
end
def projects
@projects = @group.projects.page(params[:page])
@projects = @group.projects.with_statistics.page(params[:page])
end
def update
......
......@@ -256,15 +256,6 @@ module ProjectsHelper
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)
case default_clone_protocol
when 'krb5'
......@@ -429,20 +420,6 @@ module ProjectsHelper
[@project.path_with_namespace, sha, "readme"].join('-')
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
@ref || @repository.try(:root_ref)
end
......
......@@ -11,6 +11,7 @@ module SortingHelper
sort_value_due_date_soon => sort_title_due_date_soon,
sort_value_due_date_later => sort_title_due_date_later,
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_oldest_signin => sort_title_oldest_signin,
sort_value_downvotes => sort_title_downvotes,
......@@ -94,6 +95,10 @@ module SortingHelper
'Largest repository'
end
def sort_title_largest_group
'Largest group'
end
def sort_title_recently_signin
'Recent sign in'
end
......@@ -203,7 +208,11 @@ module SortingHelper
end
def sort_value_largest_repo
'repository_size_desc'
'storage_size_desc'
end
def sort_value_largest_group
'storage_size_desc'
end
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
before_destroy { project }
after_create :execute_hooks
after_save :update_project_statistics, if: :artifacts_size_changed?
after_destroy :update_project_statistics
class << self
def first_pending
......@@ -585,5 +587,9 @@ module Ci
Ci::MaskSecret.mask!(trace, token)
trace
end
def update_project_statistics
ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size])
end
end
end
......@@ -93,11 +93,8 @@ module Ci
.select("max(#{quoted_table_name}.id)")
.group(:ref, :sha)
if ref
where(id: max_id, ref: ref)
else
where(id: max_id)
end
relation = ref ? where(ref: ref) : self
relation.where(id: max_id)
end
def self.latest_status(ref = nil)
......@@ -105,7 +102,7 @@ module Ci
end
def self.latest_successful_for(ref)
success.latest(ref).first
success.latest(ref).order(id: :desc).first
end
def self.truncate_sha(sha)
......
......@@ -64,8 +64,14 @@ class Group < Namespace
end
def sort(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
def reference_prefix
User.reference_prefix
......
......@@ -5,4 +5,13 @@ class LfsObjectsProject < ActiveRecord::Base
validates :lfs_object_id, presence: true
validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" }
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
......@@ -205,7 +205,9 @@ class MergeRequest < ActiveRecord::Base
end
def diff_size
diffs(diff_options).size
opts = diff_options || {}
raw_diffs(opts).size
end
def diff_base_commit
......
......@@ -9,6 +9,7 @@ class Namespace < ActiveRecord::Base
cache_markdown_field :description, pipeline: :description
has_many :projects, dependent: :destroy
has_many :project_statistics
belongs_to :owner, class_name: "User"
belongs_to :parent, class_name: "Namespace"
......@@ -38,6 +39,18 @@ class Namespace < ActiveRecord::Base
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
def by_path(path)
find_by('lower(path) = :value', value: path.downcase)
......
......@@ -43,6 +43,7 @@ class Project < ActiveRecord::Base
after_create :ensure_dir_exist
after_create :create_project_feature, unless: :project_feature
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
after_create :set_last_activity_at
......@@ -152,6 +153,7 @@ class Project < ActiveRecord::Base
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
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 :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
......@@ -237,6 +239,7 @@ class Project < ActiveRecord::Base
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_statistics, -> { includes(:statistics) }
# "enabled" here means "not disabled". It includes private features!
scope :with_feature_enabled, ->(feature) {
......@@ -366,8 +369,10 @@ class Project < ActiveRecord::Base
end
def sort(method)
if method == 'repository_size_desc'
reorder(repository_size: :desc, id: :desc)
if method == 'storage_size_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
order_by(method)
end
......@@ -1147,14 +1152,6 @@ class Project < ActiveRecord::Base
forked? && project == forked_from_project
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
forks.count
end
......@@ -1586,4 +1583,9 @@ class Project < ActiveRecord::Base
def full_path_changed?
path_changed? || namespace_id_changed?
end
def update_project_statistics
stats = statistics || build_statistics
stats.update(namespace_id: namespace_id)
end
end
......@@ -49,11 +49,13 @@ class ChatNotificationService < Service
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
opt[:username] = username if username
notifier = Slack::Notifier.new(webhook, opt)
opts = {}
opts[:channel] = channel_name if channel_name
opts[:username] = username if username
notifier = Slack::Notifier.new(webhook, opts)
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
true
......@@ -71,7 +73,7 @@ class ChatNotificationService < Service
fields.reject { |field| field[:name].end_with?('channel') }
end
def default_channel
def default_channel_placeholder
raise NotImplementedError
end
......@@ -103,7 +105,7 @@ class ChatNotificationService < Service
def build_event_channels
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
......
......@@ -35,7 +35,7 @@ class MattermostService < ChatNotificationService
]
end
def default_channel
def default_channel_placeholder
"#town-square"
end
end
......@@ -34,7 +34,7 @@ class SlackService < ChatNotificationService
]
end
def default_channel
def default_channel_placeholder
"#general"
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
include Gitlab::CurrentSettings
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
# and only if the provided user and project are present in GitLab.
#
......@@ -78,7 +81,17 @@ class GitPushService < BaseService
types = []
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
protected
......@@ -133,17 +146,6 @@ class GitPushService < BaseService
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
@push_data ||= Gitlab::DataBuilder::Push.build(
@project,
......
......@@ -12,7 +12,7 @@ class GitTagPushService < BaseService
project.execute_hooks(@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])
ProjectCacheWorker.perform_async(project.id)
ProjectCacheWorker.perform_async(project.id, [], [:commit_count, :repository_size])
true
end
......
......@@ -41,7 +41,7 @@ module Notes
# We must add the error after we call #save because errors are reset
# when #save is called
if only_commands
note.errors.add(:commands_only, 'Your commands have been executed!')
note.errors.add(:commands_only, 'Commands applied')
end
note.commands_changes = command_params.keys
......
......@@ -5,6 +5,9 @@
= 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'
.stats
%span.badge
= storage_counter(group.storage_size)
%span
= icon('bookmark')
= number_with_delimiter(group.projects.count)
......
......@@ -27,6 +27,8 @@
= sort_title_recently_updated
= link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do
= 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
New Group
%ul.content-list
......
......@@ -38,6 +38,18 @@
%strong
= @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
%span.light Group Git LFS status:
%strong
......@@ -66,8 +78,8 @@
%li
%strong
= link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
%span.label.label-gray
= repository_size(project)
%span.badge
= storage_counter(project.statistics.storage_size)
%span.pull-right.light
%span.monospace= project.path_with_namespace + ".git"
.panel-footer
......@@ -84,8 +96,8 @@
%li
%strong
= link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
%span.label.label-gray
= repository_size(project)
%span.badge
= storage_counter(project.statistics.storage_size)
%span.pull-right.light
%span.monospace= project.path_with_namespace + ".git"
......
......@@ -69,8 +69,8 @@
.controls
- if project.archived
%span.label.label-warning archived
%span.label.label-gray
= repository_size(project)
%span.badge
= 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 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove"
.title
......
......@@ -65,9 +65,16 @@
= @project.repository.path_to_repo
%li
%span.light Size
%strong
= repository_size(@project)
%span.light Storage:
%strong= storage_counter(@project.statistics.storage_size)
(
= 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
%span.light last commit:
......
......@@ -18,8 +18,8 @@
.pull-right
- if project.archived
%span.label.label-warning archived
%span.label.label-gray
= repository_size(project)
%span.badge
= 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 '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"
......
......@@ -21,7 +21,7 @@
- if diff_file.deleted_file
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?
%small
......
- if can?(current_user, :push_code, @project)
.pull-right
#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),
method: :post, class: 'btn btn-new btn-inverted btn-grouped has-tooltip available hide', title: @issue.to_branch_name do
New branch
......
......@@ -8,7 +8,9 @@
= @teams.one? ? 'The team' : 'Select the team'
where the slash commands will be used in
- 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
- if @teams.one?
This is the only team where you are an administrator.
......
......@@ -10,7 +10,7 @@
.timeline-content
.note-header
= link_to_member(note.project, note.author, avatar: false)
.inline.note-headline-light
.note-headline-light
= note.author.to_reference
- unless note.system
commented
......
......@@ -19,10 +19,10 @@
%ul.nav
%li
= link_to project_files_path(@project) do
Files (#{repository_size})
Files (#{storage_counter(@project.statistics.total_repository_size)})
%li
= 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
= link_to namespace_project_branches_path(@project.namespace, @project) do
#{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
......@@ -72,8 +72,8 @@
= link_to 'Set up Koding', add_koding_stack_path(@project)
- if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
%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
Set up autodeploy
= 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 auto deploy
- if @repository.commit
.project-last-commit{ class: container_class }
......
......@@ -7,26 +7,27 @@ class ProjectCacheWorker
LEASE_TIMEOUT = 15.minutes.to_i
# 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
# 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)
return unless project && project.repository.exists?
update_repository_size(project)
project.update_commit_count
update_statistics(project, statistics.map(&:to_sym))
project.repository.refresh_method_caches(refresh.map(&:to_sym))
project.repository.refresh_method_caches(files.map(&:to_sym))
end
def update_repository_size(project)
return unless try_obtain_lease_for(project.id, :update_repository_size)
def update_statistics(project, statistics = [])
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
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 @@
# end
#
ActiveSupport::Inflector.inflections do |inflect|
inflect.uncountable %w(award_emoji)
inflect.uncountable %w(award_emoji project_statistics)
inflect.acronym 'EE'
end
Rails.application.configure do |config|
config.middleware.use(Gitlab::Middleware::Multipart)
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
DOWNTIME = false
def up
disable_statement_timeout
execute("DELETE FROM services WHERE active = false AND properties = '{}';")
end
......
......@@ -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
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|
t.string "name"
t.string "path"
......@@ -1042,7 +1055,6 @@ ActiveRecord::Schema.define(version: 20161221140236) do
t.boolean "archived", default: false, null: false
t.string "avatar"
t.string "import_status"
t.float "repository_size", default: 0.0
t.text "merge_requests_template"
t.integer "star_count", default: 0, null: false
t.boolean "merge_requests_rebase_enabled", default: false
......@@ -1050,7 +1062,6 @@ ActiveRecord::Schema.define(version: 20161221140236) do
t.string "import_source"
t.integer "approvals_before_merge", default: 0, null: false
t.boolean "reset_approvals_on_push", default: true
t.integer "commit_count", default: 0
t.boolean "merge_requests_ff_only_enabled", default: false
t.text "issues_template"
t.boolean "mirror", default: false, null: false
......@@ -1488,6 +1499,7 @@ ActiveRecord::Schema.define(version: 20161221140236) do
add_foreign_key "project_authorizations", "projects", 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 "project_statistics", "projects", on_delete: :cascade
add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_merge_access_levels", "users"
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
[reconfigure 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:
| `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` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) |
```
GET /groups
......@@ -31,7 +32,6 @@ GET /groups
You can search for groups by name or path, see below.
=======
## List owned groups
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
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `statistics` | boolean | no | Include group statistics |
## List a group's projects
Get a list of projects in this group.
......
......@@ -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` |
| `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 |
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
| `statistics` | boolean | no | Include project statistics |
### List starred projects
......@@ -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` |
| `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 |
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
### List ALL projects
......@@ -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` |
| `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 |
| `statistics` | boolean | no | Include project statistics |
### Get single project
......
......@@ -23,7 +23,7 @@
- [CI/CD pipelines settings](../user/project/pipelines/settings.md)
- [Review Apps](review_apps/index.md)
- [Git submodules](git_submodules.md) Using Git submodules in your CI jobs
- [Autodeploy](autodeploy/index.md)
- [Auto deploy](autodeploy/index.md)
## Breaking changes
......
# Autodeploy
# Auto deploy
> [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`
templates for various infrastructure providers and deployment scripts
powering them. These scripts are responsible for packaging your application,
......@@ -15,7 +15,7 @@ deployment.
## 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
......@@ -24,17 +24,17 @@ credentials. For example, if you want to deploy to OpenShift you have to
enable [Kubernetes service][kubernetes-service].
1. Configure GitLab Runner to use Docker or Kubernetes executor with
[privileged mode enabled][docker-in-docker].
1. Navigate to the "Project" tab and click "Set up autodeploy" button.
![Autodeploy button](img/autodeploy_button.png)
1. Navigate to the "Project" tab and click "Set up auto deploy" button.
![Auto deploy button](img/auto_deploy_button.png)
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. Test your deployment configuration using a [Review App][review-app] that was
created automatically for you.
[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135
[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
[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor
[review-app]: ../review_apps/index.md
......@@ -101,3 +101,11 @@ The form should be titled `Edit issue`. The submit button should be labeled `Sav
| 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|
| 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`:
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
* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets)
......@@ -47,3 +53,5 @@ In `config/gitlab.yml`:
* Currently, removing LFS objects from GitLab Git LFS storage is not supported
* 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.
* 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
To prevent this from happening, set the lfs url in project Git config:
```bash
git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs"
```
......
......@@ -88,21 +88,21 @@ module API
expose :container_registry_enabled
# 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(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:user]) }
expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:user]) }
expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:user]) }
expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, 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[:current_user]) }
expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
expose(:builds_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
expose :created_at, :last_activity_at
expose :shared_runners_enabled
expose :lfs_enabled?, as: :lfs_enabled
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 :avatar_url
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 :public_builds
expose :shared_with_groups do |project, options|
......@@ -113,6 +113,16 @@ module API
expose :request_access_enabled
expose :only_allow_merge_if_all_discussions_are_resolved
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
class Member < UserBasic
......@@ -149,6 +159,15 @@ module API
expose :avatar_url
expose :web_url
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
class GroupDetail < Group
......@@ -431,7 +450,7 @@ module API
end
class Namespace < Grape::Entity
expose :id, :path, :kind
expose :id, :name, :path, :kind
end
class MemberAccess < Grape::Entity
......@@ -480,12 +499,12 @@ module API
class ProjectWithAccess < Project
expose :permissions do
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
expose :group_access, using: Entities::GroupAccess do |project, options|
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
......
......@@ -19,6 +19,20 @@ module API
optional :ldap_access, type: Integer, desc: 'A valid access level'
all_or_none_of :ldap_cn, :ldap_access
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
resource :groups do
......@@ -26,6 +40,7 @@ module API
success Entities::Group
end
params do
use :statistics_params
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 :search, type: String, desc: 'Search for a specific group'
......@@ -46,7 +61,7 @@ module API
groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
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
desc 'Get list of owned groups for authenticated user' do
......@@ -54,10 +69,10 @@ module API
end
params do
use :pagination
use :statistics_params
end
get '/owned' do
groups = current_user.owned_groups
present paginate(groups), with: Entities::Group, user: current_user
present_groups current_user.owned_groups, statistics: params[:statistics]
end
desc 'Create a group. Available only for users who can create groups.' do
......@@ -88,7 +103,7 @@ module API
)
end
present group, with: Entities::Group
present group, with: Entities::Group, current_user: current_user
else
render_api_error!("Failed to save group #{group.errors.messages}", 400)
end
......@@ -114,7 +129,7 @@ module API
authorize! :admin_group, group
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
render_validation_error!(group)
end
......@@ -125,7 +140,7 @@ module API
end
get ":id" do
group = find_group!(params[:id])
present group, with: Entities::GroupDetail
present group, with: Entities::GroupDetail, current_user: current_user
end
desc 'Remove a group.'
......@@ -156,7 +171,7 @@ module API
projects = GroupProjectsFinder.new(group).execute(current_user)
projects = filter_projects(projects)
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
desc 'Transfer a project to the group namespace. Available only for admin.' do
......@@ -172,7 +187,7 @@ module API
result = ::Projects::TransferService.new(project, current_user).execute(group)
if result
present group, with: Entities::GroupDetail
present group, with: Entities::GroupDetail, current_user: current_user
else
render_api_error!("Failed to transfer project #{project.errors.messages}", 400)
end
......
......@@ -259,7 +259,7 @@ module API
rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500)
end
# Projects helpers
# project helpers
def filter_projects(projects)
if params[:search].present?
......
......@@ -44,6 +44,15 @@ module API
resource :projects 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
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'
......@@ -56,97 +65,94 @@ module API
optional :visibility, type: String, values: %w[public internal private],
desc: 'Limit by visibility'
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
params :create_params do
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'
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
desc 'Get a list of visible projects for authenticated user' do
success Entities::BasicProjectDetails
end
params do
optional :simple, type: Boolean, default: false,
desc: 'Return only the ID, URL, name, and path of each project'
use :filter_params
use :pagination
use :collection_params
end
get '/visible' do
projects = ProjectsFinder.new.execute(current_user)
projects = filter_projects(projects)
entity = params[:simple] || !current_user ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
present paginate(projects), with: entity, user: current_user
entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
present_projects ProjectsFinder.new.execute(current_user), with: entity
end
desc 'Get a projects list for authenticated user' do
success Entities::BasicProjectDetails
end
params do
optional :simple, type: Boolean, default: false,
desc: 'Return only the ID, URL, name, and path of each project'
use :filter_params
use :pagination
use :collection_params
end
get do
authenticate!
projects = current_user.authorized_projects
projects = filter_projects(projects)
entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
present paginate(projects), with: entity, user: current_user
present_projects current_user.authorized_projects,
with: Entities::ProjectWithAccess
end
desc 'Get an owned projects list for authenticated user' do
success Entities::BasicProjectDetails
end
params do
use :filter_params
use :pagination
use :collection_params
use :statistics_params
end
get '/owned' do
authenticate!
projects = current_user.owned_projects
projects = filter_projects(projects)
present paginate(projects), with: Entities::ProjectWithAccess, user: current_user
present_projects current_user.owned_projects,
with: Entities::ProjectWithAccess,
statistics: params[:statistics]
end
desc 'Gets starred project for the authenticated user' do
success Entities::BasicProjectDetails
end
params do
use :filter_params
use :pagination
use :collection_params
end
get '/starred' do
authenticate!
projects = current_user.viewable_starred_projects
projects = filter_projects(projects)
present paginate(projects), with: Entities::Project, user: current_user
present_projects current_user.viewable_starred_projects
end
desc 'Get all projects for admin user' do
success Entities::BasicProjectDetails
end
params do
use :filter_params
use :pagination
use :collection_params
use :statistics_params
end
get '/all' do
authenticated_as_admin!
projects = Project.all
projects = filter_projects(projects)
present paginate(projects), with: Entities::ProjectWithAccess, user: current_user
present_projects Project.all, with: Entities::ProjectWithAccess, statistics: params[:statistics]
end
desc 'Search for projects the current user has access to' do
......@@ -225,7 +231,7 @@ module API
end
get ":id" do
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)
end
......
......@@ -49,8 +49,9 @@ module Gitlab
end
def url(subject)
polymorphic_url(
[ subject.project.namespace.becomes(Namespace), subject.project, subject ])
project = subject.project
namespace_project_build_url(project.namespace.becomes(Namespace), project, subject)
end
end
end
......
......@@ -30,12 +30,12 @@ module Gitlab
if subject.is_a?(Gitlab::ChatCommands::Result)
show_result(subject)
elsif subject.respond_to?(:count)
if subject.many?
multiple_resources(subject)
elsif subject.none?
if subject.none?
not_found
elsif subject.one?
single_resource(subject.first)
else
single_resource(subject)
multiple_resources(subject)
end
else
single_resource(subject)
......@@ -71,9 +71,9 @@ module Gitlab
end
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)
end
......
......@@ -61,7 +61,10 @@ module Gitlab
end
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
def cache_key
......
......@@ -15,7 +15,7 @@ module Gitlab
{
'General' => '',
'Pages' => 'Pages',
'Autodeploy' => 'autodeploy'
'Auto deploy' => 'autodeploy'
}
end
......@@ -28,7 +28,7 @@ module Gitlab
end
def dropdown_names(context)
categories = context == 'autodeploy' ? ['Autodeploy'] : ['General', 'Pages']
categories = context == 'autodeploy' ? ['Auto deploy'] : ['General', 'Pages']
super().slice(*categories)
end
end
......
......@@ -63,8 +63,7 @@ namespace :gitlab do
if project.persisted?
puts " * Created #{project.name} (#{repo_path})".color(:green)
project.update_repository_size
project.update_commit_count
ProjectCacheWorker.perform(project.id)
else
puts " * Failed trying to create #{project.name} (#{repo_path})".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
FactoryGirl.define do
factory :lfs_object do
oid "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80"
sequence(:oid) { |n| "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a%05x" % n }
size 499013
end
......
FactoryGirl.define do
factory :project_statistics do
project { create :project }
namespace { project.namespace }
end
end
......@@ -26,7 +26,7 @@ describe 'Auto deploy' do
it 'does not show a button to set up auto deploy' do
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
......@@ -37,11 +37,11 @@ describe 'Auto deploy' do
end
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
it 'includes Kubernetes as an available template', js: true do
click_link 'Set up autodeploy'
it 'includes OpenShift as an available template', js: true do
click_link 'Set up auto deploy'
click_button 'Choose a GitLab CI Yaml template'
within '.gitlab-ci-yml-selector' do
......@@ -49,8 +49,8 @@ describe 'Auto deploy' do
end
end
it 'creates a merge request using "autodeploy" branch', js: true do
click_link 'Set up autodeploy'
it 'creates a merge request using "auto-deploy" branch', js: true do
click_link 'Set up auto deploy'
click_button 'Choose a GitLab CI Yaml template'
within '.gitlab-ci-yml-selector' do
click_on 'OpenShift'
......@@ -58,7 +58,7 @@ describe 'Auto deploy' do
wait_for_ajax
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
......@@ -47,7 +47,7 @@ feature 'GFM autocomplete', feature: true, js: true do
expect_to_wrap(true, label_item, note, label.title)
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')
page.within '.timeline-content-form' do
note.native.send_keys('')
......@@ -65,6 +65,17 @@ feature 'GFM autocomplete', feature: true, js: true do
expect(page).to have_selector('.atwho-container', visible: false)
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
note = find('#note_note')
page.within '.timeline-content-form' do
......
......@@ -30,7 +30,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
write_note("/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
......@@ -51,7 +51,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
write_note("/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
......@@ -70,7 +70,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
write_note("/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
......@@ -91,7 +91,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
write_note("/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
......
......@@ -371,23 +371,25 @@ describe 'Issues', feature: true do
describe 'when I want to reset my incoming email token' do
let(:project1) { create(:project, namespace: @user.namespace) }
let(:issue) { create(:issue, project: project1) }
let!(:issue) { create(:issue, project: project1) }
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.issues << issue
visit namespace_project_issues_path(@user.namespace, project1)
end
it 'changes incoming email address token', js: true do
find('.issue-email-modal-btn').click
previous_token = find('input#issue_email').value
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
......
......@@ -31,7 +31,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
write_note("/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
end
......@@ -42,7 +42,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
write_note("/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
end
......@@ -61,7 +61,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
write_note("/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
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
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
let(:params) { { text: 'deploy staging to production' } }
let!(:build) { create(:ci_build, project: project) }
......
require 'spec_helper'
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))
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 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)
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
......@@ -212,6 +212,7 @@ project:
- path_locks
- approver_groups
- route
- statistics
award_emoji:
- awardable
- user
......
......@@ -85,4 +85,30 @@ describe Ci::Build, models: true do
it { expect(build.trace_file_path).to eq(build.old_path_to_trace) }
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
......@@ -464,6 +464,19 @@ describe Ci::Pipeline, models: true do
end
end
describe '.latest_successful_for' do
include_context 'with some outdated pipelines'
let!(:latest_successful_pipeline) do
create_pipeline(:success, 'ref', 'D')
end
it 'returns the latest successful pipeline' do
expect(described_class.latest_successful_for('ref')).
to eq(latest_successful_pipeline)
end
end
describe '#status' do
let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') }
......
require 'spec_helper'
describe LfsObjectsProject, models: true do
subject { create(:lfs_objects_project, project: project) }
let(:project) { create(:empty_project) }
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:lfs_object) }
end
describe 'validation' do
it { is_expected.to validate_presence_of(:lfs_object_id) }
it { is_expected.to validate_uniqueness_of(:lfs_object_id).scoped_to(:project_id).with_message("already exists in project") }
it { is_expected.to validate_presence_of(:project_id) }
end
describe '#update_project_statistics' do
it 'updates project statistics when the object is added' do
expect(ProjectCacheWorker).to receive(:perform_async)
.with(project.id, [], [:lfs_objects_size])
subject.save!
end
it 'updates project statistics when the object is removed' do
subject.save!
expect(ProjectCacheWorker).to receive(:perform_async)
.with(project.id, [], [:lfs_objects_size])
subject.destroy
end
end
end
......@@ -4,6 +4,7 @@ describe Namespace, models: true do
let!(:namespace) { create(:namespace) }
it { is_expected.to have_many :projects }
it { is_expected.to have_many :project_statistics }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) }
......@@ -57,6 +58,50 @@ describe Namespace, models: true do
end
end
describe '.with_statistics' do
let(:namespace) { create :namespace }
let(:project1) do
create(:empty_project,
namespace: namespace,
statistics: build(:project_statistics,
storage_size: 606,
repository_size: 101,
lfs_objects_size: 202,
build_artifacts_size: 303))
end
let(:project2) do
create(:empty_project,
namespace: namespace,
statistics: build(:project_statistics,
storage_size: 60,
repository_size: 10,
lfs_objects_size: 20,
build_artifacts_size: 30))
end
it "sums all project storage counters in the namespace" do
project1
project2
statistics = Namespace.with_statistics.find(namespace.id)
expect(statistics.storage_size).to eq 666
expect(statistics.repository_size).to eq 111
expect(statistics.lfs_objects_size).to eq 222
expect(statistics.build_artifacts_size).to eq 333
end
it "correctly handles namespaces without projects" do
statistics = Namespace.with_statistics.find(namespace.id)
expect(statistics.storage_size).to eq 0
expect(statistics.repository_size).to eq 0
expect(statistics.lfs_objects_size).to eq 0
expect(statistics.build_artifacts_size).to eq 0
end
end
describe '#move_dir' do
before do
@namespace = create :namespace
......
......@@ -6,7 +6,7 @@ describe MattermostSlashCommandsService, :models do
context 'Mattermost API' do
let(:project) { create(:empty_project) }
let(:service) { project.build_mattermost_slash_commands_service }
let(:user) { create(:user)}
let(:user) { create(:user) }
before do
Mattermost::Session.base_uri("http://mattermost.example.com")
......
......@@ -49,6 +49,7 @@ describe Project, models: true do
it { is_expected.to have_one(:gitlab_issue_tracker_service).dependent(:destroy) }
it { is_expected.to have_one(:external_wiki_service).dependent(:destroy) }
it { is_expected.to have_one(:project_feature).dependent(:destroy) }
it { is_expected.to have_one(:statistics).class_name('ProjectStatistics').dependent(:delete) }
it { is_expected.to have_one(:import_data).class_name('ProjectImportData').dependent(:destroy) }
it { is_expected.to have_one(:last_event).class_name('Event') }
it { is_expected.to have_one(:forked_from_project).through(:forked_project_link) }
......@@ -2081,6 +2082,26 @@ describe Project, models: true do
end
end
describe '#update_project_statistics' do
let(:project) { create(:empty_project) }
it "is called after creation" do
expect(project.statistics).to be_a ProjectStatistics
expect(project.statistics).to be_persisted
end
it "copies the namespace_id" do
expect(project.statistics.namespace_id).to eq project.namespace_id
end
it "updates the namespace_id when changed" do
namespace = create(:namespace)
project.update(namespace: namespace)
expect(project.statistics.namespace_id).to eq namespace.id
end
end
def enable_lfs
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
end
......
require 'rails_helper'
describe ProjectStatistics, models: true do
let(:project) { create :empty_project }
let(:statistics) { project.statistics }
describe 'constants' do
describe 'STORAGE_COLUMNS' do
it 'is an array of symbols' do
expect(described_class::STORAGE_COLUMNS).to be_kind_of Array
expect(described_class::STORAGE_COLUMNS.map(&:class).uniq).to eq [Symbol]
end
end
describe 'STATISTICS_COLUMNS' do
it 'is an array of symbols' do
expect(described_class::STATISTICS_COLUMNS).to be_kind_of Array
expect(described_class::STATISTICS_COLUMNS.map(&:class).uniq).to eq [Symbol]
end
it 'includes all storage columns' do
expect(described_class::STATISTICS_COLUMNS & described_class::STORAGE_COLUMNS).to eq described_class::STORAGE_COLUMNS
end
end
end
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:namespace) }
end
describe 'statistics columns' do
it "support values up to 8 exabytes" do
statistics.update!(
commit_count: 8.exabytes - 1,
repository_size: 2.exabytes,
lfs_objects_size: 2.exabytes,
build_artifacts_size: 4.exabytes - 1,
)
statistics.reload
expect(statistics.commit_count).to eq(8.exabytes - 1)
expect(statistics.repository_size).to eq(2.exabytes)
expect(statistics.lfs_objects_size).to eq(2.exabytes)
expect(statistics.build_artifacts_size).to eq(4.exabytes - 1)
expect(statistics.storage_size).to eq(8.exabytes - 1)
end
end
describe '#total_repository_size' do
it "sums repository and LFS object size" do
statistics.repository_size = 2
statistics.lfs_objects_size = 3
statistics.build_artifacts_size = 4
expect(statistics.total_repository_size).to eq 5
end
end
describe '#refresh!' do
before do
allow(statistics).to receive(:update_commit_count)
allow(statistics).to receive(:update_repository_size)
allow(statistics).to receive(:update_lfs_objects_size)
allow(statistics).to receive(:update_build_artifacts_size)
allow(statistics).to receive(:update_storage_size)
end
context "without arguments" do
before do
statistics.refresh!
end
it "sums all counters" do
expect(statistics).to have_received(:update_commit_count)
expect(statistics).to have_received(:update_repository_size)
expect(statistics).to have_received(:update_lfs_objects_size)
expect(statistics).to have_received(:update_build_artifacts_size)
end
end
context "when passing an only: argument" do
before do
statistics.refresh! only: [:lfs_objects_size]
end
it "only updates the given columns" do
expect(statistics).to have_received(:update_lfs_objects_size)
expect(statistics).not_to have_received(:update_commit_count)
expect(statistics).not_to have_received(:update_repository_size)
expect(statistics).not_to have_received(:update_build_artifacts_size)
end
end
end
describe '#update_commit_count' do
before do
allow(project.repository).to receive(:commit_count).and_return(23)
statistics.update_commit_count
end
it "stores the number of commits in the repository" do
expect(statistics.commit_count).to eq 23
end
end
describe '#update_repository_size' do
before do
allow(project.repository).to receive(:size).and_return(12.megabytes)
statistics.update_repository_size
end
it "stores the size of the repository" do
expect(statistics.repository_size).to eq 12.megabytes
end
end
describe '#update_lfs_objects_size' do
let!(:lfs_object1) { create(:lfs_object, size: 23.megabytes) }
let!(:lfs_object2) { create(:lfs_object, size: 34.megabytes) }
let!(:lfs_objects_project1) { create(:lfs_objects_project, project: project, lfs_object: lfs_object1) }
let!(:lfs_objects_project2) { create(:lfs_objects_project, project: project, lfs_object: lfs_object2) }
before do
statistics.update_lfs_objects_size
end
it "stores the size of related LFS objects" do
expect(statistics.lfs_objects_size).to eq 57.megabytes
end
end
describe '#update_build_artifacts_size' do
let!(:pipeline) { create(:ci_pipeline, project: project) }
let!(:build1) { create(:ci_build, pipeline: pipeline, artifacts_size: 45.megabytes) }
let!(:build2) { create(:ci_build, pipeline: pipeline, artifacts_size: 56.megabytes) }
before do
statistics.update_build_artifacts_size
end
it "stores the size of related build artifacts" do
expect(statistics.build_artifacts_size).to eq 101.megabytes
end
end
describe '#update_storage_size' do
it "sums all storage counters" do
statistics.update!(
repository_size: 2,
lfs_objects_size: 3,
)
statistics.reload
expect(statistics.storage_size).to eq 5
end
end
end
......@@ -45,6 +45,14 @@ describe API::Groups, api: true do
expect(ldap_group_link['group_access']).to eq(group1.ldap_access)
expect(ldap_group_link['provider']).to eq('ldap')
end
it "does not include statistics" do
get api("/groups", user1), statistics: true
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first).not_to include 'statistics'
end
end
context "when authenticated as admin" do
......@@ -54,6 +62,31 @@ describe API::Groups, api: true do
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
end
it "does not include statistics by default" do
get api("/groups", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first).not_to include('statistics')
end
it "includes statistics if requested" do
attributes = {
storage_size: 702,
repository_size: 123,
lfs_objects_size: 234,
build_artifacts_size: 345,
}
project1.statistics.update!(attributes)
get api("/groups", admin), statistics: true
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['statistics']).to eq attributes.stringify_keys
end
end
context "when using skip_groups in request" do
......
......@@ -49,7 +49,7 @@ describe API::Projects, api: true do
end
end
context 'when authenticated' do
context 'when authenticated as regular user' do
it 'returns an array of projects' do
get api('/projects', user)
expect(response).to have_http_status(200)
......@@ -172,6 +172,22 @@ describe API::Projects, api: true do
end
end
end
it "does not include statistics by default" do
get api('/projects/all', admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first).not_to include('statistics')
end
it "includes statistics if requested" do
get api('/projects/all', admin), statistics: true
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first).to include 'statistics'
end
end
end
......@@ -196,6 +212,32 @@ describe API::Projects, api: true do
expect(json_response.first['name']).to eq(project4.name)
expect(json_response.first['owner']['username']).to eq(user4.username)
end
it "does not include statistics by default" do
get api('/projects/owned', user4)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first).not_to include('statistics')
end
it "includes statistics if requested" do
attributes = {
commit_count: 23,
storage_size: 702,
repository_size: 123,
lfs_objects_size: 234,
build_artifacts_size: 345,
}
project4.statistics.update!(attributes)
get api('/projects/owned', user4), statistics: true
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['statistics']).to eq attributes.stringify_keys
end
end
end
......@@ -630,6 +672,18 @@ describe API::Projects, api: true do
expect(json_response['name']).to eq(project.name)
end
it 'exposes namespace fields' do
get api("/projects/#{project.id}", user)
expect(response).to have_http_status(200)
expect(json_response['namespace']).to eq({
'id' => user.namespace.id,
'name' => user.namespace.name,
'path' => user.namespace.path,
'kind' => user.namespace.kind,
})
end
describe 'permissions' do
context 'all projects' do
before { project.team << [user, :master] }
......
......@@ -607,7 +607,7 @@ describe GitPushService, services: true do
service.push_commits = [commit]
expect(ProjectCacheWorker).to receive(:perform_async).
with(project.id, %i(readme))
with(project.id, %i(readme), %i(commit_count repository_size))
service.update_caches
end
......@@ -620,7 +620,7 @@ describe GitPushService, services: true do
it 'does not flush any conditional caches' do
expect(ProjectCacheWorker).to receive(:perform_async).
with(project.id, []).
with(project.id, [], %i(commit_count repository_size)).
and_call_original
service.update_caches
......@@ -628,6 +628,25 @@ describe GitPushService, services: true do
end
end
describe '#process_commit_messages' do
let(:service) do
described_class.new(project,
user,
oldrev: sample_commit.parent_id,
newrev: sample_commit.id,
ref: 'refs/heads/master')
end
it 'only schedules a limited number of commits' do
allow(service).to receive(:push_commits).
and_return(Array.new(1000, double(:commit, to_hash: {})))
expect(ProcessCommitWorker).to receive(:perform_async).exactly(100).times
service.process_commit_messages
end
end
def execute_service(project, user, oldrev, newrev, ref)
service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref )
service.execute
......
......@@ -11,6 +11,7 @@ describe MergeRequests::MergeRequestDiffCacheService do
expect(Rails.cache).to receive(:read).with(cache_key).and_return({})
expect(Rails.cache).to receive(:write).with(cache_key, anything)
allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => true))
allow_any_instance_of(Repository).to receive(:diffable?).and_return(true)
subject.execute(merge_request)
end
......
......@@ -76,7 +76,7 @@ shared_examples 'issuable record that supports slash commands in its description
expect(page).not_to have_content '/assign @bob'
expect(page).not_to have_content '/label ~bug'
expect(page).not_to have_content '/milestone %"ASAP"'
expect(page).to have_content 'Your commands have been executed!'
expect(page).to have_content 'Commands applied'
issuable.reload
......@@ -97,7 +97,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/close")
expect(page).not_to have_content '/close'
expect(page).to have_content 'Your commands have been executed!'
expect(page).to have_content 'Commands applied'
expect(issuable.reload).to be_closed
end
......@@ -114,7 +114,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/close")
expect(page).not_to have_content '/close'
expect(page).not_to have_content 'Your commands have been executed!'
expect(page).not_to have_content 'Commands applied'
expect(issuable).to be_open
end
......@@ -132,7 +132,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/reopen")
expect(page).not_to have_content '/reopen'
expect(page).to have_content 'Your commands have been executed!'
expect(page).to have_content 'Commands applied'
expect(issuable.reload).to be_open
end
......@@ -149,7 +149,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/reopen")
expect(page).not_to have_content '/reopen'
expect(page).not_to have_content 'Your commands have been executed!'
expect(page).not_to have_content 'Commands applied'
expect(issuable).to be_closed
end
......@@ -162,7 +162,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/title Awesome new title")
expect(page).not_to have_content '/title'
expect(page).to have_content 'Your commands have been executed!'
expect(page).to have_content 'Commands applied'
expect(issuable.reload.title).to eq 'Awesome new title'
end
......@@ -179,7 +179,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/title Awesome new title")
expect(page).not_to have_content '/title'
expect(page).not_to have_content 'Your commands have been executed!'
expect(page).not_to have_content 'Commands applied'
expect(issuable.reload.title).not_to eq 'Awesome new title'
end
......@@ -191,7 +191,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/todo")
expect(page).not_to have_content '/todo'
expect(page).to have_content 'Your commands have been executed!'
expect(page).to have_content 'Commands applied'
todos = TodosFinder.new(master).execute
todo = todos.first
......@@ -222,7 +222,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/done")
expect(page).not_to have_content '/done'
expect(page).to have_content 'Your commands have been executed!'
expect(page).to have_content 'Commands applied'
expect(todo.reload).to be_done
end
......@@ -235,7 +235,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/subscribe")
expect(page).not_to have_content '/subscribe'
expect(page).to have_content 'Your commands have been executed!'
expect(page).to have_content 'Commands applied'
expect(issuable.subscribed?(master, project)).to be_truthy
end
......@@ -252,7 +252,7 @@ shared_examples 'issuable record that supports slash commands in its description
write_note("/unsubscribe")
expect(page).not_to have_content '/unsubscribe'
expect(page).to have_content 'Your commands have been executed!'
expect(page).to have_content 'Commands applied'
expect(issuable.subscribed?(master, project)).to be_falsy
end
......
......@@ -105,7 +105,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
allow(chat_service).to receive(:username).and_return(username)
expect(Slack::Notifier).to receive(:new).
with(webhook_url, username: username, channel: chat_service.default_channel).
with(webhook_url, username: username).
and_return(
double(:slack_service).as_null_object
)
......
require 'spec_helper'
describe ProjectCacheWorker do
let(:project) { create(:project) }
let(:worker) { described_class.new }
let(:project) { create(:project) }
let(:statistics) { project.statistics }
describe '#perform' do
before do
......@@ -12,7 +13,7 @@ describe ProjectCacheWorker do
context 'with a non-existing project' do
it 'does nothing' do
expect(worker).not_to receive(:update_repository_size)
expect(worker).not_to receive(:update_statistics)
worker.perform(-1)
end
......@@ -22,24 +23,19 @@ describe ProjectCacheWorker do
it 'does nothing' do
allow_any_instance_of(Repository).to receive(:exists?).and_return(false)
expect(worker).not_to receive(:update_repository_size)
expect(worker).not_to receive(:update_statistics)
worker.perform(project.id)
end
end
context 'with an existing project' do
it 'updates the repository size' do
expect(worker).to receive(:update_repository_size).and_call_original
worker.perform(project.id)
end
it 'updates the commit count' do
expect_any_instance_of(Project).to receive(:update_commit_count).
and_call_original
it 'updates the project statistics' do
expect(worker).to receive(:update_statistics)
.with(kind_of(Project), %i(repository_size))
.and_call_original
worker.perform(project.id)
worker.perform(project.id, [], %w(repository_size))
end
it 'refreshes the method caches' do
......@@ -47,7 +43,7 @@ describe ProjectCacheWorker do
with(%i(readme)).
and_call_original
worker.perform(project.id, %i(readme))
worker.perform(project.id, %w(readme))
end
context 'when in Geo secondary node' do
......@@ -68,28 +64,30 @@ describe ProjectCacheWorker do
end
end
describe '#update_repository_size' do
describe '#update_statistics' do
context 'when a lease could not be obtained' do
it 'does not update the repository size' do
allow(worker).to receive(:try_obtain_lease_for).
with(project.id, :update_repository_size).
with(project.id, :update_statistics).
and_return(false)
expect(project).not_to receive(:update_repository_size)
expect(statistics).not_to receive(:refresh!)
worker.update_repository_size(project)
worker.update_statistics(project)
end
end
context 'when a lease could be obtained' do
it 'updates the repository size' do
it 'updates the project statistics' do
allow(worker).to receive(:try_obtain_lease_for).
with(project.id, :update_repository_size).
with(project.id, :update_statistics).
and_return(true)
expect(project).to receive(:update_repository_size).and_call_original
expect(statistics).to receive(:refresh!)
.with(only: %i(repository_size))
.and_call_original
worker.update_repository_size(project)
worker.update_statistics(project, %i(repository_size))
end
end
end
......
Leiningen.gitignore
\ No newline at end of file
pom.xml
pom.xml.asc
*.jar
*.class
/lib/
/classes/
/target/
/checkouts/
.lein-deps-sum
.lein-repl-history
.lein-plugins/
.lein-failures
.nrepl-port
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