Commit 483cfb7a authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-02-06

# Conflicts:
#	app/models/ci/job_artifact.rb
#	app/views/layouts/nav/sidebar/_group.html.haml
#	app/views/projects/clusters/index.html.haml
#	app/workers/build_finished_worker.rb
#	lib/api/users.rb
#	locale/gitlab.pot
#	spec/javascripts/clusters/clusters_bundle_spec.js
#	spec/models/ci/job_artifact_spec.rb
#	spec/models/project_services/kubernetes_service_spec.rb
#	spec/requests/api/jobs_spec.rb
#	spec/uploaders/job_artifact_uploader_spec.rb

[ci skip]
parents 79f6765f 7095c2bf
...@@ -427,6 +427,8 @@ end ...@@ -427,6 +427,8 @@ end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.83.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.83.0', require: 'gitaly'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -365,7 +365,7 @@ GEM ...@@ -365,7 +365,7 @@ GEM
mime-types (~> 3.0) mime-types (~> 3.0)
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.0) retriable (>= 2.0, < 4.0)
google-protobuf (3.5.1.1) google-protobuf (3.5.1)
googleapis-common-protos-types (1.0.1) googleapis-common-protos-types (1.0.1)
google-protobuf (~> 3.0) google-protobuf (~> 3.0)
googleauth (0.5.3) googleauth (0.5.3)
...@@ -1102,6 +1102,7 @@ DEPENDENCIES ...@@ -1102,6 +1102,7 @@ DEPENDENCIES
gollum-rugged_adapter (~> 0.4.4) gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0) gon (~> 6.1.0)
google-api-client (~> 0.13.6) google-api-client (~> 0.13.6)
google-protobuf (= 3.5.1)
gpgme gpgme
grape (~> 1.0) grape (~> 1.0)
grape-entity (~> 0.6.0) grape-entity (~> 0.6.0)
......
...@@ -35,10 +35,11 @@ export default class Clusters { ...@@ -35,10 +35,11 @@ export default class Clusters {
clusterStatus, clusterStatus,
clusterStatusReason, clusterStatusReason,
helpPath, helpPath,
ingressHelpPath,
} = document.querySelector('.js-edit-cluster-form').dataset; } = document.querySelector('.js-edit-cluster-form').dataset;
this.store = new ClustersStore(); this.store = new ClustersStore();
this.store.setHelpPath(helpPath); this.store.setHelpPaths(helpPath, ingressHelpPath);
this.store.updateStatus(clusterStatus); this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason); this.store.updateStatusReason(clusterStatusReason);
this.service = new ClustersService({ this.service = new ClustersService({
...@@ -93,6 +94,7 @@ export default class Clusters { ...@@ -93,6 +94,7 @@ export default class Clusters {
props: { props: {
applications: this.state.applications, applications: this.state.applications,
helpPath: this.state.helpPath, helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath,
}, },
}); });
}, },
......
...@@ -18,6 +18,11 @@ ...@@ -18,6 +18,11 @@
required: false, required: false,
default: '', default: '',
}, },
ingressHelpPath: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
generalApplicationDescription() { generalApplicationDescription() {
...@@ -59,13 +64,28 @@ ...@@ -59,13 +64,28 @@
false, false,
); );
const externalIpParagraph = sprintf(
_.escape(s__(
`ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS
at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}`,
)), {
ingressHelpLink: `<a href="${this.ingressHelpPath}">
${_.escape(s__('ClusterIntegration|More information'))}
</a>`,
},
false,
);
return ` return `
<p> <p>
${descriptionParagraph} ${descriptionParagraph}
</p> </p>
<p class="append-bottom-0"> <p>
${extraCostParagraph} ${extraCostParagraph}
</p> </p>
<p class="settings-message append-bottom-0">
${externalIpParagraph}
</p>
`; `;
}, },
gitlabRunnerDescription() { gitlabRunnerDescription() {
......
...@@ -4,6 +4,7 @@ export default class ClusterStore { ...@@ -4,6 +4,7 @@ export default class ClusterStore {
constructor() { constructor() {
this.state = { this.state = {
helpPath: null, helpPath: null,
ingressHelpPath: null,
status: null, status: null,
statusReason: null, statusReason: null,
applications: { applications: {
...@@ -39,8 +40,9 @@ export default class ClusterStore { ...@@ -39,8 +40,9 @@ export default class ClusterStore {
}; };
} }
setHelpPath(helpPath) { setHelpPaths(helpPath, ingressHelpPath) {
this.state.helpPath = helpPath; this.state.helpPath = helpPath;
this.state.ingressHelpPath = ingressHelpPath;
} }
updateStatus(status) { updateStatus(status) {
......
...@@ -4,6 +4,11 @@ ...@@ -4,6 +4,11 @@
.page-title { .page-title {
margin-top: 0; margin-top: 0;
.color-label {
font-size: $gl-font-size;
padding: $gl-vert-padding $label-padding-modal;
}
} }
} }
......
...@@ -565,6 +565,7 @@ $jq-ui-default-color: #777; ...@@ -565,6 +565,7 @@ $jq-ui-default-color: #777;
* Label * Label
*/ */
$label-padding: 7px; $label-padding: 7px;
$label-padding-modal: 10px;
$label-gray-bg: #f8fafc; $label-gray-bg: #f8fafc;
$label-inverse-bg: #333; $label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1); $label-remove-border: rgba(0, 0, 0, .1);
......
...@@ -58,13 +58,13 @@ ...@@ -58,13 +58,13 @@
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
width: 200px; width: 200px;
margin-left: $gl-padding * 2;
margin-bottom: 0; margin-bottom: 0;
} }
.label { .label {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
vertical-align: middle;
max-width: 100%; max-width: 100%;
} }
} }
...@@ -79,26 +79,33 @@ ...@@ -79,26 +79,33 @@
width: 100px; width: 100px;
margin-left: 10px; margin-left: 10px;
margin-bottom: 0; margin-bottom: 0;
vertical-align: middle; vertical-align: top;
} }
} }
.label-description { .label-description {
display: block; display: block;
margin-bottom: 10px; margin-bottom: 10px;
margin-left: 50px;
.description-text {
margin-bottom: $gl-padding;
}
a {
color: $blue-600;
}
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
display: inline-block; display: inline-block;
width: 30%; max-width: 50%;
margin-left: 10px; margin-left: 10px;
margin-bottom: 0; margin-bottom: 0;
vertical-align: middle; vertical-align: top;
} }
} }
.label { .label {
padding: 8px 9px 9px; padding: 8px 12px;
font-size: 14px; font-size: 14px;
} }
} }
...@@ -116,6 +123,12 @@ ...@@ -116,6 +123,12 @@
} }
.manage-labels-list { .manage-labels-list {
@media(min-width: $screen-md-min) {
&.content-list li {
padding: $gl-padding 0;
}
}
> li:not(.empty-message):not(.is-not-draggable) { > li:not(.empty-message):not(.is-not-draggable) {
background-color: $white-light; background-color: $white-light;
cursor: move; cursor: move;
...@@ -133,8 +146,6 @@ ...@@ -133,8 +146,6 @@
} }
.btn-action { .btn-action {
color: $gl-text-color;
.fa { .fa {
font-size: 18px; font-size: 18px;
vertical-align: middle; vertical-align: middle;
...@@ -155,10 +166,18 @@ ...@@ -155,10 +166,18 @@
float: right; float: right;
} }
} }
@media (max-width: $screen-xs-max) {
.dropdown-menu {
min-width: 100%;
}
}
} }
.draggable-handler { .draggable-handler {
display: inline-block; display: inline-block;
vertical-align: top;
margin: 5px 0;
opacity: 0; opacity: 0;
transition: opacity .3s; transition: opacity .3s;
color: $gray-darkest; color: $gray-darkest;
...@@ -188,7 +207,7 @@ ...@@ -188,7 +207,7 @@
.toggle-priority { .toggle-priority {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: top;
button { button {
border-color: transparent; border-color: transparent;
...@@ -255,6 +274,11 @@ ...@@ -255,6 +274,11 @@
} }
.label-subscribe-button { .label-subscribe-button {
@media(min-width: $screen-md-min) {
min-width: 105px;
margin-left: $gl-padding;
}
.label-subscribe-button-icon { .label-subscribe-button-icon {
&[disabled] { &[disabled] {
opacity: 0.5; opacity: 0.5;
......
...@@ -68,7 +68,7 @@ class Groups::LabelsController < Groups::ApplicationController ...@@ -68,7 +68,7 @@ class Groups::LabelsController < Groups::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
redirect_to group_labels_path(@group), status: 302, notice: 'Label was removed' redirect_to group_labels_path(@group), status: 302, notice: "#{@label.name} deleted permanently"
end end
format.js format.js
end end
......
...@@ -76,7 +76,7 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -76,7 +76,7 @@ class Projects::WikisController < Projects::ApplicationController
@page = @project_wiki.find_page(params[:id]) @page = @project_wiki.find_page(params[:id])
if @page if @page
@page_versions = Kaminari.paginate_array(@page.versions(page: params[:page]), @page_versions = Kaminari.paginate_array(@page.versions(page: params[:page].to_i),
total_count: @page.count_versions) total_count: @page.count_versions)
.page(params[:page]) .page(params[:page])
else else
......
module GroupsHelper module GroupsHelper
def group_nav_link_paths
%w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]
end
def can_change_group_visibility_level?(group) def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group) can?(current_user, :change_visibility_level, group)
end end
......
...@@ -11,12 +11,15 @@ module Ci ...@@ -11,12 +11,15 @@ module Ci
mount_uploader :file, JobArtifactUploader mount_uploader :file, JobArtifactUploader
<<<<<<< HEAD
after_save if: :file_changed?, on: [:create, :update] do after_save if: :file_changed?, on: [:create, :update] do
run_after_commit do run_after_commit do
file.schedule_migration_to_object_storage file.schedule_migration_to_object_storage
end end
end end
=======
>>>>>>> upstream/master
delegate :open, :exists?, to: :file delegate :open, :exists?, to: :file
enum file_type: { enum file_type: {
......
...@@ -520,10 +520,13 @@ class Project < ActiveRecord::Base ...@@ -520,10 +520,13 @@ class Project < ActiveRecord::Base
@repository ||= Repository.new(full_path, self, disk_path: disk_path) @repository ||= Repository.new(full_path, self, disk_path: disk_path)
end end
def reload_repository! def cleanup
@repository&.cleanup
@repository = nil @repository = nil
end end
alias_method :reload_repository!, :cleanup
def container_registry_url def container_registry_url
if Gitlab.config.registry.enabled if Gitlab.config.registry.enabled
"#{Gitlab.config.registry.host_port}/#{full_path.downcase}" "#{Gitlab.config.registry.host_port}/#{full_path.downcase}"
......
...@@ -100,6 +100,10 @@ class Repository ...@@ -100,6 +100,10 @@ class Repository
alias_method :raw, :raw_repository alias_method :raw, :raw_repository
def cleanup
@raw_repository&.cleanup
end
# Return absolute path to repository # Return absolute path to repository
def path_to_repo def path_to_repo
@path_to_repo ||= File.expand_path( @path_to_repo ||= File.expand_path(
......
module Files module Files
class CreateService < Files::BaseService class CreateService < Files::BaseService
def create_commit! def create_commit!
handler = Lfs::FileModificationHandler.new(project, @branch_name)
handler.new_file(@file_path, @file_content) do |content_or_lfs_pointer|
create_transformed_commit(content_or_lfs_pointer)
end
end
private
def create_transformed_commit(content_or_lfs_pointer)
repository.create_file( repository.create_file(
current_user, current_user,
@file_path, @file_path,
@file_content, content_or_lfs_pointer,
message: @commit_message, message: @commit_message,
branch_name: @branch_name, branch_name: @branch_name,
author_email: @author_email, author_email: @author_email,
......
module Lfs
class FileModificationHandler
attr_reader :project, :branch_name
delegate :repository, to: :project
def initialize(project, branch_name)
@project = project
@branch_name = branch_name
end
def new_file(file_path, file_content)
if project.lfs_enabled? && lfs_file?(file_path)
lfs_pointer_file = Gitlab::Git::LfsPointerFile.new(file_content)
lfs_object = create_lfs_object!(lfs_pointer_file, file_content)
content = lfs_pointer_file.pointer
success = yield(content)
link_lfs_object!(lfs_object) if success
else
yield(file_content)
end
end
private
def lfs_file?(file_path)
repository.attributes_at(branch_name, file_path)['filter'] == 'lfs'
end
def create_lfs_object!(lfs_pointer_file, file_content)
LfsObject.find_or_create_by(oid: lfs_pointer_file.sha256, size: lfs_pointer_file.size) do |lfs_object|
lfs_object.file = CarrierWaveStringFile.new(file_content)
end
end
def link_lfs_object!(lfs_object)
project.lfs_objects << lfs_object
end
end
end
...@@ -17,6 +17,8 @@ class FileUploader < GitlabUploader ...@@ -17,6 +17,8 @@ class FileUploader < GitlabUploader
after :remove, :prune_store_dir after :remove, :prune_store_dir
after :remove, :prune_store_dir
def self.root def self.root
File.join(options.storage_path, 'uploads') File.join(options.storage_path, 'uploads')
end end
......
...@@ -107,7 +107,11 @@ ...@@ -107,7 +107,11 @@
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
#{ _('Members') } #{ _('Members') }
- if current_user && can?(current_user, :admin_group, @group) - if current_user && can?(current_user, :admin_group, @group)
<<<<<<< HEAD
= nav_link(path: %w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]) do = nav_link(path: %w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]) do
=======
= nav_link(path: group_nav_link_paths) do
>>>>>>> upstream/master
= link_to edit_group_path(@group) do = link_to edit_group_path(@group) do
.nav-icon-container .nav-icon-container
= sprite_icon('settings') = sprite_icon('settings')
......
...@@ -220,7 +220,7 @@ ...@@ -220,7 +220,7 @@
%p %p
= _('Protip:') = _('Protip:')
= link_to 'Auto DevOps', help_page_path('topics/autodevops/index.md') = link_to 'Auto DevOps', help_page_path('topics/autodevops/index.md')
%span= _('uses clusters to deploy your code!') %span= _('uses Kubernetes clusters to deploy your code!')
%hr %hr
%button.btn.btn-create.btn-xs.dismiss-feature-highlight{ type: 'button' } %button.btn.btn-create.btn-xs.dismiss-feature-highlight{ type: 'button' }
%span= _("Got it!") %span= _("Got it!")
......
...@@ -8,7 +8,10 @@ ...@@ -8,7 +8,10 @@
.top-area.adjust .top-area.adjust
.nav-text .nav-text
= s_("ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project") = s_("ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project")
<<<<<<< HEAD
= render 'projects/ee/clusters/buttons', project: @project = render 'projects/ee/clusters/buttons', project: @project
=======
>>>>>>> upstream/master
.ci-table.js-clusters-list .ci-table.js-clusters-list
.gl-responsive-table-row.table-row-header{ role: "row" } .gl-responsive-table-row.table-row-header{ role: "row" }
.table-section.section-30{ role: "rowheader" } .table-section.section-30{ role: "rowheader" }
......
...@@ -13,7 +13,8 @@ ...@@ -13,7 +13,8 @@
toggle_status: @cluster.enabled? ? 'true': 'false', toggle_status: @cluster.enabled? ? 'true': 'false',
cluster_status: @cluster.status_name, cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason, cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications') } } help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address') } }
.js-cluster-application-notice .js-cluster-application-notice
.flash-container .flash-container
......
.modal{ id: "modal-delete-label-#{label.id}", tabindex: -1 }
.modal-dialog
.modal-content
.modal-header
%button.close{ data: {dismiss: 'modal' } } &times;
%h3.page-title Delete #{render_colored_label(label, tooltip: false)} ?
.modal-body
%p
%strong= label.name
%span will be permanently deleted from #{label.is_a?(ProjectLabel)? label.project.name : label.group.name}. This cannot be undone.
.modal-footer
%a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' } Cancel
= link_to 'Delete label',
destroy_label_path(label),
title: 'Delete',
method: :delete,
class: 'btn btn-remove'
...@@ -5,10 +5,10 @@ ...@@ -5,10 +5,10 @@
- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project) - show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project) - show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project)
%li{ id: label_css_id, data: { id: label.id } } %li.label-list-item{ id: label_css_id, data: { id: label.id } }
= render "shared/label_row", label: label = render "shared/label_row", label: label
.visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown .visible-xs.visible-sm-inline-block.dropdown
%button.btn.btn-default.label-options-toggle{ type: 'button', data: { toggle: "dropdown" } } %button.btn.btn-default.label-options-toggle{ type: 'button', data: { toggle: "dropdown" } }
Options Options
= icon('caret-down') = icon('caret-down')
...@@ -46,14 +46,19 @@ ...@@ -46,14 +46,19 @@
data: {confirm: 'Remove this label? Are you sure?'}, data: {confirm: 'Remove this label? Are you sure?'},
class: 'text-danger' class: 'text-danger'
.pull-right.hidden-xs.hidden-sm.hidden-md .pull-right.hidden-xs.hidden-sm
- if show_label_merge_requests_link - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group)
= link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action btn-link') do = link_to promote_project_label_path(label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "You are about to promote #{label.title} to a group level. This will make this milestone available to all projects inside #{label.project.group.name}. The existing project label will be merged into the group level. This action cannot be reversed.", toggle: "tooltip"}, method: :post do
view merge requests %span.sr-only Promote to Group
- if show_label_issues_link = sprite_icon('level-up')
= link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action btn-link') do - if can?(current_user, :admin_label, label)
view open issues = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
%span.sr-only Edit
= sprite_icon('pencil')
%span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } }
= link_to "#", title: "Delete", class: 'btn btn-transparent btn-action remove-row', data: { toggle: "tooltip" } do
%span.sr-only Delete
= sprite_icon('remove')
- if current_user - if current_user
.label-subscription.inline .label-subscription.inline
- if can_subscribe_to_label_in_different_levels?(label) - if can_subscribe_to_label_in_different_levels?(label)
...@@ -76,14 +81,4 @@ ...@@ -76,14 +81,4 @@
%span= label_subscription_toggle_button_text(label, @project) %span= label_subscription_toggle_button_text(label, @project)
= icon('spinner spin', class: 'label-subscribe-button-loading') = icon('spinner spin', class: 'label-subscribe-button-loading')
- if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) = render 'shared/delete_label_modal', label: label
= link_to promote_project_label_path(label.project, label), title: "Promote to Group Label", class: 'btn btn-transparent btn-action', data: {confirm: "You are about to promote #{label.title} to a group level. This will make this milestone available to all projects inside #{label.project.group.name}. The existing project label will be merged into the group level. This action cannot be reversed.", toggle: "tooltip"}, method: :post do
%span.sr-only Promote to Group
= icon('level-up')
- if can?(current_user, :admin_label, label)
= link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do
%span.sr-only Edit
= icon('pencil-square-o')
= link_to destroy_label_path(label), title: "Delete", class: 'btn btn-transparent btn-action remove-row', method: :delete, data: {confirm: label_deletion_confirm_text(label), toggle: "tooltip"} do
%span.sr-only Delete
= icon('trash-o')
- subject = local_assigns[:subject]
- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project)
- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
%span.label-row %span.label-row
- if can?(current_user, :admin_label, @project) - if can?(current_user, :admin_label, @project)
.draggable-handler .draggable-handler
...@@ -13,6 +17,14 @@ ...@@ -13,6 +17,14 @@
- if defined?(@project) && @project.group.present? - if defined?(@project) && @project.group.present?
%span.label-type %span.label-type
= label.model_name.human.titleize = label.model_name.human.titleize
- if label.description
%span.label-description %span.label-description
- if label.description.present?
.description-text
= markdown_field(label, :description) = markdown_field(label, :description)
.hidden-xs.hidden-sm
- if show_label_issues_link
= link_to_label(label, subject: subject) { 'Issues' }
- if show_label_merge_requests_link
&middot;
= link_to_label(label, subject: subject, type: :merge_request) { 'Merge requests' }
...@@ -6,8 +6,11 @@ class BuildFinishedWorker ...@@ -6,8 +6,11 @@ class BuildFinishedWorker
def perform(build_id) def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build| Ci::Build.find_by(id: build_id).try do |build|
<<<<<<< HEAD
UpdateBuildMinutesService.new(build.project, nil).execute(build) UpdateBuildMinutesService.new(build.project, nil).execute(build)
=======
>>>>>>> upstream/master
# We execute that in sync as this access the files in order to access local file, and reduce IO # We execute that in sync as this access the files in order to access local file, and reduce IO
BuildTraceSectionsWorker.new.perform(build.id) BuildTraceSectionsWorker.new.perform(build.id)
BuildCoverageWorker.new.perform(build.id) BuildCoverageWorker.new.perform(build.id)
......
...@@ -19,6 +19,8 @@ class ProjectCacheWorker ...@@ -19,6 +19,8 @@ class ProjectCacheWorker
update_statistics(project, statistics.map(&:to_sym)) update_statistics(project, statistics.map(&:to_sym))
project.repository.refresh_method_caches(files.map(&:to_sym)) project.repository.refresh_method_caches(files.map(&:to_sym))
project.cleanup
end end
def update_statistics(project, statistics = []) def update_statistics(project, statistics = [])
......
---
title: Add sorting options for /users API (admin only)
merge_request: 16945
author:
type: added
---
title: Add a link to documentation on how to get external ip in the Kubernetes cluster details page
merge_request: 16937
author:
type: added
---
title: Close low level rugged repository in project cache worker
merge_request: 16930
author: Bastian Blank
type: fixed
---
title: Upgrade GitLab Workhorse to v3.6.0
merge_request:
author:
type: other
---
title: Override group sidebar links
merge_request: 16942
author: George Tsiolis
type: fixed
---
title: File Upload UI can create LFS pointers based on .gitattributes
merge_request: 16412
author:
type: fixed
---
title: Downgrade google-protobuf gem
merge_request: 16941
author:
type: other
...@@ -53,6 +53,11 @@ GET /users?blocked=true ...@@ -53,6 +53,11 @@ GET /users?blocked=true
GET /users GET /users
``` ```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `order_by` | string | no | Return projects ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
```json ```json
[ [
{ {
......
...@@ -154,8 +154,8 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps ...@@ -154,8 +154,8 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
step 'commit has ci status' do step 'commit has ci status' do
@project.enable_ci @project.enable_ci
pipeline = create :ci_pipeline, project: @project, sha: sample_commit.id @pipeline = create(:ci_pipeline, project: @project, sha: sample_commit.id)
create :ci_build, pipeline: pipeline create(:ci_build, pipeline: @pipeline)
end end
step 'repository contains ".gitlab-ci.yml" file' do step 'repository contains ".gitlab-ci.yml" file' do
...@@ -163,7 +163,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps ...@@ -163,7 +163,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end end
step 'I see commit ci info' do step 'I see commit ci info' do
expect(page).to have_content "Pipeline #1 pending" expect(page).to have_content "Pipeline ##{@pipeline.id} pending"
end end
step 'I search "submodules" commits' do step 'I search "submodules" commits' do
......
...@@ -15,8 +15,9 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps ...@@ -15,8 +15,9 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I delete all labels' do step 'I delete all labels' do
page.within '.labels' do page.within '.labels' do
page.all('.remove-row').each do page.all('.label-list-item').each do
accept_confirm { first('.remove-row').click } first('.remove-row').click
first(:link, 'Delete label').click
end end
end end
end end
......
...@@ -18,6 +18,14 @@ module API ...@@ -18,6 +18,14 @@ module API
User.find_by(id: id) || not_found!('User') User.find_by(id: id) || not_found!('User')
end end
def reorder_users(users)
if params[:order_by] && params[:sort]
users.reorder(params[:order_by] => params[:sort])
else
users
end
end
params :optional_attributes do params :optional_attributes do
optional :skype, type: String, desc: 'The Skype username' optional :skype, type: String, desc: 'The Skype username'
optional :linkedin, type: String, desc: 'The LinkedIn username' optional :linkedin, type: String, desc: 'The LinkedIn username'
...@@ -38,6 +46,13 @@ module API ...@@ -38,6 +46,13 @@ module API
# EE # EE
optional :shared_runners_minutes_limit, type: Integer, desc: 'Pipeline minutes quota for this user' optional :shared_runners_minutes_limit, type: Integer, desc: 'Pipeline minutes quota for this user'
end end
params :sort_params do
optional :order_by, type: String, values: %w[id name username created_at updated_at],
default: 'id', desc: 'Return users ordered by a field'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return users sorted in ascending and descending order'
end
end end
desc 'Get the list of users' do desc 'Get the list of users' do
...@@ -56,19 +71,24 @@ module API ...@@ -56,19 +71,24 @@ module API
optional :created_before, type: DateTime, desc: 'Return users created before the specified time' optional :created_before, type: DateTime, desc: 'Return users created before the specified time'
all_or_none_of :extern_uid, :provider all_or_none_of :extern_uid, :provider
<<<<<<< HEAD
# EE # EE
optional :skip_ldap, type: Boolean, default: false, desc: 'Skip LDAP users' optional :skip_ldap, type: Boolean, default: false, desc: 'Skip LDAP users'
=======
use :sort_params
>>>>>>> upstream/master
use :pagination use :pagination
end end
get do get do
authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?) authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?)
unless current_user&.admin? unless current_user&.admin?
params.except!(:created_after, :created_before) params.except!(:created_after, :created_before, :order_by, :sort)
end end
users = UsersFinder.new(current_user, params).execute users = UsersFinder.new(current_user, params).execute
users = reorder_users(users)
authorized = can?(current_user, :read_users_list) authorized = can?(current_user, :read_users_list)
......
class CarrierWaveStringFile < StringIO
def original_filename
""
end
end
...@@ -15,8 +15,8 @@ module Gitlab ...@@ -15,8 +15,8 @@ module Gitlab
.ancestor?(oldrev, newrev) .ancestor?(oldrev, newrev)
else else
Gitlab::Git::RevList.new( Gitlab::Git::RevList.new(
path_to_repo: project.repository.path_to_repo, project.repository.raw, oldrev: oldrev, newrev: newrev
oldrev: oldrev, newrev: newrev).missed_ref.present? ).missed_ref.present?
end end
end end
end end
......
...@@ -25,8 +25,7 @@ module Gitlab ...@@ -25,8 +25,7 @@ module Gitlab
private private
def rev_list def rev_list
::Gitlab::Git::RevList.new(path_to_repo: @repository.path_to_repo, Gitlab::Git::RevList.new(@repository, newrev: @newrev)
newrev: @newrev)
end end
end end
end end
......
module Gitlab
module Git
class LfsPointerFile
def initialize(data)
@data = data
end
def pointer
@pointer ||= <<~FILE
version https://git-lfs.github.com/spec/v1
oid sha256:#{sha256}
size #{size}
FILE
end
def size
@size ||= @data.bytesize
end
def sha256
@sha256 ||= Digest::SHA256.hexdigest(@data)
end
end
end
end
...@@ -25,7 +25,7 @@ module Gitlab ...@@ -25,7 +25,7 @@ module Gitlab
stdin.close stdin.close
if lazy_block if lazy_block
return lazy_block.call(stdout.lazy) return [lazy_block.call(stdout.lazy), 0]
else else
cmd_output << stdout.read cmd_output << stdout.read
end end
......
...@@ -128,6 +128,10 @@ module Gitlab ...@@ -128,6 +128,10 @@ module Gitlab
raise NoRepository.new('no repository for such path') raise NoRepository.new('no repository for such path')
end end
def cleanup
@rugged&.close
end
def circuit_breaker def circuit_breaker
@circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(storage) @circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(storage)
end end
...@@ -1427,6 +1431,26 @@ module Gitlab ...@@ -1427,6 +1431,26 @@ module Gitlab
end end
end end
def rev_list(including: [], excluding: [], objects: false, &block)
args = ['rev-list']
args.push(*rev_list_param(including))
exclude_param = *rev_list_param(excluding)
if exclude_param.any?
args.push('--not')
args.push(*exclude_param)
end
args.push('--objects') if objects
run_git!(args, lazy_block: block)
end
def missed_ref(oldrev, newrev)
run_git!(['rev-list', '--max-count=1', oldrev, "^#{newrev}"])
end
private private
def local_write_ref(ref_path, ref, old_ref: nil, shell: true) def local_write_ref(ref_path, ref, old_ref: nil, shell: true)
...@@ -1475,7 +1499,7 @@ module Gitlab ...@@ -1475,7 +1499,7 @@ module Gitlab
Rails.logger.error "Unable to create #{ref_path} reference for repository #{path}: #{ex}" Rails.logger.error "Unable to create #{ref_path} reference for repository #{path}: #{ex}"
end end
def run_git(args, chdir: path, env: {}, nice: false, &block) def run_git(args, chdir: path, env: {}, nice: false, lazy_block: nil, &block)
cmd = [Gitlab.config.git.bin_path, *args] cmd = [Gitlab.config.git.bin_path, *args]
cmd.unshift("nice") if nice cmd.unshift("nice") if nice
...@@ -1485,12 +1509,12 @@ module Gitlab ...@@ -1485,12 +1509,12 @@ module Gitlab
end end
circuit_breaker.perform do circuit_breaker.perform do
popen(cmd, chdir, env, &block) popen(cmd, chdir, env, lazy_block: lazy_block, &block)
end end
end end
def run_git!(args, chdir: path, env: {}, nice: false, &block) def run_git!(args, chdir: path, env: {}, nice: false, lazy_block: nil, &block)
output, status = run_git(args, chdir: chdir, env: env, nice: nice, &block) output, status = run_git(args, chdir: chdir, env: env, nice: nice, lazy_block: lazy_block, &block)
raise GitError, output unless status.zero? raise GitError, output unless status.zero?
...@@ -2372,6 +2396,10 @@ module Gitlab ...@@ -2372,6 +2396,10 @@ module Gitlab
rescue Rugged::ReferenceError rescue Rugged::ReferenceError
0 0
end end
def rev_list_param(spec)
spec == :all ? ['--all'] : spec
end
end end
end end
end end
...@@ -5,17 +5,17 @@ module Gitlab ...@@ -5,17 +5,17 @@ module Gitlab
class RevList class RevList
include Gitlab::Git::Popen include Gitlab::Git::Popen
attr_reader :oldrev, :newrev, :path_to_repo attr_reader :oldrev, :newrev, :repository
def initialize(path_to_repo:, newrev:, oldrev: nil) def initialize(repository, newrev:, oldrev: nil)
@oldrev = oldrev @oldrev = oldrev
@newrev = newrev @newrev = newrev
@path_to_repo = path_to_repo @repository = repository
end end
# This method returns an array of new commit references # This method returns an array of new commit references
def new_refs def new_refs
execute([*base_args, newrev, '--not', '--all']) repository.rev_list(including: newrev, excluding: :all).split("\n")
end end
# Finds newly added objects # Finds newly added objects
...@@ -28,67 +28,40 @@ module Gitlab ...@@ -28,67 +28,40 @@ module Gitlab
# When given a block it will yield objects as a lazy enumerator so # When given a block it will yield objects as a lazy enumerator so
# the caller can limit work done instead of processing megabytes of data # the caller can limit work done instead of processing megabytes of data
def new_objects(require_path: nil, not_in: nil, &lazy_block) def new_objects(require_path: nil, not_in: nil, &lazy_block)
args = [*base_args, newrev, *not_in_refs(not_in), '--objects'] opts = {
including: newrev,
excluding: not_in.nil? ? :all : not_in,
require_path: require_path
}
get_objects(args, require_path: require_path, &lazy_block) get_objects(opts, &lazy_block)
end end
def all_objects(require_path: nil, &lazy_block) def all_objects(require_path: nil, &lazy_block)
args = [*base_args, '--all', '--objects'] get_objects(including: :all, require_path: require_path, &lazy_block)
get_objects(args, require_path: require_path, &lazy_block)
end end
# This methods returns an array of missed references # This methods returns an array of missed references
# #
# Should become obsolete after https://gitlab.com/gitlab-org/gitaly/issues/348. # Should become obsolete after https://gitlab.com/gitlab-org/gitaly/issues/348.
def missed_ref def missed_ref
execute([*base_args, '--max-count=1', oldrev, "^#{newrev}"]) repository.missed_ref(oldrev, newrev).split("\n")
end end
private private
def not_in_refs(references)
return ['--not', '--all'] unless references
return [] if references.empty?
references.prepend('--not')
end
def execute(args) def execute(args)
output, status = popen(args, nil, Gitlab::Git::Env.to_env_hash) repository.rev_list(args).split("\n")
unless status.zero?
raise "Got a non-zero exit code while calling out `#{args.join(' ')}`: #{output}"
end
output.split("\n")
end
def lazy_execute(args, &lazy_block)
popen(args, nil, Gitlab::Git::Env.to_env_hash, lazy_block: lazy_block)
end end
def base_args def get_objects(including: [], excluding: [], require_path: nil)
[ opts = { including: including, excluding: excluding, objects: true }
Gitlab.config.git.bin_path,
"--git-dir=#{path_to_repo}",
'rev-list'
]
end
def get_objects(args, require_path: nil) repository.rev_list(opts) do |lazy_output|
if block_given?
lazy_execute(args) do |lazy_output|
objects = objects_from_output(lazy_output, require_path: require_path) objects = objects_from_output(lazy_output, require_path: require_path)
yield(objects) yield(objects)
end end
else
object_output = execute(args)
objects_from_output(object_output, require_path: require_path)
end
end end
def objects_from_output(object_output, require_path: nil) def objects_from_output(object_output, require_path: nil)
......
...@@ -96,6 +96,16 @@ module Gitlab ...@@ -96,6 +96,16 @@ module Gitlab
# :per_page - The number of items per page. # :per_page - The number of items per page.
# :limit - Total number of items to return. # :limit - Total number of items to return.
def page_versions(page_path, options = {}) def page_versions(page_path, options = {})
@repository.gitaly_migrate(:wiki_page_versions) do |is_enabled|
if is_enabled
versions = gitaly_wiki_client.page_versions(page_path, options)
# Gitaly uses gollum-lib to get the versions. Gollum defaults to 20
# per page, but also fetches 20 if `limit` or `per_page` < 20.
# Slicing returns an array with the expected number of items.
slice_bound = options[:limit] || options[:per_page] || Gollum::Page.per_page
versions[0..slice_bound]
else
current_page = gollum_page_by_path(page_path) current_page = gollum_page_by_path(page_path)
commits_from_page(current_page, options).map do |gitlab_git_commit| commits_from_page(current_page, options).map do |gitlab_git_commit|
...@@ -103,6 +113,8 @@ module Gitlab ...@@ -103,6 +113,8 @@ module Gitlab
Gitlab::Git::WikiPageVersion.new(gitlab_git_commit, gollum_page&.format) Gitlab::Git::WikiPageVersion.new(gitlab_git_commit, gollum_page&.format)
end end
end end
end
end
def count_page_versions(page_path) def count_page_versions(page_path)
@repository.count_commits(ref: 'HEAD', path: page_path) @repository.count_commits(ref: 'HEAD', path: page_path)
......
...@@ -101,6 +101,30 @@ module Gitlab ...@@ -101,6 +101,30 @@ module Gitlab
pages pages
end end
# options:
# :page - The Integer page number.
# :per_page - The number of items per page.
# :limit - Total number of items to return.
def page_versions(page_path, options)
request = Gitaly::WikiGetPageVersionsRequest.new(
repository: @gitaly_repo,
page_path: encode_binary(page_path),
page: options[:page] || 1,
per_page: options[:per_page] || Gollum::Page.per_page
)
stream = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_page_versions, request)
versions = []
stream.each do |message|
message.versions.each do |version|
versions << new_wiki_page_version(version)
end
end
versions
end
def find_file(name, revision) def find_file(name, revision)
request = Gitaly::WikiFindFileRequest.new( request = Gitaly::WikiFindFileRequest.new(
repository: @gitaly_repo, repository: @gitaly_repo,
...@@ -141,7 +165,7 @@ module Gitlab ...@@ -141,7 +165,7 @@ module Gitlab
private private
# If a block is given and the yielded value is true, iteration will be # If a block is given and the yielded value is truthy, iteration will be
# stopped early at that point; else the iterator is consumed entirely. # stopped early at that point; else the iterator is consumed entirely.
# The iterator is traversed with `next` to allow resuming the iteration. # The iterator is traversed with `next` to allow resuming the iteration.
def wiki_page_from_iterator(iterator) def wiki_page_from_iterator(iterator)
...@@ -158,10 +182,7 @@ module Gitlab ...@@ -158,10 +182,7 @@ module Gitlab
else else
wiki_page = GitalyClient::WikiPage.new(page.to_h) wiki_page = GitalyClient::WikiPage.new(page.to_h)
version = Gitlab::Git::WikiPageVersion.new( version = new_wiki_page_version(page.version)
Gitlab::Git::Commit.decorate(@repository, page.version.commit),
page.version.format
)
end end
end end
...@@ -170,6 +191,13 @@ module Gitlab ...@@ -170,6 +191,13 @@ module Gitlab
[wiki_page, version] [wiki_page, version]
end end
def new_wiki_page_version(version)
Gitlab::Git::WikiPageVersion.new(
Gitlab::Git::Commit.decorate(@repository, version.commit),
version.format
)
end
def gitaly_commit_details(commit_details) def gitaly_commit_details(commit_details)
Gitaly::WikiCommitDetails.new( Gitaly::WikiCommitDetails.new(
name: encode_binary(commit_details.name), name: encode_binary(commit_details.name),
......
...@@ -8,8 +8,13 @@ msgid "" ...@@ -8,8 +8,13 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
<<<<<<< HEAD
"POT-Creation-Date: 2018-02-06 12:32+0100\n" "POT-Creation-Date: 2018-02-06 12:32+0100\n"
"PO-Revision-Date: 2018-02-06 12:32+0100\n" "PO-Revision-Date: 2018-02-06 12:32+0100\n"
=======
"POT-Creation-Date: 2018-02-06 10:02+0100\n"
"PO-Revision-Date: 2018-02-06 10:02+0100\n"
>>>>>>> upstream/master
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -165,9 +170,12 @@ msgstr "" ...@@ -165,9 +170,12 @@ msgstr ""
msgid "All" msgid "All"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "All changes are committed" msgid "All changes are committed"
msgstr "" msgstr ""
=======
>>>>>>> upstream/master
msgid "Allows you to add and manage Kubernetes clusters." msgid "Allows you to add and manage Kubernetes clusters."
msgstr "" msgstr ""
...@@ -177,10 +185,17 @@ msgstr "" ...@@ -177,10 +185,17 @@ msgstr ""
msgid "An error occurred when toggling the notification subscription" msgid "An error occurred when toggling the notification subscription"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "An error occurred when updating the issue weight" msgid "An error occurred when updating the issue weight"
msgstr "" msgstr ""
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again." msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
=======
msgid "An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again."
msgstr ""
msgid "An error occurred while fetching markdown preview"
>>>>>>> upstream/master
msgstr "" msgstr ""
msgid "An error occurred while fetching sidebar data" msgid "An error occurred while fetching sidebar data"
...@@ -195,12 +210,18 @@ msgstr "" ...@@ -195,12 +210,18 @@ msgstr ""
msgid "An error occurred while rendering KaTeX" msgid "An error occurred while rendering KaTeX"
msgstr "" msgstr ""
msgid "An error occurred while rendering preview broadcast message"
msgstr ""
msgid "An error occurred while retrieving calendar activity" msgid "An error occurred while retrieving calendar activity"
msgstr "" msgstr ""
msgid "An error occurred while retrieving diff" msgid "An error occurred while retrieving diff"
msgstr "" msgstr ""
msgid "An error occurred while validating username"
msgstr ""
msgid "An error occurred. Please try again." msgid "An error occurred. Please try again."
msgstr "" msgstr ""
...@@ -482,9 +503,12 @@ msgstr "" ...@@ -482,9 +503,12 @@ msgstr ""
msgid "Cannot modify managed Kubernetes cluster" msgid "Cannot modify managed Kubernetes cluster"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "Change Weight" msgid "Change Weight"
msgstr "" msgstr ""
=======
>>>>>>> upstream/master
msgid "ChangeTypeActionLabel|Pick into branch" msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "" msgstr ""
...@@ -632,12 +656,15 @@ msgstr "" ...@@ -632,12 +656,15 @@ msgstr ""
msgid "Clone repository" msgid "Clone repository"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "Close" msgid "Close"
msgstr "" msgstr ""
msgid "Closed" msgid "Closed"
msgstr "" msgstr ""
=======
>>>>>>> upstream/master
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster" msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr "" msgstr ""
...@@ -792,9 +819,12 @@ msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to crea ...@@ -792,9 +819,12 @@ msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to crea
msgstr "" msgstr ""
msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}" msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}"
<<<<<<< HEAD
msgstr "" msgstr ""
msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate" msgid "ClusterIntegration|Multiple Kubernetes clusters are available in GitLab Enterprise Edition Premium and Ultimate"
=======
>>>>>>> upstream/master
msgstr "" msgstr ""
msgid "ClusterIntegration|Note:" msgid "ClusterIntegration|Note:"
...@@ -1681,11 +1711,14 @@ msgstr "" ...@@ -1681,11 +1711,14 @@ msgstr ""
msgid "Install a Runner compatible with GitLab CI" msgid "Install a Runner compatible with GitLab CI"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "Instance" msgid "Instance"
msgid_plural "Instances" msgid_plural "Instances"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
=======
>>>>>>> upstream/master
msgid "Instance does not support multiple Kubernetes clusters" msgid "Instance does not support multiple Kubernetes clusters"
msgstr "" msgstr ""
...@@ -1901,9 +1934,12 @@ msgstr "" ...@@ -1901,9 +1934,12 @@ msgstr ""
msgid "More information is available|here" msgid "More information is available|here"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "Multiple issue boards" msgid "Multiple issue boards"
msgstr "" msgstr ""
=======
>>>>>>> upstream/master
msgid "New Issue" msgid "New Issue"
msgid_plural "New Issues" msgid_plural "New Issues"
msgstr[0] "" msgstr[0] ""
...@@ -2953,9 +2989,15 @@ msgstr "" ...@@ -2953,9 +2989,15 @@ msgstr ""
msgid "There are problems accessing Git storage: " msgid "There are problems accessing Git storage: "
msgstr "" msgstr ""
msgid "There was an error loading users activity calendar."
msgstr ""
msgid "There was an error saving your notification settings." msgid "There was an error saving your notification settings."
msgstr "" msgstr ""
msgid "There was an error subscribing to this label."
msgstr ""
msgid "There was an error when reseting email token." msgid "There was an error when reseting email token."
msgstr "" msgstr ""
...@@ -3704,5 +3746,9 @@ msgstr "" ...@@ -3704,5 +3746,9 @@ msgstr ""
msgid "username" msgid "username"
msgstr "" msgstr ""
<<<<<<< HEAD
msgid "uses clusters to deploy your code!" msgid "uses clusters to deploy your code!"
=======
msgid "uses Kubernetes clusters to deploy your code!"
>>>>>>> upstream/master
msgstr "" msgstr ""
...@@ -99,7 +99,7 @@ feature 'Prioritize labels' do ...@@ -99,7 +99,7 @@ feature 'Prioritize labels' do
expect(page).to have_content 'wontfix' expect(page).to have_content 'wontfix'
# Sort labels # Sort labels
drag_to(selector: '.js-prioritized-labels', from_index: 1, to_index: 2) drag_to(selector: '.label-list-item', from_index: 1, to_index: 2)
page.within('.prioritized-labels') do page.within('.prioritized-labels') do
expect(first('li')).to have_content('feature') expect(first('li')).to have_content('feature')
......
...@@ -71,7 +71,8 @@ describe('Clusters', () => { ...@@ -71,7 +71,8 @@ describe('Clusters', () => {
helm: { status: APPLICATION_INSTALLABLE, title: 'Helm Tiller' }, helm: { status: APPLICATION_INSTALLABLE, title: 'Helm Tiller' },
}); });
expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeNull(); const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
expect(flashMessage).toBeNull();
}); });
it('shows an alert when something gets newly installed', () => { it('shows an alert when something gets newly installed', () => {
...@@ -83,8 +84,14 @@ describe('Clusters', () => { ...@@ -83,8 +84,14 @@ describe('Clusters', () => {
helm: { status: APPLICATION_INSTALLED, title: 'Helm Tiller' }, helm: { status: APPLICATION_INSTALLED, title: 'Helm Tiller' },
}); });
<<<<<<< HEAD
expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeDefined(); expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeDefined();
expect(document.querySelector('.js-cluster-application-notice .flash-text').textContent.trim()).toEqual('Helm Tiller was successfully installed on your Kubernetes cluster'); expect(document.querySelector('.js-cluster-application-notice .flash-text').textContent.trim()).toEqual('Helm Tiller was successfully installed on your Kubernetes cluster');
=======
const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
expect(flashMessage).not.toBeNull();
expect(flashMessage.textContent.trim()).toEqual('Helm Tiller was successfully installed on your Kubernetes cluster');
>>>>>>> upstream/master
}); });
it('shows an alert when multiple things gets newly installed', () => { it('shows an alert when multiple things gets newly installed', () => {
...@@ -98,8 +105,14 @@ describe('Clusters', () => { ...@@ -98,8 +105,14 @@ describe('Clusters', () => {
ingress: { status: APPLICATION_INSTALLED, title: 'Ingress' }, ingress: { status: APPLICATION_INSTALLED, title: 'Ingress' },
}); });
<<<<<<< HEAD
expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeDefined(); expect(document.querySelector('.js-cluster-application-notice .flash-text')).toBeDefined();
expect(document.querySelector('.js-cluster-application-notice .flash-text').textContent.trim()).toEqual('Helm Tiller, Ingress was successfully installed on your Kubernetes cluster'); expect(document.querySelector('.js-cluster-application-notice .flash-text').textContent.trim()).toEqual('Helm Tiller, Ingress was successfully installed on your Kubernetes cluster');
=======
const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
expect(flashMessage).not.toBeNull();
expect(flashMessage.textContent.trim()).toEqual('Helm Tiller, Ingress was successfully installed on your Kubernetes cluster');
>>>>>>> upstream/master
}); });
}); });
......
...@@ -58,6 +58,7 @@ describe('Clusters Store', () => { ...@@ -58,6 +58,7 @@ describe('Clusters Store', () => {
expect(store.state).toEqual({ expect(store.state).toEqual({
helpPath: null, helpPath: null,
ingressHelpPath: null,
status: mockResponseData.status, status: mockResponseData.status,
statusReason: mockResponseData.status_reason, statusReason: mockResponseData.status_reason,
applications: { applications: {
......
...@@ -2,18 +2,20 @@ require 'spec_helper' ...@@ -2,18 +2,20 @@ require 'spec_helper'
describe Gitlab::Checks::ForcePush do describe Gitlab::Checks::ForcePush do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw }
context "exit code checking", :skip_gitaly_mock do context "exit code checking", :skip_gitaly_mock do
it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do
allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['normal output', 0]) allow(repository).to receive(:popen).and_return(['normal output', 0])
expect { described_class.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error expect { described_class.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error
end end
it "raises a runtime error if the `popen` call to git returns a non-zero exit code" do it "raises a GitError error if the `popen` call to git returns a non-zero exit code" do
allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['error', 1]) allow(repository).to receive(:popen).and_return(['error', 1])
expect { described_class.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError) expect { described_class.force_push?(project, 'oldrev', 'newrev') }
.to raise_error(Gitlab::Git::Repository::GitError)
end end
end end
end end
require 'spec_helper'
describe Gitlab::Git::LfsPointerFile do
let(:data) { "1234\n" }
subject { described_class.new(data) }
describe '#size' do
it 'counts the bytes' do
expect(subject.size).to eq 5
end
it 'handles non ascii data' do
expect(described_class.new("ääää").size).to eq 8
end
end
describe '#sha256' do
it 'hashes the content correctly' do
expect(subject.sha256).to eq 'a883dafc480d466ee04e0d6da986bd78eb1fdd2178d04693723da3a8f95d42f4'
end
end
describe '#pointer' do
it 'starts with the LFS version' do
expect(subject.pointer).to start_with('version https://git-lfs.github.com/spec/v1')
end
it 'includes sha256' do
expect(subject.pointer).to match(/^oid sha256:[0-9a-fA-F]{64}/)
end
it 'ends with the size' do
expect(subject.pointer).to end_with("\nsize 5\n")
end
end
end
require 'spec_helper' require 'spec_helper'
describe Gitlab::Git::RevList do describe Gitlab::Git::RevList do
let(:project) { create(:project, :repository) } let(:repository) { create(:project, :repository).repository.raw }
let(:rev_list) { described_class.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) } let(:rev_list) { described_class.new(repository, newrev: 'newrev') }
let(:env_hash) do let(:env_hash) do
{ {
'GIT_OBJECT_DIRECTORY' => 'foo', 'GIT_OBJECT_DIRECTORY' => 'foo',
'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar' 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
} }
end end
let(:command_env) { { 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'foo:bar' } }
before do before do
allow(Gitlab::Git::Env).to receive(:all).and_return(env_hash.symbolize_keys) allow(Gitlab::Git::Env).to receive(:all).and_return(env_hash)
end end
def args_for_popen(args_list) def args_for_popen(args_list)
[ [Gitlab.config.git.bin_path, 'rev-list', *args_list]
Gitlab.config.git.bin_path,
"--git-dir=#{project.repository.path_to_repo}",
'rev-list',
*args_list
]
end
def stub_popen_rev_list(*additional_args, output:)
args = args_for_popen(additional_args)
expect(rev_list).to receive(:popen).with(args, nil, env_hash)
.and_return([output, 0])
end end
def stub_lazy_popen_rev_list(*additional_args, output:) def stub_popen_rev_list(*additional_args, with_lazy_block: true, output:)
params = [ params = [
args_for_popen(additional_args), args_for_popen(additional_args),
nil, repository.path,
env_hash, command_env,
hash_including(lazy_block: anything) hash_including(lazy_block: with_lazy_block ? anything : nil)
] ]
expect(rev_list).to receive(:popen).with(*params) do |*_, lazy_block:| expect(repository).to receive(:popen).with(*params) do |*_, lazy_block:|
lazy_block.call(output.lines.lazy.map(&:chomp)) output = lazy_block.call(output.lines.lazy.map(&:chomp)) if with_lazy_block
[output, 0]
end end
end end
context "#new_refs" do context "#new_refs" do
it 'calls out to `popen`' do it 'calls out to `popen`' do
stub_popen_rev_list('newrev', '--not', '--all', output: "sha1\nsha2") stub_popen_rev_list('newrev', '--not', '--all', with_lazy_block: false, output: "sha1\nsha2")
expect(rev_list.new_refs).to eq(%w[sha1 sha2]) expect(rev_list.new_refs).to eq(%w[sha1 sha2])
end end
...@@ -55,18 +46,18 @@ describe Gitlab::Git::RevList do ...@@ -55,18 +46,18 @@ describe Gitlab::Git::RevList do
it 'fetches list of newly pushed objects using rev-list' do it 'fetches list of newly pushed objects using rev-list' do
stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
expect(rev_list.new_objects).to eq(%w[sha1 sha2]) expect { |b| rev_list.new_objects(&b) }.to yield_with_args(%w[sha1 sha2])
end end
it 'can skip pathless objects' do it 'can skip pathless objects' do
stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2 path/to/file") stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2 path/to/file")
expect(rev_list.new_objects(require_path: true)).to eq(%w[sha2]) expect { |b| rev_list.new_objects(require_path: true, &b) }.to yield_with_args(%w[sha2])
end end
it 'can handle non utf-8 paths' do it 'can handle non utf-8 paths' do
non_utf_char = [0x89].pack("c*").force_encoding("UTF-8") non_utf_char = [0x89].pack("c*").force_encoding("UTF-8")
stub_lazy_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha2 πå†h/†ø/ƒîlé#{non_utf_char}\nsha1") stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha2 πå†h/†ø/ƒîlé#{non_utf_char}\nsha1")
rev_list.new_objects(require_path: true) do |object_ids| rev_list.new_objects(require_path: true) do |object_ids|
expect(object_ids.force).to eq(%w[sha2]) expect(object_ids.force).to eq(%w[sha2])
...@@ -74,7 +65,7 @@ describe Gitlab::Git::RevList do ...@@ -74,7 +65,7 @@ describe Gitlab::Git::RevList do
end end
it 'can yield a lazy enumerator' do it 'can yield a lazy enumerator' do
stub_lazy_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
rev_list.new_objects do |object_ids| rev_list.new_objects do |object_ids|
expect(object_ids).to be_a Enumerator::Lazy expect(object_ids).to be_a Enumerator::Lazy
...@@ -82,7 +73,7 @@ describe Gitlab::Git::RevList do ...@@ -82,7 +73,7 @@ describe Gitlab::Git::RevList do
end end
it 'returns the result of the block when given' do it 'returns the result of the block when given' do
stub_lazy_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2") stub_popen_rev_list('newrev', '--not', '--all', '--objects', output: "sha1\nsha2")
objects = rev_list.new_objects do |object_ids| objects = rev_list.new_objects do |object_ids|
object_ids.first object_ids.first
...@@ -94,13 +85,13 @@ describe Gitlab::Git::RevList do ...@@ -94,13 +85,13 @@ describe Gitlab::Git::RevList do
it 'can accept list of references to exclude' do it 'can accept list of references to exclude' do
stub_popen_rev_list('newrev', '--not', 'master', '--objects', output: "sha1\nsha2") stub_popen_rev_list('newrev', '--not', 'master', '--objects', output: "sha1\nsha2")
expect(rev_list.new_objects(not_in: ['master'])).to eq(%w[sha1 sha2]) expect { |b| rev_list.new_objects(not_in: ['master'], &b) }.to yield_with_args(%w[sha1 sha2])
end end
it 'handles empty list of references to exclude as listing all known objects' do it 'handles empty list of references to exclude as listing all known objects' do
stub_popen_rev_list('newrev', '--objects', output: "sha1\nsha2") stub_popen_rev_list('newrev', '--objects', output: "sha1\nsha2")
expect(rev_list.new_objects(not_in: [])).to eq(%w[sha1 sha2]) expect { |b| rev_list.new_objects(not_in: [], &b) }.to yield_with_args(%w[sha1 sha2])
end end
end end
...@@ -108,15 +99,15 @@ describe Gitlab::Git::RevList do ...@@ -108,15 +99,15 @@ describe Gitlab::Git::RevList do
it 'fetches list of all pushed objects using rev-list' do it 'fetches list of all pushed objects using rev-list' do
stub_popen_rev_list('--all', '--objects', output: "sha1\nsha2") stub_popen_rev_list('--all', '--objects', output: "sha1\nsha2")
expect(rev_list.all_objects).to eq(%w[sha1 sha2]) expect { |b| rev_list.all_objects(&b) }.to yield_with_args(%w[sha1 sha2])
end end
end end
context "#missed_ref" do context "#missed_ref" do
let(:rev_list) { described_class.new(oldrev: 'oldrev', newrev: 'newrev', path_to_repo: project.repository.path_to_repo) } let(:rev_list) { described_class.new(repository, oldrev: 'oldrev', newrev: 'newrev') }
it 'calls out to `popen`' do it 'calls out to `popen`' do
stub_popen_rev_list('--max-count=1', 'oldrev', '^newrev', output: "sha1\nsha2") stub_popen_rev_list('--max-count=1', 'oldrev', '^newrev', with_lazy_block: false, output: "sha1\nsha2")
expect(rev_list.missed_ref).to eq(%w[sha1 sha2]) expect(rev_list.missed_ref).to eq(%w[sha1 sha2])
end end
......
...@@ -15,6 +15,7 @@ describe Ci::JobArtifact do ...@@ -15,6 +15,7 @@ describe Ci::JobArtifact do
it { is_expected.to delegate_method(:open).to(:file) } it { is_expected.to delegate_method(:open).to(:file) }
it { is_expected.to delegate_method(:exists?).to(:file) } it { is_expected.to delegate_method(:exists?).to(:file) }
<<<<<<< HEAD
describe 'callbacks' do describe 'callbacks' do
subject { create(:ci_job_artifact, :archive) } subject { create(:ci_job_artifact, :archive) }
...@@ -73,6 +74,8 @@ describe Ci::JobArtifact do ...@@ -73,6 +74,8 @@ describe Ci::JobArtifact do
end end
end end
=======
>>>>>>> upstream/master
describe '#set_size' do describe '#set_size' do
it 'sets the size' do it 'sets the size' do
expect(artifact.size).to eq(106365) expect(artifact.size).to eq(106365)
......
...@@ -412,6 +412,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do ...@@ -412,6 +412,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
context 'if the services is active' do context 'if the services is active' do
it 'should return a message' do it 'should return a message' do
expect(kubernetes_service.deprecation_message).to match(/Your Kubernetes cluster information on this page is still editable/) expect(kubernetes_service.deprecation_message).to match(/Your Kubernetes cluster information on this page is still editable/)
<<<<<<< HEAD
end end
end end
...@@ -450,6 +451,8 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do ...@@ -450,6 +451,8 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
context 'if the services is active' do context 'if the services is active' do
it 'should return a message' do it 'should return a message' do
expect(kubernetes_service.deprecation_message).to match(/Your Kubernetes cluster information on this page is still editable/) expect(kubernetes_service.deprecation_message).to match(/Your Kubernetes cluster information on this page is still editable/)
=======
>>>>>>> upstream/master
end end
end end
......
...@@ -364,9 +364,11 @@ describe WikiPage do ...@@ -364,9 +364,11 @@ describe WikiPage do
end end
describe "#versions" do describe "#versions" do
shared_examples 'wiki page versions' do
let(:page) { wiki.find_page("Update") }
before do before do
create_page("Update", "content") create_page("Update", "content")
@page = wiki.find_page("Update")
end end
after do after do
...@@ -374,8 +376,22 @@ describe WikiPage do ...@@ -374,8 +376,22 @@ describe WikiPage do
end end
it "returns an array of all commits for the page" do it "returns an array of all commits for the page" do
3.times { |i| @page.update(content: "content #{i}") } 3.times { |i| page.update(content: "content #{i}") }
expect(@page.versions.count).to eq(4)
expect(page.versions.count).to eq(4)
end
it 'returns instances of WikiPageVersion' do
expect(page.versions).to all( be_a(Gitlab::Git::WikiPageVersion) )
end
end
context 'when Gitaly is enabled' do
it_behaves_like 'wiki page versions'
end
context 'when Gitaly is disabled', :disable_gitaly do
it_behaves_like 'wiki page versions'
end end
end end
......
...@@ -453,6 +453,7 @@ describe API::Jobs do ...@@ -453,6 +453,7 @@ describe API::Jobs do
end end
context 'authorized user' do context 'authorized user' do
<<<<<<< HEAD
context 'when trace is in ObjectStorage' do context 'when trace is in ObjectStorage' do
let!(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) } let!(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
...@@ -469,6 +470,8 @@ describe API::Jobs do ...@@ -469,6 +470,8 @@ describe API::Jobs do
end end
end end
=======
>>>>>>> upstream/master
context 'when trace is artifact' do context 'when trace is artifact' do
let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) } let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
......
...@@ -199,6 +199,24 @@ describe API::Users do ...@@ -199,6 +199,24 @@ describe API::Users do
expect(json_response.size).to eq(1) expect(json_response.size).to eq(1)
expect(json_response.first['username']).to eq(user.username) expect(json_response.first['username']).to eq(user.username)
end end
it 'returns the correct order when sorted by id' do
admin
user
get api('/users', admin), { order_by: 'id', sort: 'asc' }
expect(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(2)
expect(json_response.first['id']).to eq(admin.id)
expect(json_response.last['id']).to eq(user.id)
end
it 'returns 400 when provided incorrect sort params' do
get api('/users', admin), { order_by: 'magic', sort: 'asc' }
expect(response).to have_gitlab_http_status(400)
end
end end
context "when authenticated and ldap is enabled" do context "when authenticated and ldap is enabled" do
......
require "spec_helper"
describe Files::CreateService do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:user) { create(:user) }
let(:file_content) { 'Test file content' }
let(:branch_name) { project.default_branch }
let(:start_branch) { branch_name }
let(:commit_params) do
{
file_path: file_path,
commit_message: "Update File",
file_content: file_content,
file_content_encoding: "text",
start_project: project,
start_branch: start_branch,
branch_name: branch_name
}
end
subject { described_class.new(project, user, commit_params) }
before do
project.add_master(user)
end
describe "#execute" do
context 'when file matches LFS filter' do
let(:file_path) { 'test_file.lfs' }
let(:branch_name) { 'lfs' }
context 'with LFS disabled' do
it 'skips gitattributes check' do
expect(repository).not_to receive(:attributes_at)
subject.execute
end
it "doesn't create LFS pointers" do
subject.execute
blob = repository.blob_at('lfs', file_path)
expect(blob.data).not_to start_with('version https://git-lfs.github.com/spec/v1')
expect(blob.data).to eq(file_content)
end
end
context 'with LFS enabled' do
before do
allow(project).to receive(:lfs_enabled?).and_return(true)
end
it 'creates an LFS pointer' do
subject.execute
blob = repository.blob_at('lfs', file_path)
expect(blob.data).to start_with('version https://git-lfs.github.com/spec/v1')
end
it "creates an LfsObject with the file's content" do
subject.execute
expect(LfsObject.last.file.read).to eq file_content
end
it 'links the LfsObject to the project' do
expect do
subject.execute
end.to change { project.lfs_objects.count }.by(1)
end
end
end
end
end
...@@ -108,7 +108,7 @@ describe MergeRequests::RebaseService do ...@@ -108,7 +108,7 @@ describe MergeRequests::RebaseService do
context 'git commands', :disable_gitaly do context 'git commands', :disable_gitaly do
it 'sets GL_REPOSITORY env variable when calling git commands' do it 'sets GL_REPOSITORY env variable when calling git commands' do
expect(repository).to receive(:popen).exactly(3) expect(repository).to receive(:popen).exactly(3)
.with(anything, anything, hash_including('GL_REPOSITORY')) .with(anything, anything, hash_including('GL_REPOSITORY'), anything)
.and_return(['', 0]) .and_return(['', 0])
service.execute(merge_request) service.execute(merge_request)
......
...@@ -59,6 +59,29 @@ describe FileUploader do ...@@ -59,6 +59,29 @@ describe FileUploader do
end end
end end
describe 'callbacks' do
describe '#prune_store_dir after :remove' do
before do
uploader.store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
end
def store_dir
File.expand_path(uploader.store_dir, uploader.root)
end
it 'is called' do
expect(uploader).to receive(:prune_store_dir).once
uploader.remove!
end
it 'prune the store directory' do
expect { uploader.remove! }
.to change { File.exist?(store_dir) }.from(true).to(false)
end
end
end
describe '#secret' do describe '#secret' do
it 'generates a secret if none is provided' do it 'generates a secret if none is provided' do
expect(described_class).to receive(:generate_secret).and_return('secret') expect(described_class).to receive(:generate_secret).and_return('secret')
......
...@@ -12,6 +12,7 @@ describe JobArtifactUploader do ...@@ -12,6 +12,7 @@ describe JobArtifactUploader do
cache_dir: %r[artifacts/tmp/cache], cache_dir: %r[artifacts/tmp/cache],
work_dir: %r[artifacts/tmp/work] work_dir: %r[artifacts/tmp/work]
<<<<<<< HEAD
context "object store is REMOTE" do context "object store is REMOTE" do
before do before do
stub_artifacts_object_storage stub_artifacts_object_storage
...@@ -23,6 +24,8 @@ describe JobArtifactUploader do ...@@ -23,6 +24,8 @@ describe JobArtifactUploader do
store_dir: %r[\h{2}/\h{2}/\h{64}/\d{4}_\d{1,2}_\d{1,2}/\d+/\d+\z] store_dir: %r[\h{2}/\h{2}/\h{64}/\d{4}_\d{1,2}_\d{1,2}/\d+/\d+\z]
end end
=======
>>>>>>> upstream/master
describe '#open' do describe '#open' do
subject { uploader.open } subject { uploader.open }
......
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