Commit 52fa1bb5 authored by Nick Thomas's avatar Nick Thomas

Merge remote-tracking branch 'ce/master' into ce-to-ee-2017-09-21

parents 051daa03 598b1a17
......@@ -4,11 +4,16 @@ entry.
## 9.5.5 (2017-09-18)
<<<<<<< HEAD
=======
- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller)
>>>>>>> ce/master
- [FIXED] Fix division by zero error in blame age mapping. !13803 (Jeff Stubler)
- [FIXED] Fix problems sanitizing URLs with empty passwords. !14083
- [FIXED] Fix a wrong `X-Gitlab-Event` header when testing webhooks. !14108
- [FIXED] Fixes the 500 errors caused by a race condition in GPG's tmp directory handling. !14194 (Alexis Reigel)
- [FIXED] Fix Pipeline Triggers to show triggered label and predefined variables (e.g. CI_PIPELINE_TRIGGERED). !14244
<<<<<<< HEAD
- [FIXED] Disable GitLab Project Import Button if source disabled.
- [FIXED] Fix project feature being deleted when updating project with invalid visibility level.
- [FIXED] Fix new navigation wrapping and causing height to grow.
......@@ -16,6 +21,15 @@ entry.
- [FIXED] Fix buttons with different height in merge request widget.
- [FIXED] Fix broken svg in jobs dropdown for success status.
- [FIXED] Improve migrations using triggers.
=======
- [FIXED] Fix project feature being deleted when updating project with invalid visibility level.
- [FIXED] Fix new navigation wrapping and causing height to grow.
- [FIXED] Fix buttons with different height in merge request widget.
- [FIXED] Normalize styles for empty state combo button.
- [FIXED] Fix broken svg in jobs dropdown for success status.
- [FIXED] Improve migrations using triggers.
- [FIXED] Disable GitLab Project Import Button if source disabled.
>>>>>>> ce/master
- [CHANGED] Update the GPG verification semantics: A GPG signature must additionally match the committer in order to be verified. !13771 (Alexis Reigel)
- [OTHER] Fix repository equality check and avoid fetching ref if the commit is already available. This affects merge request creation performance. !13685
- [OTHER] Update documentation for confidential issue. !14117
......
......@@ -126,7 +126,7 @@ gem 'faraday_middleware-aws-signers-v4'
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '2.0.0'
gem 'gitlab-markup', '~> 1.5.1'
gem 'gitlab-markup', '~> 1.6.2'
gem 'redcarpet', '~> 3.4'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2'
......
......@@ -317,8 +317,12 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
<<<<<<< HEAD
gitlab-license (1.0.0)
gitlab-markup (1.5.1)
=======
gitlab-markup (1.6.2)
>>>>>>> ce/master
gitlab_omniauth-ldap (2.0.4)
net-ldap (~> 0.16)
omniauth (~> 1.3)
......@@ -1063,8 +1067,12 @@ DEPENDENCIES
gitaly-proto (~> 0.33.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
<<<<<<< HEAD
gitlab-license (~> 1.0)
gitlab-markup (~> 1.5.1)
=======
gitlab-markup (~> 1.6.2)
>>>>>>> ce/master
gitlab_omniauth-ldap (~> 2.0.4)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4)
......
......@@ -2,7 +2,10 @@
/* global List */
import _ from 'underscore';
import Cookies from 'js-cookie';
<<<<<<< HEAD
import boardsStoreEE from 'ee/boards/stores/boards_store_ee';
=======
>>>>>>> ce/master
import { getUrlParamsArray } from '../../lib/utils/common_utils';
window.gl = window.gl || {};
......
......@@ -134,8 +134,9 @@
width: maxWidth + 1,
height: maxHeight + 2
});
// Set swipeBar left position to match image frame
$swipeBar.css({
left: 0
left: 1
});
wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
......
......@@ -84,12 +84,15 @@ import GpgBadges from './gpg_badges';
import UserFeatureHelper from './helpers/user_feature_helper';
import initChangesDropdown from './init_changes_dropdown';
import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
<<<<<<< HEAD
// EE-only
import ApproversSelect from './approvers_select';
import AuditLogs from './audit_logs';
import initGeoInfoModal from './init_geo_info_modal';
import initGroupAnalytics from './init_group_analytics';
=======
>>>>>>> ce/master
(function() {
var Dispatcher;
......
......@@ -65,6 +65,7 @@ export default {
canCreateEnvironmentParsed() {
return convertPermissionToBoolean(this.canCreateEnvironment);
<<<<<<< HEAD
},
/**
......@@ -75,6 +76,8 @@ export default {
*/
shouldRenderPagination() {
return this.state.paginationInformation && this.state.paginationInformation.totalPages > 1;
=======
>>>>>>> ce/master
},
},
......
......@@ -375,8 +375,6 @@ header.navbar-gitlab-new {
display: flex;
width: 100%;
position: relative;
padding-top: $gl-padding;
padding-bottom: $gl-padding;
align-items: center;
border-bottom: 1px solid $border-color;
}
......@@ -388,6 +386,11 @@ header.navbar-gitlab-new {
align-self: center;
color: $gl-text-color-secondary;
@media (max-width: $screen-xs-max) {
padding-left: 17px;
border-left: 1px solid $gl-text-color-quaternary;
}
.avatar-tile {
margin-right: 4px;
border: 1px solid $border-color;
......
......@@ -445,9 +445,8 @@ $new-sidebar-collapsed-width: 50px;
background-color: transparent;
border: 0;
padding: 6px 16px;
margin: 0 16px 0 -15px;
margin: 0 0 0 -15px;
height: 46px;
border-right: 1px solid $gl-text-color-quaternary;
i {
font-size: 20px;
......@@ -455,7 +454,12 @@ $new-sidebar-collapsed-width: 50px;
}
@media (max-width: $screen-xs-max) {
display: inline-block;
display: flex;
align-items: center;
i {
font-size: 18px;
}
}
}
......
.info-well {
.admin-well-statistics,
.admin-well-features {
padding-bottom: 46px;
}
}
......@@ -727,6 +727,12 @@ ul.notes {
border-bottom-left-radius: 0;
}
.btn {
svg path {
fill: $gray-darkest;
}
}
.btn.discussion-create-issue-btn {
margin-left: -4px;
border-radius: 0;
......@@ -741,10 +747,6 @@ ul.notes {
border: 0;
}
}
.new-issue-for-discussion path {
fill: $gray-darkest;
}
}
}
......@@ -817,16 +819,6 @@ ul.notes {
vertical-align: middle;
}
.discussion-next-btn {
svg {
margin: 0;
path {
fill: $gray-darkest;
}
}
}
// Merge request notes in diffs
.diff-file {
// Diff is inline
......
......@@ -9,14 +9,12 @@ class Projects::ForksController < Projects::ApplicationController
def index
base_query = project.forks.includes(:creator)
@forks = base_query.merge(ProjectsFinder.new(current_user: current_user).execute)
forks = ForkProjectsFinder.new(project, params: params.merge(search: params[:filter_projects]), current_user: current_user).execute
@total_forks_count = base_query.size
@private_forks_count = @total_forks_count - @forks.size
@private_forks_count = @total_forks_count - forks.size
@public_forks_count = @total_forks_count - @private_forks_count
@sort = params[:sort] || 'id_desc'
@forks = @forks.search(params[:filter_projects]) if params[:filter_projects].present?
@forks = @forks.order_by(@sort).page(params[:page])
@forks = forks.page(params[:page])
respond_to do |format|
format.html
......
class ForkProjectsFinder < ProjectsFinder
def initialize(project, params: {}, current_user: nil)
project_ids = project.forks.includes(:creator).select(:id)
super(params: params, current_user: current_user, project_ids_relation: project_ids)
end
end
......@@ -94,6 +94,12 @@ module MilestonesHelper
end
end
def milestone_tooltip_title(milestone)
if milestone.due_date
[milestone.due_date.to_s(:medium), "(#{milestone_remaining_days(milestone)})"].join(' ')
end
end
def milestone_remaining_days(milestone)
if milestone.expired?
content_tag(:strong, 'Past due')
......
class Event < ActiveRecord::Base
include Sortable
include IgnorableColumn
default_scope { reorder(nil).where.not(author_id: nil) }
default_scope { reorder(nil) }
CREATED = 1
UPDATED = 2
......@@ -83,6 +83,12 @@ class Event < ActiveRecord::Base
scope :merged, -> { where(action: MERGED) }
scope :totals_by_author, -> { group(:author_id).count }
# Authors are required as they're used to display who pushed data.
#
# We're just validating the presence of the ID here as foreign key constraints
# should ensure the ID points to a valid user.
validates :author_id, presence: true
self.inheritance_column = 'action'
# "data" will be removed in 10.0 but it may be possible that JOINs happen that
......
......@@ -310,8 +310,6 @@ class Issue < ActiveRecord::Base
end
def update_project_counter_caches
return unless update_project_counter_caches?
Projects::OpenIssuesCountService.new(project).refresh_cache
end
......
......@@ -4,8 +4,6 @@ class Key < ActiveRecord::Base
include Gitlab::CurrentSettings
include Sortable
LAST_USED_AT_REFRESH_TIME = 1.day.to_i
belongs_to :user
before_validation :generate_fingerprint
......@@ -57,10 +55,7 @@ class Key < ActiveRecord::Base
end
def update_last_used_at
lease = Gitlab::ExclusiveLease.new("key_update_last_used_at:#{id}", timeout: LAST_USED_AT_REFRESH_TIME)
return unless lease.try_obtain
UseKeyWorker.perform_async(id)
Keys::LastUsedService.new(self).execute
end
def add_to_shell
......
......@@ -987,8 +987,6 @@ class MergeRequest < ActiveRecord::Base
end
def update_project_counter_caches
return unless update_project_counter_caches?
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end
......
......@@ -166,9 +166,7 @@ class Milestone < ActiveRecord::Base
# Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1"
# Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
#
def to_reference(from_project = nil, format: :iid, full: false)
return if group_milestone? && format != :name
def to_reference(from_project = nil, format: :name, full: false)
format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
......@@ -245,6 +243,10 @@ class Milestone < ActiveRecord::Base
def milestone_format_reference(format = :iid)
raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format)
if group_milestone? && format == :iid
raise ArgumentError, 'Cannot refer to a group milestone by an internal id!'
end
if format == :name && !name.include?('"')
%("#{name}")
else
......
......@@ -3,12 +3,6 @@ class PushEvent < Event
# different "action" value.
validate :validate_push_action
# Authors are required as they're used to display who pushed data.
#
# We're just validating the presence of the ID here as foreign key constraints
# should ensure the ID points to a valid user.
validates :author_id, presence: true
# The project is required to build links to commits, commit ranges, etc.
#
# We're just validating the presence of the ID here as foreign key constraints
......
......@@ -184,6 +184,7 @@ class IssuableBaseService < BaseService
after_create(issuable)
execute_hooks(issuable)
invalidate_cache_counts(issuable, users: issuable.assignees)
issuable.update_project_counter_caches
end
issuable
......@@ -195,8 +196,6 @@ class IssuableBaseService < BaseService
def after_create(issuable)
# To be overridden by subclasses
issuable.update_project_counter_caches
end
def before_update(issuable)
......@@ -205,8 +204,6 @@ class IssuableBaseService < BaseService
def after_update(issuable)
# To be overridden by subclasses
issuable.update_project_counter_caches
end
def update(issuable)
......@@ -231,6 +228,10 @@ class IssuableBaseService < BaseService
before_update(issuable)
# We have to perform this check before saving the issuable as Rails resets
# the changed fields upon calling #save.
update_project_counters = issuable.update_project_counter_caches?
if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do
......@@ -251,6 +252,8 @@ class IssuableBaseService < BaseService
after_update(issuable)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
issuable.update_project_counter_caches if update_project_counters
end
end
......
......@@ -29,6 +29,7 @@ module Issues
todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
invalidate_cache_counts(issue, users: issue.assignees)
issue.update_project_counter_caches
end
issue
......
module Keys
class LastUsedService
TIMEOUT = 1.day.to_i
attr_reader :key
# key - The Key for which to update the last used timestamp.
def initialize(key)
@key = key
end
def execute
# We _only_ want to update last_used_at and not also updated_at (which
# would be updated when using #touch).
key.update_column(:last_used_at, Time.zone.now) if update?
end
def update?
last_used = key.last_used_at
return false if last_used && (Time.zone.now - last_used) <= TIMEOUT
!!redis_lease.try_obtain
end
private
def redis_lease
Gitlab::ExclusiveLease
.new("key_update_last_used_at:#{key.id}", timeout: TIMEOUT)
end
end
end
......@@ -14,6 +14,7 @@ module MergeRequests
todo_service.close_merge_request(merge_request, current_user)
execute_hooks(merge_request, 'close')
invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
end
merge_request
......
......@@ -2,6 +2,11 @@ module Projects
# Base class for the various service classes that count project data (e.g.
# issues or forks).
class CountService
# The version of the cache format. This should be bumped whenever the
# underlying logic changes. This removes the need for explicitly flushing
# all caches.
VERSION = 1
def initialize(project)
@project = project
end
......@@ -37,7 +42,7 @@ module Projects
end
def cache_key
['projects', @project.id, cache_key_name]
['projects', 'count_service', VERSION, @project.id, cache_key_name]
end
end
end
......@@ -10,7 +10,7 @@
.row
.col-md-4
.info-well
.well-segment.admin-well
.well-segment.admin-well.admin-well-statistics
%h4 Statistics
%p
Forks
......@@ -46,7 +46,7 @@
= number_with_delimiter(User.active.count)
.col-md-4
.info-well
.well-segment.admin-well
.well-segment.admin-well.admin-well-features
%h4 Features
- sign_up = "Sign up"
%p{ "aria-label" => "#{sign_up}: status " + (signup_enabled? ? "on" : "off") }
......@@ -126,6 +126,10 @@
GitLab API
%span.pull-right
= API::API::version
%p
Gitaly
%span.pull-right
= Gitlab::GitalyClient.expected_server_version
- if Gitlab.config.pages.enabled
%p
GitLab Pages
......
......@@ -10,9 +10,6 @@
= render "projects/last_push"
%div{ class: container_class }
- if show_callout?('user_callout_dismissed')
= render 'shared/user_callout'
- if has_projects_or_name?(@projects, params)
= render 'dashboard/projects_head'
= render 'projects'
......
- if merge_request.discussions_can_be_resolved_by?(current_user) && can?(current_user, :create_issue, @project)
.btn-group{ role: "group", "v-if" => "unresolvedDiscussionCount > 0" }
.btn.btn-default.discussion-create-issue-btn.has-tooltip{ title: "Resolve all discussions in new issue",
"aria-label" => "Resolve all discussions in a new issue",
"data-container" => "body" }
= link_to custom_icon('icon_mr_issue'), new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid), title: "Resolve all discussions in new issue", class: 'new-issue-for-discussion'
= link_to custom_icon('icon_mr_issue'),
new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid),
title: 'Resolve all discussions in new issue',
aria: { label: 'Resolve all discussions in new issue' },
data: { container: 'body' },
class: 'new-issue-for-discussion btn btn-default discussion-create-issue-btn has-tooltip'
......@@ -2,7 +2,9 @@
%new-issue-for-discussion-btn{ ":discussion-id" => "'#{discussion.id}'",
"inline-template" => true }
.btn-group{ role: "group", "v-if" => "showButton" }
.btn.btn-default.discussion-create-issue-btn.has-tooltip{ title: "Resolve this discussion in a new issue",
"aria-label" => "Resolve this discussion in a new issue",
"data-container" => "body" }
= link_to custom_icon('icon_mr_issue'), new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id), title: "Resolve this discussion in a new issue", class: 'new-issue-for-discussion'
= link_to custom_icon('icon_mr_issue'),
new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id),
title: 'Resolve this discussion in a new issue',
aria: { label: 'Resolve this discussion in a new issue' },
data: { container: 'body' },
class: 'new-issue-for-discussion btn btn-default discussion-create-issue-btn has-tooltip'
......@@ -74,7 +74,9 @@
%h4.prepend-top-0.warning-title
Change username
%p
Changing your username will change path to all personal projects!
Changing your username can have unintended side effects.
= succeed '.' do
= link_to 'Learn more', help_page_path('user/profile/index', anchor: 'changing-your-username'), target: '_blank'
.col-lg-8
= form_for @user, url: update_username_profile_path, method: :put, html: {class: "update-username"} do |f|
.form-group
......
......@@ -24,7 +24,7 @@
- if issue.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
= link_to project_issues_path(issue.project, milestone_title: issue.milestone.title) do
= link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_title(issue.milestone) } do
= icon('clock-o')
= issue.milestone.title
- if issue.due_date
......
......@@ -23,7 +23,7 @@
- if merge_request.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
= link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title) do
= link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_title(merge_request.milestone) } do
= icon('clock-o')
= merge_request.milestone.title
- if merge_request.target_project.default_branch != merge_request.target_branch
......
......@@ -22,11 +22,9 @@
- if @group.persisted?
.alert.alert-warning.prepend-top-10
%ul
%li Changing group path can have unintended side effects.
%li Renaming group path will rename directory for all related projects
%li It will change web url for access group and group projects.
%li It will change the git path to repositories under this group.
Changing group path can have unintended side effects.
= succeed '.' do
= link_to 'Learn more', help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank'
.form-group.group-name-holder
= f.label :name, class: 'control-label' do
......
.user-callout{ data: { uid: 'user_callout_dismissed' } }
.bordered-box.landing.content-block
%button.btn.btn-default.close.js-close-callout{ type: 'button',
'aria-label' => 'Dismiss customize experience box' }
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
.svg-container
= custom_icon('icon_customization')
.user-callout-copy
%h4
Customize your experience
%p
Change syntax themes, default project pages, and more in preferences.
= link_to 'Check it out', profile_preferences_path, class: 'btn btn-primary js-close-callout'
......@@ -26,7 +26,7 @@
= icon('clock-o', 'aria-hidden': 'true')
%span.milestone-title
- if issuable.milestone
%span.has-tooltip{ title: "#{issuable.milestone.title}<br>#{milestone_remaining_days(issuable.milestone)}", data: { container: 'body', html: 1, placement: 'left' } }
%span.has-tooltip{ title: "#{issuable.milestone.title}<br>#{milestone_tooltip_title(issuable.milestone)}", data: { container: 'body', html: 1, placement: 'left' } }
= issuable.milestone.title
- else
None
......@@ -37,7 +37,7 @@
= link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed
- if issuable.milestone
= link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_remaining_days(issuable.milestone), data: { container: "body", html: 1 }
= link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_title(issuable.milestone), data: { container: "body", html: 1 }
- else
%span.no-value None
......
......@@ -99,8 +99,6 @@
Snippets
%div{ class: container_class }
- if @user == current_user && show_callout?('user_callout_dismissed')
= render 'shared/user_callout'
.tab-content
#activity.tab-pane
.row-content-block.calender-block.white.second-block.hidden-xs
......
class UseKeyWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
def perform(key_id)
key = Key.find(key_id)
key.touch(:last_used_at)
rescue ActiveRecord::RecordNotFound
Rails.logger.error("UseKeyWorker: couldn't find key with ID=#{key_id}, skipping job")
false
end
end
---
title: Upgrade gitlab-markup gem
merge_request: 14395
author: Markus Koller
type: other
---
title: Add an API endpoint to determine the forks of a project
merge_request:
author:
type: added
---
title: Add Gitaly version to Admin Dashboard
merge_request: 14313
author: Jacopo Beschi @jacopo-beschi
type: added
---
title: Fix the "resolve discussion in a new issue" button
merge_request: 14357
author:
type: fixed
---
title: Add tooltip for milestone due date to issue and merge request lists
merge_request: 14318
author: Vitaliy @blackst0ne Klachkov
type: added
---
title: Remove redundant WHERE from event queries
merge_request:
author:
type: other
---
title: Fix errors when moving issue with reference to a group milestone
merge_request: 14294
author:
type: fixed
---
title: Fixes the 500 errors caused by a race condition in GPG's tmp directory handling
merge_request: 14194
author: Alexis Reigel
type: fixed
---
title: Fix image diff swipe handle offset to correctly align with the frame
merge_request:
author:
type: fixed
---
title: Fix Pipeline Triggers to show triggered label and predefined variables (e.g.
CI_PIPELINE_TRIGGERED)
merge_request: 14244
author:
type: fixed
---
title: Fix project feature being deleted when updating project with invalid visibility
level
merge_request:
author:
type: fixed
---
title: Expand docs for changing username or group path
merge_request: 13914
author:
type: other
---
title: Stop using Sidekiq for updating Key#last_used_at
merge_request:
author:
type: changed
......@@ -38,7 +38,6 @@
- [invalid_gpg_signature_update, 2]
- [create_gpg_signature, 2]
- [upload_checksum, 1]
- [use_key, 1]
- [repository_fork, 1]
- [repository_import, 1]
- [project_service, 1]
......
......@@ -4,9 +4,9 @@ Sidekiq::Testing.inline! do
Gitlab::Seeder.quiet do
project_urls = [
'https://gitlab.com/gitlab-org/gitlab-test.git',
'https://gitlab.com/gitlab-org/gitlab-ce.git',
'https://gitlab.com/gitlab-org/gitlab-ci.git',
'https://gitlab.com/gitlab-org/gitlab-shell.git',
'https://gitlab.com/gnuwget/wget2.git',
'https://gitlab.com/Commit451/LabCoat.git',
'https://github.com/documentcloud/underscore.git',
'https://github.com/twitter/flight.git',
'https://github.com/twitter/typeahead.js.git',
......
......@@ -645,6 +645,98 @@ POST /projects/:id/fork
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `namespace` | integer/string | yes | The ID or path of the namespace that the project will be forked to |
## List Forks of a project
>**Note:** This feature was introduced in GitLab 10.1
List the projects accessible to the calling user that have an established, forked relationship with the specified project
```
GET /projects/:id/forks
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `archived` | boolean | no | Limit by archived status |
| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of projects matching the search criteria |
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
| `owned` | boolean | no | Limit by projects owned by the current user |
| `membership` | boolean | no | Limit by projects that the current user is a member of |
| `starred` | boolean | no | Limit by projects starred by the current user |
| `statistics` | boolean | no | Include project statistics |
| `with_issues_enabled` | boolean | no | Limit by enabled issues feature |
| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/forks"
```
Example responses:
```json
[
{
"id": 3,
"description": null,
"default_branch": "master",
"visibility": "internal",
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
"tag_list": [
"example",
"disapora project"
],
"name": "Diaspora Project Site",
"name_with_namespace": "Diaspora / Diaspora Project Site",
"path": "diaspora-project-site",
"path_with_namespace": "diaspora/diaspora-project-site",
"issues_enabled": true,
"open_issues_count": 1,
"merge_requests_enabled": true,
"jobs_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"resolve_outdated_diff_discussions": false,
"container_registry_enabled": false,
"created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
"namespace": {
"id": 3,
"name": "Diaspora",
"path": "diaspora",
"kind": "group",
"full_path": "diaspora"
},
"import_status": "none",
"archived": true,
"avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
"shared_runners_enabled": true,
"forks_count": 0,
"star_count": 1,
"public_jobs": true,
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
"request_access_enabled": false,
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
"merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
"repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
"labels": "http://example.com/api/v4/projects/1/labels",
"events": "http://example.com/api/v4/projects/1/events",
"members": "http://example.com/api/v4/projects/1/members"
}
}
]
```
## Star a project
Stars a given project. Returns status code `304` if the project is already starred.
......
......@@ -188,6 +188,27 @@ Besides giving you the option to edit any settings you've previously
set when [creating the group](#create-a-new-group), you can also
access further configurations for your group.
#### Changing a group's path
> **Note:** If you want to retain ownership over the original namespace and
protect the URL redirects, then instead of changing a group's path or renaming a
username, you can create a new group and transfer projects to it.
Changing a group's path can have unintended side effects.
* Existing web URLs for the group and anything under it (i.e. projects) will
redirect to the new URLs
* Existing Git remote URLs for projects under the group will no longer work, but
Git responses will show an error with the new remote URL
* The original namespace can be claimed again by any group or user, which will
destroy web redirects and Git remote warnings
* If you are vacating the path so it can be claimed by another group or user,
you may need to rename the group name as well since both names and paths must be
unique
> It is currently not possible to rename a namespace if it contains a
project with container registry tags, because the project cannot be moved.
#### Enforce 2FA to group members
Add a security layer to your group by
......
......@@ -8,10 +8,27 @@ experience according to the best approach to their cases.
Your `username` is a unique [`namespace`](../group/index.md#namespaces)
related to your user ID.
### Changing your username
You can change your `username` from your
[profile settings](#profile-settings). To avoid breaking
paths when you change your `username`, we suggest you follow
[this procedure from the GitLab Team Handbook](https://about.gitlab.com/handbook/tools-and-tips/#how-to-change-your-username-at-gitlabcom).
[profile settings](#profile-settings).
> **Note:** If you want to retain ownership over the original namespace and
protect the URL redirects, then instead of changing your username, you can
create a new group and transfer projects to it.
Alternatively, you can follow [this detailed procedure from the GitLab Team Handbook](https://about.gitlab.com/handbook/tools-and-tips/#how-to-change-your-username-at-gitlabcom).
Changing your username can have unintended side effects.
* Existing web URLs for the user and anything under it (i.e. projects) will
redirect to the new URLs
* Existing Git remote URLs for projects under the user will no longer work, but
Git responses will show an error with the new remote URL
* The original namespace can be claimed again by any group or user, which will
destroy any web redirects and Git remote warnings
> It is currently not possible to rename a namespace if it contains a
project with container registry tags, because the project cannot be moved.
## User profile
......
......@@ -76,8 +76,11 @@ module API
optional :import_url, type: String, desc: 'URL from which the project is imported'
end
def present_projects(options = {})
projects = ProjectsFinder.new(current_user: current_user, params: project_finder_params).execute
def load_projects
ProjectsFinder.new(current_user: current_user, params: project_finder_params).execute
end
def present_projects(projects, options = {})
projects = reorder_projects(projects)
projects = projects.with_statistics if params[:statistics]
projects = projects.with_issues_enabled if params[:with_issues_enabled]
......@@ -117,7 +120,7 @@ module API
params[:user] = user
present_projects
present_projects load_projects
end
end
......@@ -130,7 +133,7 @@ module API
use :statistics_params
end
get do
present_projects
present_projects load_projects
end
desc 'Create new project' do
......@@ -235,6 +238,18 @@ module API
end
end
desc 'List forks of this project' do
success Entities::Project
end
params do
use :collection_params
end
get ':id/forks' do
forks = ForkProjectsFinder.new(user_project, params: project_finder_params, current_user: current_user).execute
present_projects forks
end
desc 'Update an existing project' do
success Entities::Project
end
......
......@@ -79,7 +79,7 @@ module Backup
# - 1495527122_gitlab_backup.tar
# - 1495527068_2017_05_23_gitlab_backup.tar
# - 1495527097_2017_05_23_9.3.0-pre_gitlab_backup.tar
next unless file =~ /(\d+)(?:_\d{4}_\d{2}_\d{2}(_\d+\.\d+\.\d+.*)?)?_gitlab_backup\.tar$/
next unless file =~ /^(\d{10})(?:_\d{4}_\d{2}_\d{2}(_\d+\.\d+\.\d+((-|\.)(pre|rc\d))?)?)?_gitlab_backup\.tar$/
timestamp = $1.to_i
......
......@@ -29,6 +29,8 @@ module Gitlab
# http://gitlab.com/some/link/#1234, and code `puts #1234`'
#
class ReferenceRewriter
RewriteError = Class.new(StandardError)
def initialize(text, source_project, current_user)
@text = text
@source_project = source_project
......@@ -61,6 +63,10 @@ module Gitlab
cross_reference = build_cross_reference(referable, target_project)
return reference if reference == cross_reference
if cross_reference.nil?
raise RewriteError, "Unspecified reference detected for #{referable.class.name}"
end
new_text = before + cross_reference + after
substitution_valid?(new_text) ? cross_reference : reference
end
......
module Gitlab
module Git
class OperationService
include Gitlab::Git::Popen
WithBranchResult = Struct.new(:newrev, :repo_created, :branch_created) do
alias_method :repo_created?, :repo_created
alias_method :branch_created?, :branch_created
......@@ -150,7 +152,7 @@ module Gitlab
# (and have!) accidentally reset the ref to an earlier state, clobbering
# commits. See also https://github.com/libgit2/libgit2/issues/1534.
command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z]
_, status = Gitlab::Popen.popen(
_, status = popen(
command,
repository.path) do |stdin|
stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00")
......
......@@ -5,17 +5,21 @@ require 'open3'
module Gitlab
module Git
module Popen
def popen(cmd, path)
def popen(cmd, path, vars = {})
unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings"
end
vars = { "PWD" => path }
path ||= Dir.pwd
vars['PWD'] = path
options = { chdir: path }
@cmd_output = ""
@cmd_status = 0
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
yield(stdin) if block_given?
stdin.close
@cmd_output << stdout.read
@cmd_output << stderr.read
@cmd_status = wait_thr.value.exitstatus
......
......@@ -490,7 +490,7 @@ module Gitlab
# Not found -> ["", 0]
# Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
Gitlab::Popen.popen(args, @path).first.split.last
popen(args, @path).first.split.last
end
end
end
......@@ -792,9 +792,7 @@ module Gitlab
end
command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z]
message, status = Gitlab::Popen.popen(
command,
path) do |stdin|
message, status = popen(command, path) do |stdin|
stdin.write(instructions.join)
end
......
......@@ -3,6 +3,8 @@
module Gitlab
module Git
class RevList
include Gitlab::Git::Popen
attr_reader :oldrev, :newrev, :path_to_repo
def initialize(path_to_repo:, newrev:, oldrev: nil)
......@@ -26,7 +28,7 @@ module Gitlab
private
def execute(args)
output, status = Gitlab::Popen.popen(args, nil, Gitlab::Git::Env.all.stringify_keys)
output, status = popen(args, nil, Gitlab::Git::Env.all.stringify_keys)
unless status.zero?
raise "Got a non-zero exit code while calling out `#{args.join(' ')}`."
......
require 'spec_helper'
describe 'User Callouts', js: true do
let(:user) { create(:user) }
let(:another_user) { create(:user) }
let(:project) { create(:project, path: 'gitlab', name: 'sample') }
before do
sign_in(user)
project.team << [user, :master]
end
it 'takes you to the profile preferences when the link is clicked' do
visit dashboard_projects_path
click_link 'Check it out'
expect(current_path).to eq profile_preferences_path
end
it 'does not show when cookie is set' do
visit dashboard_projects_path
within('.user-callout') do
find('.close').trigger('click')
end
visit dashboard_projects_path
expect(page).not_to have_selector('.user-callout')
end
describe 'user callout should appear in two routes' do
it 'shows up on the user profile' do
visit user_path(user)
expect(find('.user-callout')).to have_content 'Customize your experience'
end
it 'shows up on the dashboard projects' do
visit dashboard_projects_path
expect(find('.user-callout')).to have_content 'Customize your experience'
end
end
it 'hides the user callout when click on the dismiss icon' do
visit user_path(user)
within('.user-callout') do
find('.close').click
end
expect(page).not_to have_selector('.user-callout')
end
it 'does not show callout on another users profile' do
visit user_path(another_user)
expect(page).not_to have_selector('.user-callout')
end
end
require 'spec_helper'
describe ForkProjectsFinder do
let(:source_project) { create(:project, :empty_repo) }
let(:private_fork) { create(:project, :private, :empty_repo, name: 'A') }
let(:internal_fork) { create(:project, :internal, :empty_repo, name: 'B') }
let(:public_fork) { create(:project, :public, :empty_repo, name: 'C') }
let(:non_member) { create(:user) }
let(:private_fork_member) { create(:user) }
before do
private_fork.add_developer(private_fork_member)
source_project.forks << private_fork
source_project.forks << internal_fork
source_project.forks << public_fork
end
describe '#execute' do
let(:finder) { described_class.new(source_project, params: {}, current_user: current_user) }
subject { finder.execute }
describe 'without a user' do
let(:current_user) { nil }
it { is_expected.to eq([public_fork]) }
end
describe 'with a user' do
let(:current_user) { non_member }
it { is_expected.to eq([public_fork, internal_fork]) }
end
describe 'with a member' do
let(:current_user) { private_fork_member }
it { is_expected.to eq([public_fork, internal_fork, private_fork]) }
end
end
end
require 'spec_helper'
describe Dashboard::ProjectsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, namespace: namespace, path: 'builds-project') }
render_views
before(:all) do
clean_frontend_fixtures('dashboard/')
end
before do
sign_in(admin)
end
after do
remove_repository(project)
end
it 'dashboard/user-callout.html.raw' do |example|
rendered = render_template('shared/_user_callout')
store_frontend_fixture(rendered, example.description)
end
private
def render_template(template_file_name)
controller.prepend_view_path(JavaScriptFixturesHelpers::FIXTURE_PATH)
controller.render_to_string(template_file_name, layout: false)
end
end
import Cookies from 'js-cookie';
import UserCallout from '~/user_callout';
const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
describe('UserCallout', function () {
const fixtureName = 'dashboard/user-callout.html.raw';
preloadFixtures(fixtureName);
beforeEach(() => {
loadFixtures(fixtureName);
Cookies.remove(USER_CALLOUT_COOKIE);
this.userCallout = new UserCallout();
this.closeButton = $('.js-close-callout.close');
this.userCalloutBtn = $('.js-close-callout:not(.close)');
});
it('hides when user clicks on the dismiss-icon', (done) => {
this.closeButton.click();
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true');
setTimeout(() => {
expect(
document.querySelector('.user-callout'),
).toBeNull();
done();
});
});
it('hides when user clicks on the "check it out" button', () => {
this.userCalloutBtn.click();
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true');
});
describe('Sets cookie with setCalloutPerProject', () => {
beforeEach(() => {
spyOn(Cookies, 'set').and.callFake(() => {});
document.querySelector('.user-callout').setAttribute('data-project-path', 'foo/bar');
this.userCallout = new UserCallout({ setCalloutPerProject: true });
});
it('sets a cookie when the user clicks the close button', () => {
this.userCalloutBtn.click();
expect(Cookies.set).toHaveBeenCalledWith('user_callout_dismissed', 'true', Object({ expires: 365, path: 'foo/bar' }));
});
});
});
......@@ -296,7 +296,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do
context 'project milestones' do
let(:milestone) { create(:milestone, project: project) }
let(:reference) { milestone.to_reference }
let(:reference) { milestone.to_reference(format: :iid) }
include_examples 'reference parsing'
......
......@@ -26,6 +26,8 @@ describe Backup::Manager do
[
'1451606400_2016_01_01_1.2.3_gitlab_backup.tar',
'1451520000_2015_12_31_4.5.6_gitlab_backup.tar',
'1451520000_2015_12_31_4.5.6-pre_gitlab_backup.tar',
'1451520000_2015_12_31_4.5.6-rc1_gitlab_backup.tar',
'1451510000_2015_12_30_gitlab_backup.tar',
'1450742400_2015_12_22_gitlab_backup.tar',
'1449878400_gitlab_backup.tar',
......@@ -57,6 +59,30 @@ describe Backup::Manager do
end
end
context 'when no valid file is found' do
let(:files) do
[
'14516064000_2016_01_01_1.2.3_gitlab_backup.tar',
'foo_1451520000_2015_12_31_4.5.6_gitlab_backup.tar',
'1451520000_2015_12_31_4.5.6-foo_gitlab_backup.tar'
]
end
before do
allow(Gitlab.config.backup).to receive(:keep_time).and_return(1)
subject.remove_old
end
it 'removes no files' do
expect(FileUtils).not_to have_received(:rm)
end
it 'prints a done message' do
expect(progress).to have_received(:puts).with('done. (0 removed)')
end
end
context 'when there are no files older than keep_time' do
before do
# Set to 30 days
......@@ -84,28 +110,30 @@ describe Backup::Manager do
it 'removes matching files with a human-readable versioned timestamp' do
expect(FileUtils).to have_received(:rm).with(files[1])
end
it 'removes matching files with a human-readable non-versioned timestamp' do
expect(FileUtils).to have_received(:rm).with(files[2])
expect(FileUtils).to have_received(:rm).with(files[3])
end
it 'removes matching files without a human-readable timestamp' do
it 'removes matching files with a human-readable non-versioned timestamp' do
expect(FileUtils).to have_received(:rm).with(files[4])
expect(FileUtils).to have_received(:rm).with(files[5])
end
it 'removes matching files without a human-readable timestamp' do
expect(FileUtils).to have_received(:rm).with(files[6])
expect(FileUtils).to have_received(:rm).with(files[7])
end
it 'does not remove files that are not old enough' do
expect(FileUtils).not_to have_received(:rm).with(files[0])
end
it 'does not remove non-matching files' do
expect(FileUtils).not_to have_received(:rm).with(files[6])
expect(FileUtils).not_to have_received(:rm).with(files[8])
end
it 'prints a done message' do
expect(progress).to have_received(:puts).with('done. (5 removed)')
expect(progress).to have_received(:puts).with('done. (7 removed)')
end
end
......@@ -121,14 +149,14 @@ describe Backup::Manager do
end
it 'removes the remaining expected files' do
expect(FileUtils).to have_received(:rm).with(files[2])
expect(FileUtils).to have_received(:rm).with(files[3])
expect(FileUtils).to have_received(:rm).with(files[4])
expect(FileUtils).to have_received(:rm).with(files[5])
expect(FileUtils).to have_received(:rm).with(files[6])
expect(FileUtils).to have_received(:rm).with(files[7])
end
it 'sets the correct removed count' do
expect(progress).to have_received(:puts).with('done. (4 removed)')
expect(progress).to have_received(:puts).with('done. (6 removed)')
end
it 'prints the error from file that could not be removed' do
......
......@@ -5,13 +5,13 @@ describe Gitlab::Checks::ForcePush do
context "exit code checking", skip_gitaly_mock: true do
it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do
allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0])
allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['normal output', 0])
expect { described_class.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error
end
it "raises a runtime error if the `popen` call to git returns a non-zero exit code" do
allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1])
allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['error', 1])
expect { described_class.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError)
end
......
require 'spec_helper'
describe Gitlab::Gfm::ReferenceRewriter do
let(:text) { 'some text' }
let(:old_project) { create(:project, name: 'old-project') }
let(:new_project) { create(:project, name: 'new-project') }
let(:group) { create(:group) }
let(:old_project) { create(:project, name: 'old-project', group: group) }
let(:new_project) { create(:project, name: 'new-project', group: group) }
let(:user) { create(:user) }
let(:old_project_ref) { old_project.to_reference(new_project) }
let(:text) { 'some text' }
before do
old_project.team << [user, :reporter]
end
......@@ -39,7 +42,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
it { is_expected.not_to include merge_request.to_reference(new_project) }
end
context 'description ambigous elements' do
context 'rewrite ambigous references' do
context 'url' do
let(:url) { 'http://gitlab.com/#1' }
let(:text) { "This references #1, but not #{url}" }
......@@ -66,23 +69,21 @@ describe Gitlab::Gfm::ReferenceRewriter do
context 'description with project labels' do
let!(:label) { create(:label, id: 123, name: 'test', project: old_project) }
let(:project_ref) { old_project.to_reference(new_project) }
context 'label referenced by id' do
let(:text) { '#1 and ~123' }
it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} }
it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~123} }
end
context 'label referenced by text' do
let(:text) { '#1 and ~"test"' }
it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} }
it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~123} }
end
end
context 'description with group labels' do
let(:old_group) { create(:group) }
let!(:group_label) { create(:group_label, id: 321, name: 'group label', group: old_group) }
let(:project_ref) { old_project.to_reference(new_project) }
before do
old_project.update(namespace: old_group)
......@@ -90,21 +91,53 @@ describe Gitlab::Gfm::ReferenceRewriter do
context 'label referenced by id' do
let(:text) { '#1 and ~321' }
it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} }
it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~321} }
end
context 'label referenced by text' do
let(:text) { '#1 and ~"group label"' }
it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} }
it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~321} }
end
end
end
end
context 'reference contains project milestone' do
let!(:milestone) do
create(:milestone, title: '9.0', project: old_project)
end
let(:text) { 'milestone: %"9.0"' }
it { is_expected.to eq %Q[milestone: #{old_project_ref}%"9.0"] }
end
context 'when referring to group milestone' do
let!(:milestone) do
create(:milestone, title: '10.0', group: group)
end
let(:text) { 'milestone %"10.0"' }
it { is_expected.to eq text }
end
context 'when referable has a nil reference' do
before do
create(:milestone, title: '9.0', project: old_project)
allow_any_instance_of(Milestone)
.to receive(:to_reference)
.and_return(nil)
end
context 'reference contains milestone' do
let(:milestone) { create(:milestone) }
let(:text) { "milestone ref: #{milestone.to_reference}" }
let(:text) { 'milestone: %"9.0"' }
it { is_expected.to eq text }
it 'raises an error that should be fixed' do
expect { subject }.to raise_error(
described_class::RewriteError,
'Unspecified reference detected for Milestone'
)
end
end
end
......
......@@ -481,7 +481,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
it 'raises an error if it failed' do
expect(Gitlab::Popen).to receive(:popen).and_return(['Error', 1])
expect(@repo).to receive(:popen).and_return(['Error', 1])
expect do
@repo.delete_refs('refs/heads/fix')
......
......@@ -14,7 +14,7 @@ describe Gitlab::Git::RevList do
let(:rev_list) { described_class.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
it 'calls out to `popen`' do
expect(Gitlab::Popen).to receive(:popen).with([
expect(rev_list).to receive(:popen).with([
Gitlab.config.git.bin_path,
"--git-dir=#{project.repository.path_to_repo}",
'rev-list',
......@@ -36,7 +36,7 @@ describe Gitlab::Git::RevList do
let(:rev_list) { described_class.new(oldrev: 'oldrev', newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
it 'calls out to `popen`' do
expect(Gitlab::Popen).to receive(:popen).with([
expect(rev_list).to receive(:popen).with([
Gitlab.config.git.bin_path,
"--git-dir=#{project.repository.path_to_repo}",
'rev-list',
......
......@@ -37,30 +37,17 @@ describe Key, :mailer do
end
describe "#update_last_used_at" do
let(:key) { create(:key) }
context 'when key was not updated during the last day' do
before do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain)
.and_return('000000')
end
it 'enqueues a UseKeyWorker job' do
expect(UseKeyWorker).to receive(:perform_async).with(key.id)
key.update_last_used_at
end
end
it 'updates the last used timestamp' do
key = build(:key)
service = double(:service)
expect(Keys::LastUsedService).to receive(:new)
.with(key)
.and_return(service)
context 'when key was updated during the last day' do
before do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain)
.and_return(false)
end
expect(service).to receive(:execute)
it 'does not enqueue a UseKeyWorker job' do
expect(UseKeyWorker).not_to receive(:perform_async)
key.update_last_used_at
end
key.update_last_used_at
end
end
end
......
......@@ -240,7 +240,7 @@ describe Milestone do
let(:milestone) { build_stubbed(:milestone, iid: 1, project: project, name: 'milestone') }
it 'returns a String reference to the object' do
expect(milestone.to_reference).to eq '%1'
expect(milestone.to_reference).to eq '%"milestone"'
end
it 'returns a reference by name when the format is set to :name' do
......@@ -248,24 +248,29 @@ describe Milestone do
end
it 'supports a cross-project reference' do
expect(milestone.to_reference(another_project)).to eq 'sample-project%1'
expect(milestone.to_reference(another_project)).to eq 'sample-project%"milestone"'
end
end
context 'for a group milestone' do
let(:milestone) { build_stubbed(:milestone, iid: 1, group: group, name: 'milestone') }
it 'returns nil with the default format' do
expect(milestone.to_reference).to be_nil
it 'returns a group milestone reference with a default format' do
expect(milestone.to_reference).to eq '%"milestone"'
end
it 'returns a reference by name when the format is set to :name' do
expect(milestone.to_reference(format: :name)).to eq '%"milestone"'
end
it 'does not supports cross-project references' do
it 'does supports cross-project references within a group' do
expect(milestone.to_reference(another_project, format: :name)).to eq '%"milestone"'
end
it 'raises an error when using iid format' do
expect { milestone.to_reference(format: :iid) }
.to raise_error(ArgumentError, 'Cannot refer to a group milestone by an internal id!')
end
end
end
......
......@@ -1232,6 +1232,59 @@ describe API::Projects do
end
end
end
describe 'GET /projects/:id/forks' do
let(:private_fork) { create(:project, :private, :empty_repo) }
let(:member) { create(:user) }
let(:non_member) { create(:user) }
before do
private_fork.add_developer(member)
end
context 'for a forked project' do
before do
post api("/projects/#{private_fork.id}/fork/#{project_fork_source.id}", admin)
private_fork.reload
expect(private_fork.forked_from_project).not_to be_nil
expect(private_fork.forked?).to be_truthy
project_fork_source.reload
expect(project_fork_source.forks.length).to eq(1)
expect(project_fork_source.forks).to include(private_fork)
end
context 'for a user that can access the forks' do
it 'returns the forks' do
get api("/projects/#{project_fork_source.id}/forks", member)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response.length).to eq(1)
expect(json_response[0]['name']).to eq(private_fork.name)
end
end
context 'for a user that cannot access the forks' do
it 'returns an empty array' do
get api("/projects/#{project_fork_source.id}/forks", non_member)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response.length).to eq(0)
end
end
end
context 'for a non-forked project' do
it 'returns an empty array' do
get api("/projects/#{project_fork_source.id}/forks")
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response.length).to eq(0)
end
end
end
end
describe "POST /projects/:id/share" do
......
......@@ -42,7 +42,7 @@ describe Issues::CloseService do
service.execute(issue)
end
it 'refreshes the number of open issues' do
it 'refreshes the number of open issues', :use_clean_rails_memory_store_caching do
expect { service.execute(issue) }
.to change { project.open_issues_count }.from(1).to(0)
end
......
......@@ -35,7 +35,7 @@ describe Issues::CreateService do
expect(issue.due_date).to eq Date.tomorrow
end
it 'refreshes the number of open issues' do
it 'refreshes the number of open issues', :use_clean_rails_memory_store_caching do
expect { issue }.to change { project.open_issues_count }.from(0).to(1)
end
......
......@@ -64,6 +64,13 @@ describe Issues::UpdateService, :mailer do
expect(issue.due_date).to eq Date.tomorrow
end
it 'refreshes the number of open issues when the issue is made confidential', :use_clean_rails_memory_store_caching do
issue # make sure the issue is created first so our counts are correct.
expect { update_issue(confidential: true) }
.to change { project.open_issues_count }.from(1).to(0)
end
it 'updates open issue counter for assignees when issue is reassigned' do
update_issue(assignee_ids: [user2.id])
......
require 'spec_helper'
describe Keys::LastUsedService do
describe '#execute', :clean_gitlab_redis_shared_state do
it 'updates the key when it has not been used recently' do
key = create(:key, last_used_at: 1.year.ago)
time = Time.zone.now
Timecop.freeze(time) { described_class.new(key).execute }
expect(key.last_used_at).to eq(time)
end
it 'does not update the key when it has been used recently' do
time = 1.minute.ago
key = create(:key, last_used_at: time)
described_class.new(key).execute
expect(key.last_used_at).to eq(time)
end
it 'does not update the updated_at field' do
# Since a lot of these updates could happen in parallel for different keys
# we want these updates to be as lightweight as possible, hence we want to
# make sure we _only_ update last_used_at and not always updated_at.
key = create(:key, last_used_at: 1.year.ago)
recorder = ActiveRecord::QueryRecorder.new do
described_class.new(key).execute
end
expect(recorder.count).to eq(1)
expect(recorder.log[0]).not_to include('updated_at')
end
end
describe '#update?', :clean_gitlab_redis_shared_state do
it 'returns true when no last used timestamp is present' do
key = build(:key, last_used_at: nil)
service = described_class.new(key)
expect(service.update?).to eq(true)
end
it 'returns true when the key needs to be updated' do
key = build(:key, last_used_at: 1.year.ago)
service = described_class.new(key)
expect(service.update?).to eq(true)
end
it 'returns false when a lease has already been obtained' do
key = build(:key, last_used_at: 1.year.ago)
service = described_class.new(key)
expect(service.update?).to eq(true)
expect(service.update?).to eq(false)
end
it 'returns false when the key does not yet need to be updated' do
key = build(:key, last_used_at: 1.minute.ago)
service = described_class.new(key)
expect(service.update?).to eq(false)
end
end
end
......@@ -52,7 +52,7 @@ describe MergeRequests::CloseService do
end
end
it 'refreshes the number of open merge requests for a valid MR' do
it 'refreshes the number of open merge requests for a valid MR', :use_clean_rails_memory_store_caching do
service = described_class.new(project, user, {})
expect { service.execute(merge_request) }
......
......@@ -37,7 +37,7 @@ describe MergeRequests::CreateService do
expect(service).to have_received(:execute_hooks).with(merge_request)
end
it 'refreshes the number of open merge requests' do
it 'refreshes the number of open merge requests', :use_clean_rails_memory_store_caching do
expect { service.execute }
.to change { project.open_merge_requests_count }.from(0).to(1)
end
......
......@@ -66,8 +66,8 @@ describe Projects::CountService do
describe '#cache_key' do
it 'returns the cache key as an Array' do
allow(service).to receive(:cache_key_name).and_return('count_service')
expect(service.cache_key).to eq(['projects', 1, 'count_service'])
allow(service).to receive(:cache_key_name).and_return('foo')
expect(service.cache_key).to eq(['projects', 'count_service', described_class::VERSION, 1, 'foo'])
end
end
end
......@@ -232,7 +232,9 @@ describe SystemNoteService do
context 'when milestone added' do
it 'sets the note text' do
expect(subject.note).to eq "changed milestone to #{milestone.to_reference}"
reference = milestone.to_reference(format: :iid)
expect(subject.note).to eq "changed milestone to #{reference}"
end
end
......
require 'spec_helper'
describe UseKeyWorker do
describe "#perform" do
it "updates the key's last_used_at attribute to the current time when it exists" do
worker = described_class.new
key = create(:key)
current_time = Time.zone.now
Timecop.freeze(current_time) do
expect { worker.perform(key.id) }
.to change { key.reload.last_used_at }.from(nil).to be_like_time(current_time)
end
end
it "returns false and skips the job when the key doesn't exist" do
worker = described_class.new
key = create(:key)
expect(worker.perform(key.id + 1)).to eq false
end
end
end
......@@ -271,10 +271,6 @@ production:
--version="$CI_PIPELINE_ID-$CI_JOB_ID" \
"$name" \
chart/
if [[ "$track" == "stable" ]]; then
kubectl rollout status -n "$KUBE_NAMESPACE" -w "deployment/${CI_ENVIRONMENT_SLUG}-auto-deploy"
fi
}
function install_dependencies() {
......
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