Merge remote-tracking branch 'upstream/master' into 27762-add-default-artifacts-expiration
* upstream/master: (26 commits) Allow searching issues for strings containing colons Fix Spinach failure [ci skip] UX Guide: Add personas Reuse User#find_by_any_email on UserFormatter#find_by_email Use leading periods on UserFormatter#find_by_external_uid Add CHANGELOG entry Add a simple cache for Gitlab::GithubImport::Client#user GitHub Importer - Find users based on their email address Don't use potentially slow ForkService in spec Added support for Authentiq Back-Channel Logout Added documentation for permalinks to most recent build artifacts Unify issues search behavior by always filtering when ALL labels matches Update GitLab Pages to v0.3.1 Document Timecop usage for time-sensitive tests Add process for requesting approval for licenses Rename karma job to rake karma Update positioning of nav scroll arrows Fix issue where files on a fork could not be edited Move tanuki to left of title; increase max-width of title Refactor project stats & last_commit CSS; left align main nav items ...
This commit is contained in:
commit
aae71188ee
|
@ -240,7 +240,7 @@ rake db:seed_fu:
|
|||
paths:
|
||||
- log/development.log
|
||||
|
||||
karma:
|
||||
rake karma:
|
||||
cache:
|
||||
paths:
|
||||
- vendor/ruby
|
||||
|
@ -387,7 +387,7 @@ pages:
|
|||
<<: *dedicated-runner
|
||||
dependencies:
|
||||
- coverage
|
||||
- karma
|
||||
- rake karma
|
||||
- lint:javascript:report
|
||||
script:
|
||||
- mv public/ .public/
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.3.0
|
||||
0.3.1
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -34,7 +34,7 @@ gem 'omniauth-saml', '~> 1.7.0'
|
|||
gem 'omniauth-shibboleth', '~> 1.2.0'
|
||||
gem 'omniauth-twitter', '~> 1.2.0'
|
||||
gem 'omniauth_crowd', '~> 2.2.0'
|
||||
gem 'omniauth-authentiq', '~> 0.2.0'
|
||||
gem 'omniauth-authentiq', '~> 0.3.0'
|
||||
gem 'rack-oauth2', '~> 1.2.1'
|
||||
gem 'jwt', '~> 1.5.6'
|
||||
|
||||
|
|
|
@ -448,7 +448,7 @@ GEM
|
|||
rack (>= 1.0, < 3)
|
||||
omniauth-auth0 (1.4.1)
|
||||
omniauth-oauth2 (~> 1.1)
|
||||
omniauth-authentiq (0.2.2)
|
||||
omniauth-authentiq (0.3.0)
|
||||
omniauth-oauth2 (~> 1.3, >= 1.3.1)
|
||||
omniauth-azure-oauth2 (0.0.6)
|
||||
jwt (~> 1.0)
|
||||
|
@ -925,7 +925,7 @@ DEPENDENCIES
|
|||
oj (~> 2.17.4)
|
||||
omniauth (~> 1.3.2)
|
||||
omniauth-auth0 (~> 1.4.1)
|
||||
omniauth-authentiq (~> 0.2.0)
|
||||
omniauth-authentiq (~> 0.3.0)
|
||||
omniauth-azure-oauth2 (~> 0.0.6)
|
||||
omniauth-cas3 (~> 1.1.2)
|
||||
omniauth-facebook (~> 4.0.0)
|
||||
|
|
|
@ -173,7 +173,7 @@
|
|||
tokens.forEach((token) => {
|
||||
const condition = gl.FilteredSearchTokenKeys
|
||||
.searchByConditionKeyValue(token.key, token.value.toLowerCase());
|
||||
const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key);
|
||||
const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key) || {};
|
||||
const keyParam = param ? `${token.key}_${param}` : token.key;
|
||||
let tokenPath = '';
|
||||
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
require('./filtered_search_token_keys');
|
||||
|
||||
(() => {
|
||||
class FilteredSearchTokenizer {
|
||||
static processTokens(input) {
|
||||
const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key);
|
||||
// Regex extracts `(token):(symbol)(value)`
|
||||
// Values that start with a double quote must end in a double quote (same for single)
|
||||
const tokenRegex = /(\w+):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\S+))/g;
|
||||
const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
|
||||
const tokens = [];
|
||||
let lastToken = null;
|
||||
const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
|
||||
|
|
|
@ -148,16 +148,11 @@ header {
|
|||
}
|
||||
|
||||
.header-logo {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
display: inline-block;
|
||||
margin: 0 8px 0 3px;
|
||||
position: relative;
|
||||
top: 7px;
|
||||
transition-duration: .3s;
|
||||
z-index: 999;
|
||||
|
||||
#logo {
|
||||
position: relative;
|
||||
left: -50%;
|
||||
}
|
||||
|
||||
svg,
|
||||
img {
|
||||
|
@ -167,15 +162,6 @@ header {
|
|||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-max) {
|
||||
right: 20px;
|
||||
left: auto;
|
||||
|
||||
#logo {
|
||||
left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
|
@ -183,7 +169,7 @@ header {
|
|||
padding-right: 20px;
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
max-width: 385px;
|
||||
max-width: 450px;
|
||||
display: inline-block;
|
||||
line-height: $header-height;
|
||||
font-weight: normal;
|
||||
|
@ -193,10 +179,6 @@ header {
|
|||
vertical-align: top;
|
||||
white-space: nowrap;
|
||||
|
||||
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-max) {
|
||||
max-width: 190px;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
@mixin fade($gradient-direction, $gradient-color) {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
width: 43px;
|
||||
|
@ -18,7 +18,7 @@
|
|||
|
||||
.fa {
|
||||
position: relative;
|
||||
top: 5px;
|
||||
top: 6px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,6 @@
|
|||
}
|
||||
|
||||
&.sub-nav {
|
||||
text-align: center;
|
||||
background-color: $gray-normal;
|
||||
|
||||
.container-fluid {
|
||||
|
@ -287,7 +286,6 @@
|
|||
background: $gray-light;
|
||||
border-bottom: 1px solid $border-color;
|
||||
transition: padding $sidebar-transition-duration;
|
||||
text-align: center;
|
||||
|
||||
.container-fluid {
|
||||
position: relative;
|
||||
|
@ -353,7 +351,7 @@
|
|||
right: -5px;
|
||||
|
||||
.fa {
|
||||
right: -7px;
|
||||
right: -28px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -383,7 +381,7 @@
|
|||
left: 0;
|
||||
|
||||
.fa {
|
||||
left: 10px;
|
||||
left: -4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -222,6 +222,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dropdown-toggle,
|
||||
.dropdown-menu {
|
||||
color: $gl-text-color-secondary;
|
||||
|
|
|
@ -268,6 +268,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.project-repo-buttons {
|
||||
.project-action-button .dropdown-menu {
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.split-one {
|
||||
display: inline-table;
|
||||
margin-right: 12px;
|
||||
|
@ -645,29 +652,23 @@ pre.light-well {
|
|||
}
|
||||
}
|
||||
|
||||
.container-fluid.project-stats-container {
|
||||
@media (max-width: $screen-xs-max) {
|
||||
padding: 12px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.project-last-commit {
|
||||
background-color: $gray-light;
|
||||
padding: 12px $gl-padding;
|
||||
border: 1px solid $border-color;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
margin-top: $gl-padding;
|
||||
}
|
||||
|
||||
&.container-fluid {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
background-color: $gray-light;
|
||||
border: 1px solid $border-color;
|
||||
border-right-width: 0;
|
||||
border-left-width: 0;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
border-right-width: 1px;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&.container-limited {
|
||||
@media (min-width: 1281px) {
|
||||
border-radius: $border-radius-base;
|
||||
}
|
||||
@media (min-width: $screen-sm-min) {
|
||||
border-radius: $border-radius-base;
|
||||
}
|
||||
|
||||
.ci-status {
|
||||
|
|
|
@ -104,23 +104,15 @@ module CreatesCommit
|
|||
if can?(current_user, :push_code, @project)
|
||||
# Edit file in this project
|
||||
@mr_source_project = @project
|
||||
|
||||
if @project.forked?
|
||||
# Merge request from this project to fork origin
|
||||
@mr_target_project = @project.forked_from_project
|
||||
@mr_target_branch = @mr_target_project.repository.root_ref
|
||||
else
|
||||
# Merge request to this project
|
||||
@mr_target_project = @project
|
||||
@mr_target_branch = @ref || @target_branch
|
||||
end
|
||||
else
|
||||
# Merge request from fork to this project
|
||||
@mr_source_project = current_user.fork_of(@project)
|
||||
@mr_target_project = @project
|
||||
@mr_target_branch = @ref || @target_branch
|
||||
end
|
||||
|
||||
# Merge request to this project
|
||||
@mr_target_project = @project
|
||||
@mr_target_branch = @ref || @target_branch
|
||||
|
||||
@mr_source_branch = guess_mr_source_branch
|
||||
end
|
||||
|
||||
|
|
|
@ -78,6 +78,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
handle_omniauth
|
||||
end
|
||||
|
||||
def authentiq
|
||||
if params['sid']
|
||||
handle_service_ticket oauth['provider'], params['sid']
|
||||
end
|
||||
handle_omniauth
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_omniauth
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.page-with-sidebar{ class: page_gutter_class }
|
||||
- if defined?(nav) && nav
|
||||
.layout-nav
|
||||
.container-fluid
|
||||
%div{ class: container_class }
|
||||
= render "layouts/nav/#{nav}"
|
||||
.content-wrapper{ class: "#{layout_nav_class}" }
|
||||
= yield :sub_nav
|
||||
|
|
|
@ -61,12 +61,12 @@
|
|||
%div
|
||||
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
|
||||
|
||||
%h1.title= title
|
||||
|
||||
.header-logo
|
||||
= link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
|
||||
= brand_header_logo
|
||||
|
||||
%h1.title= title
|
||||
|
||||
= yield :header_content
|
||||
|
||||
= render 'shared/outdated_browser'
|
||||
|
|
|
@ -13,69 +13,70 @@
|
|||
= render "home_panel"
|
||||
|
||||
- if current_user && can?(current_user, :download_code, @project)
|
||||
%nav.project-stats{ class: container_class }
|
||||
%ul.nav
|
||||
%li
|
||||
= link_to project_files_path(@project) do
|
||||
Files (#{storage_counter(@project.statistics.total_repository_size)})
|
||||
%li
|
||||
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
|
||||
#{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)})
|
||||
%li
|
||||
= link_to namespace_project_branches_path(@project.namespace, @project) do
|
||||
#{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
|
||||
%li
|
||||
= link_to namespace_project_tags_path(@project.namespace, @project) do
|
||||
#{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
|
||||
|
||||
- if default_project_view != 'readme' && @repository.readme
|
||||
.project-stats-container{ class: container_class }
|
||||
%nav.project-stats
|
||||
%ul.nav
|
||||
%li
|
||||
= link_to 'Readme', readme_path(@project)
|
||||
|
||||
- if @repository.changelog
|
||||
= link_to project_files_path(@project) do
|
||||
Files (#{storage_counter(@project.statistics.total_repository_size)})
|
||||
%li
|
||||
= link_to 'Changelog', changelog_path(@project)
|
||||
|
||||
- if @repository.license_blob
|
||||
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
|
||||
#{'Commit'.pluralize(@project.statistics.commit_count)} (#{number_with_delimiter(@project.statistics.commit_count)})
|
||||
%li
|
||||
= link_to license_short_name(@project), license_path(@project)
|
||||
|
||||
- if @repository.contribution_guide
|
||||
= link_to namespace_project_branches_path(@project.namespace, @project) do
|
||||
#{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
|
||||
%li
|
||||
= link_to 'Contribution guide', contribution_guide_path(@project)
|
||||
= link_to namespace_project_tags_path(@project.namespace, @project) do
|
||||
#{'Tag'.pluralize(@repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
|
||||
|
||||
- if @repository.gitlab_ci_yml
|
||||
%li
|
||||
= link_to 'CI configuration', ci_configuration_path(@project)
|
||||
- if default_project_view != 'readme' && @repository.readme
|
||||
%li
|
||||
= link_to 'Readme', readme_path(@project)
|
||||
|
||||
- if current_user && can_push_branch?(@project, @project.default_branch)
|
||||
- unless @repository.changelog
|
||||
%li.missing
|
||||
= link_to add_special_file_path(@project, file_name: 'CHANGELOG') do
|
||||
Add Changelog
|
||||
- unless @repository.license_blob
|
||||
%li.missing
|
||||
= link_to add_special_file_path(@project, file_name: 'LICENSE') do
|
||||
Add License
|
||||
- unless @repository.contribution_guide
|
||||
%li.missing
|
||||
= link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
|
||||
Add Contribution guide
|
||||
- unless @repository.gitlab_ci_yml
|
||||
%li.missing
|
||||
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
|
||||
Set up CI
|
||||
- if koding_enabled? && @repository.koding_yml.blank?
|
||||
%li.missing
|
||||
= link_to 'Set up Koding', add_koding_stack_path(@project)
|
||||
- if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
|
||||
%li.missing
|
||||
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do
|
||||
Set up auto deploy
|
||||
- if @repository.changelog
|
||||
%li
|
||||
= link_to 'Changelog', changelog_path(@project)
|
||||
|
||||
- if @repository.commit
|
||||
.project-last-commit{ class: container_class }
|
||||
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
|
||||
- if @repository.license_blob
|
||||
%li
|
||||
= link_to license_short_name(@project), license_path(@project)
|
||||
|
||||
- if @repository.contribution_guide
|
||||
%li
|
||||
= link_to 'Contribution guide', contribution_guide_path(@project)
|
||||
|
||||
- if @repository.gitlab_ci_yml
|
||||
%li
|
||||
= link_to 'CI configuration', ci_configuration_path(@project)
|
||||
|
||||
- if current_user && can_push_branch?(@project, @project.default_branch)
|
||||
- unless @repository.changelog
|
||||
%li.missing
|
||||
= link_to add_special_file_path(@project, file_name: 'CHANGELOG') do
|
||||
Add Changelog
|
||||
- unless @repository.license_blob
|
||||
%li.missing
|
||||
= link_to add_special_file_path(@project, file_name: 'LICENSE') do
|
||||
Add License
|
||||
- unless @repository.contribution_guide
|
||||
%li.missing
|
||||
= link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
|
||||
Add Contribution guide
|
||||
- unless @repository.gitlab_ci_yml
|
||||
%li.missing
|
||||
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
|
||||
Set up CI
|
||||
- if koding_enabled? && @repository.koding_yml.blank?
|
||||
%li.missing
|
||||
= link_to 'Set up Koding', add_koding_stack_path(@project)
|
||||
- if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
|
||||
%li.missing
|
||||
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', target_branch: 'auto-deploy', context: 'autodeploy') do
|
||||
Set up auto deploy
|
||||
|
||||
- if @repository.commit
|
||||
.project-last-commit
|
||||
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
|
||||
|
||||
%div{ class: container_class }
|
||||
- if @project.archived?
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Set dropdown height fixed to 250px and make it scrollable
|
||||
merge_request: 9063
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Unify issues search behavior by always filtering when ALL labels matches
|
||||
merge_request: 8849
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Left align navigation
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Pick up option from GDK to disable webpack dev server livereload
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Allow searching issues for strings containing colons
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Adds remote logout functionality to the Authentiq OAuth provider
|
||||
merge_request: 9381
|
||||
author: Alexandros Keramidas
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Added documentation for permalinks to most recent build artifacts.
|
||||
merge_request: 8934
|
||||
author: Christian Godenschwager
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: GitHub Importer - Find users based on GitHub email address
|
||||
merge_request: 8958
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Update GitLab Pages to v0.3.1
|
||||
merge_request:
|
||||
author:
|
|
@ -240,6 +240,17 @@ Devise.setup do |config|
|
|||
true
|
||||
end
|
||||
end
|
||||
if provider['name'] == 'authentiq'
|
||||
provider['args'][:remote_sign_out_handler] = lambda do |request|
|
||||
authentiq_session = request.params['sid']
|
||||
if Gitlab::OAuth::Session.valid?(:authentiq, authentiq_session)
|
||||
Gitlab::OAuth::Session.destroy(:authentiq, authentiq_session)
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if provider['name'] == 'shibboleth'
|
||||
provider['args'][:fail_with_empty_uid] = true
|
||||
|
|
|
@ -10,6 +10,7 @@ var ROOT_PATH = path.resolve(__dirname, '..');
|
|||
var IS_PRODUCTION = process.env.NODE_ENV === 'production';
|
||||
var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1;
|
||||
var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
|
||||
var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
|
||||
|
||||
var config = {
|
||||
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
|
||||
|
@ -114,6 +115,7 @@ if (IS_DEV_SERVER) {
|
|||
port: DEV_SERVER_PORT,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
stats: 'errors-only',
|
||||
inline: DEV_SERVER_LIVERELOAD
|
||||
};
|
||||
config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath;
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ Authentiq will generate a Client ID and the accompanying Client Secret for you t
|
|||
5. The `scope` is set to request the user's name, email (required and signed), and permission to send push notifications to sign in on subsequent visits.
|
||||
See [OmniAuth Authentiq strategy](https://github.com/AuthentiqID/omniauth-authentiq#scopes-and-redirect-uri-configuration) for more information on scopes and modifiers.
|
||||
|
||||
6. Change 'YOUR_CLIENT_ID' and 'YOUR_CLIENT_SECRET' to the Client credentials you received in step 1.
|
||||
6. Change `YOUR_CLIENT_ID` and `YOUR_CLIENT_SECRET` to the Client credentials you received in step 1.
|
||||
|
||||
7. Save the configuration file.
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ GET /issues?milestone=1.0.0&state=opened
|
|||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `state` | string | no | Return all issues or just those that are `opened` or `closed`|
|
||||
| `labels` | string | no | Comma-separated list of label names, issues with any of the labels will be returned |
|
||||
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned |
|
||||
| `milestone` | string| no | The milestone title |
|
||||
| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
|
||||
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
|
||||
|
@ -188,7 +188,7 @@ GET /projects/:id/issues?milestone=1.0.0&state=opened
|
|||
| `id` | integer | yes | The ID of a project |
|
||||
| `iid` | integer | no | Return the issue having the given `iid` |
|
||||
| `state` | string | no | Return all issues or just those that are `opened` or `closed`|
|
||||
| `labels` | string | no | Comma-separated list of label names, issues with any of the labels will be returned |
|
||||
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned |
|
||||
| `milestone` | string| no | The milestone title |
|
||||
| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
|
||||
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
|
||||
|
|
|
@ -28,3 +28,5 @@ changes are in V4:
|
|||
- Return pagination headers for all endpoints that return an array
|
||||
- Removed `DELETE projects/:id/deploy_keys/:key_id/disable`. Use `DELETE projects/:id/deploy_keys/:key_id` instead
|
||||
- Moved `PUT /users/:id/(block|unblock)` to `POST /users/:id/(block|unblock)`
|
||||
- Labels filter on `projects/:id/issues` and `/issues` now matches only issues containing all labels (i.e.: Logical AND, not OR)
|
||||
|
||||
|
|
|
@ -64,6 +64,10 @@ Libraries with the following licenses are unacceptable for use:
|
|||
- [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects.
|
||||
- [Open Software License (OSL)][OSL]: is a copyleft license. In addition, the FSF [recommend against its use][OSL-GNU].
|
||||
|
||||
## Requesting Approval for Licenses
|
||||
|
||||
Libraries that are not listed in the [Acceptable Licenses][Acceptable-Licenses] or [Unacceptable Licenses][Unacceptable-Licenses] list can be submitted to the legal team for review. Please create an issue in the [Organization Repository][Org-Repo] and cc `@gl-legal`. After a decision has been made, the original requestor is responsible for updating this document.
|
||||
|
||||
## Notes
|
||||
|
||||
Decisions regarding the GNU GPL licenses are based on information provided by [The GNU Project][GNU-GPL-FAQ], as well as [the Open Source Initiative][OSI-GPL], which both state that linking GPL libraries makes the program itself GPL.
|
||||
|
@ -96,3 +100,6 @@ Gems which are included only in the "development" or "test" groups by Bundler ar
|
|||
[OSI-GPL]: https://opensource.org/faq#linking-proprietary-code
|
||||
[OSL]: https://opensource.org/licenses/OSL-3.0
|
||||
[OSL-GNU]: https://www.gnu.org/licenses/license-list.en.html#OSL
|
||||
[Org-Repo]: https://gitlab.com/gitlab-com/organization
|
||||
[Acceptable-Licenses]: #acceptable-licenses
|
||||
[Unacceptable-Licenses]: #unacceptable-licenses
|
||||
|
|
|
@ -95,6 +95,25 @@ so we need to set some guidelines for their use going forward:
|
|||
|
||||
[lets-not]: https://robots.thoughtbot.com/lets-not
|
||||
|
||||
### Time-sensitive tests
|
||||
|
||||
[Timecop](https://github.com/travisjeffery/timecop) is available in our
|
||||
Ruby-based tests for verifying things that are time-sensitive. Any test that
|
||||
exercises or verifies something time-sensitive should make use of Timecop to
|
||||
prevent transient test failures.
|
||||
|
||||
Example:
|
||||
|
||||
```ruby
|
||||
it 'is overdue' do
|
||||
issue = build(:issue, due_date: Date.tomorrow)
|
||||
|
||||
Timecop.freeze(3.days.from_now) do
|
||||
expect(issue).to be_overdue
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Test speed
|
||||
|
||||
GitLab has a massive test suite that, without parallelization, can take more
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
|
@ -1,16 +1,164 @@
|
|||
# Users
|
||||
## UX Personas
|
||||
* [Nazim Ramesh](#nazim-ramesh)
|
||||
- Small to medium size organisations using GitLab CE
|
||||
* [James Mackey](#james-mackey)
|
||||
- Medium to large size organisations using CE or EE
|
||||
- Small organisations using EE
|
||||
* [Karolina Plaskaty](#karolina-plaskaty)
|
||||
- Using GitLab.com for personal/hobby projects
|
||||
- Would like to use GitLab at work
|
||||
- Working for a medium to large size organisation
|
||||
|
||||
> TODO: Create personas. Understand the similarities and differences across the below spectrums.
|
||||
<hr>
|
||||
|
||||
## Users by organization
|
||||
### Nazim Ramesh
|
||||
- Small to medium size organisations using GitLab CE
|
||||
|
||||
- Enterprise
|
||||
- Medium company
|
||||
- Small company
|
||||
- Open source communities
|
||||
<img src="img/steven-lyons.png" width="300px">
|
||||
|
||||
## Users by role
|
||||
#### Demographics
|
||||
|
||||
- Admin
|
||||
- Manager
|
||||
- Developer
|
||||
- **Age**<br>32 years old
|
||||
- **Location**<br>Germany
|
||||
- **Education**<br>Bachelor of Science in Computer Science
|
||||
- **Occupation**<br>Full-stack web developer
|
||||
- **Programming experience**<br>Over 10 years
|
||||
- **Frequently used programming languages**<br>JavaScript, SQL, PHP
|
||||
- **Hobbies / interests**<br>Functional programming, open source, gaming, web development and web security.
|
||||
|
||||
#### Motivations
|
||||
Steven works for a software development company which currently hires around 80 people. When Steven first joined the company, the engineering team were using Subversion (SVN) as their primary form of source control. However, Steven felt SVN was not flexible enough to work with many feature branches and noticed that developers with less experience of source control struggled with the central-repository nature of SVN. Armed with a wishlist of features, Steven began comparing source control tools. A search for “self-hosted Git server repository management” returned GitLab. In his own words, Steven explains why he wanted the engineering team to start using GitLab:
|
||||
|
||||
>
|
||||
“I wanted them to switch away from SVN. I needed a server application to manage repositories. The common tools that were around just didn’t meet the requirements. Most of them were too simple or plain...GitLab provided all the required features. Also costs had to be low, since we don’t have a big budget for those things...the Community Edition was perfect in this regard.”
|
||||
>
|
||||
|
||||
In his role as a full-stack web developer, Steven could recommend products that he would like the engineering team to use, but final approval lay with his line manager, Mike, VP of Engineering. Steven recalls that he was met with reluctance from his colleagues when he raised moving to Git and using GitLab.
|
||||
|
||||
>
|
||||
“The biggest challenge...why should we change anything at all from the status quo? We needed to switch from SVN to Git. They knew they needed to learn Git and a Git workflow...using Git was scary to my colleagues...they thought it was more complex than SVN to use.”
|
||||
>
|
||||
|
||||
Undeterred, Steven decided to migrate a couple of projects across to GitLab.
|
||||
|
||||
>
|
||||
“Old SVN users couldn’t see the benefits of Git at first. It took a month or two to convince them.”
|
||||
>
|
||||
|
||||
Slowly, by showing his colleagues how easy it was to use Git, the majority of the team’s projects were migrated to GitLab.
|
||||
|
||||
The engineering team have been using GitLab CE for around 2 years now. Steven credits himself as being entirely responsible for his company’s decision to move to GitLab.
|
||||
|
||||
#### Frustrations
|
||||
##### Adoption to GitLab has been slow
|
||||
Not only has the engineering team had to get to grips with Git, they’ve also had to adapt to using GitLab. Due to lack of training and existing skills in other tools, the full feature set of GitLab CE is not being utilised. Steven sold GitLab to his manager as an ‘all in one’ tool which would replace multiple tools used within the company, thus saving costs. Steven hasn’t had the time to integrate the legacy tools to GitLab and he’s struggling to convince his peers to change their habits.
|
||||
|
||||
##### Missing Features
|
||||
Steven’s company want GitLab to be able to do everything. There isn’t a large budget for software, so they’re selective about what tools are implemented. It needs to add real value to the company. In order for GitLab to be widely adopted and to meet the requirements of different roles within the company, it needs a host of features. When an individual within Steven’s company wants to know if GitLab has a specific feature or does a particular thing, Steven is the person to ask. He becomes the point of contact to investigate, build or sometimes just raise the feature request. Steven gets frustrated when GitLab isn’t able to do what he or his colleagues need it to do.
|
||||
|
||||
##### Regressions and bugs
|
||||
Steven often has to calm down his colleagues, when a release contains regressions or new bugs. As he puts it “every new version adds something awesome, but breaks something”. He feels that “old issues for "minor" annoyances get quickly buried in the mass of open issues and linger for a very long time. More generally, I have the feeling that GitLab focus on adding new functionalities, but overlook a bunch of annoying minor regressions or introduced bugs.” Due to limited resource and expertise within the team, not only is it difficult to remain up-to-date with the frequent release cycle, it’s also counterproductive to fix workflows every month.
|
||||
|
||||
##### Uses too much RAM and CPU
|
||||
>
|
||||
“Memory usages mean that if we host it from a cloud based host like AWS, we spend almost as much on the instance as what we would pay GitHub”
|
||||
>
|
||||
|
||||
##### UI/UX
|
||||
GitLab’s interface initially attracted Steven when he was comparing version control software. He thought it would help his less technical colleagues to adapt to using Git and perhaps, GitLab could be rolled out to other areas of the business, beyond engineering. However, using GitLab’s interface daily has left him frustrated at the lack of personalisation / control over his user experience. He’s also regularly lost in a maze of navigation. Whilst he acknowledges that GitLab listens to its users and that the interface is improving, he becomes annoyed when the changes are too progressive. “Too frequent UI changes. Most of them tend to turn out great after a few cycles of fixes, but the frequency is still far too high for me to feel comfortable to always stay on the current release.”
|
||||
|
||||
#### Goals
|
||||
* To convince his colleagues to fully adopt GitLab CE, thus improving workflow and collaboration.
|
||||
* To use a feature rich version control platform that covers all stages of the development lifecycle, in order to reduce dependencies on other tools.
|
||||
* To use an intuitive and stable product, so he can spend more time on his core job responsibilities and less time bug-fixing, guiding colleagues, etc.
|
||||
|
||||
<hr>
|
||||
|
||||
### James Mackey
|
||||
- Medium to large size organisations using CE or EE
|
||||
- Small organisations using EE
|
||||
|
||||
<img src="img/james-mackey.png" width="300px">
|
||||
|
||||
#### Demographics
|
||||
|
||||
- **Age**<br>36 years old
|
||||
- **Location**<br>US
|
||||
- **Education**<br>Masters degree in Computer Science
|
||||
- **Occupation**<br>Full-stack web developer
|
||||
- **Programming experience**<br>Over 10 years
|
||||
- **Frequently used programming languages**<br>JavaScript, SQL, Node.js, Java, PHP, Python
|
||||
- **Hobbies / interests**<br>DevOps, open source, web development, science, automation and electronics.
|
||||
|
||||
#### Motivations
|
||||
James works for a research company which currently hires around 800 staff. He began using GitLab.com back in 2013 for his own open source, hobby projects and loved “the simplicity of installation, administration and use”. After using GitLab for over a year, he began to wonder about using it at work. James explains:
|
||||
|
||||
>
|
||||
“We first installed the CE edition...on a staging server for a PoC and asked a beta team to use it, specifically for the Merge Request features. Soon other teams began asking us to be beta users too, because the team that was already using GitLab was really enjoying it.”
|
||||
>
|
||||
|
||||
James and his colleagues also reviewed competitor products including GitHub Enterprise, but they found it “less innovative and with considerable costs...GitLab had the features we wanted at a much lower cost per head than GitHub”.
|
||||
|
||||
The company James works for provides employees with a discretionary budget to spend how they want on software, so James and his team decided to upgrade to EE.
|
||||
|
||||
James feels partially responsible for his organisation’s decision to start using GitLab.
|
||||
|
||||
>
|
||||
“It's still up to the teams themselves [to decide] which tools to use. We just had a great experience moving our daily development to GitLab, so other teams have followed the path or are thinking about switching.”
|
||||
>
|
||||
|
||||
#### Frustrations
|
||||
##### Third Party Integration
|
||||
Some of GitLab EE’s features are too basic, in particular, issues boards which do not have the level of reporting that James and his team need. Subsequently, they still need to use GitLab EE in conjunction with other tools, such as JIRA. Whilst James feels it isn’t essential for GitLab to meet all his needs (his company are happy for him to use, and pay for, multiple tools), he sometimes isn’t sure what is/isn’t possible with plugins and what level of custom development he and his team will need to do.
|
||||
|
||||
##### UX/UI
|
||||
James and his team use CI quite heavily for several projects. Whilst they’ve welcomed improvements to the builds and pipelines interface, they still have some difficulty following build process on the different tabs under Pipelines. Some confusion has arisen from not knowing where to find different pieces of information or how to get to the next stages logs from the current stage’s log output screen. They feel more intuitive linking and flow may alleviate the problem. Generally, they feel GitLab’s navigation needs to reviewed and optimised.
|
||||
|
||||
##### Permissions
|
||||
>
|
||||
“There is no granular control over user or group permissions. The permissions for a project are too tightly coupled to the permissions for Gitlab CI/build pipelines.”
|
||||
>
|
||||
|
||||
#### Goals
|
||||
* To be able to integrate third party tools easily with GitLab EE and to create custom integrations and patches where needed.
|
||||
* To use GitLab EE primarily for code hosting, merge requests, continuous integration and issue management. Steven and his team want to be able to understand and use these particular features easily.
|
||||
* To able to share one instance of GitLab EE with multiple teams across the business. Advanced user management, the ability to separate permissions on different parts of the source code, etc are important to Steven.
|
||||
|
||||
<hr>
|
||||
|
||||
### Karolina Plaskaty
|
||||
- Using GitLab.com for personal/hobby projects
|
||||
- Would like to use GitLab at work
|
||||
- Working for a medium to large size organisation
|
||||
|
||||
<img src="img/harry-robison.png" width="300px">
|
||||
|
||||
#### Demographics
|
||||
|
||||
- **Age**<br>26 years old
|
||||
- **Location**<br>UK
|
||||
- **Education**<br>Self taught
|
||||
- **Occupation**<br>Junior web-developer
|
||||
- **Programming experience**<br>6 years
|
||||
- **Frequently used programming languages**<br>JavaScript and SQL
|
||||
- **Hobbies / interests**<br>Web development, mobile development, UX, open source, gaming and travel.
|
||||
|
||||
#### Motivations
|
||||
Harry has been using GitLab.com for around a year. He roughly spends 8 hours every week programming, of that, 2 hours is spent contributing to open source projects. Harry contributes to open source projects to gain programming experience and to give back to the community. He likes GitLab.com for its free private repositories and range of features which provide him with everything he needs for his personal projects. Harry is also a massive fan of GitLab’s values and the fact that it isn’t a “behemoth of a company”. He explains that “displaying every single thing (doc, culture, assumptions, development...) in the open gives me greater confidence to choose Gitlab personally and to recommend it at work.” He’s also an avid reader of GitLab’s blog.
|
||||
|
||||
Harry works for a software development company which currently hires around 500 people. Harry would love to use GitLab at work but the company has used GitHub Enterprise for a number of years. He describes management at his company as “old fashioned” and explains that it’s “less of a technical issue and more of a cultural issue” to convince upper management to move to GitLab. Harry is also relatively new to the company so he’s apprehensive about pushing too hard to change version control platforms.
|
||||
|
||||
#### Frustrations
|
||||
##### Unable to use GitLab at work
|
||||
Harry wants to use GitLab at work but isn’t sure how to approach the subject with management. In his current role, he doesn’t feel that he has the authority to request GitLab.
|
||||
|
||||
##### Performance
|
||||
GitLab.com is frequently slow and unavailable. Harry has also heard that GitLab is a “memory hog” which has deterred him from running GitLab on his own machine for just hobby / personal projects.
|
||||
|
||||
##### UX/UI
|
||||
Harry has an interest in UX and therefore has strong opinions about how GitLab should look and feel. He feels the interface is cluttered, “it has too many links/buttons” and the navigation “feels a bit weird sometimes. I get lost if I don’t pay attention.” As Harry also enjoys contributing to open-source projects, it’s important to him that GitLab is well designed for public repositories, he doesn’t feel that GitLab currently achieves this.
|
||||
|
||||
#### Goals
|
||||
* To develop his programming experience and to learn from other developers.
|
||||
* To contribute to both his own and other open source projects.
|
||||
* To use a fast and intuitive version control platform.
|
|
@ -90,18 +90,43 @@ inside GitLab that make that possible.
|
|||
It is possible to download the latest artifacts of a job via a well known URL
|
||||
so you can use it for scripting purposes.
|
||||
|
||||
The structure of the URL is the following:
|
||||
The structure of the URL to download the whole artifacts archive is the following:
|
||||
|
||||
```
|
||||
https://example.com/<namespace>/<project>/builds/artifacts/<ref>/download?job=<job_name>
|
||||
```
|
||||
|
||||
For example, to download the latest artifacts of the job named `rspec 6 20` of
|
||||
To download a single file from the artifacts use the following URL:
|
||||
|
||||
```
|
||||
https://example.com/<namespace>/<project>/builds/artifacts/<ref>/file/<path_to_file>?job=<job_name>
|
||||
```
|
||||
|
||||
For example, to download the latest artifacts of the job named `coverage` of
|
||||
the `master` branch of the `gitlab-ce` project that belongs to the `gitlab-org`
|
||||
namespace, the URL would be:
|
||||
|
||||
```
|
||||
https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/download?job=rspec+6+20
|
||||
https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/download?job=coverage
|
||||
```
|
||||
|
||||
To download the file `coverage/index.html` from the same
|
||||
artifacts use the following URL:
|
||||
|
||||
```
|
||||
https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/file/coverage/index.html?job=coverage
|
||||
```
|
||||
|
||||
There is also a URL to browse the latest job artifacts:
|
||||
|
||||
```
|
||||
https://example.com/<namespace>/<project>/builds/artifacts/<ref>/browse?job=<job_name>
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/browse?job=coverage
|
||||
```
|
||||
|
||||
The latest builds are also exposed in the UI in various places. Specifically,
|
||||
|
|
|
@ -5,6 +5,7 @@ Feature: Revert Merge Requests
|
|||
And I am signed in as a developer of the project
|
||||
And I am on the Merge Request detail page
|
||||
And I click on Accept Merge Request
|
||||
And I am on the Merge Request detail page
|
||||
|
||||
@javascript
|
||||
Scenario: I revert a merge request
|
||||
|
|
|
@ -10,17 +10,9 @@ module API
|
|||
|
||||
args.delete(:id)
|
||||
args[:milestone_title] = args.delete(:milestone)
|
||||
args[:label_name] = args.delete(:labels)
|
||||
|
||||
match_all_labels = args.delete(:match_all_labels)
|
||||
labels = args.delete(:labels)
|
||||
args[:label_name] = labels if match_all_labels
|
||||
|
||||
issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations
|
||||
|
||||
# TODO: Remove in 9.0 pass `label_name: args.delete(:labels)` to IssuesFinder
|
||||
if !match_all_labels && labels.present?
|
||||
issues = issues.includes(:labels).where('labels.title' => labels.split(','))
|
||||
end
|
||||
issues = IssuesFinder.new(current_user, args).execute
|
||||
|
||||
issues.reorder(args[:order_by] => args[:sort])
|
||||
end
|
||||
|
@ -77,7 +69,7 @@ module API
|
|||
get ":id/issues" do
|
||||
group = find_group!(params[:id])
|
||||
|
||||
issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true)
|
||||
issues = find_issues(group_id: group.id, state: params[:state] || 'opened')
|
||||
|
||||
present paginate(issues), with: Entities::Issue, current_user: current_user
|
||||
end
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
module Gitlab
|
||||
module GithubImport
|
||||
class BaseFormatter
|
||||
attr_reader :formatter, :project, :raw_data
|
||||
attr_reader :client, :formatter, :project, :raw_data
|
||||
|
||||
def initialize(project, raw_data)
|
||||
def initialize(project, raw_data, client = nil)
|
||||
@project = project
|
||||
@raw_data = raw_data
|
||||
@client = client
|
||||
@formatter = Gitlab::ImportFormatter.new
|
||||
end
|
||||
|
||||
|
@ -18,19 +19,6 @@ module Gitlab
|
|||
def url
|
||||
raw_data.url || ''
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def gitlab_user_id(github_id)
|
||||
User.joins(:identities).
|
||||
find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s).
|
||||
try(:id)
|
||||
end
|
||||
|
||||
def gitlab_author_id
|
||||
return @gitlab_author_id if defined?(@gitlab_author_id)
|
||||
@gitlab_author_id = gitlab_user_id(raw_data.user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,6 +10,7 @@ module Gitlab
|
|||
@access_token = access_token
|
||||
@host = host.to_s.sub(%r{/+\z}, '')
|
||||
@api_version = api_version
|
||||
@users = {}
|
||||
|
||||
if access_token
|
||||
::Octokit.auto_paginate = false
|
||||
|
@ -64,6 +65,13 @@ module Gitlab
|
|||
api.respond_to?(method) || super
|
||||
end
|
||||
|
||||
def user(login)
|
||||
return nil unless login.present?
|
||||
return @users[login] if @users.key?(login)
|
||||
|
||||
@users[login] = api.user(login)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def api_endpoint
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
module Gitlab
|
||||
module GithubImport
|
||||
class CommentFormatter < BaseFormatter
|
||||
attr_writer :author_id
|
||||
|
||||
def attributes
|
||||
{
|
||||
project: project,
|
||||
|
@ -17,11 +19,11 @@ module Gitlab
|
|||
private
|
||||
|
||||
def author
|
||||
raw_data.user.login
|
||||
@author ||= UserFormatter.new(client, raw_data.user)
|
||||
end
|
||||
|
||||
def author_id
|
||||
gitlab_author_id || project.creator_id
|
||||
author.gitlab_id || project.creator_id
|
||||
end
|
||||
|
||||
def body
|
||||
|
@ -52,10 +54,10 @@ module Gitlab
|
|||
end
|
||||
|
||||
def note
|
||||
if gitlab_author_id
|
||||
if author.gitlab_id
|
||||
body
|
||||
else
|
||||
formatter.author_line(author) + body
|
||||
formatter.author_line(author.login) + body
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ module Gitlab
|
|||
def import_issues
|
||||
fetch_resources(:issues, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues|
|
||||
issues.each do |raw|
|
||||
gh_issue = IssueFormatter.new(project, raw)
|
||||
gh_issue = IssueFormatter.new(project, raw, client)
|
||||
|
||||
begin
|
||||
issuable =
|
||||
|
@ -131,7 +131,8 @@ module Gitlab
|
|||
def import_pull_requests
|
||||
fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
|
||||
pull_requests.each do |raw|
|
||||
gh_pull_request = PullRequestFormatter.new(project, raw)
|
||||
gh_pull_request = PullRequestFormatter.new(project, raw, client)
|
||||
|
||||
next unless gh_pull_request.valid?
|
||||
|
||||
begin
|
||||
|
@ -209,14 +210,16 @@ module Gitlab
|
|||
ActiveRecord::Base.no_touching do
|
||||
comments.each do |raw|
|
||||
begin
|
||||
comment = CommentFormatter.new(project, raw)
|
||||
comment = CommentFormatter.new(project, raw, client)
|
||||
|
||||
# GH does not return info about comment's parent, so we guess it by checking its URL!
|
||||
*_, parent, iid = URI(raw.html_url).path.split('/')
|
||||
if parent == 'issues'
|
||||
issuable = Issue.find_by(project_id: project.id, iid: iid)
|
||||
else
|
||||
issuable = MergeRequest.find_by(target_project_id: project.id, iid: iid)
|
||||
end
|
||||
|
||||
issuable = if parent == 'issues'
|
||||
Issue.find_by(project_id: project.id, iid: iid)
|
||||
else
|
||||
MergeRequest.find_by(target_project_id: project.id, iid: iid)
|
||||
end
|
||||
|
||||
next unless issuable
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
module Gitlab
|
||||
module GithubImport
|
||||
class IssuableFormatter < BaseFormatter
|
||||
attr_writer :assignee_id, :author_id
|
||||
|
||||
def project_association
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
@ -23,18 +25,24 @@ module Gitlab
|
|||
raw_data.assignee.present?
|
||||
end
|
||||
|
||||
def assignee_id
|
||||
if assigned?
|
||||
gitlab_user_id(raw_data.assignee.id)
|
||||
end
|
||||
end
|
||||
|
||||
def author
|
||||
raw_data.user.login
|
||||
@author ||= UserFormatter.new(client, raw_data.user)
|
||||
end
|
||||
|
||||
def author_id
|
||||
gitlab_author_id || project.creator_id
|
||||
@author_id ||= author.gitlab_id || project.creator_id
|
||||
end
|
||||
|
||||
def assignee
|
||||
if assigned?
|
||||
@assignee ||= UserFormatter.new(client, raw_data.assignee)
|
||||
end
|
||||
end
|
||||
|
||||
def assignee_id
|
||||
return @assignee_id if defined?(@assignee_id)
|
||||
|
||||
@assignee_id = assignee.try(:gitlab_id)
|
||||
end
|
||||
|
||||
def body
|
||||
|
@ -42,10 +50,10 @@ module Gitlab
|
|||
end
|
||||
|
||||
def description
|
||||
if gitlab_author_id
|
||||
if author.gitlab_id
|
||||
body
|
||||
else
|
||||
formatter.author_line(author) + body
|
||||
formatter.author_line(author.login) + body
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
module Gitlab
|
||||
module GithubImport
|
||||
class UserFormatter
|
||||
attr_reader :client, :raw
|
||||
|
||||
delegate :id, :login, to: :raw, allow_nil: true
|
||||
|
||||
def initialize(client, raw)
|
||||
@client = client
|
||||
@raw = raw
|
||||
end
|
||||
|
||||
def gitlab_id
|
||||
return @gitlab_id if defined?(@gitlab_id)
|
||||
|
||||
@gitlab_id = find_by_external_uid || find_by_email
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def email
|
||||
@email ||= client.user(raw.login).try(:email)
|
||||
end
|
||||
|
||||
def find_by_email
|
||||
return nil unless email
|
||||
|
||||
User.find_by_any_email(email)
|
||||
.try(:id)
|
||||
end
|
||||
|
||||
def find_by_external_uid
|
||||
return nil unless id
|
||||
|
||||
identities = ::Identity.arel_table
|
||||
|
||||
User.select(:id)
|
||||
.joins(:identities).where(identities[:provider].eq(:github)
|
||||
.and(identities[:extern_uid].eq(id)))
|
||||
.first
|
||||
.try(:id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -86,32 +86,47 @@ describe Projects::BlobController do
|
|||
end
|
||||
|
||||
context 'when user has forked project' do
|
||||
let(:guest) { create(:user) }
|
||||
let!(:forked_project) { Projects::ForkService.new(project, guest).execute }
|
||||
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, source_branch: "fork-test-1", target_branch: "master") }
|
||||
let(:forked_project_link) { create(:forked_project_link, forked_from_project: project) }
|
||||
let!(:forked_project) { forked_project_link.forked_to_project }
|
||||
let(:guest) { forked_project.owner }
|
||||
|
||||
before { sign_in(guest) }
|
||||
before do
|
||||
sign_in(guest)
|
||||
end
|
||||
|
||||
it "redirects to forked project new merge request" do
|
||||
default_params[:target_branch] = "fork-test-1"
|
||||
default_params[:create_merge_request] = 1
|
||||
context 'when editing on the fork' do
|
||||
before do
|
||||
default_params[:namespace_id] = forked_project.namespace.to_param
|
||||
default_params[:project_id] = forked_project.to_param
|
||||
end
|
||||
|
||||
allow_any_instance_of(Files::UpdateService).to receive(:commit).and_return(:success)
|
||||
it 'redirects to blob' do
|
||||
put :update, default_params
|
||||
|
||||
put :update, default_params
|
||||
expect(response).to redirect_to(namespace_project_blob_path(forked_project.namespace, forked_project, 'master/CHANGELOG'))
|
||||
end
|
||||
end
|
||||
|
||||
expect(response).to redirect_to(
|
||||
new_namespace_project_merge_request_path(
|
||||
forked_project.namespace,
|
||||
forked_project,
|
||||
merge_request: {
|
||||
source_project_id: forked_project.id,
|
||||
target_project_id: project.id,
|
||||
source_branch: "fork-test-1",
|
||||
target_branch: "master"
|
||||
}
|
||||
context 'when editing on the original repository' do
|
||||
it "redirects to forked project new merge request" do
|
||||
default_params[:target_branch] = "fork-test-1"
|
||||
default_params[:create_merge_request] = 1
|
||||
|
||||
put :update, default_params
|
||||
|
||||
expect(response).to redirect_to(
|
||||
new_namespace_project_merge_request_path(
|
||||
forked_project.namespace,
|
||||
forked_project,
|
||||
merge_request: {
|
||||
source_project_id: forked_project.id,
|
||||
target_project_id: project.id,
|
||||
source_branch: "fork-test-1",
|
||||
target_branch: "master"
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -99,6 +99,29 @@ require('~/filtered_search/filtered_search_tokenizer');
|
|||
expect(results.tokens[2].value).toBe('Doing');
|
||||
expect(results.tokens[2].symbol).toBe('~');
|
||||
});
|
||||
|
||||
it('returns search value for invalid tokens', () => {
|
||||
const results = gl.FilteredSearchTokenizer.processTokens('fake:token');
|
||||
expect(results.lastToken).toBe('fake:token');
|
||||
expect(results.searchToken).toBe('fake:token');
|
||||
expect(results.tokens.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('returns search value and token for mix of valid and invalid tokens', () => {
|
||||
const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token');
|
||||
expect(results.tokens.length).toEqual(1);
|
||||
expect(results.tokens[0].key).toBe('label');
|
||||
expect(results.tokens[0].value).toBe('real');
|
||||
expect(results.tokens[0].symbol).toBe('');
|
||||
expect(results.lastToken).toBe('fake:token');
|
||||
expect(results.searchToken).toBe('fake:token');
|
||||
});
|
||||
|
||||
it('returns search value for invalid symbols', () => {
|
||||
const results = gl.FilteredSearchTokenizer.processTokens('std::includes');
|
||||
expect(results.lastToken).toBe('std::includes');
|
||||
expect(results.searchToken).toBe('std::includes');
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::GithubImport::CommentFormatter, lib: true do
|
||||
let(:client) { double }
|
||||
let(:project) { create(:empty_project) }
|
||||
let(:octocat) { double(id: 123456, login: 'octocat') }
|
||||
let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
|
||||
let(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') }
|
||||
let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') }
|
||||
let(:base) do
|
||||
|
@ -16,7 +17,11 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do
|
|||
}
|
||||
end
|
||||
|
||||
subject(:comment) { described_class.new(project, raw)}
|
||||
subject(:comment) { described_class.new(project, raw, client) }
|
||||
|
||||
before do
|
||||
allow(client).to receive(:user).and_return(octocat)
|
||||
end
|
||||
|
||||
describe '#attributes' do
|
||||
context 'when do not reference a portion of the diff' do
|
||||
|
@ -69,8 +74,15 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do
|
|||
context 'when author is a GitLab user' do
|
||||
let(:raw) { double(base.merge(user: octocat)) }
|
||||
|
||||
it 'returns GitLab user id as author_id' do
|
||||
it 'returns GitLab user id associated with GitHub id as author_id' do
|
||||
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
|
||||
|
||||
expect(comment.attributes.fetch(:author_id)).to eq gl_user.id
|
||||
end
|
||||
|
||||
it 'returns GitLab user id associated with GitHub email as author_id' do
|
||||
gl_user = create(:user, email: octocat.email)
|
||||
|
||||
expect(comment.attributes.fetch(:author_id)).to eq gl_user.id
|
||||
end
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
|
|||
allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound)
|
||||
allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error)
|
||||
|
||||
allow_any_instance_of(Octokit::Client).to receive(:user).and_return(octocat)
|
||||
allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label1, label2])
|
||||
allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone])
|
||||
allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2])
|
||||
|
@ -53,7 +54,8 @@ describe Gitlab::GithubImport::Importer, lib: true do
|
|||
allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil }))
|
||||
allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2])
|
||||
end
|
||||
let(:octocat) { double(id: 123456, login: 'octocat') }
|
||||
|
||||
let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
|
||||
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
|
||||
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
|
||||
let(:label1) do
|
||||
|
@ -125,6 +127,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
|
|||
)
|
||||
end
|
||||
|
||||
let!(:user) { create(:user, email: octocat.email) }
|
||||
let(:repository) { double(id: 1, fork: false) }
|
||||
let(:source_sha) { create(:commit, project: project).id }
|
||||
let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) }
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::GithubImport::IssueFormatter, lib: true do
|
||||
let(:client) { double }
|
||||
let!(:project) { create(:empty_project, namespace: create(:namespace, path: 'octocat')) }
|
||||
let(:octocat) { double(id: 123456, login: 'octocat') }
|
||||
let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
|
||||
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
|
||||
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
|
||||
|
||||
|
@ -23,7 +24,11 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
|
|||
}
|
||||
end
|
||||
|
||||
subject(:issue) { described_class.new(project, raw_data) }
|
||||
subject(:issue) { described_class.new(project, raw_data, client) }
|
||||
|
||||
before do
|
||||
allow(client).to receive(:user).and_return(octocat)
|
||||
end
|
||||
|
||||
shared_examples 'Gitlab::GithubImport::IssueFormatter#attributes' do
|
||||
context 'when issue is open' do
|
||||
|
@ -75,11 +80,17 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
|
|||
expect(issue.attributes.fetch(:assignee_id)).to be_nil
|
||||
end
|
||||
|
||||
it 'returns GitLab user id as assignee_id when is a GitLab user' do
|
||||
it 'returns GitLab user id associated with GitHub id as assignee_id' do
|
||||
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
|
||||
|
||||
expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id
|
||||
end
|
||||
|
||||
it 'returns GitLab user id associated with GitHub email as assignee_id' do
|
||||
gl_user = create(:user, email: octocat.email)
|
||||
|
||||
expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it has a milestone' do
|
||||
|
@ -100,16 +111,22 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
|
|||
context 'when author is a GitLab user' do
|
||||
let(:raw_data) { double(base_data.merge(user: octocat)) }
|
||||
|
||||
it 'returns project#creator_id as author_id when is not a GitLab user' do
|
||||
it 'returns project creator_id as author_id when is not a GitLab user' do
|
||||
expect(issue.attributes.fetch(:author_id)).to eq project.creator_id
|
||||
end
|
||||
|
||||
it 'returns GitLab user id as author_id when is a GitLab user' do
|
||||
it 'returns GitLab user id associated with GitHub id as author_id' do
|
||||
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
|
||||
|
||||
expect(issue.attributes.fetch(:author_id)).to eq gl_user.id
|
||||
end
|
||||
|
||||
it 'returns GitLab user id associated with GitHub email as author_id' do
|
||||
gl_user = create(:user, email: octocat.email)
|
||||
|
||||
expect(issue.attributes.fetch(:author_id)).to eq gl_user.id
|
||||
end
|
||||
|
||||
it 'returns description without created at tag line' do
|
||||
create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
|
||||
let(:client) { double }
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:source_sha) { create(:commit, project: project).id }
|
||||
let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
|
||||
|
@ -10,7 +11,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
|
|||
let(:target_repo) { repository }
|
||||
let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) }
|
||||
let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') }
|
||||
let(:octocat) { double(id: 123456, login: 'octocat') }
|
||||
let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
|
||||
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
|
||||
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
|
||||
let(:base_data) do
|
||||
|
@ -32,7 +33,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
|
|||
}
|
||||
end
|
||||
|
||||
subject(:pull_request) { described_class.new(project, raw_data) }
|
||||
subject(:pull_request) { described_class.new(project, raw_data, client) }
|
||||
|
||||
before do
|
||||
allow(client).to receive(:user).and_return(octocat)
|
||||
end
|
||||
|
||||
shared_examples 'Gitlab::GithubImport::PullRequestFormatter#attributes' do
|
||||
context 'when pull request is open' do
|
||||
|
@ -121,26 +126,38 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
|
|||
expect(pull_request.attributes.fetch(:assignee_id)).to be_nil
|
||||
end
|
||||
|
||||
it 'returns GitLab user id as assignee_id when is a GitLab user' do
|
||||
it 'returns GitLab user id associated with GitHub id as assignee_id' do
|
||||
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
|
||||
|
||||
expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id
|
||||
end
|
||||
|
||||
it 'returns GitLab user id associated with GitHub email as assignee_id' do
|
||||
gl_user = create(:user, email: octocat.email)
|
||||
|
||||
expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id
|
||||
end
|
||||
end
|
||||
|
||||
context 'when author is a GitLab user' do
|
||||
let(:raw_data) { double(base_data.merge(user: octocat)) }
|
||||
|
||||
it 'returns project#creator_id as author_id when is not a GitLab user' do
|
||||
it 'returns project creator_id as author_id when is not a GitLab user' do
|
||||
expect(pull_request.attributes.fetch(:author_id)).to eq project.creator_id
|
||||
end
|
||||
|
||||
it 'returns GitLab user id as author_id when is a GitLab user' do
|
||||
it 'returns GitLab user id associated with GitHub id as author_id' do
|
||||
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
|
||||
|
||||
expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id
|
||||
end
|
||||
|
||||
it 'returns GitLab user id associated with GitHub email as author_id' do
|
||||
gl_user = create(:user, email: octocat.email)
|
||||
|
||||
expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id
|
||||
end
|
||||
|
||||
it 'returns description without created at tag line' do
|
||||
create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
|
||||
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::GithubImport::UserFormatter, lib: true do
|
||||
let(:client) { double }
|
||||
let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
|
||||
|
||||
subject(:user) { described_class.new(client, octocat) }
|
||||
|
||||
before do
|
||||
allow(client).to receive(:user).and_return(octocat)
|
||||
end
|
||||
|
||||
describe '#gitlab_id' do
|
||||
context 'when GitHub user is a GitLab user' do
|
||||
it 'return GitLab user id when user associated their account with GitHub' do
|
||||
gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
|
||||
|
||||
expect(user.gitlab_id).to eq gl_user.id
|
||||
end
|
||||
|
||||
it 'returns GitLab user id when user primary email matches GitHub email' do
|
||||
gl_user = create(:user, email: octocat.email)
|
||||
|
||||
expect(user.gitlab_id).to eq gl_user.id
|
||||
end
|
||||
|
||||
it 'returns GitLab user id when any of user linked emails matches GitHub email' do
|
||||
gl_user = create(:user, email: 'johndoe@example.com')
|
||||
create(:email, user: gl_user, email: octocat.email)
|
||||
|
||||
expect(user.gitlab_id).to eq gl_user.id
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns nil when GitHub user is not a GitLab user' do
|
||||
expect(user.gitlab_id).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -117,14 +117,20 @@ describe API::Issues, api: true do
|
|||
expect(json_response.first['labels']).to eq([label.title])
|
||||
end
|
||||
|
||||
it 'returns an array of labeled issues when at least one label matches' do
|
||||
get api("/issues?labels=#{label.title},foo,bar", user)
|
||||
it 'returns an array of labeled issues when all labels matches' do
|
||||
label_b = create(:label, title: 'foo', project: project)
|
||||
label_c = create(:label, title: 'bar', project: project)
|
||||
|
||||
create(:label_link, label: label_b, target: issue)
|
||||
create(:label_link, label: label_c, target: issue)
|
||||
|
||||
get api("/issues", user), labels: "#{label.title},#{label_b.title},#{label_c.title}"
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response.length).to eq(1)
|
||||
expect(json_response.first['labels']).to eq([label.title])
|
||||
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
|
||||
end
|
||||
|
||||
it 'returns an empty array if no issue matches labels' do
|
||||
|
@ -356,6 +362,21 @@ describe API::Issues, api: true do
|
|||
expect(json_response.length).to eq(0)
|
||||
end
|
||||
|
||||
it 'returns an array of labeled issues when all labels matches' do
|
||||
label_b = create(:label, title: 'foo', project: group_project)
|
||||
label_c = create(:label, title: 'bar', project: group_project)
|
||||
|
||||
create(:label_link, label: label_b, target: group_issue)
|
||||
create(:label_link, label: label_c, target: group_issue)
|
||||
|
||||
get api("#{base_url}", user), labels: "#{group_label.title},#{label_b.title},#{label_c.title}"
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response.length).to eq(1)
|
||||
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title])
|
||||
end
|
||||
|
||||
it 'returns an empty array if no group issue matches labels' do
|
||||
get api("#{base_url}?labels=foo,bar", user)
|
||||
|
||||
|
@ -549,14 +570,28 @@ describe API::Issues, api: true do
|
|||
expect(json_response.first['labels']).to eq([label.title])
|
||||
end
|
||||
|
||||
it 'returns an array of labeled project issues where all labels match' do
|
||||
get api("#{base_url}/issues?labels=#{label.title},foo,bar", user)
|
||||
it 'returns an array of labeled issues when all labels matches' do
|
||||
label_b = create(:label, title: 'foo', project: project)
|
||||
label_c = create(:label, title: 'bar', project: project)
|
||||
|
||||
create(:label_link, label: label_b, target: issue)
|
||||
create(:label_link, label: label_c, target: issue)
|
||||
|
||||
get api("#{base_url}/issues", user), labels: "#{label.title},#{label_b.title},#{label_c.title}"
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response).to include_pagination_headers
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response.length).to eq(1)
|
||||
expect(json_response.first['labels']).to eq([label.title])
|
||||
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
|
||||
end
|
||||
|
||||
it 'returns an empty array if not all labels matches' do
|
||||
get api("#{base_url}/issues?labels=#{label.title},foo", user)
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(json_response).to be_an Array
|
||||
expect(json_response.length).to eq(0)
|
||||
end
|
||||
|
||||
it 'returns an empty array if no project issue matches labels' do
|
||||
|
|
Loading…
Reference in New Issue