Commit 3c69d253 authored by Stan Hu's avatar Stan Hu

Merge branch 'ce-to-ee-2018-07-02' into 'master'

CE upstream - 2018-07-02 09:22 UTC

Closes #5864

See merge request gitlab-org/gitlab-ee!6346
parents a11c8496 609a9558
......@@ -106,11 +106,11 @@ class Repository
"#<#{self.class.name}:#{@disk_path}>"
end
def commit(ref = 'HEAD')
def commit(ref = nil)
return nil unless exists?
return ref if ref.is_a?(::Commit)
find_commit(ref)
find_commit(ref || root_ref)
end
# Finding a commit by the passed SHA
......@@ -290,6 +290,10 @@ class Repository
)
end
def cached_methods
CACHED_METHODS
end
def expire_tags_cache
expire_method_caches(%i(tag_names tag_count))
@tags = nil
......@@ -430,7 +434,7 @@ class Repository
# Runs code after the HEAD of a repository is changed.
def after_change_head
expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys)
expire_all_method_caches
end
# Runs code after a repository has been forked/imported.
......
---
title: Fix label and milestone duplicated records and IID errors
merge_request: 19961
author:
type: fixed
---
title: Expire correct method caches after HEAD changed
merge_request:
author:
type: fixed
......@@ -121,6 +121,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
| `provider` | Always `AWS` for compatible hosts | AWS |
| `aws_access_key_id` | AWS credentials, or compatible | |
| `aws_secret_access_key` | AWS credentials, or compatible | |
| `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 |
| `region` | AWS region | us-east-1 |
| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
......
......@@ -79,6 +79,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
| `provider` | Always `AWS` for compatible hosts | AWS |
| `aws_access_key_id` | AWS credentials, or compatible | |
| `aws_secret_access_key` | AWS credentials, or compatible | |
| `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 |
| `region` | AWS region | us-east-1 |
| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
......
......@@ -134,9 +134,20 @@ In order to do that, follow the steps:
```yaml
image: docker:stable
# When using dind, it's wise to use the overlayfs driver for
# improved performance.
variables:
# When using dind service we need to instruct docker, to talk with the
# daemon started inside of the service. The daemon is available with
# a network connection instead of the default /var/run/docker.sock socket.
#
# The 'docker' hostname is the alias of the service container as described at
# https://docs.gitlab.com/ee/ci/docker/using_docker_images.html#accessing-the-services
#
# Note that if you're using Kubernetes executor, the variable should be set to
# tcp://localhost:2375 because of how Kubernetes executor connects services
# to the job container
DOCKER_HOST: tcp://docker:2375/
# When using dind, it's wise to use the overlayfs driver for
# improved performance.
DOCKER_DRIVER: overlay2
services:
......@@ -293,6 +304,7 @@ services:
variables:
CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH
DOCKER_HOST: tcp://docker:2375
DOCKER_DRIVER: overlay2
before_script:
......@@ -391,6 +403,9 @@ could look like:
image: docker:stable
services:
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_DRIVER: overlay2
stage: build
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com
......@@ -410,6 +425,8 @@ services:
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_DRIVER: overlay2
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
before_script:
......@@ -445,6 +462,8 @@ stages:
- deploy
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_DRIVER: overlay2
CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project/my-image:$CI_COMMIT_REF_NAME
CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project/my-image:latest
......
......@@ -326,6 +326,16 @@ For installations from source:
1. [Restart GitLab] for the changes to take effect
#### Specifying a custom directory for backups
Note: This option only works for remote storage. If you want to group your backups
you can pass a `DIRECTORY` environment variable:
```
sudo gitlab-rake gitlab:backup:create DIRECTORY=daily
sudo gitlab-rake gitlab:backup:create DIRECTORY=weekly
```
### Uploading to locally mounted shares
You may also send backups to a mounted share (`NFS` / `CIFS` / `SMB` / etc.) by
......@@ -369,15 +379,6 @@ For installations from source:
remote_directory: 'gitlab_backups'
```
### Specifying a custom directory for backups
If you want to group your backups you can pass a `DIRECTORY` environment variable:
```
sudo gitlab-rake gitlab:backup:create DIRECTORY=daily
sudo gitlab-rake gitlab:backup:create DIRECTORY=weekly
```
### Backup archive permissions
The backup archives created by GitLab (`1393513186_2014_02_27_gitlab_backup.tar`)
......
......@@ -90,6 +90,7 @@ Here is a configuration example with S3.
| `provider` | The provider name | AWS |
| `aws_access_key_id` | AWS credentials, or compatible | `ABC123DEF456` |
| `aws_secret_access_key` | AWS credentials, or compatible | `ABC123DEF456ABC123DEF456ABC123DEF456` |
| `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 |
| `region` | AWS region | us-east-1 |
| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
| `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
......
module Gitlab
module ImportExport
# Given a class, it finds or creates a new object
# (initializes in the case of Label) at group or project level.
# If it does not exist in the group, it creates it at project level.
#
# Example:
# `GroupProjectObjectBuilder.build(Label, label_attributes)`
# finds or initializes a label with the given attributes.
#
# It also adds some logic around Group Labels/Milestones for edge cases.
class GroupProjectObjectBuilder
def self.build(*args)
Project.transaction do
new(*args).find
end
end
def initialize(klass, attributes)
@klass = klass < Label ? Label : klass
@attributes = attributes
@group = @attributes['group']
@project = @attributes['project']
end
def find
find_object || @klass.create(project_attributes)
end
private
def find_object
@klass.where(where_clause).first
end
def where_clause
@attributes.slice('title').map do |key, value|
scope_clause = table[:project_id].eq(@project.id)
scope_clause = scope_clause.or(table[:group_id].eq(@group.id)) if @group
table[key].eq(value).and(scope_clause)
end.reduce(:or)
end
def table
@table ||= @klass.arel_table
end
def project_attributes
@attributes.except('group').tap do |atts|
if label?
atts['type'] = 'ProjectLabel' # Always create project labels
elsif milestone?
if atts['group_id'] # Transform new group milestones into project ones
atts['iid'] = nil
atts.delete('group_id')
else
claim_iid
end
end
end
end
def label?
@klass == Label
end
def milestone?
@klass == Milestone
end
# If an existing group milestone used the IID
# claim the IID back and set the group milestone to use one available
# This is necessary to fix situations like the following:
# - Importing into a user namespace project with exported group milestones
# where the IID of the Group milestone could conflict with a project one.
def claim_iid
# The milestone has to be a group milestone, as it's the only case where
# we set the IID as the maximum. The rest of them are fixed.
milestone = @project.milestones.find_by(iid: @attributes['iid'])
return unless milestone
milestone.iid = nil
milestone.ensure_project_iid!
milestone.save!
end
end
end
end
module Gitlab
module ImportExport
class ProjectTreeRestorer
# Relations which cannot have both group_id and project_id at the same time
RESTRICT_PROJECT_AND_GROUP = %i(milestone milestones).freeze
# Relations which cannot be saved at project level (and have a group assigned)
GROUP_MODELS = [GroupLabel, Milestone].freeze
def initialize(user:, shared:, project:)
@path = File.join(shared.export_path, 'project.json')
......@@ -70,12 +70,23 @@ module Gitlab
def save_relation_hash(relation_hash_batch, relation_key)
relation_hash = create_relation(relation_key, relation_hash_batch)
remove_group_models(relation_hash) if relation_hash.is_a?(Array)
@saved = false unless restored_project.append_or_update_attribute(relation_key, relation_hash)
# Restore the project again, extra query that skips holding the AR objects in memory
@restored_project = Project.find(@project_id)
end
# Remove project models that became group models as we found them at group level.
# This no longer required saving them at the root project level.
# For example, in the case of an existing group label that matched the title.
def remove_group_models(relation_hash)
relation_hash.reject! do |value|
GROUP_MODELS.include?(value.class) && value.group_id
end
end
def default_relation_list
reader.tree.reject do |model|
model.is_a?(Hash) && model[:project_members]
......@@ -170,7 +181,7 @@ module Gitlab
def create_relation(relation, relation_hash_list)
relation_array = [relation_hash_list].flatten.map do |relation_hash|
Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
relation_hash: parsed_relation_hash(relation_hash, relation.to_sym),
relation_hash: relation_hash,
members_mapper: members_mapper,
user: @user,
project: @restored_project,
......@@ -180,18 +191,6 @@ module Gitlab
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
end
def parsed_relation_hash(relation_hash, relation_type)
if RESTRICT_PROJECT_AND_GROUP.include?(relation_type)
params = {}
params['group_id'] = restored_project.group.try(:id) if relation_hash['group_id']
params['project_id'] = restored_project.id if relation_hash['project_id']
else
params = { 'group_id' => restored_project.group.try(:id), 'project_id' => restored_project.id }
end
relation_hash.merge(params)
end
def reader
@reader ||= Gitlab::ImportExport::Reader.new(shared: @shared)
end
......
......@@ -55,6 +55,8 @@ module Gitlab
@project = project
@imported_object_retries = 0
@relation_hash['project_id'] = @project.id
# Remove excluded keys from relation_hash
# We don't do this in the parsed_relation_hash because of the 'transformed attributes'
# For example, MergeRequestDiffFiles exports its diff attribute as utf8_diff. Then,
......@@ -81,15 +83,12 @@ module Gitlab
case @relation_name
when :merge_request_diff_files then setup_diff
when :notes then setup_note
when :project_label, :project_labels then setup_label
when :milestone, :milestones then setup_milestone
when 'Ci::Pipeline' then setup_pipeline
else
@relation_hash['project_id'] = @project.id
end
update_user_references
update_project_references
update_group_references
remove_duplicate_assignees
reset_tokens!
......@@ -152,39 +151,23 @@ module Gitlab
end
def update_project_references
project_id = @relation_hash.delete('project_id')
# If source and target are the same, populate them with the new project ID.
if @relation_hash['source_project_id']
@relation_hash['source_project_id'] = same_source_and_target? ? project_id : MergeRequestParser::FORKED_PROJECT_ID
@relation_hash['source_project_id'] = same_source_and_target? ? @relation_hash['project_id'] : MergeRequestParser::FORKED_PROJECT_ID
end
# project_id may not be part of the export, but we always need to populate it if required.
@relation_hash['project_id'] = project_id
@relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id']
@relation_hash['target_project_id'] = @relation_hash['project_id'] if @relation_hash['target_project_id']
end
def same_source_and_target?
@relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id']
end
def setup_label
# If there's no group, move the label to a project label
if @relation_hash['type'] == 'GroupLabel' && @relation_hash['group_id']
@relation_hash['project_id'] = nil
@relation_name = :group_label
else
@relation_hash['group_id'] = nil
@relation_hash['type'] = 'ProjectLabel'
end
end
def update_group_references
return unless EXISTING_OBJECT_CHECK.include?(@relation_name)
return unless @relation_hash['group_id']
def setup_milestone
if @relation_hash['group_id']
@relation_hash['group_id'] = @project.group.id
else
@relation_hash['project_id'] = @project.id
end
@relation_hash['group_id'] = @project.group&.id
end
def reset_tokens!
......@@ -272,15 +255,7 @@ module Gitlab
end
def existing_object
@existing_object ||=
begin
existing_object = find_or_create_object!
# Done in two steps, as MySQL behaves differently than PostgreSQL using
# the +find_or_create_by+ method and does not return the ID the second time.
existing_object.update!(parsed_relation_hash)
existing_object
end
@existing_object ||= find_or_create_object!
end
def unknown_service?
......@@ -289,29 +264,16 @@ module Gitlab
end
def find_or_create_object!
finder_attributes = if @relation_name == :group_label
%w[title group_id]
elsif parsed_relation_hash['project_id']
%w[title project_id]
else
%w[title group_id]
end
finder_hash = parsed_relation_hash.slice(*finder_attributes)
if label?
label = relation_class.find_or_initialize_by(finder_hash)
parsed_relation_hash.delete('priorities') if label.persisted?
label.save!
label
else
relation_class.find_or_create_by(finder_hash)
return relation_class.find_or_create_by(project_id: @project.id) if @relation_name == :project_feature
# Can't use IDs as validation exists calling `group` or `project` attributes
finder_hash = parsed_relation_hash.tap do |hash|
hash['group'] = @project.group if relation_class.attribute_method?('group_id')
hash['project'] = @project
hash.delete('project_id')
end
end
def label?
@relation_name.to_s.include?('label')
GroupProjectObjectBuilder.build(relation_class, finder_hash)
end
end
end
......
......@@ -25,6 +25,11 @@ module Gitlab
raise NotImplementedError
end
# List of cached methods. Should be overridden by the including class
def cached_methods
raise NotImplementedError
end
# Caches the supplied block both in a cache and in an instance variable.
#
# The cache key and instance variable are named the same way as the value of
......@@ -67,6 +72,11 @@ module Gitlab
# Expires the caches of a specific set of methods
def expire_method_caches(methods)
methods.each do |key|
unless cached_methods.include?(key.to_sym)
Rails.logger.error "Requested to expire non-existent method '#{key}' for Repository"
next
end
cache.expire(key)
ivar = cache_instance_variable_name(key)
......
require 'spec_helper'
describe Gitlab::ImportExport::GroupProjectObjectBuilder do
let(:project) do
create(:project,
:builds_disabled,
:issues_disabled,
name: 'project',
path: 'project',
group: create(:group))
end
context 'labels' do
it 'finds the right group label' do
group_label = create(:group_label, 'name': 'group label', 'group': project.group)
expect(described_class.build(Label,
'title' => 'group label',
'project' => project,
'group' => project.group)).to eq(group_label)
end
it 'creates a new label' do
label = described_class.build(Label,
'title' => 'group label',
'project' => project,
'group' => project.group)
expect(label.persisted?).to be true
end
end
context 'milestones' do
it 'finds the right group milestone' do
milestone = create(:milestone, 'name' => 'group milestone', 'group' => project.group)
expect(described_class.build(Milestone,
'title' => 'group milestone',
'project' => project,
'group' => project.group)).to eq(milestone)
end
it 'creates a new milestone' do
milestone = described_class.build(Milestone,
'title' => 'group milestone',
'project' => project,
'group' => project.group)
expect(milestone.persisted?).to be true
end
end
end
......@@ -7,7 +7,7 @@
"milestones": [
{
"id": 1,
"title": "Project milestone",
"title": "A milestone",
"project_id": 8,
"description": "Project-level milestone",
"due_date": null,
......@@ -66,8 +66,8 @@
"group_milestone_id": null,
"milestone": {
"id": 1,
"title": "Project milestone",
"project_id": 8,
"title": "A milestone",
"group_id": 8,
"description": "Project-level milestone",
"due_date": null,
"created_at": "2016-06-14T15:02:04.415Z",
......@@ -86,7 +86,7 @@
"updated_at": "2017-08-15T18:37:40.795Z",
"label": {
"id": 6,
"title": "Another project label",
"title": "Another label",
"color": "#A8D695",
"project_id": null,
"created_at": "2017-08-15T18:37:19.698Z",
......
{
"description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
"import_type": "gitlab_project",
"creator_id": 123,
"visibility_level": 10,
"archived": false,
"issues": [
{
"id": 1,
"title": "Fugiat est minima quae maxime non similique.",
"assignee_id": null,
"project_id": 8,
"author_id": 1,
"created_at": "2017-07-07T18:13:01.138Z",
"updated_at": "2017-08-15T18:37:40.807Z",
"branch_name": null,
"description": "Quam totam fuga numquam in eveniet.",
"state": "opened",
"iid": 20,
"updated_by_id": 1,
"confidential": false,
"due_date": null,
"moved_to_id": null,
"lock_version": null,
"time_estimate": 0,
"closed_at": null,
"last_edited_at": null,
"last_edited_by_id": null,
"group_milestone_id": null,
"milestone": {
"id": 1,
"title": "Group-level milestone",
"description": "Group-level milestone",
"due_date": null,
"created_at": "2016-06-14T15:02:04.415Z",
"updated_at": "2016-06-14T15:02:04.415Z",
"state": "active",
"iid": 1,
"group_id": 8
}
},
{
"id": 2,
"title": "est minima quae maxime non similique.",
"assignee_id": null,
"project_id": 8,
"author_id": 1,
"created_at": "2017-07-07T18:13:01.138Z",
"updated_at": "2017-08-15T18:37:40.807Z",
"branch_name": null,
"description": "Quam totam fuga numquam in eveniet.",
"state": "opened",
"iid": 21,
"updated_by_id": 1,
"confidential": false,
"due_date": null,
"moved_to_id": null,
"lock_version": null,
"time_estimate": 0,
"closed_at": null,
"last_edited_at": null,
"last_edited_by_id": null,
"group_milestone_id": null,
"milestone": {
"id": 2,
"title": "Another milestone",
"project_id": 8,
"description": "milestone",
"due_date": null,
"created_at": "2016-06-14T15:02:04.415Z",
"updated_at": "2016-06-14T15:02:04.415Z",
"state": "active",
"iid": 1,
"group_id": null
}
}
],
"snippets": [],
"hooks": []
}
......@@ -189,8 +189,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
@project.pipelines.zip([2, 2, 2, 2, 2])
.each do |(pipeline, expected_status_size)|
expect(pipeline.statuses.size).to eq(expected_status_size)
end
expect(pipeline.statuses.size).to eq(expected_status_size)
end
end
end
......@@ -246,13 +246,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(project.issues.size).to eq(results.fetch(:issues, 0))
end
it 'has issue with group label and project label' do
labels = project.issues.first.labels
expect(labels.where(type: "ProjectLabel").count).to eq(results.fetch(:first_issue_labels, 0))
expect(labels.where(type: "ProjectLabel").where.not(group_id: nil).count).to eq(0)
end
it 'does not set params that are excluded from import_export settings' do
expect(project.import_type).to be_nil
expect(project.creator_id).not_to eq 123
......@@ -268,12 +261,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
it 'has group milestone' do
expect(project.group.milestones.size).to eq(results.fetch(:milestones, 0))
end
it 'has issue with group label' do
labels = project.issues.first.labels
expect(labels.where(type: "GroupLabel").count).to eq(results.fetch(:first_issue_labels, 0))
end
end
context 'Light JSON' do
......@@ -360,13 +347,72 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
it_behaves_like 'restores project correctly',
issues: 2,
labels: 1,
milestones: 1,
milestones: 2,
first_issue_labels: 1
it_behaves_like 'restores group correctly',
labels: 1,
milestones: 1,
labels: 0,
milestones: 0,
first_issue_labels: 1
end
context 'with existing group models' do
let!(:project) do
create(:project,
:builds_disabled,
:issues_disabled,
name: 'project',
path: 'project',
group: create(:group))
end
before do
project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
end
it 'imports labels' do
create(:group_label, name: 'Another label', group: project.group)
expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
restored_project_json
expect(project.labels.count).to eq(1)
end
it 'imports milestones' do
create(:milestone, name: 'A milestone', group: project.group)
expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
restored_project_json
expect(project.group.milestones.count).to eq(1)
expect(project.milestones.count).to eq(0)
end
end
context 'with clashing milestones on IID' do
let!(:project) do
create(:project,
:builds_disabled,
:issues_disabled,
name: 'project',
path: 'project',
group: create(:group))
end
it 'preserves the project milestone IID' do
project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.milestone-iid.json")
expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
restored_project_json
expect(project.milestones.count).to eq(2)
expect(Milestone.find_by_title('Another milestone').iid).to eq(1)
expect(Milestone.find_by_title('Group-level milestone').iid).to eq(2)
end
end
end
end
......@@ -67,10 +67,18 @@ describe Gitlab::RepositoryCacheAdapter do
describe '#expire_method_caches' do
it 'expires the caches of the given methods' do
expect(cache).to receive(:expire).with(:readme)
expect(cache).to receive(:expire).with(:rendered_readme)
expect(cache).to receive(:expire).with(:gitignore)
repository.expire_method_caches(%i(readme gitignore))
repository.expire_method_caches(%i(rendered_readme gitignore))
end
it 'does not expire caches for non-existent methods' do
expect(cache).not_to receive(:expire).with(:nonexistent)
expect(Rails.logger).to(
receive(:error).with("Requested to expire non-existent method 'nonexistent' for Repository"))
repository.expire_method_caches(%i(nonexistent))
end
end
end
......@@ -479,6 +479,14 @@ describe Repository do
end
end
context 'when ref is not specified' do
it 'is using a root ref' do
expect(repository).to receive(:find_commit).with('master')
repository.commit
end
end
context 'when ref is not valid' do
context 'when preceding tree element exists' do
it 'returns nil' do
......@@ -1689,19 +1697,29 @@ describe Repository do
end
describe '#after_change_head' do
it 'flushes the readme cache' do
it 'flushes the method caches' do
expect(repository).to receive(:expire_method_caches).with([
:readme,
:size,
:commit_count,
:rendered_readme,
:contribution_guide,
:changelog,
:license,
:contributing,
:license_blob,
:license_key,
:gitignore,
:koding,
:gitlab_ci,
:koding_yml,
:gitlab_ci_yml,
:branch_names,
:tag_names,
:branch_count,
:tag_count,
:avatar,
:issue_template,
:merge_request_template,
:xcode_config
:exists?,
:root_ref,
:has_visible_content?,
:issue_template_names,
:merge_request_template_names,
:xcode_project?
])
repository.after_change_head
......
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