Commit 4a1acdd6 authored by Yorick Peterse's avatar Yorick Peterse

Merge branch 'master' into 8-8-stable

parents 4dc12b03 dea36800
...@@ -21,6 +21,7 @@ AllCops: ...@@ -21,6 +21,7 @@ AllCops:
- 'lib/email_validator.rb' - 'lib/email_validator.rb'
- 'lib/gitlab/upgrader.rb' - 'lib/gitlab/upgrader.rb'
- 'lib/gitlab/seeder.rb' - 'lib/gitlab/seeder.rb'
- 'lib/templates/**/*'
##################### Style ################################## ##################### Style ##################################
......
...@@ -10,6 +10,7 @@ v 8.8.0 (unreleased) ...@@ -10,6 +10,7 @@ v 8.8.0 (unreleased)
- Escape HTML in commit titles in system note messages - Escape HTML in commit titles in system note messages
- Improve multiple branch push performance by memoizing permission checking - Improve multiple branch push performance by memoizing permission checking
- Log to application.log when an admin starts and stops impersonating a user - Log to application.log when an admin starts and stops impersonating a user
- Changing the confidentiality of an issue now creates a new system note (Alex Moore-Niemi)
- Updated gitlab_git to 10.1.0 - Updated gitlab_git to 10.1.0
- GitAccess#protected_tag? no longer loads all tags just to check if a single one exists - GitAccess#protected_tag? no longer loads all tags just to check if a single one exists
- Reduce delay in destroying a project from 1-minute to immediately - Reduce delay in destroying a project from 1-minute to immediately
...@@ -21,6 +22,7 @@ v 8.8.0 (unreleased) ...@@ -21,6 +22,7 @@ v 8.8.0 (unreleased)
- Bump mail_room to 0.7.0 to fix stuck IDLE connections - Bump mail_room to 0.7.0 to fix stuck IDLE connections
- Remove future dates from contribution calendar graph. - Remove future dates from contribution calendar graph.
- Support e-mail notifications for comments on project snippets - Support e-mail notifications for comments on project snippets
- Fix API leak of notes of unauthorized issues, snippets and merge requests
- Use ActionDispatch Remote IP for Akismet checking - Use ActionDispatch Remote IP for Akismet checking
- Fix error when visiting commit builds page before build was updated - Fix error when visiting commit builds page before build was updated
- Add 'l' shortcut to open Label dropdown on issuables and 'i' to create new issue on a project - Add 'l' shortcut to open Label dropdown on issuables and 'i' to create new issue on a project
...@@ -38,6 +40,7 @@ v 8.8.0 (unreleased) ...@@ -38,6 +40,7 @@ v 8.8.0 (unreleased)
- Create tags using Rugged for performance reasons. !3745 - Create tags using Rugged for performance reasons. !3745
- API: Expose Issue#user_notes_count. !3126 (Anton Popov) - API: Expose Issue#user_notes_count. !3126 (Anton Popov)
- Don't show forks button when user can't view forks - Don't show forks button when user can't view forks
- Fix atom feed links and rendering
- Files over 5MB can only be viewed in their raw form, files over 1MB without highlighting !3718 - Files over 5MB can only be viewed in their raw form, files over 1MB without highlighting !3718
- Add support for supressing text diffs using .gitattributes on the default branch (Matt Oakes) - Add support for supressing text diffs using .gitattributes on the default branch (Matt Oakes)
- Add eager load paths to help prevent dependency load issues in Sidekiq workers. !3724 - Add eager load paths to help prevent dependency load issues in Sidekiq workers. !3724
...@@ -57,9 +60,14 @@ v 8.8.0 (unreleased) ...@@ -57,9 +60,14 @@ v 8.8.0 (unreleased)
- Redesign navigation for profile and group pages - Redesign navigation for profile and group pages
- Add counter metrics for rails cache - Add counter metrics for rails cache
- Import pull requests from GitHub where the source or target branches were removed - Import pull requests from GitHub where the source or target branches were removed
- All Grape API helpers are now instrumented
- Improve Issue formatting for the Slack Service (Jeroen van Baarsen)
- Fixed advice on invalid permissions on upload path !2948 (Ludovic Perrine)
v 8.7.6 v 8.7.6
- Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko) - Fix links on wiki pages for relative url setups. !4131 (Artem Sidorenko)
- Fix import from GitLab.com to a private instance failure. !4181
- Fix external imports not finding the import data. !4106
v 8.7.5 v 8.7.5
- Fix relative links in wiki pages. !4050 - Fix relative links in wiki pages. !4050
...@@ -889,7 +897,7 @@ v 8.1.3 ...@@ -889,7 +897,7 @@ v 8.1.3
- Use issue editor as cross reference comment author when issue is edited with a new mention - Use issue editor as cross reference comment author when issue is edited with a new mention
- Add Facebook authentication - Add Facebook authentication
v 8.1.2 v 8.1.1
- Fix cloning Wiki repositories via HTTP (Stan Hu) - Fix cloning Wiki repositories via HTTP (Stan Hu)
- Add migration to remove satellites directory - Add migration to remove satellites directory
- Fix specific runners visibility - Fix specific runners visibility
...@@ -1514,20 +1522,17 @@ v 7.10.0 ...@@ -1514,20 +1522,17 @@ v 7.10.0
- Fix stuck Merge Request merging events from old installations (Ben Bodenmiller) - Fix stuck Merge Request merging events from old installations (Ben Bodenmiller)
- Fix merge request comments on files with multiple commits - Fix merge request comments on files with multiple commits
- Fix Resource Owner Password Authentication Flow - Fix Resource Owner Password Authentication Flow
v 7.9.4
- Security: Fix project import URL regex to prevent arbitary local repos from being imported
- Fixed issue where only 25 commits would load in file listings
- Fix LDAP identities after config update
v 7.9.3
- Contains no changes
- Add icons to Add dropdown items. - Add icons to Add dropdown items.
- Allow admin to create public deploy keys that are accessible to any project. - Allow admin to create public deploy keys that are accessible to any project.
- Warn when gitlab-shell version doesn't match requirement. - Warn when gitlab-shell version doesn't match requirement.
- Skip email confirmation when set by admin or via LDAP. - Skip email confirmation when set by admin or via LDAP.
- Only allow users to reference groups, projects, issues, MRs, commits they have access to. - Only allow users to reference groups, projects, issues, MRs, commits they have access to.
v 7.9.4
- Security: Fix project import URL regex to prevent arbitary local repos from being imported
- Fixed issue where only 25 commits would load in file listings
- Fix LDAP identities after config update
v 7.9.3 v 7.9.3
- Contains no changes - Contains no changes
......
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
} }
} }
.zen-cotrol { .zen-control {
padding: 0; padding: 0;
color: #555; color: #555;
background: none; background: none;
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
} }
.no-ssh-key-message, .project-limit-message { .no-ssh-key-message, .project-limit-message {
background-color: #f28d35; background-color: #f28d35;
margin-bottom: 16px; margin-bottom: 0;
} }
.new_project, .new_project,
.edit_project { .edit_project {
......
...@@ -9,6 +9,6 @@ class Admin::AbuseReportsController < Admin::ApplicationController ...@@ -9,6 +9,6 @@ class Admin::AbuseReportsController < Admin::ApplicationController
abuse_report.remove_user(deleted_by: current_user) if params[:remove_user] abuse_report.remove_user(deleted_by: current_user) if params[:remove_user]
abuse_report.destroy abuse_report.destroy
render nothing: true head :ok
end end
end end
...@@ -32,7 +32,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController ...@@ -32,7 +32,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
respond_to do |format| respond_to do |format|
format.html { redirect_back_or_default(default: { action: 'index' }) } format.html { redirect_back_or_default(default: { action: 'index' }) }
format.js { render nothing: true } format.js { head :ok }
end end
end end
......
...@@ -6,7 +6,7 @@ class Admin::KeysController < Admin::ApplicationController ...@@ -6,7 +6,7 @@ class Admin::KeysController < Admin::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.js { render nothing: true } format.js { head :ok }
end end
end end
......
...@@ -11,7 +11,7 @@ class Admin::SpamLogsController < Admin::ApplicationController ...@@ -11,7 +11,7 @@ class Admin::SpamLogsController < Admin::ApplicationController
redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed." redirect_to admin_spam_logs_path, notice: "User #{spam_log.user.username} was successfully removed."
else else
spam_log.destroy spam_log.destroy
render nothing: true head :ok
end end
end end
end end
...@@ -154,7 +154,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -154,7 +154,7 @@ class Admin::UsersController < Admin::ApplicationController
respond_to do |format| respond_to do |format|
format.html { redirect_back_or_admin_user(notice: "Successfully removed email.") } format.html { redirect_back_or_admin_user(notice: "Successfully removed email.") }
format.js { render nothing: true } format.js { head :ok }
end end
end end
......
...@@ -6,7 +6,7 @@ module ToggleSubscriptionAction ...@@ -6,7 +6,7 @@ module ToggleSubscriptionAction
subscribable_resource.toggle_subscription(current_user) subscribable_resource.toggle_subscription(current_user)
render nothing: true head :ok
end end
private private
......
...@@ -12,7 +12,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -12,7 +12,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
respond_to do |format| respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: todo_notice } format.html { redirect_to dashboard_todos_path, notice: todo_notice }
format.js { render nothing: true } format.js { head :ok }
format.json do format.json do
render json: { count: @todos.size, done_count: current_user.todos.done.count } render json: { count: @todos.size, done_count: current_user.todos.done.count }
end end
...@@ -24,7 +24,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -24,7 +24,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
respond_to do |format| respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' } format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { render nothing: true } format.js { head :ok }
format.json do format.json do
find_todos find_todos
render json: { count: @todos.size, done_count: current_user.todos.done.count } render json: { count: @todos.size, done_count: current_user.todos.done.count }
......
...@@ -40,7 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -40,7 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
respond_to do |format| respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
format.js { render nothing: true } format.js { head :ok }
end end
end end
......
...@@ -24,7 +24,7 @@ class Profiles::EmailsController < Profiles::ApplicationController ...@@ -24,7 +24,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
respond_to do |format| respond_to do |format|
format.html { redirect_to profile_emails_url } format.html { redirect_to profile_emails_url }
format.js { render nothing: true } format.js { head :ok }
end end
end end
......
...@@ -32,7 +32,7 @@ class Profiles::KeysController < Profiles::ApplicationController ...@@ -32,7 +32,7 @@ class Profiles::KeysController < Profiles::ApplicationController
respond_to do |format| respond_to do |format|
format.html { redirect_to profile_keys_url } format.html { redirect_to profile_keys_url }
format.js { render nothing: true } format.js { head :ok }
end end
end end
......
class Projects::ContainerRegistryController < Projects::ApplicationController
before_action :verify_registry_enabled
before_action :authorize_read_container_image!
before_action :authorize_update_container_image!, only: [:destroy]
layout 'project'
def index
@tags = container_registry_repository.tags
end
def destroy
url = namespace_project_container_registry_index_path(project.namespace, project)
if tag.delete
redirect_to url
else
redirect_to url, alert: 'Failed to remove tag'
end
end
private
def verify_registry_enabled
render_404 unless Gitlab.config.registry.enabled
end
def container_registry_repository
@container_registry_repository ||= project.container_registry_repository
end
def tag
@tag ||= container_registry_repository.tag(params[:id])
end
end
...@@ -20,6 +20,7 @@ class Projects::ImportsController < Projects::ApplicationController ...@@ -20,6 +20,7 @@ class Projects::ImportsController < Projects::ApplicationController
@project.import_retry @project.import_retry
else else
@project.import_start @project.import_start
@project.add_import_job
end end
end end
......
...@@ -75,7 +75,7 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -75,7 +75,7 @@ class Projects::MilestonesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html { redirect_to namespace_project_milestones_path } format.html { redirect_to namespace_project_milestones_path }
format.js { render nothing: true } format.js { head :ok }
end end
end end
......
...@@ -43,7 +43,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -43,7 +43,7 @@ class Projects::NotesController < Projects::ApplicationController
end end
respond_to do |format| respond_to do |format|
format.js { render nothing: true } format.js { head :ok }
end end
end end
...@@ -52,7 +52,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -52,7 +52,7 @@ class Projects::NotesController < Projects::ApplicationController
note.update_attribute(:attachment, nil) note.update_attribute(:attachment, nil)
respond_to do |format| respond_to do |format|
format.js { render nothing: true } format.js { head :ok }
end end
end end
......
...@@ -55,7 +55,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -55,7 +55,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
format.html do format.html do
redirect_to namespace_project_project_members_path(@project.namespace, @project) redirect_to namespace_project_project_members_path(@project.namespace, @project)
end end
format.js { render nothing: true } format.js { head :ok }
end end
end end
...@@ -81,7 +81,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -81,7 +81,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html { redirect_to dashboard_projects_path, notice: "You left the project." } format.html { redirect_to dashboard_projects_path, notice: "You left the project." }
format.js { render nothing: true } format.js { head :ok }
end end
else else
if current_user == @project.owner if current_user == @project.owner
......
...@@ -39,7 +39,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController ...@@ -39,7 +39,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html { redirect_to namespace_project_protected_branches_path } format.html { redirect_to namespace_project_protected_branches_path }
format.js { render nothing: true } format.js { head :ok }
end end
end end
......
...@@ -33,6 +33,10 @@ module GitlabRoutingHelper ...@@ -33,6 +33,10 @@ module GitlabRoutingHelper
namespace_project_builds_path(project.namespace, project, *args) namespace_project_builds_path(project.namespace, project, *args)
end end
def project_container_registry_path(project, *args)
namespace_project_container_registry_index_path(project.namespace, project, *args)
end
def activity_project_path(project, *args) def activity_project_path(project, *args)
activity_namespace_project_path(project.namespace, project, *args) activity_namespace_project_path(project.namespace, project, *args)
end end
......
...@@ -124,11 +124,7 @@ module ProjectsHelper ...@@ -124,11 +124,7 @@ module ProjectsHelper
end end
def license_short_name(project) def license_short_name(project)
no_license_key = project.repository.license_key.nil? || return 'LICENSE' if project.repository.license_key.nil?
# Back-compat if cache contains 'no-license', can be removed in a few weeks
project.repository.license_key == 'no-license'
return 'LICENSE' if no_license_key
license = Licensee::License.new(project.repository.license_key) license = Licensee::License.new(project.repository.license_key)
...@@ -152,6 +148,10 @@ module ProjectsHelper ...@@ -152,6 +148,10 @@ module ProjectsHelper
nav_tabs << :builds nav_tabs << :builds
end end
if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project)
nav_tabs << :container_registry
end
if can?(current_user, :admin_project, project) if can?(current_user, :admin_project, project)
nav_tabs << :settings nav_tabs << :settings
end end
......
...@@ -110,6 +110,10 @@ class Namespace < ActiveRecord::Base ...@@ -110,6 +110,10 @@ class Namespace < ActiveRecord::Base
# Ensure old directory exists before moving it # Ensure old directory exists before moving it
gitlab_shell.add_namespace(path_was) gitlab_shell.add_namespace(path_was)
if any_project_has_container_registry_tags?
raise Exception.new('Namespace cannot be moved, because at least one project has tags in container registry')
end
if gitlab_shell.mv_namespace(path_was, path) if gitlab_shell.mv_namespace(path_was, path)
Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
...@@ -131,6 +135,10 @@ class Namespace < ActiveRecord::Base ...@@ -131,6 +135,10 @@ class Namespace < ActiveRecord::Base
end end
end end
def any_project_has_container_registry_tags?
projects.any?(&:has_container_registry_tags?)
end
def send_update_instructions def send_update_instructions
projects.each do |project| projects.each do |project|
project.send_move_instructions("#{path_was}/#{project.path}") project.send_move_instructions("#{path_was}/#{project.path}")
......
...@@ -171,17 +171,17 @@ class Project < ActiveRecord::Base ...@@ -171,17 +171,17 @@ class Project < ActiveRecord::Base
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
scope :sorted_by_names, -> { joins(:namespace).reorder('namespaces.name ASC, projects.name ASC') }
scope :without_user, ->(user) { where('projects.id NOT IN (:ids)', ids: user.authorized_projects.map(&:id) ) }
scope :without_team, ->(team) { team.projects.present? ? where('projects.id NOT IN (:ids)', ids: team.projects.map(&:id)) : scoped }
scope :not_in_group, ->(group) { where('projects.id NOT IN (:ids)', ids: group.project_ids ) }
scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) } scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
scope :in_group_namespace, -> { joins(:group) }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) } scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) } scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
scope :non_archived, -> { where(archived: false) } scope :non_archived, -> { where(archived: false) }
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct } scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
scope :active, -> { joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC') }
scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) }
state_machine :import_status, initial: :none do state_machine :import_status, initial: :none do
event :import_start do event :import_start do
...@@ -204,23 +204,10 @@ class Project < ActiveRecord::Base ...@@ -204,23 +204,10 @@ class Project < ActiveRecord::Base
state :finished state :finished
state :failed state :failed
after_transition any => :started, do: :schedule_add_import_job
after_transition any => :finished, do: :clear_import_data after_transition any => :finished, do: :clear_import_data
end end
class << self class << self
def abandoned
where('projects.last_activity_at < ?', 6.months.ago)
end
def with_push
joins(:events).where('events.action = ?', Event::PUSHED)
end
def active
joins(:issues, :notes, :merge_requests).order('issues.created_at, notes.created_at, merge_requests.created_at DESC')
end
# Searches for a list of projects based on the query given in `query`. # Searches for a list of projects based on the query given in `query`.
# #
# On PostgreSQL this method uses "ILIKE" to perform a case-insensitive # On PostgreSQL this method uses "ILIKE" to perform a case-insensitive
...@@ -282,10 +269,6 @@ class Project < ActiveRecord::Base ...@@ -282,10 +269,6 @@ class Project < ActiveRecord::Base
projects.iwhere('projects.path' => project_path).take projects.iwhere('projects.path' => project_path).take
end end
def find_by_ci_id(id)
find_by(ci_id: id.to_i)
end
def visibility_levels def visibility_levels
Gitlab::VisibilityLevel.options Gitlab::VisibilityLevel.options
end end
...@@ -316,10 +299,6 @@ class Project < ActiveRecord::Base ...@@ -316,10 +299,6 @@ class Project < ActiveRecord::Base
joins(join_body).reorder('join_note_counts.amount DESC') joins(join_body).reorder('join_note_counts.amount DESC')
end end
def visible_to_user(user)
where(id: user.authorized_projects.select(:id).reorder(nil))
end
end end
def team def team
...@@ -330,12 +309,30 @@ class Project < ActiveRecord::Base ...@@ -330,12 +309,30 @@ class Project < ActiveRecord::Base
@repository ||= Repository.new(path_with_namespace, self) @repository ||= Repository.new(path_with_namespace, self)
end end
def container_registry_url def container_registry_repository
if container_registry_enabled? && Gitlab.config.registry.enabled return unless Gitlab.config.registry.enabled
"#{Gitlab.config.registry.host_with_port}/#{path_with_namespace}"
@container_registry_repository ||= begin
token = Auth::ContainerRegistryAuthenticationService.full_access_token(path_with_namespace)
url = Gitlab.config.registry.api_url
host_port = Gitlab.config.registry.host_port
registry = ContainerRegistry::Registry.new(url, token: token, path: host_port)
registry.repository(path_with_namespace)
end
end
def container_registry_repository_url
if Gitlab.config.registry.enabled
"#{Gitlab.config.registry.host_port}/#{path_with_namespace}"
end end
end end
def has_container_registry_tags?
return unless container_registry_repository
container_registry_repository.tags.any?
end
def commit(id = 'HEAD') def commit(id = 'HEAD')
repository.commit(id) repository.commit(id)
end end
...@@ -349,10 +346,6 @@ class Project < ActiveRecord::Base ...@@ -349,10 +346,6 @@ class Project < ActiveRecord::Base
id && persisted? id && persisted?
end end
def schedule_add_import_job
run_after_commit(:add_import_job)
end
def add_import_job def add_import_job
if forked? if forked?
job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path)
...@@ -376,14 +369,14 @@ class Project < ActiveRecord::Base ...@@ -376,14 +369,14 @@ class Project < ActiveRecord::Base
end end
def import_url=(value) def import_url=(value)
import_url = Gitlab::ImportUrl.new(value) import_url = Gitlab::UrlSanitizer.new(value)
create_or_update_import_data(credentials: import_url.credentials) create_or_update_import_data(credentials: import_url.credentials)
super(import_url.sanitized_url) super(import_url.sanitized_url)
end end
def import_url def import_url
if import_data && super if import_data && super
import_url = Gitlab::ImportUrl.new(super, credentials: import_data.credentials) import_url = Gitlab::UrlSanitizer.new(super, credentials: import_data.credentials)
import_url.full_url import_url.full_url
else else
super super
...@@ -751,6 +744,11 @@ class Project < ActiveRecord::Base ...@@ -751,6 +744,11 @@ class Project < ActiveRecord::Base
expire_caches_before_rename(old_path_with_namespace) expire_caches_before_rename(old_path_with_namespace)
if has_container_registry_tags?
# we currently doesn't support renaming repository if it contains tags in container registry
raise Exception.new('Project cannot be renamed, because tags are present in its container registry')
end
if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace) if gitlab_shell.mv_repository(old_path_with_namespace, new_path_with_namespace)
# If repository moved successfully we need to send update instructions to users. # If repository moved successfully we need to send update instructions to users.
# However we cannot allow rollback since we moved repository # However we cannot allow rollback since we moved repository
......
...@@ -34,7 +34,12 @@ class SlackService ...@@ -34,7 +34,12 @@ class SlackService
private private
def message def message
"#{user_name} #{state} #{issue_link} in #{project_link}: *#{title}*" case state
when "opened"
"[#{project_link}] Issue #{state} by #{user_name}"
else
"[#{project_link}] Issue #{issue_link} #{state} by #{user_name}"
end
end end
def opened_issue? def opened_issue?
...@@ -42,7 +47,11 @@ class SlackService ...@@ -42,7 +47,11 @@ class SlackService
end end
def description_message def description_message
[{ text: format(description), color: attachment_color }] [{
title: issue_title,
title_link: issue_url,
text: format(description),
color: "#C95823" }]
end end
def project_link def project_link
...@@ -50,7 +59,11 @@ class SlackService ...@@ -50,7 +59,11 @@ class SlackService
end end
def issue_link def issue_link
"[issue ##{issue_iid}](#{issue_url})" "[#{issue_title}](#{issue_url})"
end
def issue_title
"##{issue_iid} #{title}"
end end
end end
end end
...@@ -397,11 +397,6 @@ class User < ActiveRecord::Base ...@@ -397,11 +397,6 @@ class User < ActiveRecord::Base
owned_groups.select(:id), namespace.id).joins(:namespace) owned_groups.select(:id), namespace.id).joins(:namespace)
end end
# Team membership in authorized projects
def tm_in_authorized_projects
ProjectMember.where(source_id: authorized_projects.map(&:id), user_id: self.id)
end
def is_admin? def is_admin?
admin admin
end end
...@@ -491,10 +486,6 @@ class User < ActiveRecord::Base ...@@ -491,10 +486,6 @@ class User < ActiveRecord::Base
"#{name} (#{username})" "#{name} (#{username})"
end end
def tm_of(project)
project.project_member_by_id(self.id)
end
def already_forked?(project) def already_forked?(project)
!!fork_of(project) !!fork_of(project)
end end
......
...@@ -6,7 +6,7 @@ module Auth ...@@ -6,7 +6,7 @@ module Auth
return error('not found', 404) unless registry.enabled return error('not found', 404) unless registry.enabled
if params[:offline_token] if params[:offline_token]
return error('forbidden', 403) unless current_user return error('unauthorized', 401) unless current_user
else else
return error('forbidden', 403) unless scope return error('forbidden', 403) unless scope
end end
...@@ -14,6 +14,17 @@ module Auth ...@@ -14,6 +14,17 @@ module Auth
{ token: authorized_token(scope).encoded } { token: authorized_token(scope).encoded }
end end
def self.full_access_token(*names)
registry = Gitlab.config.registry
token = JSONWebToken::RSAToken.new(registry.key)
token.issuer = registry.issuer
token.audience = AUDIENCE
token[:access] = names.map do |name|
{ type: 'repository', name: name, actions: %w(pull push) }
end
token.encoded
end
private private
def authorized_token(*accesses) def authorized_token(*accesses)
......
...@@ -24,6 +24,10 @@ module Issues ...@@ -24,6 +24,10 @@ module Issues
todo_service.reassigned_issue(issue, current_user) todo_service.reassigned_issue(issue, current_user)
end end
if issue.previous_changes.include?('confidential')
create_confidentiality_note(issue)
end
added_labels = issue.labels - old_labels added_labels = issue.labels - old_labels
if added_labels.present? if added_labels.present?
notification_service.relabeled_issue(issue, added_labels, current_user) notification_service.relabeled_issue(issue, added_labels, current_user)
...@@ -37,5 +41,11 @@ module Issues ...@@ -37,5 +41,11 @@ module Issues
def close_service def close_service
Issues::CloseService Issues::CloseService
end end
private
def create_confidentiality_note(issue)
SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user)
end
end end
end end
...@@ -6,6 +6,7 @@ module Projects ...@@ -6,6 +6,7 @@ module Projects
def execute def execute
forked_from_project_id = params.delete(:forked_from_project_id) forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data)
@project = Project.new(params) @project = Project.new(params)
...@@ -49,16 +50,14 @@ module Projects ...@@ -49,16 +50,14 @@ module Projects
@project.build_forked_project_link(forked_from_project_id: forked_from_project_id) @project.build_forked_project_link(forked_from_project_id: forked_from_project_id)
end end
Project.transaction do save_project_and_import_data(import_data)
@project.save
if @project.persisted? && !@project.import? @project.import_start if @project.import?
raise 'Failed to create repository' unless @project.create_repository
end
end
after_create_actions if @project.persisted? after_create_actions if @project.persisted?
@project.add_import_job if @project.import?
@project @project
rescue => e rescue => e
message = "Unable to save project: #{e.message}" message = "Unable to save project: #{e.message}"
...@@ -93,8 +92,16 @@ module Projects ...@@ -93,8 +92,16 @@ module Projects
unless @project.group unless @project.group
@project.team << [current_user, :master, current_user] @project.team << [current_user, :master, current_user]
end end
end
@project.import_start if @project.import? def save_project_and_import_data(import_data)
Project.transaction do
@project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data
if @project.save && !@project.import?
raise 'Failed to create repository' unless @project.create_repository
end
end
end end
end end
end end
...@@ -26,6 +26,10 @@ module Projects ...@@ -26,6 +26,10 @@ module Projects
Project.transaction do Project.transaction do
project.destroy! project.destroy!
unless remove_registry_tags
raise_error('Failed to remove project container registry. Please try again or contact administrator')
end
unless remove_repository(repo_path) unless remove_repository(repo_path)
raise_error('Failed to remove project repository. Please try again or contact administrator') raise_error('Failed to remove project repository. Please try again or contact administrator')
end end
...@@ -59,6 +63,12 @@ module Projects ...@@ -59,6 +63,12 @@ module Projects
end end
end end
def remove_registry_tags
return true unless Gitlab.config.registry.enabled
project.container_registry_repository.delete_tags
end
def raise_error(message) def raise_error(message)
raise DestroyError.new(message) raise DestroyError.new(message)
end end
......
...@@ -34,6 +34,11 @@ module Projects ...@@ -34,6 +34,11 @@ module Projects
raise TransferError.new("Project with same path in target namespace already exists") raise TransferError.new("Project with same path in target namespace already exists")
end end
if project.has_container_registry_tags?
# we currently doesn't support renaming repository if it contains tags in container registry
raise TransferError.new('Project cannot be transferred, because tags are present in its container registry')
end
project.expire_caches_before_rename(old_path) project.expire_caches_before_rename(old_path)
# Apply new namespace id and visibility level # Apply new namespace id and visibility level
......
...@@ -169,12 +169,26 @@ class SystemNoteService ...@@ -169,12 +169,26 @@ class SystemNoteService
# #
# Returns the created Note object # Returns the created Note object
def self.change_title(noteable, project, author, old_title) def self.change_title(noteable, project, author, old_title)
return unless noteable.respond_to?(:title)
body = "Title changed from **#{old_title}** to **#{noteable.title}**" body = "Title changed from **#{old_title}** to **#{noteable.title}**"
create_note(noteable: noteable, project: project, author: author, note: body) create_note(noteable: noteable, project: project, author: author, note: body)
end end
# Called when the confidentiality changes
#
# issue - Issue object
# project - Project owning the issue
# author - User performing the change
#
# Example Note text:
#
# "Made the issue confidential"
#
# Returns the created Note object
def self.change_issue_confidentiality(issue, project, author)
body = issue.confidential ? 'Made the issue confidential' : 'Made the issue visible'
create_note(noteable: issue, project: project, author: author, note: body)
end
# Called when a branch in Noteable is changed # Called when a branch in Noteable is changed
# #
# noteable - Noteable object # noteable - Noteable object
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.nav-block .nav-block
- if current_user - if current_user
.controls .controls
= link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do = link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn' do
%i.fa.fa-rss %i.fa.fa-rss
= render 'shared/event_filter' = render 'shared/event_filter'
......
xml.instruct! xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
xml.title "#{@user.name} issues" xml.title "#{@group.name} issues"
xml.link href: issues_dashboard_url(format: :atom, private_token: @user.private_token), rel: "self", type: "application/atom+xml" xml.link href: issues_group_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html" xml.link href: issues_group_url, rel: "alternate", type: "text/html"
xml.id issues_dashboard_url xml.id issues_group_url
xml.updated @issues.first.created_at.xmlschema if @issues.any? xml.updated @issues.first.created_at.xmlschema if @issues.any?
@issues.each do |issue| @issues.each do |issue|
......
...@@ -46,6 +46,13 @@ ...@@ -46,6 +46,13 @@
Builds Builds
%span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all)) %span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all))
- if project_nav_tab? :container_registry
= nav_link(controller: %w(container_registry)) do
= link_to project_container_registry_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
= icon('hdd-o fw')
%span
Container Registry
- if project_nav_tab? :graphs - if project_nav_tab? :graphs
= nav_link(controller: %w(graphs)) do = nav_link(controller: %w(graphs)) do
= link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
%a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 } %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 }
Preview Preview
%li.pull-right %li.pull-right
%button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 } %button.zen-control.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 }
Go full screen Go full screen
.md-write-holder .md-write-holder
......
...@@ -4,5 +4,5 @@ ...@@ -4,5 +4,5 @@
= f.text_area attr, class: classes, placeholder: placeholder = f.text_area attr, class: classes, placeholder: placeholder
- else - else
= text_area_tag attr, nil, class: classes, placeholder: placeholder = text_area_tag attr, nil, class: classes, placeholder: placeholder
%a.zen-cotrol.zen-control-leave.js-zen-leave{ href: "#" } %a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
= icon('compress') = icon('compress')
- header_title project_title(@project, "Container Registry", project_container_registry_path(@project))
%tr.tag
%td
= escape_once(tag.name)
= clipboard_button(clipboard_text: "docker pull #{tag.path}")
%td
- if layer = tag.layers.first
%span.has-tooltip{ title: "#{layer.revision}" }
= layer.short_revision
- else
\-
%td
= number_to_human_size(tag.total_size)
&middot;
= pluralize(tag.layers.size, "layer")
%td
= time_ago_in_words(tag.created_at)
- if can?(current_user, :update_container_image, @project)
%td.content
.controls.hidden-xs.pull-right
= link_to namespace_project_container_registry_path(@project.namespace, @project, tag.name), class: 'btn btn-remove has-tooltip', title: "Remove", data: { confirm: "Are you sure?" }, method: :delete do
= icon("trash cred")
- page_title "Container Registry"
= render "header_title"
%hr
%ul.content-list
.light.prepend-top-default
%p
A 'container image' is a snapshot of a container.
You can host your container images with GitLab.
%br
To start using container images hosted on GitLab you first need to login:
%pre
%code
docker login #{Gitlab.config.registry.host_port}
%br
Then you are free to create and upload a container image with build and push commands:
%pre
docker build -t #{escape_once(@project.container_registry_repository_url)} .
%br
docker push #{escape_once(@project.container_registry_repository_url)}
- if @tags.blank?
%li
.nothing-here-block No images in Container Registry for this project.
- else
.table-holder
%table.table.tags
%thead
%tr
%th Name
%th Image ID
%th Size
%th Created
- if can?(current_user, :update_container_image, @project)
%th
- @tags.each do |tag|
= render 'tag', tag: tag
\ No newline at end of file
...@@ -13,7 +13,7 @@ class RepositoryImportWorker ...@@ -13,7 +13,7 @@ class RepositoryImportWorker
result = Projects::ImportService.new(project, current_user).execute result = Projects::ImportService.new(project, current_user).execute
if result[:status] == :error if result[:status] == :error
project.update(import_error: result[:message]) project.update(import_error: Gitlab::UrlSanitizer.sanitize(result[:message]))
project.import_fail project.import_fail
return return
end end
......
...@@ -183,6 +183,7 @@ production: &base ...@@ -183,6 +183,7 @@ production: &base
# api_url: http://localhost:5000/ # api_url: http://localhost:5000/
# key: config/registry.key # key: config/registry.key
# issuer: omnibus-certificate # issuer: omnibus-certificate
# path: shared/registry
# #
# 2. GitLab CI settings # 2. GitLab CI settings
......
...@@ -249,9 +249,12 @@ Settings.artifacts['max_size'] ||= 100 # in megabytes ...@@ -249,9 +249,12 @@ Settings.artifacts['max_size'] ||= 100 # in megabytes
Settings['registry'] ||= Settingslogic.new({}) Settings['registry'] ||= Settingslogic.new({})
Settings.registry['enabled'] ||= false Settings.registry['enabled'] ||= false
Settings.registry['host'] ||= "example.com" Settings.registry['host'] ||= "example.com"
Settings.registry['port'] ||= nil
Settings.registry['api_url'] ||= "http://localhost:5000/" Settings.registry['api_url'] ||= "http://localhost:5000/"
Settings.registry['key'] ||= nil Settings.registry['key'] ||= nil
Settings.registry['issuer'] ||= nil Settings.registry['issuer'] ||= nil
Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.registry['port']].compact.join(':')
Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry'), Rails.root)
# #
# Git LFS # Git LFS
......
...@@ -118,6 +118,8 @@ if Gitlab::Metrics.enabled? ...@@ -118,6 +118,8 @@ if Gitlab::Metrics.enabled?
# Instrument the classes used for checking if somebody has push access. # Instrument the classes used for checking if somebody has push access.
config.instrument_instance_methods(Gitlab::GitAccess) config.instrument_instance_methods(Gitlab::GitAccess)
config.instrument_instance_methods(Gitlab::GitAccessWiki) config.instrument_instance_methods(Gitlab::GitAccessWiki)
config.instrument_instance_methods(API::Helpers)
end end
GC::Profiler.enable GC::Profiler.enable
......
...@@ -693,6 +693,8 @@ Rails.application.routes.draw do ...@@ -693,6 +693,8 @@ Rails.application.routes.draw do
end end
end end
resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex }
resources :milestones, constraints: { id: /\d+/ } do resources :milestones, constraints: { id: /\d+/ } do
member do member do
put :sort_issues put :sort_issues
......
...@@ -24,7 +24,7 @@ class RemoveWrongImportUrlFromProjects < ActiveRecord::Migration ...@@ -24,7 +24,7 @@ class RemoveWrongImportUrlFromProjects < ActiveRecord::Migration
def process_projects_with_wrong_url def process_projects_with_wrong_url
projects_with_wrong_import_url.each do |project| projects_with_wrong_import_url.each do |project|
begin begin
import_url = Gitlab::ImportUrl.new(project["import_url"]) import_url = Gitlab::UrlSanitizer.new(project["import_url"])
update_import_url(import_url, project) update_import_url(import_url, project)
update_import_data(import_url, project) update_import_data(import_url, project)
......
...@@ -33,7 +33,7 @@ following locations: ...@@ -33,7 +33,7 @@ following locations:
- [Build triggers](build_triggers.md) - [Build triggers](build_triggers.md)
- [Build Variables](build_variables.md) - [Build Variables](build_variables.md)
- [Runners](runners.md) - [Runners](runners.md)
- [Licenses](licenses.md) - [Open source license templates](licenses.md)
## Authentication ## Authentication
......
...@@ -275,7 +275,7 @@ POST /projects/:id/runners ...@@ -275,7 +275,7 @@ POST /projects/:id/runners
| `runner_id` | integer | yes | The ID of a runner | | `runner_id` | integer | yes | The ID of a runner |
``` ```
curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/project/9/runners" -F "runner_id=9" curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" -F "runner_id=9"
``` ```
Example response: Example response:
...@@ -306,7 +306,7 @@ DELETE /projects/:id/runners/:runner_id ...@@ -306,7 +306,7 @@ DELETE /projects/:id/runners/:runner_id
| `runner_id` | integer | yes | The ID of a runner | | `runner_id` | integer | yes | The ID of a runner |
``` ```
curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/project/9/runners/9" curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9"
``` ```
Example response: Example response:
......
...@@ -212,8 +212,8 @@ If you want to receive e-mail notifications about the result status of the ...@@ -212,8 +212,8 @@ If you want to receive e-mail notifications about the result status of the
builds, you should explicitly enable the **Builds Emails** service under your builds, you should explicitly enable the **Builds Emails** service under your
project's settings. project's settings.
For more information read the [Builds emails service documentation] For more information read the
(../../project_services/builds_emails.md). [Builds emails service documentation](../../project_services/builds_emails.md).
## Builds badge ## Builds badge
......
...@@ -33,7 +33,7 @@ POST /projects/:id/trigger/builds ...@@ -33,7 +33,7 @@ POST /projects/:id/trigger/builds
The required parameters are the trigger's `token` and the Git `ref` on which The required parameters are the trigger's `token` and the Git `ref` on which
the trigger will be performed. Valid refs are the branch, the tag or the commit the trigger will be performed. Valid refs are the branch, the tag or the commit
SHA. The `:id` of a project can be found by [querying the API](../api/projects.md) SHA. The `:id` of a project can be found by [querying the API](../../api/projects.md)
or by visiting the **Triggers** page which provides self-explanatory examples. or by visiting the **Triggers** page which provides self-explanatory examples.
When a rebuild is triggered, the information is exposed in GitLab's UI under When a rebuild is triggered, the information is exposed in GitLab's UI under
......
...@@ -8,7 +8,10 @@ In addition, having to take a server offline for a an upgrade small or big is ...@@ -8,7 +8,10 @@ In addition, having to take a server offline for a an upgrade small or big is
a big burden for most organizations. For this reason it is important that your a big burden for most organizations. For this reason it is important that your
migrations are written carefully, can be applied online and adhere to the style guide below. migrations are written carefully, can be applied online and adhere to the style guide below.
It's advised to have offline migrations only in major GitLab releases. Migrations should not require GitLab installations to be taken offline unless
_absolutely_ necessary. If a migration requires downtime this should be
clearly mentioned during the review process as well as being documented in the
monthly release post.
When writing your migrations, also consider that databases might have stale data When writing your migrations, also consider that databases might have stale data
or inconsistencies and guard for that. Try to make as little assumptions as possible or inconsistencies and guard for that. Try to make as little assumptions as possible
...@@ -58,6 +61,45 @@ remove_index :namespaces, column: :name if index_exists?(:namespaces, :name) ...@@ -58,6 +61,45 @@ remove_index :namespaces, column: :name if index_exists?(:namespaces, :name)
If you need to add an unique index please keep in mind there is possibility of existing duplicates. If it is possible write a separate migration for handling this situation. It can be just removing or removing with overwriting all references to these duplicates depend on situation. If you need to add an unique index please keep in mind there is possibility of existing duplicates. If it is possible write a separate migration for handling this situation. It can be just removing or removing with overwriting all references to these duplicates depend on situation.
When adding an index make sure to use the method `add_concurrent_index` instead
of the regular `add_index` method. The `add_concurrent_index` method
automatically creates concurrent indexes when using PostgreSQL, removing the
need for downtime. To use this method you must disable transactions by calling
the method `disable_ddl_transaction!` in the body of your migration class like
so:
```
class MyMigration < ActiveRecord::Migration
disable_ddl_transaction!
def change
end
end
```
## Adding Columns With Default Values
When adding columns with default values you should use the method
`add_column_with_default`. This method ensures the table is updated without
requiring downtime. This method is not reversible so you must manually define
the `up` and `down` methods in your migration class.
For example, to add the column `foo` to the `projects` table with a default
value of `10` you'd write the following:
```
class MyMigration < ActiveRecord::Migration
def up
add_column_with_default(:projects, :foo, :integer, 10)
end
def down
remove_column(:projects, :foo)
end
end
```
## Testing ## Testing
Make sure that your migration works with MySQL and PostgreSQL with data. An empty database does not guarantee that your migration is correct. Make sure that your migration works with MySQL and PostgreSQL with data. An empty database does not guarantee that your migration is correct.
...@@ -89,4 +131,4 @@ select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(i ...@@ -89,4 +131,4 @@ select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(i
execute("UPDATE taggings SET tag_id = #{origin_tag_id} WHERE tag_id IN(#{duplicate_ids.join(",")})") execute("UPDATE taggings SET tag_id = #{origin_tag_id} WHERE tag_id IN(#{duplicate_ids.join(",")})")
execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})") execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})")
end end
``` ```
\ No newline at end of file
...@@ -402,7 +402,7 @@ There are two ways to create links, inline-style and reference-style. ...@@ -402,7 +402,7 @@ There are two ways to create links, inline-style and reference-style.
[I'm a reference-style link][Arbitrary case-insensitive reference text] [I'm a reference-style link][Arbitrary case-insensitive reference text]
[I'm a relative reference to a repository file](LICENSE) [I'm a relative reference to a repository file](LICENSE)[^1]
[You can use numbers for reference-style link definitions][1] [You can use numbers for reference-style link definitions][1]
...@@ -594,3 +594,4 @@ By including colons in the header row, you can align the text within that column ...@@ -594,3 +594,4 @@ By including colons in the header row, you can align the text within that column
[rouge]: http://rouge.jneen.net/ "Rouge website" [rouge]: http://rouge.jneen.net/ "Rouge website"
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website" [redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
[^1]: This link will be broken if you see this document from the Help page or docs.gitlab.com
...@@ -39,6 +39,7 @@ documentation](../workflow/add-user/add-user.md). ...@@ -39,6 +39,7 @@ documentation](../workflow/add-user/add-user.md).
| Cancel and retry builds | | | ✓ | ✓ | ✓ | | Cancel and retry builds | | | ✓ | ✓ | ✓ |
| Create or update commit status | | | ✓ | ✓ | ✓ | | Create or update commit status | | | ✓ | ✓ | ✓ |
| Update a container registry | | | ✓ | ✓ | ✓ | | Update a container registry | | | ✓ | ✓ | ✓ |
| Remove a container registry image | | | ✓ | ✓ | ✓ |
| Create new milestones | | | | ✓ | ✓ | | Create new milestones | | | | ✓ | ✓ |
| Add new team members | | | | ✓ | ✓ | | Add new team members | | | | ✓ | ✓ |
| Push to protected branches | | | | ✓ | ✓ | | Push to protected branches | | | | ✓ | ✓ |
......
...@@ -127,7 +127,7 @@ To prevent this from happening, set the lfs url in project Git config: ...@@ -127,7 +127,7 @@ To prevent this from happening, set the lfs url in project Git config:
```bash ```bash
git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs/objects/batch" git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs"
``` ```
### Credentials are always required when pushing an object ### Credentials are always required when pushing an object
......
...@@ -19,20 +19,24 @@ module API ...@@ -19,20 +19,24 @@ module API
# GET /projects/:id/issues/:noteable_id/notes # GET /projects/:id/issues/:noteable_id/notes
# GET /projects/:id/snippets/:noteable_id/notes # GET /projects/:id/snippets/:noteable_id/notes
get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do
@noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym])
# We exclude notes that are cross-references and that cannot be viewed if can?(current_user, noteable_read_ability_name(@noteable), @noteable)
# by the current user. By doing this exclusion at this level and not # We exclude notes that are cross-references and that cannot be viewed
# at the DB query level (which we cannot in that case), the current # by the current user. By doing this exclusion at this level and not
# page can have less elements than :per_page even if # at the DB query level (which we cannot in that case), the current
# there's more than one page. # page can have less elements than :per_page even if
notes = # there's more than one page.
# paginate() only works with a relation. This could lead to a notes =
# mismatch between the pagination headers info and the actual notes # paginate() only works with a relation. This could lead to a
# array returned, but this is really a edge-case. # mismatch between the pagination headers info and the actual notes
paginate(@noteable.notes). # array returned, but this is really a edge-case.
reject { |n| n.cross_reference_not_visible_for?(current_user) } paginate(@noteable.notes).
present notes, with: Entities::Note reject { |n| n.cross_reference_not_visible_for?(current_user) }
present notes, with: Entities::Note
else
not_found!("Notes")
end
end end
# Get a single +noteable+ note # Get a single +noteable+ note
...@@ -45,13 +49,14 @@ module API ...@@ -45,13 +49,14 @@ module API
# GET /projects/:id/issues/:noteable_id/notes/:note_id # GET /projects/:id/issues/:noteable_id/notes/:note_id
# GET /projects/:id/snippets/:noteable_id/notes/:note_id # GET /projects/:id/snippets/:noteable_id/notes/:note_id
get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do
@noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) @noteable = user_project.send(noteables_str.to_sym).find(params[noteable_id_str.to_sym])
@note = @noteable.notes.find(params[:note_id]) @note = @noteable.notes.find(params[:note_id])
can_read_note = can?(current_user, noteable_read_ability_name(@noteable), @noteable) && !@note.cross_reference_not_visible_for?(current_user)
if @note.cross_reference_not_visible_for?(current_user) if can_read_note
not_found!("Note")
else
present @note, with: Entities::Note present @note, with: Entities::Note
else
not_found!("Note")
end end
end end
...@@ -136,5 +141,11 @@ module API ...@@ -136,5 +141,11 @@ module API
end end
end end
end end
helpers do
def noteable_read_ability_name(noteable)
"read_#{noteable.class.to_s.underscore.downcase}".to_sym
end
end
end end
end end
...@@ -157,7 +157,7 @@ module Backup ...@@ -157,7 +157,7 @@ module Backup
end end
def archives_to_backup def archives_to_backup
%w{uploads builds artifacts lfs}.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact %w{uploads builds artifacts lfs registry}.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact
end end
def folders_to_backup def folders_to_backup
......
require 'backup/files'
module Backup
class Registry < Files
def initialize
super('registry', Settings.registry.path)
end
def create_files_dir
Dir.mkdir(app_files_dir, 0700)
end
end
end
module ContainerRegistry
class Blob
attr_reader :repository, :config
delegate :registry, :client, to: :repository
def initialize(repository, config)
@repository = repository
@config = config || {}
end
def valid?
digest.present?
end
def path
"#{repository.path}@#{digest}"
end
def digest
config['digest']
end
def type
config['mediaType']
end
def size
config['size']
end
def revision
digest.split(':')[1]
end
def short_revision
revision[0..8]
end
def delete
client.delete_blob(repository.name, digest)
end
def data
@data ||= client.blob(repository.name, digest, type)
end
end
end
require 'faraday'
require 'faraday_middleware'
module ContainerRegistry
class Client
attr_accessor :uri
MANIFEST_VERSION = 'application/vnd.docker.distribution.manifest.v2+json'
def initialize(base_uri, options = {})
@base_uri = base_uri
@faraday = Faraday.new(@base_uri) do |conn|
initialize_connection(conn, options)
end
end
def repository_tags(name)
@faraday.get("/v2/#{name}/tags/list").body
end
def repository_manifest(name, reference)
@faraday.get("/v2/#{name}/manifests/#{reference}").body
end
def repository_tag_digest(name, reference)
response = @faraday.head("/v2/#{name}/manifests/#{reference}")
response.headers['docker-content-digest'] if response.success?
end
def delete_repository_tag(name, reference)
@faraday.delete("/v2/#{name}/manifests/#{reference}").success?
end
def blob(name, digest, type = nil)
headers = {}
headers['Accept'] = type if type
@faraday.get("/v2/#{name}/blobs/#{digest}", nil, headers).body
end
def delete_blob(name, digest)
@faraday.delete("/v2/#{name}/blobs/#{digest}").success?
end
private
def initialize_connection(conn, options)
conn.request :json
conn.headers['Accept'] = MANIFEST_VERSION
conn.response :json, content_type: /\bjson$/
if options[:user] && options[:password]
conn.request(:basic_auth, options[:user].to_s, options[:password].to_s)
elsif options[:token]
conn.request(:authorization, :bearer, options[:token].to_s)
end
conn.adapter :net_http
end
end
end
module ContainerRegistry
class Config
attr_reader :tag, :blob, :data
def initialize(tag, blob)
@tag, @blob = tag, blob
@data = JSON.parse(blob.data)
end
def [](key)
return unless data
data[key]
end
end
end
module ContainerRegistry
class Registry
attr_reader :uri, :client, :path
def initialize(uri, options = {})
@uri = uri
@path = options[:path] || default_path
@client = ContainerRegistry::Client.new(uri, options)
end
def repository(name)
ContainerRegistry::Repository.new(self, name)
end
private
def default_path
@uri.sub(/^https?:\/\//, '')
end
end
end
module ContainerRegistry
class Repository
attr_reader :registry, :name
delegate :client, to: :registry
def initialize(registry, name)
@registry, @name = registry, name
end
def path
[registry.path, name].compact.join('/')
end
def tag(tag)
ContainerRegistry::Tag.new(self, tag)
end
def manifest
return @manifest if defined?(@manifest)
@manifest = client.repository_tags(name)
end
def valid?
manifest.present?
end
def tags
return @tags if defined?(@tags)
return [] unless manifest && manifest['tags']
@tags = manifest['tags'].map do |tag|
ContainerRegistry::Tag.new(self, tag)
end
end
def blob(config)
ContainerRegistry::Blob.new(self, config)
end
def delete_tags
return unless tags
tags.all?(&:delete)
end
end
end
module ContainerRegistry
class Tag
attr_reader :repository, :name
delegate :registry, :client, to: :repository
def initialize(repository, name)
@repository, @name = repository, name
end
def valid?
manifest.present?
end
def manifest
return @manifest if defined?(@manifest)
@manifest = client.repository_manifest(repository.name, name)
end
def path
"#{repository.path}:#{name}"
end
def [](key)
return unless manifest
manifest[key]
end
def digest
return @digest if defined?(@digest)
@digest = client.repository_tag_digest(repository.name, name)
end
def config_blob
return @config_blob if defined?(@config_blob)
return unless manifest && manifest['config']
@config_blob = repository.blob(manifest['config'])
end
def config
return unless config_blob
@config ||= ContainerRegistry::Config.new(self, config_blob)
end
def created_at
return unless config
@created_at ||= DateTime.rfc3339(config['created'])
end
def layers
return @layers if defined?(@layers)
return unless manifest
@layers = manifest['layers'].map do |layer|
repository.blob(layer)
end
end
def total_size
return unless layers
layers.map(&:size).sum
end
def delete
return unless digest
client.delete_repository_tag(repository.name, digest)
end
end
end
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
end end
def execute def execute
project = ::Projects::CreateService.new( ::Projects::CreateService.new(
current_user, current_user,
name: repo["name"], name: repo["name"],
path: repo["slug"], path: repo["slug"],
...@@ -21,11 +21,8 @@ module Gitlab ...@@ -21,11 +21,8 @@ module Gitlab
import_type: "bitbucket", import_type: "bitbucket",
import_source: "#{repo["owner"]}/#{repo["slug"]}", import_source: "#{repo["owner"]}/#{repo["slug"]}",
import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git", import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git",
import_data: { credentials: { bb_session: session_data } }
).execute ).execute
project.create_or_update_import_data(credentials: { bb_session: session_data })
project
end end
end end
end end
......
module Gitlab
module Database
module MigrationHelpers
# Creates a new index, concurrently when supported
#
# On PostgreSQL this method creates an index concurrently, on MySQL this
# creates a regular index.
#
# Example:
#
# add_concurrent_index :users, :some_column
#
# See Rails' `add_index` for more info on the available arguments.
def add_concurrent_index(*args)
if transaction_open?
raise 'add_concurrent_index can not be run inside a transaction, ' \
'you can disable transactions by calling disable_ddl_transaction! ' \
'in the body of your migration class'
end
if Database.postgresql?
args << { algorithm: :concurrently }
end
add_index(*args)
end
# Updates the value of a column in batches.
#
# This method updates the table in batches of 5% of the total row count.
# Any data inserted while running this method (or after it has finished
# running) is _not_ updated automatically.
#
# This method _only_ updates rows where the column's value is set to NULL.
#
# table - The name of the table.
# column - The name of the column to update.
# value - The value for the column.
def update_column_in_batches(table, column, value)
quoted_table = quote_table_name(table)
quoted_column = quote_column_name(column)
quoted_value = quote(value)
processed = 0
total = exec_query("SELECT COUNT(*) AS count FROM #{quoted_table}").
to_hash.
first['count'].
to_i
# Update in batches of 5% with an upper limit of 5000 rows.
batch_size = ((total / 100.0) * 5.0).ceil
while processed < total
start_row = exec_query(%Q{
SELECT id
FROM #{quoted_table}
ORDER BY id ASC
LIMIT 1 OFFSET #{processed}
}).to_hash.first
stop_row = exec_query(%Q{
SELECT id
FROM #{quoted_table}
ORDER BY id ASC
LIMIT 1 OFFSET #{processed + batch_size}
}).to_hash.first
query = %Q{
UPDATE #{quoted_table}
SET #{quoted_column} = #{quoted_value}
WHERE id >= #{start_row['id']}
}
if stop_row
query += " AND id < #{stop_row['id']}"
end
execute(query)
processed += batch_size
end
end
# Adds a column with a default value without locking an entire table.
#
# This method runs the following steps:
#
# 1. Add the column with a default value of NULL.
# 2. Update all existing rows in batches.
# 3. Change the default value of the column to the specified value.
# 4. Update any remaining rows.
#
# These steps ensure a column can be added to a large and commonly used
# table without locking the entire table for the duration of the table
# modification.
#
# table - The name of the table to update.
# column - The name of the column to add.
# type - The column type (e.g. `:integer`).
# default - The default value for the column.
# allow_null - When set to `true` the column will allow NULL values, the
# default is to not allow NULL values.
def add_column_with_default(table, column, type, default:, allow_null: false)
if transaction_open?
raise 'add_column_with_default can not be run inside a transaction, ' \
'you can disable transactions by calling disable_ddl_transaction! ' \
'in the body of your migration class'
end
transaction do
add_column(table, column, type, default: nil)
# Changing the default before the update ensures any newly inserted
# rows already use the proper default value.
change_column_default(table, column, default)
end
begin
transaction do
update_column_in_batches(table, column, default)
end
# We want to rescue _all_ exceptions here, even those that don't inherit
# from StandardError.
rescue Exception => error # rubocop: disable all
remove_column(table, column)
raise error
end
change_column_null(table, column, false) unless allow_null
end
end
end
end
...@@ -12,7 +12,7 @@ module Gitlab ...@@ -12,7 +12,7 @@ module Gitlab
end end
def execute def execute
project = ::Projects::CreateService.new( ::Projects::CreateService.new(
current_user, current_user,
name: repo.safe_name, name: repo.safe_name,
path: repo.path, path: repo.path,
...@@ -21,12 +21,9 @@ module Gitlab ...@@ -21,12 +21,9 @@ module Gitlab
visibility_level: Gitlab::VisibilityLevel::INTERNAL, visibility_level: Gitlab::VisibilityLevel::INTERNAL,
import_type: 'fogbugz', import_type: 'fogbugz',
import_source: repo.name, import_source: repo.name,
import_url: Project::UNKNOWN_IMPORT_URL import_url: Project::UNKNOWN_IMPORT_URL,
import_data: { data: { 'repo' => repo.raw_data, 'user_map' => user_map }, credentials: { fb_session: fb_session } }
).execute ).execute
project.create_or_update_import_data(data: { 'repo' => repo.raw_data, 'user_map' => user_map }, credentials: { fb_session: fb_session })
project
end end
end end
end end
......
...@@ -5,7 +5,7 @@ module Gitlab ...@@ -5,7 +5,7 @@ module Gitlab
def initialize(project) def initialize(project)
@project = project @project = project
credentials = import_data credentials = project.import_data
if credentials && credentials[:password] if credentials && credentials[:password]
@client = Client.new(credentials[:password]) @client = Client.new(credentials[:password])
@formatter = Gitlab::ImportFormatter.new @formatter = Gitlab::ImportFormatter.new
......
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
end end
def execute def execute
project = ::Projects::CreateService.new( ::Projects::CreateService.new(
current_user, current_user,
name: repo.name, name: repo.name,
path: repo.name, path: repo.name,
...@@ -21,12 +21,9 @@ module Gitlab ...@@ -21,12 +21,9 @@ module Gitlab
visibility_level: Gitlab::VisibilityLevel::PUBLIC, visibility_level: Gitlab::VisibilityLevel::PUBLIC,
import_type: "google_code", import_type: "google_code",
import_source: repo.name, import_source: repo.name,
import_url: repo.import_url import_url: repo.import_url,
import_data: { data: { 'repo' => repo.raw_data, 'user_map' => user_map } }
).execute ).execute
project.create_or_update_import_data(data: { 'repo' => repo.raw_data, 'user_map' => user_map })
project
end end
end end
end end
......
...@@ -96,5 +96,9 @@ module Gitlab ...@@ -96,5 +96,9 @@ module Gitlab
(?<![\/.]) (?# rule #6-7) (?<![\/.]) (?# rule #6-7)
}x.freeze }x.freeze
end end
def container_registry_reference_regex
git_reference_regex
end
end end
end end
require_relative "svg/whitelist"
module Gitlab module Gitlab
module Sanitizers module Sanitizers
module SVG module SVG
...@@ -12,14 +10,14 @@ module Gitlab ...@@ -12,14 +10,14 @@ module Gitlab
DATA_ATTR_PATTERN = /\Adata-(?!xml)[a-z_][\w.\u00E0-\u00F6\u00F8-\u017F\u01DD-\u02AF-]*\z/u DATA_ATTR_PATTERN = /\Adata-(?!xml)[a-z_][\w.\u00E0-\u00F6\u00F8-\u017F\u01DD-\u02AF-]*\z/u
def scrub(node) def scrub(node)
unless ALLOWED_ELEMENTS.include?(node.name) unless Whitelist::ALLOWED_ELEMENTS.include?(node.name)
node.unlink node.unlink
else else
node.attributes.each do |attr_name, attr| node.attributes.each do |attr_name, attr|
valid_attributes = ALLOWED_ATTRIBUTES[node.name] valid_attributes = Whitelist::ALLOWED_ATTRIBUTES[node.name]
unless valid_attributes && valid_attributes.include?(attr_name) unless valid_attributes && valid_attributes.include?(attr_name)
if ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS.include?(node.name) && if Whitelist::ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS.include?(node.name) &&
attr_name.start_with?('data-') attr_name.start_with?('data-')
# Arbitrary data attributes are allowed. Verify that the attribute # Arbitrary data attributes are allowed. Verify that the attribute
# is a valid data attribute. # is a valid data attribute.
......
This source diff could not be displayed because it is too large. You can view the blob instead.
module Gitlab module Gitlab
class ImportUrl class UrlSanitizer
def self.sanitize(content)
regexp = URI::Parser.new.make_regexp(['http', 'https', 'ssh', 'git'])
content.gsub(regexp) { |url| new(url).masked_url }
end
def initialize(url, credentials: nil) def initialize(url, credentials: nil)
@url = URI.parse(URI.encode(url)) @url = Addressable::URI.parse(URI.encode(url))
@credentials = credentials @credentials = credentials
end end
...@@ -9,6 +15,13 @@ module Gitlab ...@@ -9,6 +15,13 @@ module Gitlab
@sanitized_url ||= safe_url.to_s @sanitized_url ||= safe_url.to_s
end end
def masked_url
url = @url.dup
url.password = "*****" unless url.password.nil?
url.user = "*****" unless url.user.nil?
url.to_s
end
def credentials def credentials
@credentials ||= { user: @url.user, password: @url.password } @credentials ||= { user: @url.user, password: @url.password }
end end
......
...@@ -14,6 +14,7 @@ namespace :gitlab do ...@@ -14,6 +14,7 @@ namespace :gitlab do
Rake::Task["gitlab:backup:builds:create"].invoke Rake::Task["gitlab:backup:builds:create"].invoke
Rake::Task["gitlab:backup:artifacts:create"].invoke Rake::Task["gitlab:backup:artifacts:create"].invoke
Rake::Task["gitlab:backup:lfs:create"].invoke Rake::Task["gitlab:backup:lfs:create"].invoke
Rake::Task["gitlab:backup:registry:create"].invoke
backup = Backup::Manager.new backup = Backup::Manager.new
backup.pack backup.pack
...@@ -54,6 +55,7 @@ namespace :gitlab do ...@@ -54,6 +55,7 @@ namespace :gitlab do
Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds') Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds')
Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts') Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts')
Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs') Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs')
Rake::Task['gitlab:backup:registry:restore'].invoke unless backup.skipped?('registry')
Rake::Task['gitlab:shell:setup'].invoke Rake::Task['gitlab:shell:setup'].invoke
backup.cleanup backup.cleanup
...@@ -173,6 +175,25 @@ namespace :gitlab do ...@@ -173,6 +175,25 @@ namespace :gitlab do
end end
end end
namespace :registry do
task create: :environment do
$progress.puts "Dumping container registry images ... ".blue
if ENV["SKIP"] && ENV["SKIP"].include?("registry")
$progress.puts "[SKIPPED]".cyan
else
Backup::Registry.new.dump
$progress.puts "done".green
end
end
task restore: :environment do
$progress.puts "Restoring container registry images ... ".blue
Backup::Registry.new.restore
$progress.puts "done".green
end
end
def configure_cron_mode def configure_cron_mode
if ENV['CRON'] if ENV['CRON']
# We need an object we can say 'puts' and 'print' to; let's use a # We need an object we can say 'puts' and 'print' to; let's use a
......
...@@ -303,7 +303,7 @@ namespace :gitlab do ...@@ -303,7 +303,7 @@ namespace :gitlab do
else else
puts "no".red puts "no".red
try_fixing_it( try_fixing_it(
"sudo find #{upload_path} -type d -not -path #{upload_path} -exec chmod 0700 {} \\;" "sudo chmod 700 #{upload_path}"
) )
for_more_information( for_more_information(
see_installation_guide_section "GitLab" see_installation_guide_section "GitLab"
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class <%= migration_class_name %> < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
create_table :<%= table_name %> do |t|
<% attributes.each do |attribute| -%>
<% if attribute.password_digest? -%>
t.string :password_digest<%= attribute.inject_options %>
<% else -%>
t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
<% end -%>
<% end -%>
<% if options[:timestamps] %>
t.timestamps null: false
<% end -%>
end
<% attributes_with_index.each do |attribute| -%>
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
<% end -%>
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class <%= migration_class_name %> < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
<%- if migration_action == 'add' -%>
def change
<% attributes.each do |attribute| -%>
<%- if attribute.reference? -%>
add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
<%- else -%>
add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
<%- if attribute.has_index? -%>
add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
<%- end -%>
<%- end -%>
<%- end -%>
end
<%- elsif migration_action == 'join' -%>
def change
create_join_table :<%= join_tables.first %>, :<%= join_tables.second %> do |t|
<%- attributes.each do |attribute| -%>
<%= '# ' unless attribute.has_index? -%>t.index <%= attribute.index_name %><%= attribute.inject_index_options %>
<%- end -%>
end
end
<%- else -%>
def change
<% attributes.each do |attribute| -%>
<%- if migration_action -%>
<%- if attribute.reference? -%>
remove_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
<%- else -%>
<%- if attribute.has_index? -%>
remove_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
<%- end -%>
remove_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
<%- end -%>
<%- end -%>
<%- end -%>
end
<%- end -%>
end
...@@ -42,7 +42,7 @@ describe Projects::RawController do ...@@ -42,7 +42,7 @@ describe Projects::RawController do
before do before do
public_project.lfs_objects << lfs_object public_project.lfs_objects << lfs_object
allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true) allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true)
allow(controller).to receive(:send_file) { controller.render nothing: true } allow(controller).to receive(:send_file) { controller.head :ok }
end end
it 'serves the file' do it 'serves the file' do
......
require 'spec_helper'
describe "Container Registry" do
let(:project) { create(:empty_project) }
let(:repository) { project.container_registry_repository }
let(:tag_name) { 'latest' }
let(:tags) { [tag_name] }
before do
login_as(:user)
project.team << [@user, :developer]
stub_container_registry_tags(*tags)
stub_container_registry_config(enabled: true)
allow(Auth::ContainerRegistryAuthenticationService).to receive(:full_access_token).and_return('token')
end
describe 'GET /:project/container_registry' do
before do
visit namespace_project_container_registry_index_path(project.namespace, project)
end
context 'when no tags' do
let(:tags) { [] }
it { expect(page).to have_content('No images in Container Registry for this project') }
end
context 'when there are tags' do
it { expect(page).to have_content(tag_name)}
end
end
describe 'DELETE /:project/container_registry/tag' do
before do
visit namespace_project_container_registry_index_path(project.namespace, project)
end
it do
expect_any_instance_of(::ContainerRegistry::Tag).to receive(:delete).and_return(true)
click_on 'Remove'
end
end
end
{"architecture":"amd64","config":{"Hostname":"b14cd8298755","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":null,"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"b14cd82987550b01af9a666a2f4c996280a6152e66873134fae5a0f223dc5976","container_config":{"Hostname":"b14cd8298755","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":["/bin/sh","-c","#(nop) ADD file:033ab063740d9ff4dcfb1c69eccf25f91d88729f57cd5a73050e014e3e094aa0 in /"],"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"created":"2016-04-01T20:53:00.160300546Z","docker_version":"1.9.1","history":[{"created":"2016-04-01T20:53:00.160300546Z","created_by":"/bin/sh -c #(nop) ADD file:033ab063740d9ff4dcfb1c69eccf25f91d88729f57cd5a73050e014e3e094aa0 in /"}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:c56b7dabbc7aa730eeab07668bdcbd7e3d40855047ca9a0cc1bfed23a2486111"]}}
{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/octet-stream","size":1145,"digest":"sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":2319870,"digest":"sha256:420890c9e918b6668faaedd9000e220190f2493b0693ee563ebd7b4cc754a57d"}]}
require 'spec_helper'
describe ContainerRegistry::Blob do
let(:digest) { 'sha256:0123456789012345' }
let(:config) do
{
'digest' => digest,
'mediaType' => 'binary',
'size' => 1000
}
end
let(:registry) { ContainerRegistry::Registry.new('http://example.com') }
let(:repository) { registry.repository('group/test') }
let(:blob) { repository.blob(config) }
it { expect(blob).to respond_to(:repository) }
it { expect(blob).to delegate_method(:registry).to(:repository) }
it { expect(blob).to delegate_method(:client).to(:repository) }
context '#path' do
subject { blob.path }
it { is_expected.to eq('example.com/group/test@sha256:0123456789012345') }
end
context '#digest' do
subject { blob.digest }
it { is_expected.to eq(digest) }
end
context '#type' do
subject { blob.type }
it { is_expected.to eq('binary') }
end
context '#revision' do
subject { blob.revision }
it { is_expected.to eq('0123456789012345') }
end
context '#short_revision' do
subject { blob.short_revision }
it { is_expected.to eq('012345678') }
end
context '#delete' do
before do
stub_request(:delete, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345').
to_return(status: 200)
end
subject { blob.delete }
it { is_expected.to be_truthy }
end
end
require 'spec_helper'
describe ContainerRegistry::Registry do
let(:path) { nil }
let(:registry) { described_class.new('http://example.com', path: path) }
subject { registry }
it { is_expected.to respond_to(:client) }
it { is_expected.to respond_to(:uri) }
it { is_expected.to respond_to(:path) }
it { expect(subject.repository('test')).to_not be_nil }
context '#path' do
subject { registry.path }
context 'path from URL' do
it { is_expected.to eq('example.com') }
end
context 'custom path' do
let(:path) { 'registry.example.com' }
it { is_expected.to eq(path) }
end
end
end
require 'spec_helper'
describe ContainerRegistry::Repository do
let(:registry) { ContainerRegistry::Registry.new('http://example.com') }
let(:repository) { registry.repository('group/test') }
it { expect(repository).to respond_to(:registry) }
it { expect(repository).to delegate_method(:client).to(:registry) }
it { expect(repository.tag('test')).to_not be_nil }
context '#path' do
subject { repository.path }
it { is_expected.to eq('example.com/group/test') }
end
context 'manifest processing' do
before do
stub_request(:get, 'http://example.com/v2/group/test/tags/list').
with(headers: { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' }).
to_return(
status: 200,
body: JSON.dump(tags: ['test']),
headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json' })
end
context '#manifest' do
subject { repository.manifest }
it { is_expected.to_not be_nil }
end
context '#valid?' do
subject { repository.valid? }
it { is_expected.to be_truthy }
end
context '#tags' do
subject { repository.tags }
it { is_expected.to_not be_empty }
end
end
context '#delete_tags' do
let(:tag) { ContainerRegistry::Tag.new(repository, 'tag') }
before { expect(repository).to receive(:tags).twice.and_return([tag]) }
subject { repository.delete_tags }
context 'succeeds' do
before { expect(tag).to receive(:delete).and_return(true) }
it { is_expected.to be_truthy }
end
context 'any fails' do
before { expect(tag).to receive(:delete).and_return(false) }
it { is_expected.to be_falsey }
end
end
end
require 'spec_helper'
describe ContainerRegistry::Tag do
let(:registry) { ContainerRegistry::Registry.new('http://example.com') }
let(:repository) { registry.repository('group/test') }
let(:tag) { repository.tag('tag') }
let(:headers) { { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' } }
it { expect(tag).to respond_to(:repository) }
it { expect(tag).to delegate_method(:registry).to(:repository) }
it { expect(tag).to delegate_method(:client).to(:repository) }
context '#path' do
subject { tag.path }
it { is_expected.to eq('example.com/group/test:tag') }
end
context 'manifest processing' do
before do
stub_request(:get, 'http://example.com/v2/group/test/manifests/tag').
with(headers: headers).
to_return(
status: 200,
body: File.read(Rails.root + 'spec/fixtures/container_registry/tag_manifest.json'),
headers: { 'Content-Type' => 'application/vnd.docker.distribution.manifest.v2+json' })
end
context '#layers' do
subject { tag.layers }
it { expect(subject.length).to eq(1) }
end
context '#total_size' do
subject { tag.total_size }
it { is_expected.to eq(2319870) }
end
context 'config processing' do
before do
stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac').
with(headers: { 'Accept' => 'application/octet-stream' }).
to_return(
status: 200,
body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json'))
end
context '#config' do
subject { tag.config }
it { is_expected.to_not be_nil }
end
context '#created_at' do
subject { tag.created_at }
it { is_expected.to_not be_nil }
end
end
end
context 'manifest digest' do
before do
stub_request(:head, 'http://example.com/v2/group/test/manifests/tag').
with(headers: headers).
to_return(status: 200, headers: { 'Docker-Content-Digest' => 'sha256:digest' })
end
context '#digest' do
subject { tag.digest }
it { is_expected.to eq('sha256:digest') }
end
context '#delete' do
before do
stub_request(:delete, 'http://example.com/v2/group/test/manifests/sha256:digest').
with(headers: headers).
to_return(status: 200)
end
subject { tag.delete }
it { is_expected.to be_truthy }
end
end
end
require 'spec_helper'
describe Gitlab::Database::MigrationHelpers, lib: true do
let(:model) do
Class.new do
include Gitlab::Database::MigrationHelpers
def method_missing(name, *args, &block)
ActiveRecord::Base.connection.send(name, *args, &block)
end
end.new
end
describe '#add_concurrent_index' do
context 'outside a transaction' do
before do
expect(model).to receive(:transaction_open?).and_return(false)
end
context 'using PostgreSQL' do
it 'creates the index concurrently' do
expect(Gitlab::Database).to receive(:postgresql?).and_return(true)
expect(model).to receive(:add_index).
with(:users, :foo, algorithm: :concurrently)
model.add_concurrent_index(:users, :foo)
end
end
context 'using MySQL' do
it 'creates a regular index' do
expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
expect(model).to receive(:add_index).
with(:users, :foo)
model.add_concurrent_index(:users, :foo)
end
end
end
context 'inside a transaction' do
it 'raises RuntimeError' do
expect(model).to receive(:transaction_open?).and_return(true)
expect { model.add_concurrent_index(:users, :foo) }.
to raise_error(RuntimeError)
end
end
end
describe '#update_column_in_batches' do
before do
create_list(:empty_project, 5)
end
it 'updates all the rows in a table' do
model.update_column_in_batches(:projects, :import_error, 'foo')
expect(Project.where(import_error: 'foo').count).to eq(5)
end
end
describe '#add_column_with_default' do
context 'outside of a transaction' do
before do
expect(model).to receive(:transaction_open?).and_return(false)
expect(model).to receive(:transaction).twice.and_yield
expect(model).to receive(:add_column).
with(:projects, :foo, :integer, default: nil)
expect(model).to receive(:change_column_default).
with(:projects, :foo, 10)
end
it 'adds the column while allowing NULL values' do
expect(model).to receive(:update_column_in_batches).
with(:projects, :foo, 10)
expect(model).not_to receive(:change_column_null)
model.add_column_with_default(:projects, :foo, :integer,
default: 10,
allow_null: true)
end
it 'adds the column while not allowing NULL values' do
expect(model).to receive(:update_column_in_batches).
with(:projects, :foo, 10)
expect(model).to receive(:change_column_null).
with(:projects, :foo, false)
model.add_column_with_default(:projects, :foo, :integer, default: 10)
end
it 'removes the added column whenever updating the rows fails' do
expect(model).to receive(:update_column_in_batches).
with(:projects, :foo, 10).
and_raise(RuntimeError)
expect(model).to receive(:remove_column).
with(:projects, :foo)
expect do
model.add_column_with_default(:projects, :foo, :integer, default: 10)
end.to raise_error(RuntimeError)
end
end
context 'inside a transaction' do
it 'raises RuntimeError' do
expect(model).to receive(:transaction_open?).and_return(true)
expect do
model.add_column_with_default(:projects, :foo, :integer, default: 10)
end.to raise_error(RuntimeError)
end
end
end
end
require 'spec_helper'
describe Gitlab::ImportUrl do
let(:credentials) { { user: 'blah', password: 'password' } }
let(:import_url) do
Gitlab::ImportUrl.new("https://github.com/me/project.git", credentials: credentials)
end
describe :full_url do
it { expect(import_url.full_url).to eq("https://blah:password@github.com/me/project.git") }
end
describe :sanitized_url do
it { expect(import_url.sanitized_url).to eq("https://github.com/me/project.git") }
end
describe :credentials do
it { expect(import_url.credentials).to eq(credentials) }
end
end
require 'spec_helper'
describe Gitlab::UrlSanitizer, lib: true do
let(:credentials) { { user: 'blah', password: 'password' } }
let(:url_sanitizer) do
described_class.new("https://github.com/me/project.git", credentials: credentials)
end
describe '.sanitize' do
def sanitize_url(url)
# We want to try with multi-line content because is how error messages are formatted
described_class.sanitize(%Q{
remote: Not Found
fatal: repository '#{url}' not found
})
end
it 'mask the credentials from HTTP URLs' do
filtered_content = sanitize_url('http://user:pass@test.com/root/repoC.git/')
expect(filtered_content).to include("http://*****:*****@test.com/root/repoC.git/")
end
it 'mask the credentials from HTTPS URLs' do
filtered_content = sanitize_url('https://user:pass@test.com/root/repoA.git/')
expect(filtered_content).to include("https://*****:*****@test.com/root/repoA.git/")
end
it 'mask credentials from SSH URLs' do
filtered_content = sanitize_url('ssh://user@host.test/path/to/repo.git')
expect(filtered_content).to include("ssh://*****@host.test/path/to/repo.git")
end
it 'does not modify Git URLs' do
# git protocol does not support authentication
filtered_content = sanitize_url('git://host.test/path/to/repo.git')
expect(filtered_content).to include("git://host.test/path/to/repo.git")
end
it 'does not modify scp-like URLs' do
filtered_content = sanitize_url('user@server:project.git')
expect(filtered_content).to include("user@server:project.git")
end
end
describe '#sanitized_url' do
it { expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") }
end
describe '#credentials' do
it { expect(url_sanitizer.credentials).to eq(credentials) }
end
describe '#full_url' do
it { expect(url_sanitizer.full_url).to eq("https://blah:password@github.com/me/project.git") }
it 'supports scp-like URLs' do
sanitizer = described_class.new('user@server:project.git')
expect(sanitizer.full_url).to eq('user@server:project.git')
end
end
end
...@@ -70,6 +70,20 @@ describe Namespace, models: true do ...@@ -70,6 +70,20 @@ describe Namespace, models: true do
allow(@namespace).to receive(:path).and_return(new_path) allow(@namespace).to receive(:path).and_return(new_path)
expect(@namespace.move_dir).to be_truthy expect(@namespace.move_dir).to be_truthy
end end
context "when any project has container tags" do
before do
stub_container_registry_config(enabled: true)
stub_container_registry_tags('tag')
create(:empty_project, namespace: @namespace)
allow(@namespace).to receive(:path_was).and_return(@namespace.path)
allow(@namespace).to receive(:path).and_return('new_path')
end
it { expect { @namespace.move_dir }.to raise_error('Namespace cannot be moved, because at least one project has tags in container registry') }
end
end end
describe :rm_dir do describe :rm_dir do
......
...@@ -25,7 +25,7 @@ describe SlackService::IssueMessage, models: true do ...@@ -25,7 +25,7 @@ describe SlackService::IssueMessage, models: true do
} }
end end
let(:color) { '#345' } let(:color) { '#C95823' }
context '#initialize' do context '#initialize' do
before do before do
...@@ -40,10 +40,11 @@ describe SlackService::IssueMessage, models: true do ...@@ -40,10 +40,11 @@ describe SlackService::IssueMessage, models: true do
context 'open' do context 'open' do
it 'returns a message regarding opening of issues' do it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq( expect(subject.pretext).to eq(
'Test User opened <url|issue #100> in <somewhere.com|project_name>: '\ '<somewhere.com|[project_name>] Issue opened by Test User')
'*Issue title*')
expect(subject.attachments).to eq([ expect(subject.attachments).to eq([
{ {
title: "#100 Issue title",
title_link: "url",
text: "issue description", text: "issue description",
color: color, color: color,
} }
...@@ -56,10 +57,10 @@ describe SlackService::IssueMessage, models: true do ...@@ -56,10 +57,10 @@ describe SlackService::IssueMessage, models: true do
args[:object_attributes][:action] = 'close' args[:object_attributes][:action] = 'close'
args[:object_attributes][:state] = 'closed' args[:object_attributes][:state] = 'closed'
end end
it 'returns a message regarding closing of issues' do it 'returns a message regarding closing of issues' do
expect(subject.pretext). to eq( expect(subject.pretext). to eq(
'Test User closed <url|issue #100> in <somewhere.com|project_name>: '\ '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by Test User')
'*Issue title*')
expect(subject.attachments).to be_empty expect(subject.attachments).to be_empty
end end
end end
......
...@@ -634,11 +634,11 @@ describe Project, models: true do ...@@ -634,11 +634,11 @@ describe Project, models: true do
# Project#gitlab_shell returns a new instance of Gitlab::Shell on every # Project#gitlab_shell returns a new instance of Gitlab::Shell on every
# call. This makes testing a bit easier. # call. This makes testing a bit easier.
allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
end
it 'renames a repository' do
allow(project).to receive(:previous_changes).and_return('path' => ['foo']) allow(project).to receive(:previous_changes).and_return('path' => ['foo'])
end
it 'renames a repository' do
ns = project.namespace_dir ns = project.namespace_dir
expect(gitlab_shell).to receive(:mv_repository). expect(gitlab_shell).to receive(:mv_repository).
...@@ -663,6 +663,17 @@ describe Project, models: true do ...@@ -663,6 +663,17 @@ describe Project, models: true do
project.rename_repo project.rename_repo
end end
context 'container registry with tags' do
before do
stub_container_registry_config(enabled: true)
stub_container_registry_tags('tag')
end
subject { project.rename_repo }
it { expect{subject}.to raise_error(Exception) }
end
end end
describe '#expire_caches_before_rename' do describe '#expire_caches_before_rename' do
...@@ -772,4 +783,71 @@ describe Project, models: true do ...@@ -772,4 +783,71 @@ describe Project, models: true do
expect(project.protected_branch?('foo')).to eq(false) expect(project.protected_branch?('foo')).to eq(false)
end end
end end
describe '#container_registry_repository' do
let(:project) { create(:empty_project) }
before { stub_container_registry_config(enabled: true) }
subject { project.container_registry_repository }
it { is_expected.to_not be_nil }
end
describe '#container_registry_repository_url' do
let(:project) { create(:empty_project) }
subject { project.container_registry_repository_url }
before { stub_container_registry_config(**registry_settings) }
context 'for enabled registry' do
let(:registry_settings) do
{
enabled: true,
host_port: 'example.com',
}
end
it { is_expected.to_not be_nil }
end
context 'for disabled registry' do
let(:registry_settings) do
{
enabled: false
}
end
it { is_expected.to be_nil }
end
end
describe '#has_container_registry_tags?' do
let(:project) { create(:empty_project) }
subject { project.has_container_registry_tags? }
context 'for enabled registry' do
before { stub_container_registry_config(enabled: true) }
context 'with tags' do
before { stub_container_registry_tags('test', 'test2') }
it { is_expected.to be_truthy }
end
context 'when no tags' do
before { stub_container_registry_tags }
it { is_expected.to be_falsey }
end
end
context 'for disabled registry' do
before { stub_container_registry_config(enabled: false) }
it { is_expected.to be_falsey }
end
end
end end
This diff is collapsed.
...@@ -5,19 +5,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -5,19 +5,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
let(:current_user) { nil } let(:current_user) { nil }
let(:current_params) { {} } let(:current_params) { {} }
let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) } let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) }
let(:registry_settings) do
{
enabled: true,
issuer: 'rspec',
key: nil
}
end
let(:payload) { JWT.decode(subject[:token], rsa_key).first } let(:payload) { JWT.decode(subject[:token], rsa_key).first }
subject { described_class.new(current_project, current_user, current_params).execute } subject { described_class.new(current_project, current_user, current_params).execute }
before do before do
allow(Gitlab.config.registry).to receive_messages(registry_settings) stub_container_registry_config(enabled: true, issuer: 'rspec', key: nil)
allow_any_instance_of(JSONWebToken::RSAToken).to receive(:key).and_return(rsa_key) allow_any_instance_of(JSONWebToken::RSAToken).to receive(:key).and_return(rsa_key)
end end
...@@ -57,6 +50,11 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -57,6 +50,11 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end end
end end
shared_examples 'an unauthorized' do
it { is_expected.to include(http_status: 401) }
it { is_expected.to_not include(:token) }
end
shared_examples 'a forbidden' do shared_examples 'a forbidden' do
it { is_expected.to include(http_status: 403) } it { is_expected.to include(http_status: 403) }
it { is_expected.to_not include(:token) } it { is_expected.to_not include(:token) }
...@@ -123,7 +121,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -123,7 +121,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
{ offline_token: true } { offline_token: true }
end end
it_behaves_like 'a forbidden' it_behaves_like 'an unauthorized'
end end
context 'allow to pull and push images' do context 'allow to pull and push images' do
...@@ -164,6 +162,20 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -164,6 +162,20 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end end
end end
end end
context 'for project without container registry' do
let(:project) { create(:empty_project, :public, container_registry_enabled: false) }
before { project.update(container_registry_enabled: false) }
context 'disallow when pulling' do
let(:current_params) do
{ scope: "repository:#{project.path_with_namespace}:pull" }
end
it_behaves_like 'a forbidden'
end
end
end end
context 'unauthorized' do context 'unauthorized' do
...@@ -172,7 +184,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do ...@@ -172,7 +184,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
{ offline_token: true } { offline_token: true }
end end
it_behaves_like 'a forbidden' it_behaves_like 'an unauthorized'
end end
context 'for invalid scope' do context 'for invalid scope' do
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment