Commit 9a1c93fe authored by Nick Thomas's avatar Nick Thomas

Merge branch 'ce-to-ee-2018-03-09' into 'master'

CE upstream - 2018-03-09 18:26 UTC

Closes gitaly#1051

See merge request gitlab-org/gitlab-ee!4922
parents 5cf4da85 ca28b27e
......@@ -26,7 +26,7 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
- [Priority labels (~Deliverable and ~Stretch)](#priority-labels-deliverable-and-stretch)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#priority-labels-deliverable-stretch-next-patch-release)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker)
......@@ -99,8 +99,8 @@ coach is going to finish the merge request we assign the
## Helping others
Please help other GitLab users when you can. The channels people will reach out
on can be found on the [getting help page][getting-help].
Please help other GitLab users when you can.
The methods people will use to seek help can be found on the [getting help page][getting-help].
Sign up for the mailing list, answer GitLab questions on StackOverflow or
respond in the IRC channel. You can also sign up on [CodeTriage][codetriage] to help with
......@@ -127,7 +127,7 @@ Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
- Priority: ~Deliverable, ~Stretch
- Priority: ~Deliverable, ~Stretch, ~"Next Patch Release"
All labels, their meaning and priority are defined on the
[labels page][labels-page].
......@@ -186,7 +186,7 @@ indicate if an issue needs backend work, frontend work, or both.
Team labels are always capitalized so that they show up as the first label for
any issue.
### Priority labels (~Deliverable and ~Stretch)
### Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")
Priority labels help us clearly communicate expectations of the work for the
release. There are two levels of priority labels:
......@@ -196,6 +196,13 @@ release. There are two levels of priority labels:
- ~Stretch: Issues that are a stretch goal for delivering in the current
milestone. If these issues are not done in the current release, they will
strongly be considered for the next release.
- ~"Next Patch Release": Issues to put in the next patch release. Work on these
first, and add the "Pick Into X" label to the merge request, along with the
appropriate milestone.
Each issue scheduled for the current milestone should be labeled ~Deliverable
or ~"Stretch". Any open issue for a previous milestone should be labeled
~"Next Patch Release", or otherwise rescheduled to a different milestone.
### Severity labels (~S1, ~S2, etc.)
......@@ -705,3 +712,4 @@ When your code contains more than 500 changes, any major breaking changes, or an
[^1]: Please note that specs other than JavaScript specs are considered backend
code.
\ No newline at end of file
......@@ -18,6 +18,7 @@ class Repository
].freeze
include Gitlab::ShellAdapter
include Gitlab::RepositoryCacheAdapter
prepend EE::Repository
include Elastic::RepositoriesSearch
......@@ -64,22 +65,6 @@ class Repository
merge_request_template: :merge_request_template_names
}.freeze
# Wraps around the given method and caches its output in Redis and an instance
# variable.
#
# This only works for methods that do not take any arguments.
def self.cache_method(name, fallback: nil, memoize_only: false)
original = :"_uncached_#{name}"
alias_method(original, name)
define_method(name) do
cache_method_output(name, fallback: fallback, memoize_only: memoize_only) do
__send__(original) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
def initialize(full_path, project, disk_path: nil, is_wiki: false)
@full_path = full_path
@disk_path = disk_path || full_path
......@@ -309,17 +294,6 @@ class Repository
expire_method_caches(CACHED_METHODS)
end
# Expires the caches of a specific set of methods
def expire_method_caches(methods)
methods.each do |key|
cache.expire(key)
ivar = cache_instance_variable_name(key)
remove_instance_variable(ivar) if instance_variable_defined?(ivar)
end
end
def expire_avatar_cache
expire_method_caches(%i(avatar))
end
......@@ -987,49 +961,6 @@ class Repository
end
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
# the `key` argument.
#
# This method will return `nil` if the corresponding instance variable is also
# set to `nil`. This ensures we don't keep yielding the block when it returns
# `nil`.
#
# key - The name of the key to cache the data in.
# fallback - A value to fall back to in the event of a Git error.
def cache_method_output(key, fallback: nil, memoize_only: false, &block)
ivar = cache_instance_variable_name(key)
if instance_variable_defined?(ivar)
instance_variable_get(ivar)
else
# If the repository doesn't exist and a fallback was specified we return
# that value inmediately. This saves us Rugged/gRPC invocations.
return fallback unless fallback.nil? || exists?
begin
value =
if memoize_only
yield
else
cache.fetch(key, &block)
end
instance_variable_set(ivar, value)
rescue Gitlab::Git::Repository::NoRepository
# Even if the above `#exists?` check passes these errors might still
# occur (for example because of a non-existing HEAD). We want to
# gracefully handle this and not cache anything
fallback
end
end
end
def cache_instance_variable_name(key)
:"@#{key.to_s.tr('?!', '')}"
end
def file_on_head(type)
if head = tree(:head)
head.blobs.find do |blob|
......@@ -1084,8 +1015,7 @@ class Repository
end
def cache
# TODO: should we use UUIDs here? We could move repositories without clearing this cache
@cache ||= RepositoryCache.new(full_path, @project.id)
@cache ||= Gitlab::RepositoryCache.new(self)
end
def tags_sorted_by_committed_date
......
......@@ -17,15 +17,7 @@ module MergeRequests
return @conflicts_can_be_resolved_in_ui = false unless merge_request.has_complete_diff_refs?
return @conflicts_can_be_resolved_in_ui = false if merge_request.branch_missing?
begin
# Try to parse each conflict. If the MR's mergeable status hasn't been
# updated, ensure that we don't say there are conflicts to resolve
# when there are no conflict files.
conflicts.files.each(&:lines)
@conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
rescue Gitlab::Git::CommandError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing
@conflicts_can_be_resolved_in_ui = false
end
@conflicts_can_be_resolved_in_ui = conflicts.can_be_resolved_in_ui?
end
def conflicts
......
---
title: Cache MergeRequests can_be_resolved_in_ui? git operations
merge_request: 17589
author:
type: performance
---
title: Submit a single batch blob RPC to Gitaly per HTTP request when viewing diffs
merge_request:
author:
type: performance
......@@ -228,4 +228,10 @@ module Gitlab
Gitlab::Routing.add_helpers(MilestonesRoutingHelper)
end
end
# This method is used for smooth upgrading from the current Rails 4.x to Rails 5.0.
# https://gitlab.com/gitlab-org/gitlab-ce/issues/14286
def self.rails5?
ENV["RAILS5"].in?(%w[1 true])
end
end
......@@ -3,20 +3,21 @@ class AddSectionNameIdIndexOnCiBuildTraceSections < ActiveRecord::Migration
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
INDEX_NAME = 'index_ci_build_trace_sections_on_section_name_id'
disable_ddl_transaction!
def up
# MySQL may already have this as a foreign key
unless index_exists?(:ci_build_trace_sections, :section_name_id)
add_concurrent_index :ci_build_trace_sections, :section_name_id
unless index_exists?(:ci_build_trace_sections, :section_name_id, name: INDEX_NAME)
add_concurrent_index :ci_build_trace_sections, :section_name_id, name: INDEX_NAME
end
end
def down
# We cannot remove index for MySQL because it's needed for foreign key
if Gitlab::Database.postgresql?
remove_concurrent_index :ci_build_trace_sections, :section_name_id
remove_concurrent_index :ci_build_trace_sections, :section_name_id, name: INDEX_NAME
end
end
end
class RescheduleCommitsCountForMergeRequestDiff < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'AddMergeRequestDiffCommitsCount'.freeze
BATCH_SIZE = 5000
DELAY_INTERVAL = 5.minutes.to_i
class MergeRequestDiff < ActiveRecord::Base
self.table_name = 'merge_request_diffs'
include ::EachBatch
end
disable_ddl_transaction!
def up
say 'Populating the MergeRequestDiff `commits_count` (reschedule)'
execute("SET statement_timeout TO '60s'") if Gitlab::Database.postgresql?
MergeRequestDiff.where(commits_count: nil).each_batch(of: BATCH_SIZE) do |relation, index|
start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
delay = index * DELAY_INTERVAL
BackgroundMigrationWorker.perform_in(delay, MIGRATION, [start_id, end_id])
end
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180308052825) do
ActiveRecord::Schema.define(version: 20180309121820) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......
......@@ -240,3 +240,31 @@ sudo gitlab-rake gitlab:tcp_check[example.com,80]
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:tcp_check[example.com,80] RAILS_ENV=production
```
## Clear exclusive lease (DANGER)
GitLab uses a shared lock mechanism: `ExclusiveLease` to prevent simultaneous operations
in a shared resource. An example is running periodic garbage collection on repositories.
In very specific situations, a operation locked by an Exclusive Lease can fail without
releasing the lock. If you can't wait for it to expire, you can run this task to manually
clear it.
To clear all exclusive leases:
DANGER: **DANGER**:
Don't run it while GitLab or Sidekiq is running
```bash
sudo gitlab-rake gitlab:exclusive_lease:clear
```
To specify a lease `type` or lease `type + id`, specify a scope:
```bash
# to clear all leases for repository garbage collection:
sudo gitlab-rake gitlab:exclusive_lease:clear[project_housekeeping:*]
# to clear a lease for repository garbage collection in a specific project: (id=4)
sudo gitlab-rake gitlab:exclusive_lease:clear[project_housekeeping:4]
```
......@@ -310,6 +310,18 @@ enable them.
You can make use of [environment variables](#helm-chart-variables) to automatically
scale your pod replicas.
It's important to note that when a project is deployed to a Kubernetes cluster,
it relies on a Docker image that has been pushed to the
[GitLab Container Registry](../../user/project/container_registry.md). Kubernetes
fetches this image and uses it to run the application. If the project is public,
the image can be accessed by Kubernetes without any authentication, allowing us
to have deployments more usable. If the project is private/internal, the
Registry requires credentials to pull the image. Currently, this is addressed
by providing `CI_JOB_TOKEN` as the password that can be used, but this token will
no longer be valid as soon as the deployment job finishes. This means that
Kubernetes can run the application, but in case it should be restarted or
executed somewhere else, it cannot be accessed again.
### Auto Monitoring
NOTE: **Note:**
......
......@@ -16,19 +16,29 @@ say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'P
Acme'. But what if 'Project Acme' already belongs to another group, say 'Open Source'?
This is where the group sharing feature can be of use.
To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the **Settings > Members** section.
To share 'Project Acme' with the 'Engineering' group:
![share project with groups](img/share_project_with_groups.png)
1. For 'Project Acme' use the left navigation menu to go to **Settings > Members**
Then select the 'Share with group' tab by clicking it.
![share project with groups](img/share_project_with_groups.png)
Now you can add the 'Engineering' group with the maximum access level of your choice. Click 'Share' to share it.
1. Select the 'Share with group' tab
1. Add the 'Engineering' group with the maximum access level of your choice
1. Click **Share** to share it
![share project with groups tab](img/share_project_with_groups_tab.png)
![share project with groups tab](img/share_project_with_groups_tab.png)
After sharing 'Project Acme' with 'Engineering', the project will be listed on the group dashboard.
1. After sharing 'Project Acme' with 'Engineering', the project will be listed
on the group dashboard
!['Project Acme' is listed as a shared project for 'Engineering'](img/other_group_sees_shared_project.png)
!['Project Acme' is listed as a shared project for 'Engineering'](img/other_group_sees_shared_project.png)
Note that you can only share a project with:
- groups for which you have an explicitly defined membership
- groups that contain a nested subgroup or project for which you have an explicitly defined role
Admins are able to share projects with any group in the system.
## Maximum access level
......
Feature: Project Active Tab
Background:
Given I sign in as a user
And I own a project
# Main Tabs
Scenario: On Project Home
Given I visit my project's home page
Then the active sub tab should be Home
And no other sub tabs should be active
And the active main tab should be Project
Scenario: On Project Repository
Given I visit my project's files page
Then the active main tab should be Repository
And no other main tabs should be active
Scenario: On Project Issues
Given I visit my project's issues page
Then the active main tab should be Issues
And no other main tabs should be active
Scenario: On Project Merge Requests
Given I visit my project's merge requests page
Then the active main tab should be Merge Requests
And no other main tabs should be active
Scenario: On Project Wiki
Given I visit my project's wiki page
Then the active main tab should be Wiki
And no other main tabs should be active
Scenario: On Project Members
Given I visit my project's members page
Then the active main tab should be Members
And no other main tabs should be active
# Sub Tabs: Home
Scenario: On Project Home/Show
Given I visit my project's home page
Then the active sub tab should be Home
And no other sub tabs should be active
And the active main tab should be Project
And no other main tabs should be active
Scenario: On Project Home/Activity
Given I visit my project's home page
And I click the "Activity" tab
Then the active sub tab should be Activity
And no other sub tabs should be active
And the active main tab should be Project
# Sub Tabs: Settings
Scenario: On Project Settings/Integrations
Given I visit my project's settings page
And I click the "Integrations" tab
Then the active sub tab should be Integrations
And no other sub tabs should be active
And the active main tab should be Settings
Scenario: On Project Settings/Repository
Given I visit my project's settings page
And I click the "Repository" tab
Then the active sub tab should be Repository
And no other sub tabs should be active
And the active main tab should be Settings
# Sub Tabs: Repository
Scenario: On Project Repository/Files
Given I visit my project's files page
Then the active sub tab should be Files
And no other sub tabs should be active
And the active main tab should be Repository
Scenario: On Project Repository/Commits
Given I visit my project's commits page
Then the active sub tab should be Commits
And no other sub tabs should be active
And the active main tab should be Repository
Scenario: On Project Repository/Graph
Given I visit my project's graph page
Then the active sub tab should be Graph
And no other sub tabs should be active
And the active main tab should be Repository
Scenario: On Project Repository/Compare
Given I visit my project's commits page
And I click the "Compare" tab
Then the active sub tab should be Compare
And no other sub tabs should be active
And the active main tab should be Repository
Scenario: On Project Repository/Charts
Given I visit my project's commits page
And I click the "Charts" tab
Then the active sub tab should be Charts
And no other sub tabs should be active
And the active main tab should be Repository
Scenario: On Project Repository/Branches
Given I visit my project's commits page
And I click the "Branches" tab
Then the active sub tab should be Branches
And no other sub tabs should be active
And the active main tab should be Repository
Scenario: On Project Repository/Tags
Given I visit my project's commits page
And I click the "Tags" tab
Then the active sub tab should be Tags
And no other sub tabs should be active
And the active main tab should be Repository
Scenario: On Project Issues/Browse
Given I visit my project's issues page
Then the active main tab should be Issues
And no other main tabs should be active
Scenario: On Project Issues/Milestones
Given I visit my project's issues page
And I click the "Milestones" sub tab
Then the active main tab should be Issues
Then the active sub tab should be Milestones
And no other main tabs should be active
And no other sub tabs should be active
Scenario: On Project Issues/Labels
Given I visit my project's issues page
And I click the "Labels" sub tab
Then the active main tab should be Issues
Then the active sub tab should be Labels
And no other main tabs should be active
And no other sub tabs should be active
Feature: Project Fork
Background:
Given I sign in as a user
And I am a member of project "Shop"
When I visit project "Shop" page
Scenario: User fork a project
Given I click link "Fork"
When I fork to my namespace
Then I should see the forked project page
Scenario: User already has forked the project
Given I already have a project named "Shop" in my namespace
And I click link "Fork"
When I fork to my namespace
Then I should see a "Name has already been taken" warning
Scenario: Merge request on canonical repo goes to fork merge request page
Given I click link "Fork"
And I fork to my namespace
Then I should see the forked project page
When I visit project "Shop" page
Then I should see "New merge request"
And I goto the Merge Requests page
Then I should see "New merge request"
And I click link "New merge request"
Then I should see the new merge request page for my namespace
Scenario: Viewing forks of a Project
Given I click link "Fork"
When I fork to my namespace
And I visit the forks page of the "Shop" project
Then I should see my fork on the list
Scenario: Viewing forks of a Project that has no repo
Given I click link "Fork"
When I fork to my namespace
And I make forked repo invalid
And I visit the forks page of the "Shop" project
Then I should see my fork on the list
Scenario: Viewing private forks of a Project
Given There is an existent fork of the "Shop" project
And I click link "Fork"
When I fork to my namespace
And I visit the forks page of the "Shop" project
Then I should see my fork on the list
And I should not see the other fork listed
And I should see a private fork notice
class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedProject
include SharedActiveTab
include SharedProjectTab
# Sub Tabs: Home
step 'I click the "Team" tab' do
click_link('Members')
end
step 'I click the "Attachments" tab' do
click_link('Attachments')
end
step 'I click the "Snippets" tab' do
page.within('.layout-nav') do
click_link('Snippets')
end
end
step 'I click the "Edit Project"' do
page.within '.nav-sidebar' do
click_link('Edit Project')
end
end
step 'I click the "Integrations" tab' do
page.within '.nav-sidebar' do
click_link('Integrations')
end
end
step 'I click the "Repository" tab' do
page.within '.sidebar-top-level-items > .active' do
click_link('Repository')
end
end
step 'I click the "Activity" tab' do
page.within '.sidebar-top-level-items > .active' do
click_link('Activity')
end
end
step 'the active sub tab should be Members' do
ensure_active_sub_tab('Members')
end
step 'the active sub tab should be Integrations' do
ensure_active_sub_tab('Integrations')
end
step 'the active sub tab should be Repository' do
ensure_active_sub_tab('Repository')
end
step 'the active sub tab should be Pages' do
ensure_active_sub_tab('Pages')
end
step 'the active sub tab should be Activity' do
ensure_active_sub_tab('Activity')
end
# Sub Tabs: Commits
step 'I click the "Compare" tab' do
click_link('Compare')
end
step 'I click the "Branches" tab' do
page.within '.nav-sidebar' do
click_link('Branches')
end
end
step 'I click the "Tags" tab' do
click_link('Tags')
end
step 'I click the "Charts" tab' do
page.within('.sidebar-top-level-items > .active') do
click_link('Charts')
end
end
step 'the active sub tab should be Compare' do
ensure_active_sub_tab('Compare')
end
step 'the active sub tab should be Branches' do
ensure_active_sub_tab('Branches')
end
step 'the active sub tab should be Tags' do
ensure_active_sub_tab('Tags')
end
# Sub Tabs: Issues
step 'I click the "Milestones" sub tab' do
page.within('.nav-sidebar') do
click_link('Milestones')
end
end
step 'I click the "Labels" sub tab' do
page.within('.nav-sidebar') do
click_link('Labels')
end
end
step 'the active sub tab should be Issues' do
ensure_active_sub_tab('Issues')
end
step 'the active sub tab should be Milestones' do
ensure_active_sub_tab('Milestones')
end
step 'the active sub tab should be Labels' do
ensure_active_sub_tab('Labels')
end
end
class Spinach::Features::ProjectFork < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedProject
step 'I click link "Fork"' do
expect(page).to have_content "Shop"
click_link "Fork"
end
step 'I am a member of project "Shop"' do
@project = create(:project, :repository, name: "Shop")
@project.add_reporter(@user)
end
step 'I should see the forked project page' do
expect(page).to have_content "Forked from"
end
step 'I already have a project named "Shop" in my namespace' do
@my_project = create(:project, :repository, name: "Shop", namespace: current_user.namespace)
end
step 'I should see a "Name has already been taken" warning' do
expect(page).to have_content "Name has already been taken"
end
step 'I fork to my namespace' do
page.within '.fork-thumbnail-container' do
click_link current_user.name
end
end
step 'I should see "New merge request"' do
expect(page).to have_content(/new merge request/i)
end
step 'I goto the Merge Requests page' do
page.within '.nav-sidebar' do
first(:link, "Merge Requests").click
end
end
step 'I click link "New merge request"' do
page.within '#content-body' do
page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request')
end
end
step 'I should see the new merge request page for my namespace' do
current_path.should have_content(/#{current_user.namespace.name}/i)
end
step 'I visit the forks page of the "Shop" project' do
@project = Project.where(name: 'Shop').first
visit project_forks_path(@project)
end
step 'I should see my fork on the list' do
page.within('.js-projects-list-holder') do
project = @user.fork_of(@project.reload)
expect(page).to have_content("#{project.namespace.human_name} / #{project.name}")
end
end
step 'I make forked repo invalid' do
project = @user.fork_of(@project.reload)
project.path = 'test-crappy-path'
project.save!
end
step 'There is an existent fork of the "Shop" project' do
user = create(:user, name: 'Mike')
@project.add_reporter(user)
@forked_project = Projects::ForkService.new(@project, user).execute
end
step 'I should not see the other fork listed' do
expect(page).not_to have_content("#{@forked_project.namespace.human_name} / #{@forked_project.name}")
end
step 'I should see a private fork notice' do
expect(page).to have_content("1 private fork")
end
end
......@@ -19,7 +19,7 @@ module Gitlab
WHERE merge_request_diffs.id = merge_request_diff_commits.merge_request_diff_id
)'.squish
MergeRequestDiff.where(id: start_id..stop_id).update_all(update)
MergeRequestDiff.where(id: start_id..stop_id).where(commits_count: nil).update_all(update)
end
end
end
......
module Gitlab
module Conflict
class FileCollection
include Gitlab::RepositoryCacheAdapter
attr_reader :merge_request, :resolver
def initialize(merge_request)
our_commit = merge_request.source_branch_head.raw
their_commit = merge_request.target_branch_head.raw
target_repo = merge_request.target_project.repository.raw
@target_repo = merge_request.target_project.repository
@source_repo = merge_request.source_project.repository.raw
@resolver = Gitlab::Git::Conflict::Resolver.new(target_repo, our_commit.id, their_commit.id)
@our_commit_id = our_commit.id
@their_commit_id = their_commit.id
@resolver = Gitlab::Git::Conflict::Resolver.new(@target_repo.raw, @our_commit_id, @their_commit_id)
@merge_request = merge_request
end
......@@ -30,6 +34,17 @@ module Gitlab
end
end
def can_be_resolved_in_ui?
# Try to parse each conflict. If the MR's mergeable status hasn't been
# updated, ensure that we don't say there are conflicts to resolve
# when there are no conflict files.
files.each(&:lines)
files.any?
rescue Gitlab::Git::CommandError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing
false
end
cache_method :can_be_resolved_in_ui?
def file_for_path(old_path, new_path)
files.find { |file| file.their_path == old_path && file.our_path == new_path }
end
......@@ -56,6 +71,19 @@ Merge branch '#{merge_request.target_branch}' into '#{merge_request.source_branc
#{conflict_filenames.join("\n")}
EOM
end
private
def cache
@cache ||= begin
# Use the commit ids as a namespace so if the MR branches get
# updated we instantiate the cache under a different namespace. That
# way don't have to worry about explicitly invalidating the cache
namespace = "#{@our_commit_id}:#{@their_commit_id}"
Gitlab::RepositoryCache.new(@target_repo, extra_namespace: namespace)
end
end
end
end
end
......@@ -13,7 +13,16 @@ module Gitlab
end
def diff_files
super.tap { |_| store_highlight_cache }
# Make sure to _not_ send any method call to Gitlab::Diff::File
# _before_ all of them were collected (`super`). Premature method calls will
# trigger N+1 RPCs to Gitaly through BatchLoader records (Blob.lazy).
#
diff_files = super
diff_files.each { |diff_file| cache_highlight!(diff_file) if cacheable?(diff_file) }
store_highlight_cache
diff_files
end
def real_size
......@@ -22,13 +31,6 @@ module Gitlab
private
# Extracted method to highlight in the same iteration to the diff_collection.
def decorate_diff!(diff)
diff_file = super
cache_highlight!(diff_file) if cacheable?(diff_file)
diff_file
end
def highlight_diff_file_from_cache!(diff_file, cache_diff_lines)
diff_file.highlighted_diff_lines = cache_diff_lines.map do |line|
Gitlab::Diff::Line.init_from_hash(line)
......
......@@ -41,6 +41,16 @@ module Gitlab
"gitlab:exclusive_lease:#{key}"
end
# Removes any existing exclusive_lease from redis
# Don't run this in a live system without making sure no one is using the leases
def self.reset_all!(scope = '*')
Gitlab::Redis::SharedState.with do |redis|
redis.scan_each(match: redis_shared_state_key(scope)).each do |key|
redis.del(key)
end
end
end
def initialize(key, uuid: nil, timeout:)
@redis_shared_state_key = self.class.redis_shared_state_key(key)
@timeout = timeout
......
# Interface to the Redis-backed cache store
module Gitlab
class RepositoryCache
attr_reader :repository, :namespace, :backend
def initialize(repository, extra_namespace: nil, backend: Rails.cache)
@repository = repository
@namespace = "#{repository.full_path}:#{repository.project.id}"
@namespace += ":#{extra_namespace}" if extra_namespace
@backend = backend
end
def cache_key(type)
"#{type}:#{namespace}"
end
def expire(key)
backend.delete(cache_key(key))
end
def fetch(key, &block)
backend.fetch(cache_key(key), &block)
end
def exist?(key)
backend.exist?(cache_key(key))
end
def read(key)
backend.read(cache_key(key))
end
end
end
module Gitlab
module RepositoryCacheAdapter
extend ActiveSupport::Concern
class_methods do
# Wraps around the given method and caches its output in Redis and an instance
# variable.
#
# This only works for methods that do not take any arguments.
def cache_method(name, fallback: nil, memoize_only: false)
original = :"_uncached_#{name}"
alias_method(original, name)
define_method(name) do
cache_method_output(name, fallback: fallback, memoize_only: memoize_only) do
__send__(original) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
end
# RepositoryCache to be used. Should be overridden by the including class
def cache
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
# the `key` argument.
#
# This method will return `nil` if the corresponding instance variable is also
# set to `nil`. This ensures we don't keep yielding the block when it returns
# `nil`.
#
# key - The name of the key to cache the data in.
# fallback - A value to fall back to in the event of a Git error.
def cache_method_output(key, fallback: nil, memoize_only: false, &block)
ivar = cache_instance_variable_name(key)
if instance_variable_defined?(ivar)
instance_variable_get(ivar)
else
# If the repository doesn't exist and a fallback was specified we return
# that value inmediately. This saves us Rugged/gRPC invocations.
return fallback unless fallback.nil? || cache.repository.exists?
begin
value =
if memoize_only
yield
else
cache.fetch(key, &block)
end
instance_variable_set(ivar, value)
rescue Gitlab::Git::Repository::NoRepository
# Even if the above `#exists?` check passes these errors might still
# occur (for example because of a non-existing HEAD). We want to
# gracefully handle this and not cache anything
fallback
end
end
end
# Expires the caches of a specific set of methods
def expire_method_caches(methods)
methods.each do |key|
cache.expire(key)
ivar = cache_instance_variable_name(key)
remove_instance_variable(ivar) if instance_variable_defined?(ivar)
end
end
private
def cache_instance_variable_name(key)
:"@#{key.to_s.tr('?!', '')}"
end
end
end
# Interface to the Redis-backed cache store used by the Repository model
class RepositoryCache
attr_reader :namespace, :backend, :project_id
def initialize(namespace, project_id, backend = Rails.cache)
@namespace = namespace
@backend = backend
@project_id = project_id
end
def cache_key(type)
"#{type}:#{namespace}:#{project_id}"
end
def expire(key)
backend.delete(cache_key(key))
end
def fetch(key, &block)
backend.fetch(cache_key(key), &block)
end
def exist?(key)
backend.exist?(cache_key(key))
end
def read(key)
backend.read(cache_key(key))
end
end
namespace :gitlab do
namespace :exclusive_lease do
desc 'GitLab | Clear existing exclusive leases for specified scope (default: *)'
task :clear, [:scope] => [:environment] do |_, args|
args[:scope].nil? ? Gitlab::ExclusiveLease.reset_all! : Gitlab::ExclusiveLease.reset_all!(args[:scope])
puts 'All exclusive lease entries were removed.'
end
end
end
......@@ -24,8 +24,8 @@ def docs_branch
# The maximum string length a file can have on a filesystem (ext4)
# is 63 characters. Let's use something smaller to be 100% sure.
max = 42
# Prefix the remote branch with 'preview-' in order to avoid
# name conflicts in the rare case the branch name already
# Prefix the remote branch with the slug of the project in order
# to avoid name conflicts in the rare case the branch name already
# exists in the docs repo and truncate to max length.
"#{slug}-#{ENV["CI_COMMIT_REF_SLUG"]}"[0...max]
end
......@@ -41,12 +41,21 @@ def create_remote_branch
Gitlab.create_branch(GITLAB_DOCS_REPO, docs_branch, 'master')
puts "=> Remote branch '#{docs_branch}' created"
# Get the latest pipeline ID which is also the first
pipeline_id = Gitlab.pipelines(GITLAB_DOCS_REPO, { ref: docs_branch }).last.id
pipelines = nil
# Wait until the pipeline is started
loop do
sleep 1
puts "=> Waiting for pipeline to start..."
pipelines = Gitlab.pipelines(GITLAB_DOCS_REPO, { ref: docs_branch })
break if pipelines.any?
end
# Get the first pipeline ID which should be the only one for the branch
pipeline_id = pipelines.first.id
# Cancel the pipeline
Gitlab.cancel_pipeline(GITLAB_DOCS_REPO, pipeline_id)
puts "=> Canceled uneeded pipeline #{pipeline_id} for '#{docs_branch}'"
rescue Gitlab::Error::BadRequest
puts "=> Remote branch '#{docs_branch}' already exists"
end
......
require 'spec_helper'
describe 'Project active tab' do
let(:user) { create :user }
let(:project) { create(:project, :repository) }
before do
project.add_master(user)
sign_in(user)
end
def click_tab(title)
page.within '.sidebar-top-level-items > .active' do
click_link(title)
end
end
shared_examples 'page has active tab' do |title|
it "activates #{title} tab" do
expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
expect(find('.sidebar-top-level-items > li.active')).to have_content(title)
end
end
shared_examples 'page has active sub tab' do |title|
it "activates #{title} sub tab" do
expect(page).to have_selector('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)', count: 1)
expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)'))
.to have_content(title)
end
end
context 'on project Home' do
before do
visit project_path(project)
end
it_behaves_like 'page has active tab', 'Overview'
it_behaves_like 'page has active sub tab', 'Details'
context 'on project Home/Activity' do
before do
click_tab('Activity')
end
it_behaves_like 'page has active tab', 'Overview'
it_behaves_like 'page has active sub tab', 'Activity'
end
end
context 'on project Repository' do
before do
root_ref = project.repository.root_ref
visit project_tree_path(project, root_ref)
end
it_behaves_like 'page has active tab', 'Repository'
%w(Files Commits Graph Compare Charts Branches Tags).each do |sub_menu|
context "on project Repository/#{sub_menu}" do
before do
click_tab(sub_menu)
end
it_behaves_like 'page has active tab', 'Repository'
it_behaves_like 'page has active sub tab', sub_menu
end
end
end
context 'on project Issues' do
before do
visit project_issues_path(project)
end
it_behaves_like 'page has active tab', 'Issues'
%w(Milestones Labels).each do |sub_menu|
context "on project Issues/#{sub_menu}" do
before do
click_tab(sub_menu)
end
it_behaves_like 'page has active tab', 'Issues'
it_behaves_like 'page has active sub tab', sub_menu
end
end
end
context 'on project Merge Requests' do
before do
visit project_merge_requests_path(project)
end
it_behaves_like 'page has active tab', 'Merge Requests'
end
context 'on project Wiki' do
before do
visit project_wiki_path(project, :home)
end
it_behaves_like 'page has active tab', 'Wiki'
end
context 'on project Members' do
before do
visit project_project_members_path(project)
end
it_behaves_like 'page has active tab', 'Members'
end
context 'on project Settings' do
before do
visit edit_project_path(project)
end
context 'on project Settings/Integrations' do
before do
click_tab('Integrations')
end
it_behaves_like 'page has active tab', 'Settings'
it_behaves_like 'page has active sub tab', 'Integrations'
end
context 'on project Settings/Repository' do
before do
click_tab('Repository')
end
it_behaves_like 'page has active tab', 'Settings'
it_behaves_like 'page has active sub tab', 'Repository'
end
end
end
......@@ -25,6 +25,110 @@ describe 'Project fork' do
expect(page).to have_css('a.disabled', text: 'Fork')
end
it 'forks the project' do
visit project_path(project)
click_link 'Fork'
page.within '.fork-thumbnail-container' do
click_link user.name
end
expect(page).to have_content 'Forked from'
visit project_path(project)
expect(page).to have_content(/new merge request/i)
page.within '.nav-sidebar' do
first(:link, 'Merge Requests').click
end
expect(page).to have_content(/new merge request/i)
page.within '#content-body' do
click_link('New merge request')
end
expect(current_path).to have_content(/#{user.namespace.name}/i)
end
it 'shows the forked project on the list' do
visit project_path(project)
click_link 'Fork'
page.within '.fork-thumbnail-container' do
click_link user.name
end
visit project_forks_path(project)
forked_project = user.fork_of(project.reload)
page.within('.js-projects-list-holder') do
expect(page).to have_content("#{forked_project.namespace.human_name} / #{forked_project.name}")
end
forked_project.update!(path: 'test-crappy-path')
visit project_forks_path(project)
page.within('.js-projects-list-holder') do
expect(page).to have_content("#{forked_project.namespace.human_name} / #{forked_project.name}")
end
end
context 'when the project is private' do
let(:project) { create(:project, :repository) }
let(:another_user) { create(:user, name: 'Mike') }
before do
project.add_reporter(user)
project.add_reporter(another_user)
end
it 'renders private forks of the project' do
visit project_path(project)
another_project_fork = Projects::ForkService.new(project, another_user).execute
click_link 'Fork'
page.within '.fork-thumbnail-container' do
click_link user.name
end
visit project_forks_path(project)
page.within('.js-projects-list-holder') do
user_project_fork = user.fork_of(project.reload)
expect(page).to have_content("#{user_project_fork.namespace.human_name} / #{user_project_fork.name}")
end
expect(page).not_to have_content("#{another_project_fork.namespace.human_name} / #{another_project_fork.name}")
expect(page).to have_content("1 private fork")
end
end
context 'when the user already forked the project' do
before do
create(:project, :repository, name: project.name, namespace: user.namespace)
end
it 'renders error' do
visit project_path(project)
click_link 'Fork'
page.within '.fork-thumbnail-container' do
click_link user.name
end
expect(page).to have_content "Name has already been taken"
end
end
context 'master in group' do
let(:group) { create(:group) }
......
......@@ -37,6 +37,18 @@ describe Gitlab::BackgroundMigration::AddMergeRequestDiffCommitsCount, :migratio
expect(diff.reload.commits_count).to eq(0)
end
it 'skips diffs that have commits_count already set' do
timestamp = 2.days.ago
diff = merge_request_diffs_table.create!(
merge_request_id: merge_request.id,
commits_count: 0,
updated_at: timestamp)
subject.perform(diff.id, diff.id)
expect(diff.reload.updated_at).to be_within(1.second).of(timestamp)
end
it 'migrates multiple diffs to the correct values' do
diffs = Array.new(3).map.with_index { |_, i| create_diff!(i, commits: 3) }
......
......@@ -10,6 +10,38 @@ describe Gitlab::Conflict::FileCollection do
end
end
describe '#cache' do
it 'specifies a custom namespace with the merge request commit ids' do
our_commit = merge_request.source_branch_head.raw
their_commit = merge_request.target_branch_head.raw
custom_namespace = "#{our_commit.id}:#{their_commit.id}"
expect(file_collection.send(:cache).namespace).to include(custom_namespace)
end
end
describe '#can_be_resolved_in_ui?' do
it 'returns true if conflicts for this collection can be resolved in the UI' do
expect(file_collection.can_be_resolved_in_ui?).to be true
end
it "returns false if conflicts for this collection can't be resolved in the UI" do
expect(file_collection).to receive(:files).and_raise(Gitlab::Git::Conflict::Resolver::ConflictSideMissing)
expect(file_collection.can_be_resolved_in_ui?).to be false
end
it 'caches the result' do
expect(file_collection).to receive(:files).twice.and_call_original
expect(file_collection.can_be_resolved_in_ui?).to be true
expect(file_collection).not_to receive(:files)
expect(file_collection.can_be_resolved_in_ui?).to be true
end
end
describe '#default_commit_message' do
it 'matches the format of the git CLI commit message' do
expect(file_collection.default_commit_message).to eq(<<EOM.chomp)
......
......@@ -133,4 +133,16 @@ describe Gitlab::ExclusiveLease, :clean_gitlab_redis_shared_state do
expect(lease.ttl).to be_nil
end
end
describe '.reset_all!' do
it 'removes all existing lease keys from redis' do
uuid = described_class.new(unique_key, timeout: 3600).try_obtain
expect(described_class.get_uuid(unique_key)).to eq(uuid)
described_class.reset_all!
expect(described_class.get_uuid(unique_key)).to be_falsey
end
end
end
require 'spec_helper'
describe Gitlab::RepositoryCacheAdapter do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:cache) { repository.send(:cache) }
describe '#cache_method_output', :use_clean_rails_memory_store_caching do
let(:fallback) { 10 }
context 'with a non-existing repository' do
let(:project) { create(:project) } # No repository
subject do
repository.cache_method_output(:cats, fallback: fallback) do
repository.cats_call_stub
end
end
it 'returns the fallback value' do
expect(subject).to eq(fallback)
end
it 'avoids calling the original method' do
expect(repository).not_to receive(:cats_call_stub)
subject
end
end
context 'with a method throwing a non-existing-repository error' do
subject do
repository.cache_method_output(:cats, fallback: fallback) do
raise Gitlab::Git::Repository::NoRepository
end
end
it 'returns the fallback value' do
expect(subject).to eq(fallback)
end
it 'does not cache the data' do
subject
expect(repository.instance_variable_defined?(:@cats)).to eq(false)
expect(cache.exist?(:cats)).to eq(false)
end
end
context 'with an existing repository' do
it 'caches the output' do
object = double
expect(object).to receive(:number).once.and_return(10)
2.times do
val = repository.cache_method_output(:cats) { object.number }
expect(val).to eq(10)
end
expect(repository.send(:cache).exist?(:cats)).to eq(true)
expect(repository.instance_variable_get(:@cats)).to eq(10)
end
end
end
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(:gitignore)
repository.expire_method_caches(%i(readme gitignore))
end
end
end
require 'spec_helper'
describe RepositoryCache do
let(:project) { create(:project) }
describe Gitlab::RepositoryCache do
let(:backend) { double('backend').as_null_object }
let(:cache) { described_class.new('example', project.id, backend) }
let(:project) { create(:project) }
let(:repository) { project.repository }
let(:namespace) { "#{repository.full_path}:#{project.id}" }
let(:cache) { described_class.new(repository, backend: backend) }
describe '#cache_key' do
subject { cache.cache_key(:foo) }
it 'includes the namespace' do
expect(cache.cache_key(:foo)).to eq "foo:example:#{project.id}"
expect(subject).to eq "foo:#{namespace}"
end
context 'with a given namespace' do
let(:extra_namespace) { 'my:data' }
let(:cache) do
described_class.new(repository, extra_namespace: extra_namespace,
backend: backend)
end
it 'includes the full namespace' do
expect(subject).to eq "foo:#{namespace}:#{extra_namespace}"
end
end
end
describe '#expire' do
it 'expires the given key from the cache' do
cache.expire(:foo)
expect(backend).to have_received(:delete).with("foo:example:#{project.id}")
expect(backend).to have_received(:delete).with("foo:#{namespace}")
end
end
describe '#fetch' do
it 'fetches the given key from the cache' do
cache.fetch(:bar)
expect(backend).to have_received(:fetch).with("bar:example:#{project.id}")
expect(backend).to have_received(:fetch).with("bar:#{namespace}")
end
it 'accepts a block' do
p = -> {}
cache.fetch(:baz, &p)
expect(backend).to have_received(:fetch).with("baz:example:#{project.id}", &p)
expect(backend).to have_received(:fetch).with("baz:#{namespace}", &p)
end
end
end
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20180309121820_reschedule_commits_count_for_merge_request_diff')
describe RescheduleCommitsCountForMergeRequestDiff, :migration, :sidekiq do
let(:merge_request_diffs) { table(:merge_request_diffs) }
let(:merge_requests) { table(:merge_requests) }
let(:projects) { table(:projects) }
let(:namespaces) { table(:namespaces) }
before do
stub_const("#{described_class.name}::BATCH_SIZE", 1)
namespaces.create!(id: 1, name: 'gitlab', path: 'gitlab')
projects.create!(id: 1, namespace_id: 1)
merge_requests.create!(id: 1, target_project_id: 1, source_project_id: 1, target_branch: 'feature', source_branch: 'master')
merge_request_diffs.create!(id: 1, merge_request_id: 1)
merge_request_diffs.create!(id: 2, merge_request_id: 1)
merge_request_diffs.create!(id: 3, merge_request_id: 1, commits_count: 0)
merge_request_diffs.create!(id: 4, merge_request_id: 1)
end
it 'correctly schedules background migrations' do
Sidekiq::Testing.fake! do
Timecop.freeze do
migrate!
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, 1, 1)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 2, 2)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(15.minutes, 4, 4)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
end
end
......@@ -2243,15 +2243,6 @@ describe Repository do
end
end
describe '#expire_method_caches' do
it 'expires the caches of the given methods' do
expect_any_instance_of(RepositoryCache).to receive(:expire).with(:readme)
expect_any_instance_of(RepositoryCache).to receive(:expire).with(:gitignore)
repository.expire_method_caches(%i(readme gitignore))
end
end
describe '#expire_all_method_caches' do
it 'expires the caches of all methods' do
expect(repository).to receive(:expire_method_caches)
......@@ -2430,66 +2421,6 @@ describe Repository do
end
end
describe '#cache_method_output', :use_clean_rails_memory_store_caching do
let(:fallback) { 10 }
context 'with a non-existing repository' do
let(:project) { create(:project) } # No repository
subject do
repository.cache_method_output(:cats, fallback: fallback) do
repository.cats_call_stub
end
end
it 'returns the fallback value' do
expect(subject).to eq(fallback)
end
it 'avoids calling the original method' do
expect(repository).not_to receive(:cats_call_stub)
subject
end
end
context 'with a method throwing a non-existing-repository error' do
subject do
repository.cache_method_output(:cats, fallback: fallback) do
raise Gitlab::Git::Repository::NoRepository
end
end
it 'returns the fallback value' do
expect(subject).to eq(fallback)
end
it 'does not cache the data' do
subject
expect(repository.instance_variable_defined?(:@cats)).to eq(false)
expect(repository.send(:cache).exist?(:cats)).to eq(false)
end
end
context 'with an existing repository' do
it 'caches the output' do
object = double
expect(object).to receive(:number).once.and_return(10)
2.times do
val = repository.cache_method_output(:cats) { object.number }
expect(val).to eq(10)
end
expect(repository.send(:cache).exist?(:cats)).to eq(true)
expect(repository.instance_variable_get(:@cats)).to eq(10)
end
end
end
describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches)
......
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