Commit cf7386f3 authored by Marcia Ramos's avatar Marcia Ramos

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into pages-guides

parents 69b2bc4d 7af8bb26
...@@ -240,7 +240,7 @@ rake db:seed_fu: ...@@ -240,7 +240,7 @@ rake db:seed_fu:
paths: paths:
- log/development.log - log/development.log
karma: rake karma:
cache: cache:
paths: paths:
- vendor/ruby - vendor/ruby
...@@ -387,7 +387,7 @@ pages: ...@@ -387,7 +387,7 @@ pages:
<<: *dedicated-runner <<: *dedicated-runner
dependencies: dependencies:
- coverage - coverage
- karma - rake karma
- lint:javascript:report - lint:javascript:report
script: script:
- mv public/ .public/ - mv public/ .public/
......
source 'https://rubygems.org' source 'https://rubygems.org'
gem 'rails', '4.2.7.1' gem 'rails', '4.2.8'
gem 'rails-deprecated_sanitizer', '~> 1.0.3' gem 'rails-deprecated_sanitizer', '~> 1.0.3'
# Responders respond_to and respond_with # Responders respond_to and respond_with
...@@ -34,7 +34,7 @@ gem 'omniauth-saml', '~> 1.7.0' ...@@ -34,7 +34,7 @@ gem 'omniauth-saml', '~> 1.7.0'
gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.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 'rack-oauth2', '~> 1.2.1'
gem 'jwt', '~> 1.5.6' gem 'jwt', '~> 1.5.6'
......
...@@ -3,40 +3,39 @@ GEM ...@@ -3,40 +3,39 @@ GEM
specs: specs:
RedCloth (4.3.2) RedCloth (4.3.2)
ace-rails-ap (4.1.0) ace-rails-ap (4.1.0)
actionmailer (4.2.7.1) actionmailer (4.2.8)
actionpack (= 4.2.7.1) actionpack (= 4.2.8)
actionview (= 4.2.7.1) actionview (= 4.2.8)
activejob (= 4.2.7.1) activejob (= 4.2.8)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
actionpack (4.2.7.1) actionpack (4.2.8)
actionview (= 4.2.7.1) actionview (= 4.2.8)
activesupport (= 4.2.7.1) activesupport (= 4.2.8)
rack (~> 1.6) rack (~> 1.6)
rack-test (~> 0.6.2) rack-test (~> 0.6.2)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (4.2.7.1) actionview (4.2.8)
activesupport (= 4.2.7.1) activesupport (= 4.2.8)
builder (~> 3.1) builder (~> 3.1)
erubis (~> 2.7.0) erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.3)
activejob (4.2.7.1) activejob (4.2.8)
activesupport (= 4.2.7.1) activesupport (= 4.2.8)
globalid (>= 0.3.0) globalid (>= 0.3.0)
activemodel (4.2.7.1) activemodel (4.2.8)
activesupport (= 4.2.7.1) activesupport (= 4.2.8)
builder (~> 3.1) builder (~> 3.1)
activerecord (4.2.7.1) activerecord (4.2.8)
activemodel (= 4.2.7.1) activemodel (= 4.2.8)
activesupport (= 4.2.7.1) activesupport (= 4.2.8)
arel (~> 6.0) arel (~> 6.0)
activerecord_sane_schema_dumper (0.2) activerecord_sane_schema_dumper (0.2)
rails (>= 4, < 5) rails (>= 4, < 5)
activesupport (4.2.7.1) activesupport (4.2.8)
i18n (~> 0.7) i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1) minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4) thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1) tzinfo (~> 1.1)
...@@ -47,7 +46,7 @@ GEM ...@@ -47,7 +46,7 @@ GEM
activerecord (>= 3.0) activerecord (>= 3.0)
akismet (2.0.0) akismet (2.0.0)
allocations (1.0.5) allocations (1.0.5)
arel (6.0.3) arel (6.0.4)
asana (0.4.0) asana (0.4.0)
faraday (~> 0.9) faraday (~> 0.9)
faraday_middleware (~> 0.9) faraday_middleware (~> 0.9)
...@@ -86,7 +85,7 @@ GEM ...@@ -86,7 +85,7 @@ GEM
sass (>= 3.3.4) sass (>= 3.3.4)
brakeman (3.4.1) brakeman (3.4.1)
browser (2.2.0) browser (2.2.0)
builder (3.2.2) builder (3.2.3)
bullet (5.2.0) bullet (5.2.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0) uniform_notifier (~> 1.10.0)
...@@ -127,7 +126,7 @@ GEM ...@@ -127,7 +126,7 @@ GEM
execjs execjs
coffee-script-source (1.10.0) coffee-script-source (1.10.0)
colorize (0.7.7) colorize (0.7.7)
concurrent-ruby (1.0.2) concurrent-ruby (1.0.4)
connection_pool (2.2.1) connection_pool (2.2.1)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
...@@ -354,7 +353,7 @@ GEM ...@@ -354,7 +353,7 @@ GEM
json (~> 1.8) json (~> 1.8)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.8.2) httpclient (2.8.2)
i18n (0.7.0) i18n (0.8.0)
ice_nine (0.11.1) ice_nine (0.11.1)
influxdb (0.2.3) influxdb (0.2.3)
cause cause
...@@ -370,7 +369,7 @@ GEM ...@@ -370,7 +369,7 @@ GEM
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
jquery-ui-rails (5.0.5) jquery-ui-rails (5.0.5)
railties (>= 3.2.16) railties (>= 3.2.16)
json (1.8.3) json (1.8.6)
json-schema (2.6.2) json-schema (2.6.2)
addressable (~> 2.3.8) addressable (~> 2.3.8)
jwt (1.5.6) jwt (1.5.6)
...@@ -429,9 +428,8 @@ GEM ...@@ -429,9 +428,8 @@ GEM
net-ssh (3.0.1) net-ssh (3.0.1)
netrc (0.11.0) netrc (0.11.0)
newrelic_rpm (3.16.0.318) newrelic_rpm (3.16.0.318)
nokogiri (1.6.8) nokogiri (1.6.8.1)
mini_portile2 (~> 2.1.0) mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
numerizer (0.1.1) numerizer (0.1.1)
oauth (0.5.1) oauth (0.5.1)
oauth2 (1.2.0) oauth2 (1.2.0)
...@@ -448,7 +446,7 @@ GEM ...@@ -448,7 +446,7 @@ GEM
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
omniauth-auth0 (1.4.1) omniauth-auth0 (1.4.1)
omniauth-oauth2 (~> 1.1) omniauth-oauth2 (~> 1.1)
omniauth-authentiq (0.2.2) omniauth-authentiq (0.3.0)
omniauth-oauth2 (~> 1.3, >= 1.3.1) omniauth-oauth2 (~> 1.3, >= 1.3.1)
omniauth-azure-oauth2 (0.0.6) omniauth-azure-oauth2 (0.0.6)
jwt (~> 1.0) jwt (~> 1.0)
...@@ -506,7 +504,6 @@ GEM ...@@ -506,7 +504,6 @@ GEM
parser (2.3.1.4) parser (2.3.1.4)
ast (~> 2.2) ast (~> 2.2)
pg (0.18.4) pg (0.18.4)
pkg-config (1.1.7)
poltergeist (1.9.0) poltergeist (1.9.0)
capybara (~> 2.1) capybara (~> 2.1)
cliver (~> 0.3.1) cliver (~> 0.3.1)
...@@ -548,28 +545,28 @@ GEM ...@@ -548,28 +545,28 @@ GEM
rack rack
rack-test (0.6.3) rack-test (0.6.3)
rack (>= 1.0) rack (>= 1.0)
rails (4.2.7.1) rails (4.2.8)
actionmailer (= 4.2.7.1) actionmailer (= 4.2.8)
actionpack (= 4.2.7.1) actionpack (= 4.2.8)
actionview (= 4.2.7.1) actionview (= 4.2.8)
activejob (= 4.2.7.1) activejob (= 4.2.8)
activemodel (= 4.2.7.1) activemodel (= 4.2.8)
activerecord (= 4.2.7.1) activerecord (= 4.2.8)
activesupport (= 4.2.7.1) activesupport (= 4.2.8)
bundler (>= 1.3.0, < 2.0) bundler (>= 1.3.0, < 2.0)
railties (= 4.2.7.1) railties (= 4.2.8)
sprockets-rails sprockets-rails
rails-deprecated_sanitizer (1.0.3) rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha) activesupport (>= 4.2.0.alpha)
rails-dom-testing (1.0.7) rails-dom-testing (1.0.8)
activesupport (>= 4.2.0.beta, < 5.0) activesupport (>= 4.2.0.beta, < 5.0)
nokogiri (~> 1.6.0) nokogiri (~> 1.6)
rails-deprecated_sanitizer (>= 1.0.1) rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3) rails-html-sanitizer (1.0.3)
loofah (~> 2.0) loofah (~> 2.0)
railties (4.2.7.1) railties (4.2.8)
actionpack (= 4.2.7.1) actionpack (= 4.2.8)
activesupport (= 4.2.7.1) activesupport (= 4.2.8)
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.1.0) rainbow (2.1.0)
...@@ -733,10 +730,10 @@ GEM ...@@ -733,10 +730,10 @@ GEM
spring (>= 0.9.1) spring (>= 0.9.1)
spring-commands-spinach (1.1.0) spring-commands-spinach (1.1.0)
spring (>= 0.9.1) spring (>= 0.9.1)
sprockets (3.7.0) sprockets (3.7.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
sprockets-rails (3.1.1) sprockets-rails (3.2.0)
actionpack (>= 4.0) actionpack (>= 4.0)
activesupport (>= 4.0) activesupport (>= 4.0)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
...@@ -760,7 +757,7 @@ GEM ...@@ -760,7 +757,7 @@ GEM
daemons (~> 1.0, >= 1.0.9) daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4) eventmachine (~> 1.0, >= 1.0.4)
rack (>= 1, < 3) rack (>= 1, < 3)
thor (0.19.1) thor (0.19.4)
thread_safe (0.3.5) thread_safe (0.3.5)
tilt (2.0.5) tilt (2.0.5)
timecop (0.8.1) timecop (0.8.1)
...@@ -925,7 +922,7 @@ DEPENDENCIES ...@@ -925,7 +922,7 @@ DEPENDENCIES
oj (~> 2.17.4) oj (~> 2.17.4)
omniauth (~> 1.3.2) omniauth (~> 1.3.2)
omniauth-auth0 (~> 1.4.1) omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.2.0) omniauth-authentiq (~> 0.3.0)
omniauth-azure-oauth2 (~> 0.0.6) omniauth-azure-oauth2 (~> 0.0.6)
omniauth-cas3 (~> 1.1.2) omniauth-cas3 (~> 1.1.2)
omniauth-facebook (~> 4.0.0) omniauth-facebook (~> 4.0.0)
...@@ -949,7 +946,7 @@ DEPENDENCIES ...@@ -949,7 +946,7 @@ DEPENDENCIES
rack-cors (~> 0.4.0) rack-cors (~> 0.4.0)
rack-oauth2 (~> 1.2.1) rack-oauth2 (~> 1.2.1)
rack-proxy (~> 0.6.0) rack-proxy (~> 0.6.0)
rails (= 4.2.7.1) rails (= 4.2.8)
rails-deprecated_sanitizer (~> 1.0.3) rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0) rainbow (~> 2.1.0)
rblineprof (~> 0.3.6) rblineprof (~> 0.3.6)
......
...@@ -29,7 +29,7 @@ We're hiring developers, support people, and production engineers all the time, ...@@ -29,7 +29,7 @@ We're hiring developers, support people, and production engineers all the time,
There are two editions of GitLab: There are two editions of GitLab:
- GitLab Community Edition (CE) is available freely under the MIT Expat license. - GitLab Community Edition (CE) is available freely under the MIT Expat license.
- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/features/#compare) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/). - GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/products/#compare-options) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/products/).
## Website ## Website
......
...@@ -173,7 +173,7 @@ ...@@ -173,7 +173,7 @@
tokens.forEach((token) => { tokens.forEach((token) => {
const condition = gl.FilteredSearchTokenKeys const condition = gl.FilteredSearchTokenKeys
.searchByConditionKeyValue(token.key, token.value.toLowerCase()); .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; const keyParam = param ? `${token.key}_${param}` : token.key;
let tokenPath = ''; let tokenPath = '';
......
require('./filtered_search_token_keys');
(() => { (() => {
class FilteredSearchTokenizer { class FilteredSearchTokenizer {
static processTokens(input) { static processTokens(input) {
const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key);
// Regex extracts `(token):(symbol)(value)` // Regex extracts `(token):(symbol)(value)`
// Values that start with a double quote must end in a double quote (same for single) // 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 = []; const tokens = [];
let lastToken = null; let lastToken = null;
const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
......
...@@ -47,9 +47,10 @@ ...@@ -47,9 +47,10 @@
} }
// Only filter asynchronously only if option remote is set // Only filter asynchronously only if option remote is set
if (this.options.remote) { if (this.options.remote) {
$inputContainer.parent().addClass('is-loading');
clearTimeout(timeout); clearTimeout(timeout);
return timeout = setTimeout(function() { return timeout = setTimeout(function() {
$inputContainer.parent().addClass('is-loading');
return this.options.query(this.input.val(), function(data) { return this.options.query(this.input.val(), function(data) {
$inputContainer.parent().removeClass('is-loading'); $inputContainer.parent().removeClass('is-loading');
return this.options.callback(data); return this.options.callback(data);
......
...@@ -148,16 +148,11 @@ header { ...@@ -148,16 +148,11 @@ header {
} }
.header-logo { .header-logo {
position: absolute; display: inline-block;
left: 50%; margin: 0 8px 0 3px;
position: relative;
top: 7px; top: 7px;
transition-duration: .3s; transition-duration: .3s;
z-index: 999;
#logo {
position: relative;
left: -50%;
}
svg, svg,
img { img {
...@@ -167,15 +162,6 @@ header { ...@@ -167,15 +162,6 @@ header {
&:hover { &:hover {
cursor: pointer; cursor: pointer;
} }
@media (max-width: $screen-xs-max) {
right: 20px;
left: auto;
#logo {
left: auto;
}
}
} }
.title { .title {
...@@ -183,7 +169,7 @@ header { ...@@ -183,7 +169,7 @@ header {
padding-right: 20px; padding-right: 20px;
margin: 0; margin: 0;
font-size: 18px; font-size: 18px;
max-width: 385px; max-width: 450px;
display: inline-block; display: inline-block;
line-height: $header-height; line-height: $header-height;
font-weight: normal; font-weight: normal;
...@@ -193,10 +179,6 @@ header { ...@@ -193,10 +179,6 @@ header {
vertical-align: top; vertical-align: top;
white-space: nowrap; white-space: nowrap;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
max-width: 300px;
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
max-width: 190px; max-width: 190px;
} }
......
@mixin fade($gradient-direction, $gradient-color) { @mixin fade($gradient-direction, $gradient-color) {
visibility: hidden; visibility: hidden;
opacity: 0; opacity: 0;
z-index: 2; z-index: 1;
position: absolute; position: absolute;
bottom: 12px; bottom: 12px;
width: 43px; width: 43px;
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
.fa { .fa {
position: relative; position: relative;
top: 5px; top: 6px;
font-size: 18px; font-size: 18px;
} }
} }
...@@ -79,7 +79,6 @@ ...@@ -79,7 +79,6 @@
} }
&.sub-nav { &.sub-nav {
text-align: center;
background-color: $gray-normal; background-color: $gray-normal;
.container-fluid { .container-fluid {
...@@ -287,7 +286,6 @@ ...@@ -287,7 +286,6 @@
background: $gray-light; background: $gray-light;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
transition: padding $sidebar-transition-duration; transition: padding $sidebar-transition-duration;
text-align: center;
.container-fluid { .container-fluid {
position: relative; position: relative;
...@@ -353,7 +351,7 @@ ...@@ -353,7 +351,7 @@
right: -5px; right: -5px;
.fa { .fa {
right: -7px; right: -28px;
} }
} }
...@@ -383,7 +381,7 @@ ...@@ -383,7 +381,7 @@
left: 0; left: 0;
.fa { .fa {
left: 10px; left: -4px;
} }
} }
} }
......
...@@ -222,6 +222,11 @@ ...@@ -222,6 +222,11 @@
} }
} }
.dropdown-menu {
max-height: 250px;
overflow-y: auto;
}
.dropdown-toggle, .dropdown-toggle,
.dropdown-menu { .dropdown-menu {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
......
...@@ -268,6 +268,13 @@ ...@@ -268,6 +268,13 @@
} }
} }
.project-repo-buttons {
.project-action-button .dropdown-menu {
max-height: 250px;
overflow-y: auto;
}
}
.split-one { .split-one {
display: inline-table; display: inline-table;
margin-right: 12px; margin-right: 12px;
...@@ -645,29 +652,23 @@ pre.light-well { ...@@ -645,29 +652,23 @@ pre.light-well {
} }
} }
.project-last-commit { .container-fluid.project-stats-container {
@media (min-width: $screen-sm-min) { @media (max-width: $screen-xs-max) {
margin-top: $gl-padding; padding: 12px 0;
} }
}
&.container-fluid { .project-last-commit {
padding-top: 12px; background-color: $gray-light;
padding-bottom: 12px; padding: 12px $gl-padding;
background-color: $gray-light; border: 1px solid $border-color;
border: 1px solid $border-color;
border-right-width: 0;
border-left-width: 0;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
border-right-width: 1px; margin-top: $gl-padding;
border-left-width: 1px;
}
} }
&.container-limited { @media (min-width: $screen-sm-min) {
@media (min-width: 1281px) { border-radius: $border-radius-base;
border-radius: $border-radius-base;
}
} }
.ci-status { .ci-status {
......
...@@ -43,6 +43,12 @@ ...@@ -43,6 +43,12 @@
} }
} }
.todo-avatar,
.todo-actions {
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
}
.todo-actions { .todo-actions {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
...@@ -55,8 +61,9 @@ ...@@ -55,8 +61,9 @@
} }
.todo-item { .todo-item {
-webkit-flex: auto; -webkit-flex: 0 1 100%;
flex: auto; flex: 0 1 100%;
min-width: 0;
} }
} }
...@@ -74,8 +81,29 @@ ...@@ -74,8 +81,29 @@
.todo-item { .todo-item {
.todo-title { .todo-title {
@include str-truncated(calc(100% - 174px)); display: flex;
overflow: visible;
& > .title-item {
-webkit-flex: 0 0 auto;
flex: 0 0 auto;
margin: 0 2px;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
.todo-label {
-webkit-flex: 0 1 auto;
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
} }
.status-box { .status-box {
...@@ -154,10 +182,12 @@ ...@@ -154,10 +182,12 @@
.todo-item { .todo-item {
.todo-title { .todo-title {
white-space: normal; flex-flow: row wrap;
overflow: visible;
max-width: 100%;
margin-bottom: 10px; margin-bottom: 10px;
.todo-label {
white-space: normal;
}
} }
.todo-body { .todo-body {
......
...@@ -104,23 +104,15 @@ module CreatesCommit ...@@ -104,23 +104,15 @@ module CreatesCommit
if can?(current_user, :push_code, @project) if can?(current_user, :push_code, @project)
# Edit file in this project # Edit file in this project
@mr_source_project = @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 else
# Merge request from fork to this project # Merge request from fork to this project
@mr_source_project = current_user.fork_of(@project) @mr_source_project = current_user.fork_of(@project)
@mr_target_project = @project
@mr_target_branch = @ref || @target_branch
end end
# Merge request to this project
@mr_target_project = @project
@mr_target_branch = @ref || @target_branch
@mr_source_branch = guess_mr_source_branch @mr_source_branch = guess_mr_source_branch
end end
......
...@@ -78,6 +78,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -78,6 +78,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
handle_omniauth handle_omniauth
end end
def authentiq
if params['sid']
handle_service_ticket oauth['provider'], params['sid']
end
handle_omniauth
end
private private
def handle_omniauth def handle_omniauth
......
module NamespacesHelper module NamespacesHelper
def namespace_id_from(params)
params.dig(:project, :namespace_id) || params[:namespace_id]
end
def namespaces_options(selected = :current_user, display_path: false, extra_group: nil) def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
groups = current_user.owned_groups + current_user.masters_groups groups = current_user.owned_groups + current_user.masters_groups
......
...@@ -22,6 +22,10 @@ module Spammable ...@@ -22,6 +22,10 @@ module Spammable
delegate :ip_address, :user_agent, to: :user_agent_detail, allow_nil: true delegate :ip_address, :user_agent, to: :user_agent_detail, allow_nil: true
end end
def submittable_as_spam_by?(current_user)
current_user && current_user.admin? && submittable_as_spam?
end
def submittable_as_spam? def submittable_as_spam?
if user_agent_detail if user_agent_detail
user_agent_detail.submittable? && current_application_settings.akismet_enabled user_agent_detail.submittable? && current_application_settings.akismet_enabled
......
...@@ -7,6 +7,10 @@ module Users ...@@ -7,6 +7,10 @@ module Users
end end
def execute(user, options = {}) def execute(user, options = {})
unless current_user.admin? || current_user == user
raise Gitlab::Access::AccessDeniedError, "#{current_user} tried to destroy user #{user}!"
end
if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present? if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present?
user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user' user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user'
return user return user
......
%li{ class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data: { url: todo_target_path(todo) } } %li{ class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data: { url: todo_target_path(todo) } }
= author_avatar(todo, size: 40) .todo-avatar
= author_avatar(todo, size: 40)
.todo-item.todo-block .todo-item.todo-block
.todo-title.title .todo-title.title
- unless todo.build_failed? || todo.unmergeable? - unless todo.build_failed? || todo.unmergeable?
= todo_target_state_pill(todo) = todo_target_state_pill(todo)
%span.author-name .title-item.author-name
- if todo.author - if todo.author
= link_to_author(todo) = link_to_author(todo)
- else - else
(removed) (removed)
%span.action-name .title-item.action-name
= todo_action_name(todo) = todo_action_name(todo)
%span.todo-label .title-item.todo-label
- if todo.target - if todo.target
= todo_target_link(todo) = todo_target_link(todo)
- else - else
(removed) (removed)
&middot; #{time_ago_with_tooltip(todo.created_at)} .title-item
= todo_due_date(todo) &middot;
.title-item
#{time_ago_with_tooltip(todo.created_at)}
= todo_due_date(todo)
.todo-body .todo-body
.todo-note .todo-note
......
.page-with-sidebar{ class: page_gutter_class } .page-with-sidebar{ class: page_gutter_class }
- if defined?(nav) && nav - if defined?(nav) && nav
.layout-nav .layout-nav
.container-fluid %div{ class: container_class }
= render "layouts/nav/#{nav}" = render "layouts/nav/#{nav}"
.content-wrapper{ class: "#{layout_nav_class}" } .content-wrapper{ class: "#{layout_nav_class}" }
= yield :sub_nav = yield :sub_nav
......
...@@ -61,12 +61,12 @@ ...@@ -61,12 +61,12 @@
%div %div
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
%h1.title= title
.header-logo .header-logo
= link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
= brand_header_logo = brand_header_logo
%h1.title= title
= yield :header_content = yield :header_content
= render 'shared/outdated_browser' = render 'shared/outdated_browser'
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.pull-right .pull-right
- if can?(current_user, :update_pipeline, pipeline.project) - if can?(current_user, :update_pipeline, pipeline.project)
- if pipeline.builds.latest.failed.any?(&:retryable?) - if pipeline.builds.latest.failed.any?(&:retryable?)
= link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post = link_to "Retry", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'js-retry-button btn btn-grouped btn-primary', method: :post
- if pipeline.builds.running_or_pending.any? - if pipeline.builds.running_or_pending.any?
= link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
......
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%li %li
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
- if @issue.submittable_as_spam? && current_user.admin? - if @issue.submittable_as_spam_by?(current_user)
%li %li
= link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam' = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
- if can?(current_user, :update_issue, @issue) - if can?(current_user, :update_issue, @issue)
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
- if @issue.submittable_as_spam? && current_user.admin? - if @issue.submittable_as_spam_by?(current_user)
= link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam' = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
- if current_user.can_select_namespace? - if current_user.can_select_namespace?
.input-group-addon .input-group-addon
= root_url = root_url
= f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1} = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1}
- else - else
.input-group-addon.static-namespace .input-group-addon.static-namespace
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
.header-action-buttons .header-action-buttons
- if can?(current_user, :update_pipeline, @pipeline.project) - if can?(current_user, :update_pipeline, @pipeline.project)
- if @pipeline.retryable? - if @pipeline.retryable?
= link_to "Retry failed", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'btn btn-inverted-secondary', method: :post = link_to "Retry", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'js-retry-button btn btn-inverted-secondary', method: :post
- if @pipeline.cancelable? - if @pipeline.cancelable?
= link_to "Cancel running", cancel_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post = link_to "Cancel running", cancel_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
= f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light' = f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light'
.input-group .input-group
%span.input-group-addon / %span.input-group-addon /
= f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered' = f.text_field :build_coverage_regex, class: 'form-control', placeholder: 'Regular expression'
%span.input-group-addon / %span.input-group-addon /
%p.help-block %p.help-block
A regular expression that will be used to find the test coverage A regular expression that will be used to find the test coverage
......
...@@ -13,69 +13,70 @@ ...@@ -13,69 +13,70 @@
= render "home_panel" = render "home_panel"
- if current_user && can?(current_user, :download_code, @project) - if current_user && can?(current_user, :download_code, @project)
%nav.project-stats{ class: container_class } .project-stats-container{ class: container_class }
%ul.nav %nav.project-stats
%li %ul.nav
= 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
%li %li
= link_to 'Readme', readme_path(@project) = link_to project_files_path(@project) do
Files (#{storage_counter(@project.statistics.total_repository_size)})
- if @repository.changelog
%li %li
= link_to 'Changelog', changelog_path(@project) = 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)})
- if @repository.license_blob
%li %li
= link_to license_short_name(@project), license_path(@project) = link_to namespace_project_branches_path(@project.namespace, @project) do
#{'Branch'.pluralize(@repository.branch_count)} (#{number_with_delimiter(@repository.branch_count)})
- if @repository.contribution_guide
%li %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 - if default_project_view != 'readme' && @repository.readme
%li %li
= link_to 'CI configuration', ci_configuration_path(@project) = link_to 'Readme', readme_path(@project)
- if @repository.changelog
%li
= link_to 'Changelog', changelog_path(@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) - if current_user && can_push_branch?(@project, @project.default_branch)
- unless @repository.changelog - unless @repository.changelog
%li.missing %li.missing
= link_to add_special_file_path(@project, file_name: 'CHANGELOG') do = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do
Add Changelog Add Changelog
- unless @repository.license_blob - unless @repository.license_blob
%li.missing %li.missing
= link_to add_special_file_path(@project, file_name: 'LICENSE') do = link_to add_special_file_path(@project, file_name: 'LICENSE') do
Add License Add License
- unless @repository.contribution_guide - unless @repository.contribution_guide
%li.missing %li.missing
= link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
Add Contribution guide Add Contribution guide
- unless @repository.gitlab_ci_yml - unless @repository.gitlab_ci_yml
%li.missing %li.missing
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
Set up CI Set up CI
- if koding_enabled? && @repository.koding_yml.blank? - if koding_enabled? && @repository.koding_yml.blank?
%li.missing %li.missing
= link_to 'Set up Koding', add_koding_stack_path(@project) = link_to 'Set up Koding', add_koding_stack_path(@project)
- if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present? - if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
%li.missing %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 = 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 Set up auto deploy
- if @repository.commit - if @repository.commit
.project-last-commit{ class: container_class } .project-last-commit
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
%div{ class: container_class } %div{ class: container_class }
- if @project.archived? - if @project.archived?
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
- if can?(current_user, :create_project_snippet, @project) - if can?(current_user, :create_project_snippet, @project)
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do
New snippet New snippet
- if @snippet.submittable_as_spam? && current_user.admin? - if @snippet.submittable_as_spam_by?(current_user)
= link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
.visible-xs-block.dropdown .visible-xs-block.dropdown
...@@ -31,6 +31,6 @@ ...@@ -31,6 +31,6 @@
%li %li
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do
Edit Edit
- if @snippet.submittable_as_spam? && current_user.admin? - if @snippet.submittable_as_spam_by?(current_user)
%li %li
= link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
.results.prepend-top-10 .results.prepend-top-10
- if @scope == 'commits' - if @scope == 'commits'
%ul.list-unstyled %ul.content-list.commit-list.table-list.table-wide
= render partial: "search/results/commit", collection: @search_objects = render partial: "search/results/commit", collection: @search_objects
- else - else
.search-results .search-results
......
...@@ -12,41 +12,34 @@ ...@@ -12,41 +12,34 @@
%span.light= time_ago_with_tooltip(snippet.created_at) %span.light= time_ago_with_tooltip(snippet.created_at)
%h4.snippet-title %h4.snippet-title
- snippet_path = reliable_snippet_path(snippet) - snippet_path = reliable_snippet_path(snippet)
= link_to snippet_path do .file-holder
.file-holder .js-file-title.file-title
.js-file-title.file-title = link_to snippet_path do
%i.fa.fa-file %i.fa.fa-file
%strong= snippet.file_name %strong= snippet.file_name
- if markup?(snippet.file_name) - if markup?(snippet.file_name)
.file-content.wiki .file-content.wiki
- snippet_chunks.each do |chunk|
- unless chunk[:data].empty?
= render_markup(snippet.file_name, chunk[:data])
- else
.file-content.code
.nothing-here-block Empty file
- else
.file-content.code.js-syntax-highlight
.line-numbers
- snippet_chunks.each do |chunk| - snippet_chunks.each do |chunk|
- unless chunk[:data].empty? - unless chunk[:data].empty?
= render_markup(snippet.file_name, chunk[:data]) - Gitlab::Git::Util.count_lines(chunk[:data]).times do |index|
- offset = defined?(chunk[:start_line]) ? chunk[:start_line] : 1
- i = index + offset
= link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}", class: "diff-line-num" do
%i.fa.fa-link
= i
.blob-content
- snippet_chunks.each do |chunk|
- unless chunk[:data].empty?
= highlight(snippet.file_name, chunk[:data], repository: nil, plain: snippet.no_highlighting?)
- else - else
.file-content.code .file-content.code
.nothing-here-block Empty file .nothing-here-block Empty file
- else
.file-content.code.js-syntax-highlight
.line-numbers
- snippet_chunks.each do |chunk|
- unless chunk[:data].empty?
- Gitlab::Git::Util.count_lines(chunk[:data]).times do |index|
- offset = defined?(chunk[:start_line]) ? chunk[:start_line] : 1
- i = index + offset
= link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}", class: "diff-line-num" do
%i.fa.fa-link
= i
- unless snippet == snippet_chunks.last
%a.diff-line-num
= "."
%pre.code
%code
- snippet_chunks.each do |chunk|
- unless chunk[:data].empty?
= chunk[:data]
- unless chunk == snippet_chunks.last
%a
= "..."
- else
.file-content.code
.nothing-here-block Empty file
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
= dropdown_title("Change permissions") = dropdown_title("Change permissions")
.dropdown-content .dropdown-content
%ul %ul
- Gitlab::Access.options.each do |role, role_id| - member.class.access_level_roles.each do |role, role_id|
%li %li
= link_to role, "javascript:void(0)", = link_to role, "javascript:void(0)",
class: ("is-active" if member.access_level == role_id), class: ("is-active" if member.access_level == role_id),
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
Delete Delete
= link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do
New snippet New snippet
- if @snippet.submittable_as_spam? && current_user.admin? - if @snippet.submittable_as_spam_by?(current_user)
= link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam'
.visible-xs-block.dropdown .visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
...@@ -28,6 +28,6 @@ ...@@ -28,6 +28,6 @@
%li %li
= link_to edit_snippet_path(@snippet) do = link_to edit_snippet_path(@snippet) do
Edit Edit
- if @snippet.submittable_as_spam? && current_user.admin? - if @snippet.submittable_as_spam_by?(current_user)
%li %li
= link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post
...@@ -7,5 +7,7 @@ class DeleteUserWorker ...@@ -7,5 +7,7 @@ class DeleteUserWorker
current_user = User.find(current_user_id) current_user = User.find(current_user_id)
Users::DestroyService.new(current_user).execute(delete_user, options.symbolize_keys) Users::DestroyService.new(current_user).execute(delete_user, options.symbolize_keys)
rescue Gitlab::Access::AccessDeniedError => e
Rails.logger.warn("User could not be destroyed: #{e}")
end end
end end
---
title: Set dropdown height fixed to 250px and make it scrollable
merge_request: 9063
author:
---
title: Unify issues search behavior by always filtering when ALL labels matches
merge_request: 8849
author:
---
title: Left align navigation
merge_request:
author:
---
title: Truncate long Todo titles for non-mobile screens
merge_request: 9311
author:
---
title: Pick up option from GDK to disable webpack dev server livereload
merge_request:
author:
---
title: Allow searching issues for strings containing colons
merge_request:
author:
---
title: Changed coverage reg expression placeholder text to be more like a placeholder
merge_request:
author:
---
title: Adds remote logout functionality to the Authentiq OAuth provider
merge_request: 9381
author: Alexandros Keramidas
---
title: 'API: Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar`'
merge_request: 9328
author: Robert Schilling
---
title: 'API: Use POST requests to mark todos as done'
merge_request: 9410
author: Robert Schilling
---
title: Added documentation for permalinks to most recent build artifacts.
merge_request: 8934
author: Christian Godenschwager
---
title: Fixed commit search UI
merge_request:
author:
---
title: GitHub Importer - Find users based on GitHub email address
merge_request: 8958
author:
---
title: Added option to update to owner for group members
merge_request:
author:
---
title: Rename retry failed button on pipeline page to just retry
merge_request:
author:
---
title: Add user deletion permission check in `Users::DestroyService`
merge_request:
author:
---
title: Fix snippets search result spacing
merge_request:
author:
---
title: Update GitLab Pages to v0.3.1
merge_request:
author:
...@@ -240,6 +240,17 @@ Devise.setup do |config| ...@@ -240,6 +240,17 @@ Devise.setup do |config|
true true
end end
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' if provider['name'] == 'shibboleth'
provider['args'][:fail_with_empty_uid] = true provider['args'][:fail_with_empty_uid] = true
......
...@@ -10,6 +10,7 @@ var ROOT_PATH = path.resolve(__dirname, '..'); ...@@ -10,6 +10,7 @@ var ROOT_PATH = path.resolve(__dirname, '..');
var IS_PRODUCTION = process.env.NODE_ENV === 'production'; var IS_PRODUCTION = process.env.NODE_ENV === 'production';
var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1; 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_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
var config = { var config = {
context: path.join(ROOT_PATH, 'app/assets/javascripts'), context: path.join(ROOT_PATH, 'app/assets/javascripts'),
...@@ -114,6 +115,7 @@ if (IS_DEV_SERVER) { ...@@ -114,6 +115,7 @@ if (IS_DEV_SERVER) {
port: DEV_SERVER_PORT, port: DEV_SERVER_PORT,
headers: { 'Access-Control-Allow-Origin': '*' }, headers: { 'Access-Control-Allow-Origin': '*' },
stats: 'errors-only', stats: 'errors-only',
inline: DEV_SERVER_LIVERELOAD
}; };
config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath; config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath;
} }
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddIndexToUserAgentDetail < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def change
add_concurrent_index(:user_agent_details, [:subject_id, :subject_type])
end
end
...@@ -1218,6 +1218,8 @@ ActiveRecord::Schema.define(version: 20170215200045) do ...@@ -1218,6 +1218,8 @@ ActiveRecord::Schema.define(version: 20170215200045) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
end end
add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree
create_table "users", force: :cascade do |t| create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false t.string "encrypted_password", default: "", null: false
......
...@@ -54,7 +54,7 @@ Authentiq will generate a Client ID and the accompanying Client Secret for you t ...@@ -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. 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. 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. 7. Save the configuration file.
......
...@@ -30,7 +30,7 @@ GET /issues?milestone=1.0.0&state=opened ...@@ -30,7 +30,7 @@ GET /issues?milestone=1.0.0&state=opened
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `state` | string | no | Return all issues or just those that are `opened` or `closed`| | `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 | | `milestone` | string| no | The milestone title |
| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `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` | | `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 ...@@ -188,7 +188,7 @@ GET /projects/:id/issues?milestone=1.0.0&state=opened
| `id` | integer | yes | The ID of a project | | `id` | integer | yes | The ID of a project |
| `iid` | integer | no | Return the issue having the given `iid` | | `iid` | integer | no | Return the issue having the given `iid` |
| `state` | string | no | Return all issues or just those that are `opened` or `closed`| | `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 | | `milestone` | string| no | The milestone title |
| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `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` | | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
......
...@@ -163,7 +163,7 @@ Example of response ...@@ -163,7 +163,7 @@ Example of response
} }
``` ```
## Retry failed builds in a pipeline ## Retry builds in a pipeline
> [Introduced][ce-5837] in GitLab 8.11 > [Introduced][ce-5837] in GitLab 8.11
......
...@@ -609,7 +609,7 @@ Example response: ...@@ -609,7 +609,7 @@ Example response:
Unstars a given project. Returns status code `304` if the project is not starred. Unstars a given project. Returns status code `304` if the project is not starred.
``` ```
DELETE /projects/:id/star POST /projects/:id/unstar
``` ```
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
...@@ -617,7 +617,7 @@ DELETE /projects/:id/star ...@@ -617,7 +617,7 @@ DELETE /projects/:id/star
| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | | `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash ```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star" curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/unstar"
``` ```
Example response: Example response:
...@@ -1194,4 +1194,4 @@ Parameters: ...@@ -1194,4 +1194,4 @@ Parameters:
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `query` | string | yes | A string contained in the project name | | `query` | string | yes | A string contained in the project name |
| `order_by` | string | no | Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields | | `order_by` | string | no | Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order | | `sort` | string | no | Return requests sorted in `asc` or `desc` order |
\ No newline at end of file
...@@ -184,7 +184,7 @@ Marks a single pending todo given by its ID for the current user as done. The ...@@ -184,7 +184,7 @@ Marks a single pending todo given by its ID for the current user as done. The
todo marked as done is returned in the response. todo marked as done is returned in the response.
``` ```
DELETE /todos/:id POST /todos/:id/mark_as_done
``` ```
Parameters: Parameters:
...@@ -194,7 +194,7 @@ Parameters: ...@@ -194,7 +194,7 @@ Parameters:
| `id` | integer | yes | The ID of a todo | | `id` | integer | yes | The ID of a todo |
```bash ```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130 curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130/mark_as_done
``` ```
Example Response: Example Response:
...@@ -277,20 +277,15 @@ Example Response: ...@@ -277,20 +277,15 @@ Example Response:
## Mark all todos as done ## Mark all todos as done
Marks all pending todos for the current user as done. It returns the number of marked todos. Marks all pending todos for the current user as done. It returns the HTTP status code `204` with an empty response.
``` ```
DELETE /todos POST /todos/mark_as_done
``` ```
```bash ```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/donmark_as_donee
``` ```
Example Response:
```json
3
```
[ce-3188]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3188 [ce-3188]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3188
...@@ -13,6 +13,7 @@ changes are in V4: ...@@ -13,6 +13,7 @@ changes are in V4:
- Project snippets do not return deprecated field `expires_at` - Project snippets do not return deprecated field `expires_at`
- Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`) - Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`)
- Status 409 returned for POST `project/:id/members` when a member already exists - Status 409 returned for POST `project/:id/members` when a member already exists
- Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar`
- Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix) - Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix)
- `/licences` - `/licences`
- `/licences/:key` - `/licences/:key`
...@@ -23,7 +24,9 @@ changes are in V4: ...@@ -23,7 +24,9 @@ changes are in V4:
- `/gitlab_ci_ymls/:key` - `/gitlab_ci_ymls/:key`
- `/dockerfiles/:key` - `/dockerfiles/:key`
- Moved `/projects/fork/:id` to `/projects/:id/fork` - Moved `/projects/fork/:id` to `/projects/:id/fork`
- Moved `DELETE /todos` to `POST /todos/mark_as_done` and `DELETE /todos/:todo_id` to `POST /todos/:todo_id/mark_as_done`
- Endpoints `/projects/owned`, `/projects/visible`, `/projects/starred` & `/projects/all` are consolidated into `/projects` using query parameters - Endpoints `/projects/owned`, `/projects/visible`, `/projects/starred` & `/projects/all` are consolidated into `/projects` using query parameters
- Return pagination headers for all endpoints that return an array - 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 - 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)` - 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)
...@@ -39,13 +39,15 @@ accessible during the build process. ...@@ -39,13 +39,15 @@ accessible during the build process.
## What is an image ## What is an image
The `image` keyword is the name of the docker image that is present in the The `image` keyword is the name of the docker image the docker executor
local Docker Engine (list all images with `docker images`) or any image that will run to perform the CI tasks.
can be found at [Docker Hub][hub]. For more information about images and Docker
Hub please read the [Docker Fundamentals][] documentation.
In short, with `image` we refer to the docker image, which will be used to By default the executor will only pull images from [Docker Hub][hub],
create a container on which your job will run. but this can be configured in the `gitlab-runner/config.toml` by setting
the [docker pull policy][] to allow using local images.
For more information about images and Docker Hub please read
the [Docker Fundamentals][] documentation.
## What is a service ## What is a service
...@@ -271,6 +273,7 @@ containers as well as all volumes (`-v`) that were created with the container ...@@ -271,6 +273,7 @@ containers as well as all volumes (`-v`) that were created with the container
creation. creation.
[Docker Fundamentals]: https://docs.docker.com/engine/understanding-docker/ [Docker Fundamentals]: https://docs.docker.com/engine/understanding-docker/
[docker pull policy]: https://docs.gitlab.com/runner/executors/docker.html#how-pull-policies-work
[hub]: https://hub.docker.com/ [hub]: https://hub.docker.com/
[linking-containers]: https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/ [linking-containers]: https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/
[tutum/wordpress]: https://hub.docker.com/r/tutum/wordpress/ [tutum/wordpress]: https://hub.docker.com/r/tutum/wordpress/
......
...@@ -64,6 +64,10 @@ Libraries with the following licenses are unacceptable for use: ...@@ -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. - [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]. - [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 ## 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. 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 ...@@ -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 [OSI-GPL]: https://opensource.org/faq#linking-proprietary-code
[OSL]: https://opensource.org/licenses/OSL-3.0 [OSL]: https://opensource.org/licenses/OSL-3.0
[OSL-GNU]: https://www.gnu.org/licenses/license-list.en.html#OSL [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: ...@@ -95,6 +95,25 @@ so we need to set some guidelines for their use going forward:
[lets-not]: https://robots.thoughtbot.com/lets-not [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 ### Test speed
GitLab has a massive test suite that, without parallelization, can take more GitLab has a massive test suite that, without parallelization, can take more
......
...@@ -176,4 +176,4 @@ Portions of this page are modifications based on work created and shared by the ...@@ -176,4 +176,4 @@ Portions of this page are modifications based on work created and shared by the
[products]: https://about.gitlab.com/products/ "GitLab products page" [products]: https://about.gitlab.com/products/ "GitLab products page"
[serial comma]: https://en.wikipedia.org/wiki/Serial_comma "“Serial comma” in Wikipedia" [serial comma]: https://en.wikipedia.org/wiki/Serial_comma "“Serial comma” in Wikipedia"
[android project]: http://source.android.com/ [android project]: http://source.android.com/
[creative commons]: http://creativecommons.org/licenses/by/2.5/ [creative commons]: http://creativecommons.org/licenses/by/2.5/
\ No newline at end of file
# 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 <img src="img/steven-lyons.png" width="300px">
- Medium company
- Small company
- Open source communities
## Users by role #### Demographics
- Admin - **Age**<br>32 years old
- Manager - **Location**<br>Germany
- Developer - **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.
\ No newline at end of file
...@@ -90,18 +90,43 @@ inside GitLab that make that possible. ...@@ -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 It is possible to download the latest artifacts of a job via a well known URL
so you can use it for scripting purposes. 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> 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` the `master` branch of the `gitlab-ce` project that belongs to the `gitlab-org`
namespace, the URL would be: 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, The latest builds are also exposed in the UI in various places. Specifically,
......
...@@ -5,6 +5,7 @@ Feature: Revert Merge Requests ...@@ -5,6 +5,7 @@ Feature: Revert Merge Requests
And I am signed in as a developer of the project And I am signed in as a developer of the project
And I am on the Merge Request detail page And I am on the Merge Request detail page
And I click on Accept Merge Request And I click on Accept Merge Request
And I am on the Merge Request detail page
@javascript @javascript
Scenario: I revert a merge request Scenario: I revert a merge request
......
...@@ -19,6 +19,7 @@ module API ...@@ -19,6 +19,7 @@ module API
mount ::API::V3::Repositories mount ::API::V3::Repositories
mount ::API::V3::SystemHooks mount ::API::V3::SystemHooks
mount ::API::V3::Tags mount ::API::V3::Tags
mount ::API::V3::Todos
mount ::API::V3::Templates mount ::API::V3::Templates
mount ::API::V3::Users mount ::API::V3::Users
end end
......
...@@ -10,17 +10,9 @@ module API ...@@ -10,17 +10,9 @@ module API
args.delete(:id) args.delete(:id)
args[:milestone_title] = args.delete(:milestone) args[:milestone_title] = args.delete(:milestone)
args[:label_name] = args.delete(:labels)
match_all_labels = args.delete(:match_all_labels) issues = IssuesFinder.new(current_user, args).execute
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.reorder(args[:order_by] => args[:sort]) issues.reorder(args[:order_by] => args[:sort])
end end
...@@ -77,7 +69,7 @@ module API ...@@ -77,7 +69,7 @@ module API
get ":id/issues" do get ":id/issues" do
group = find_group!(params[:id]) 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 present paginate(issues), with: Entities::Issue, current_user: current_user
end end
......
...@@ -23,7 +23,7 @@ module API ...@@ -23,7 +23,7 @@ module API
pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope]) pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
present paginate(pipelines), with: Entities::Pipeline present paginate(pipelines), with: Entities::Pipeline
end end
desc 'Create a new pipeline' do desc 'Create a new pipeline' do
detail 'This feature was introduced in GitLab 8.14' detail 'This feature was introduced in GitLab 8.14'
success Entities::Pipeline success Entities::Pipeline
...@@ -58,7 +58,7 @@ module API ...@@ -58,7 +58,7 @@ module API
present pipeline, with: Entities::Pipeline present pipeline, with: Entities::Pipeline
end end
desc 'Retry failed builds in the pipeline' do desc 'Retry builds in the pipeline' do
detail 'This feature was introduced in GitLab 8.11.' detail 'This feature was introduced in GitLab 8.11.'
success Entities::Pipeline success Entities::Pipeline
end end
......
...@@ -266,7 +266,7 @@ module API ...@@ -266,7 +266,7 @@ module API
desc 'Unstar a project' do desc 'Unstar a project' do
success Entities::Project success Entities::Project
end end
delete ':id/star' do post ':id/unstar' do
if current_user.starred?(user_project) if current_user.starred?(user_project)
current_user.toggle_star(user_project) current_user.toggle_star(user_project)
user_project.reload user_project.reload
......
...@@ -58,7 +58,7 @@ module API ...@@ -58,7 +58,7 @@ module API
params do params do
requires :id, type: Integer, desc: 'The ID of the todo being marked as done' requires :id, type: Integer, desc: 'The ID of the todo being marked as done'
end end
delete ':id' do post ':id/mark_as_done' do
todo = current_user.todos.find(params[:id]) todo = current_user.todos.find(params[:id])
TodoService.new.mark_todos_as_done([todo], current_user) TodoService.new.mark_todos_as_done([todo], current_user)
...@@ -66,9 +66,11 @@ module API ...@@ -66,9 +66,11 @@ module API
end end
desc 'Mark all todos as done' desc 'Mark all todos as done'
delete do post '/mark_as_done' do
todos = find_todos todos = find_todos
TodoService.new.mark_todos_as_done(todos, current_user) TodoService.new.mark_todos_as_done(todos, current_user)
no_content!
end end
end end
end end
......
module API
module V3
class Todos < Grape::API
before { authenticate! }
resource :todos do
desc 'Mark a todo as done' do
success ::API::Entities::Todo
end
params do
requires :id, type: Integer, desc: 'The ID of the todo being marked as done'
end
delete ':id' do
todo = current_user.todos.find(params[:id])
TodoService.new.mark_todos_as_done([todo], current_user)
present todo.reload, with: ::API::Entities::Todo, current_user: current_user
end
desc 'Mark all todos as done'
delete do
todos = TodosFinder.new(current_user, params).execute
TodoService.new.mark_todos_as_done(todos, current_user)
end
end
end
end
end
...@@ -54,7 +54,7 @@ module Gitlab ...@@ -54,7 +54,7 @@ module Gitlab
disable_statement_timeout disable_statement_timeout
key_name = "fk_#{source}_#{target}_#{column}" key_name = concurrent_foreign_key_name(source, column)
# Using NOT VALID allows us to create a key without immediately # Using NOT VALID allows us to create a key without immediately
# validating it. This means we keep the ALTER TABLE lock only for a # validating it. This means we keep the ALTER TABLE lock only for a
...@@ -74,6 +74,15 @@ module Gitlab ...@@ -74,6 +74,15 @@ module Gitlab
execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{key_name};") execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{key_name};")
end end
# Returns the name for a concurrent foreign key.
#
# PostgreSQL constraint names have a limit of 63 bytes. The logic used
# here is based on Rails' foreign_key_name() method, which unfortunately
# is private so we can't rely on it directly.
def concurrent_foreign_key_name(table, column)
"fk_#{Digest::SHA256.hexdigest("#{table}_#{column}_fk").first(10)}"
end
# Long-running migrations may take more than the timeout allowed by # Long-running migrations may take more than the timeout allowed by
# the database. Disable the session's statement timeout to ensure # the database. Disable the session's statement timeout to ensure
# migrations don't get killed prematurely. (PostgreSQL only) # migrations don't get killed prematurely. (PostgreSQL only)
......
module Gitlab module Gitlab
module GithubImport module GithubImport
class BaseFormatter 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 @project = project
@raw_data = raw_data @raw_data = raw_data
@client = client
@formatter = Gitlab::ImportFormatter.new @formatter = Gitlab::ImportFormatter.new
end end
...@@ -18,19 +19,6 @@ module Gitlab ...@@ -18,19 +19,6 @@ module Gitlab
def url def url
raw_data.url || '' raw_data.url || ''
end 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 end
end end
...@@ -10,6 +10,7 @@ module Gitlab ...@@ -10,6 +10,7 @@ module Gitlab
@access_token = access_token @access_token = access_token
@host = host.to_s.sub(%r{/+\z}, '') @host = host.to_s.sub(%r{/+\z}, '')
@api_version = api_version @api_version = api_version
@users = {}
if access_token if access_token
::Octokit.auto_paginate = false ::Octokit.auto_paginate = false
...@@ -64,6 +65,13 @@ module Gitlab ...@@ -64,6 +65,13 @@ module Gitlab
api.respond_to?(method) || super api.respond_to?(method) || super
end end
def user(login)
return nil unless login.present?
return @users[login] if @users.key?(login)
@users[login] = api.user(login)
end
private private
def api_endpoint def api_endpoint
......
module Gitlab module Gitlab
module GithubImport module GithubImport
class CommentFormatter < BaseFormatter class CommentFormatter < BaseFormatter
attr_writer :author_id
def attributes def attributes
{ {
project: project, project: project,
...@@ -17,11 +19,11 @@ module Gitlab ...@@ -17,11 +19,11 @@ module Gitlab
private private
def author def author
raw_data.user.login @author ||= UserFormatter.new(client, raw_data.user)
end end
def author_id def author_id
gitlab_author_id || project.creator_id author.gitlab_id || project.creator_id
end end
def body def body
...@@ -52,10 +54,10 @@ module Gitlab ...@@ -52,10 +54,10 @@ module Gitlab
end end
def note def note
if gitlab_author_id if author.gitlab_id
body body
else else
formatter.author_line(author) + body formatter.author_line(author.login) + body
end end
end end
......
...@@ -110,7 +110,7 @@ module Gitlab ...@@ -110,7 +110,7 @@ module Gitlab
def import_issues def import_issues
fetch_resources(:issues, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues| fetch_resources(:issues, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues|
issues.each do |raw| issues.each do |raw|
gh_issue = IssueFormatter.new(project, raw) gh_issue = IssueFormatter.new(project, raw, client)
begin begin
issuable = issuable =
...@@ -131,7 +131,8 @@ module Gitlab ...@@ -131,7 +131,8 @@ module Gitlab
def import_pull_requests def import_pull_requests
fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests| fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
pull_requests.each do |raw| 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? next unless gh_pull_request.valid?
begin begin
...@@ -209,14 +210,16 @@ module Gitlab ...@@ -209,14 +210,16 @@ module Gitlab
ActiveRecord::Base.no_touching do ActiveRecord::Base.no_touching do
comments.each do |raw| comments.each do |raw|
begin 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! # 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('/') *_, parent, iid = URI(raw.html_url).path.split('/')
if parent == 'issues'
issuable = Issue.find_by(project_id: project.id, iid: iid) issuable = if parent == 'issues'
else Issue.find_by(project_id: project.id, iid: iid)
issuable = MergeRequest.find_by(target_project_id: project.id, iid: iid) else
end MergeRequest.find_by(target_project_id: project.id, iid: iid)
end
next unless issuable next unless issuable
......
module Gitlab module Gitlab
module GithubImport module GithubImport
class IssuableFormatter < BaseFormatter class IssuableFormatter < BaseFormatter
attr_writer :assignee_id, :author_id
def project_association def project_association
raise NotImplementedError raise NotImplementedError
end end
...@@ -23,18 +25,24 @@ module Gitlab ...@@ -23,18 +25,24 @@ module Gitlab
raw_data.assignee.present? raw_data.assignee.present?
end end
def assignee_id def author
@author ||= UserFormatter.new(client, raw_data.user)
end
def author_id
@author_id ||= author.gitlab_id || project.creator_id
end
def assignee
if assigned? if assigned?
gitlab_user_id(raw_data.assignee.id) @assignee ||= UserFormatter.new(client, raw_data.assignee)
end end
end end
def author def assignee_id
raw_data.user.login return @assignee_id if defined?(@assignee_id)
end
def author_id @assignee_id = assignee.try(:gitlab_id)
gitlab_author_id || project.creator_id
end end
def body def body
...@@ -42,10 +50,10 @@ module Gitlab ...@@ -42,10 +50,10 @@ module Gitlab
end end
def description def description
if gitlab_author_id if author.gitlab_id
body body
else else
formatter.author_line(author) + body formatter.author_line(author.login) + body
end end
end end
......
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 ...@@ -86,32 +86,47 @@ describe Projects::BlobController do
end end
context 'when user has forked project' do context 'when user has forked project' do
let(:guest) { create(:user) } let(:forked_project_link) { create(:forked_project_link, forked_from_project: project) }
let!(:forked_project) { Projects::ForkService.new(project, guest).execute } let!(:forked_project) { forked_project_link.forked_to_project }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, source_branch: "fork-test-1", target_branch: "master") } let(:guest) { forked_project.owner }
before { sign_in(guest) } before do
sign_in(guest)
it "redirects to forked project new merge request" do end
default_params[:target_branch] = "fork-test-1"
default_params[:create_merge_request] = 1 context 'when editing on the fork' do
before do
allow_any_instance_of(Files::UpdateService).to receive(:commit).and_return(:success) default_params[:namespace_id] = forked_project.namespace.to_param
default_params[:project_id] = forked_project.to_param
put :update, default_params end
expect(response).to redirect_to( it 'redirects to blob' do
new_namespace_project_merge_request_path( put :update, default_params
forked_project.namespace,
forked_project, expect(response).to redirect_to(namespace_project_blob_path(forked_project.namespace, forked_project, 'master/CHANGELOG'))
merge_request: { end
source_project_id: forked_project.id, end
target_project_id: project.id,
source_branch: "fork-test-1", context 'when editing on the original repository' do
target_branch: "master" 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 end
end end
......
...@@ -153,7 +153,7 @@ describe 'Commits' do ...@@ -153,7 +153,7 @@ describe 'Commits' do
expect(page).to have_content pipeline.git_author_name expect(page).to have_content pipeline.git_author_name
expect(page).to have_link('Download artifacts') expect(page).to have_link('Download artifacts')
expect(page).not_to have_link('Cancel running') expect(page).not_to have_link('Cancel running')
expect(page).not_to have_link('Retry failed') expect(page).not_to have_link('Retry')
end end
end end
...@@ -172,7 +172,7 @@ describe 'Commits' do ...@@ -172,7 +172,7 @@ describe 'Commits' do
expect(page).to have_content pipeline.git_author_name expect(page).to have_content pipeline.git_author_name
expect(page).not_to have_link('Download artifacts') expect(page).not_to have_link('Download artifacts')
expect(page).not_to have_link('Cancel running') expect(page).not_to have_link('Cancel running')
expect(page).not_to have_link('Retry failed') expect(page).not_to have_link('Retry')
end end
end end
end end
......
...@@ -30,6 +30,21 @@ feature 'Groups members list', feature: true do ...@@ -30,6 +30,21 @@ feature 'Groups members list', feature: true do
expect(second_row).to be_blank expect(second_row).to be_blank
end end
it 'updates user to owner level', :js do
group.add_owner(user1)
group.add_developer(user2)
visit group_group_members_path(group)
page.within(second_row) do
click_button('Developer')
click_link('Owner')
expect(page).to have_button('Owner')
end
end
def first_row def first_row
page.all('ul.content-list > li')[0] page.all('ul.content-list > li')[0]
end end
......
...@@ -19,6 +19,51 @@ feature "New project", feature: true do ...@@ -19,6 +19,51 @@ feature "New project", feature: true do
end end
end end
context "Namespace selector" do
context "with user namespace" do
before do
visit new_project_path
end
it "selects the user namespace" do
namespace = find("#project_namespace_id")
expect(namespace.text).to eq user.username
end
end
context "with group namespace" do
let(:group) { create(:group, :private, owner: user) }
before do
group.add_owner(user)
visit new_project_path(namespace_id: group.id)
end
it "selects the group namespace" do
namespace = find("#project_namespace_id option[selected]")
expect(namespace.text).to eq group.name
end
context "on validation error" do
before do
fill_in('project_path', with: 'private-group-project')
choose('Internal')
click_button('Create project')
expect(page).to have_css '.project-edit-errors .alert.alert-danger'
end
it "selects the group namespace" do
namespace = find("#project_namespace_id option[selected]")
expect(namespace.text).to eq group.name
end
end
end
end
context 'Import project options' do context 'Import project options' do
before do before do
visit new_project_path visit new_project_path
......
...@@ -54,7 +54,7 @@ describe 'Pipeline', :feature, :js do ...@@ -54,7 +54,7 @@ describe 'Pipeline', :feature, :js do
expect(page).to have_content('Build') expect(page).to have_content('Build')
expect(page).to have_content('Test') expect(page).to have_content('Test')
expect(page).to have_content('Deploy') expect(page).to have_content('Deploy')
expect(page).to have_content('Retry failed') expect(page).to have_content('Retry')
expect(page).to have_content('Cancel running') expect(page).to have_content('Cancel running')
end end
...@@ -164,9 +164,9 @@ describe 'Pipeline', :feature, :js do ...@@ -164,9 +164,9 @@ describe 'Pipeline', :feature, :js do
it { expect(page).not_to have_content('retried') } it { expect(page).not_to have_content('retried') }
context 'when retrying' do context 'when retrying' do
before { click_on 'Retry failed' } before { find('.js-retry-button').trigger('click') }
it { expect(page).not_to have_content('Retry failed') } it { expect(page).not_to have_content('Retry') }
end end
end end
...@@ -198,7 +198,7 @@ describe 'Pipeline', :feature, :js do ...@@ -198,7 +198,7 @@ describe 'Pipeline', :feature, :js do
expect(page).to have_content(build_failed.id) expect(page).to have_content(build_failed.id)
expect(page).to have_content(build_running.id) expect(page).to have_content(build_running.id)
expect(page).to have_content(build_external.id) expect(page).to have_content(build_external.id)
expect(page).to have_content('Retry failed') expect(page).to have_content('Retry')
expect(page).to have_content('Cancel running') expect(page).to have_content('Cancel running')
expect(page).to have_link('Play') expect(page).to have_link('Play')
end end
...@@ -226,9 +226,9 @@ describe 'Pipeline', :feature, :js do ...@@ -226,9 +226,9 @@ describe 'Pipeline', :feature, :js do
it { expect(page).not_to have_content('retried') } it { expect(page).not_to have_content('retried') }
context 'when retrying' do context 'when retrying' do
before { click_on 'Retry failed' } before { find('.js-retry-button').trigger('click') }
it { expect(page).not_to have_content('Retry failed') } it { expect(page).not_to have_content('Retry') }
it { expect(page).to have_selector('.retried') } it { expect(page).to have_selector('.retried') }
end end
end end
......
...@@ -20,6 +20,8 @@ feature 'Ref switcher', feature: true, js: true do ...@@ -20,6 +20,8 @@ feature 'Ref switcher', feature: true, js: true do
input.set 'binary' input.set 'binary'
wait_for_ajax wait_for_ajax
expect(find('.dropdown-content ul')).to have_selector('li', count: 6)
page.within '.dropdown-content ul' do page.within '.dropdown-content ul' do
input.native.send_keys :enter input.native.send_keys :enter
end end
......
...@@ -99,6 +99,29 @@ require('~/filtered_search/filtered_search_tokenizer'); ...@@ -99,6 +99,29 @@ require('~/filtered_search/filtered_search_tokenizer');
expect(results.tokens[2].value).toBe('Doing'); expect(results.tokens[2].value).toBe('Doing');
expect(results.tokens[2].symbol).toBe('~'); 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');
});
}); });
}); });
})(); })();
...@@ -101,6 +101,16 @@ describe Gitlab::Database::MigrationHelpers, lib: true do ...@@ -101,6 +101,16 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
end end
end end
describe '#concurrent_foreign_key_name' do
it 'returns the name for a foreign key' do
name = model.concurrent_foreign_key_name(:this_is_a_very_long_table_name,
:with_a_very_long_column_name)
expect(name).to be_an_instance_of(String)
expect(name.length).to eq(13)
end
end
describe '#disable_statement_timeout' do describe '#disable_statement_timeout' do
context 'using PostgreSQL' do context 'using PostgreSQL' do
it 'disables statement timeouts' do it 'disables statement timeouts' do
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::GithubImport::CommentFormatter, lib: true do describe Gitlab::GithubImport::CommentFormatter, lib: true do
let(:client) { double }
let(:project) { create(:empty_project) } 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(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') }
let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') } let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') }
let(:base) do let(:base) do
...@@ -16,7 +17,11 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do ...@@ -16,7 +17,11 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do
} }
end 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 describe '#attributes' do
context 'when do not reference a portion of the diff' do context 'when do not reference a portion of the diff' do
...@@ -69,8 +74,15 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do ...@@ -69,8 +74,15 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do
context 'when author is a GitLab user' do context 'when author is a GitLab user' do
let(:raw) { double(base.merge(user: octocat)) } 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') 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 expect(comment.attributes.fetch(:author_id)).to eq gl_user.id
end end
......
...@@ -44,6 +44,7 @@ describe Gitlab::GithubImport::Importer, lib: true do ...@@ -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(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(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(: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(:milestones).and_return([milestone, milestone])
allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2]) allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2])
...@@ -53,7 +54,8 @@ describe Gitlab::GithubImport::Importer, lib: true do ...@@ -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(:last_response).and_return(double(rels: { next: nil }))
allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2]) allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2])
end 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(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
let(:label1) do let(:label1) do
...@@ -125,6 +127,7 @@ describe Gitlab::GithubImport::Importer, lib: true do ...@@ -125,6 +127,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
) )
end end
let!(:user) { create(:user, email: octocat.email) }
let(:repository) { double(id: 1, fork: false) } let(:repository) { double(id: 1, fork: false) }
let(:source_sha) { create(:commit, project: project).id } let(:source_sha) { create(:commit, project: project).id }
let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) } let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) }
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::GithubImport::IssueFormatter, lib: true do describe Gitlab::GithubImport::IssueFormatter, lib: true do
let(:client) { double }
let!(:project) { create(:empty_project, namespace: create(:namespace, path: 'octocat')) } 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(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
...@@ -23,7 +24,11 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do ...@@ -23,7 +24,11 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
} }
end 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 shared_examples 'Gitlab::GithubImport::IssueFormatter#attributes' do
context 'when issue is open' do context 'when issue is open' do
...@@ -75,11 +80,17 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do ...@@ -75,11 +80,17 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
expect(issue.attributes.fetch(:assignee_id)).to be_nil expect(issue.attributes.fetch(:assignee_id)).to be_nil
end 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') gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id
end 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 end
context 'when it has a milestone' do context 'when it has a milestone' do
...@@ -100,16 +111,22 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do ...@@ -100,16 +111,22 @@ describe Gitlab::GithubImport::IssueFormatter, lib: true do
context 'when author is a GitLab user' do context 'when author is a GitLab user' do
let(:raw_data) { double(base_data.merge(user: octocat)) } 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 expect(issue.attributes.fetch(:author_id)).to eq project.creator_id
end 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') gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
expect(issue.attributes.fetch(:author_id)).to eq gl_user.id expect(issue.attributes.fetch(:author_id)).to eq gl_user.id
end 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 it 'returns description without created at tag line' do
create(:omniauth_user, extern_uid: octocat.id, provider: 'github') create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::GithubImport::PullRequestFormatter, lib: true do describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
let(:client) { double }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:source_sha) { create(:commit, project: project).id } let(:source_sha) { create(:commit, project: project).id }
let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).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 ...@@ -10,7 +11,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
let(:target_repo) { repository } let(:target_repo) { repository }
let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) } 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(: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(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
let(:base_data) do let(:base_data) do
...@@ -32,7 +33,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do ...@@ -32,7 +33,11 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
} }
end 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 shared_examples 'Gitlab::GithubImport::PullRequestFormatter#attributes' do
context 'when pull request is open' do context 'when pull request is open' do
...@@ -121,26 +126,38 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do ...@@ -121,26 +126,38 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
expect(pull_request.attributes.fetch(:assignee_id)).to be_nil expect(pull_request.attributes.fetch(:assignee_id)).to be_nil
end 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') gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id
end 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 end
context 'when author is a GitLab user' do context 'when author is a GitLab user' do
let(:raw_data) { double(base_data.merge(user: octocat)) } 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 expect(pull_request.attributes.fetch(:author_id)).to eq project.creator_id
end 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') gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id
end 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 it 'returns description without created at tag line' do
create(:omniauth_user, extern_uid: octocat.id, provider: 'github') create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
......
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
...@@ -14,8 +14,9 @@ describe Issue, 'Spammable' do ...@@ -14,8 +14,9 @@ describe Issue, 'Spammable' do
end end
describe 'InstanceMethods' do describe 'InstanceMethods' do
let(:issue) { build(:issue, spam: true) }
it 'should be invalid if spam' do it 'should be invalid if spam' do
issue = build(:issue, spam: true)
expect(issue.valid?).to be_falsey expect(issue.valid?).to be_falsey
end end
...@@ -29,5 +30,20 @@ describe Issue, 'Spammable' do ...@@ -29,5 +30,20 @@ describe Issue, 'Spammable' do
expect(issue.check_for_spam?).to eq(false) expect(issue.check_for_spam?).to eq(false)
end end
end end
describe '#submittable_as_spam_by?' do
let(:admin) { build(:admin) }
let(:user) { build(:user) }
before do
allow(issue).to receive(:submittable_as_spam?).and_return(true)
end
it 'tests if the user can submit spam' do
expect(issue.submittable_as_spam_by?(admin)).to be(true)
expect(issue.submittable_as_spam_by?(user)).to be(false)
expect(issue.submittable_as_spam_by?(nil)).to be_nil
end
end
end end
end end
...@@ -21,10 +21,6 @@ describe API::CommitStatuses, api: true do ...@@ -21,10 +21,6 @@ describe API::CommitStatuses, api: true do
let!(:master) { project.pipelines.create(sha: commit.id, ref: 'master') } let!(:master) { project.pipelines.create(sha: commit.id, ref: 'master') }
let!(:develop) { project.pipelines.create(sha: commit.id, ref: 'develop') } let!(:develop) { project.pipelines.create(sha: commit.id, ref: 'develop') }
it_behaves_like 'a paginated resources' do
let(:request) { get api(get_url, reporter) }
end
context "reporter user" do context "reporter user" do
let(:statuses_id) { json_response.map { |status| status['id'] } } let(:statuses_id) { json_response.map { |status| status['id'] } }
...@@ -45,6 +41,7 @@ describe API::CommitStatuses, api: true do ...@@ -45,6 +41,7 @@ describe API::CommitStatuses, api: true do
it 'returns latest commit statuses' do it 'returns latest commit statuses' do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(status3.id, status4.id, status5.id, status6.id) expect(statuses_id).to contain_exactly(status3.id, status4.id, status5.id, status6.id)
json_response.sort_by!{ |status| status['id'] } json_response.sort_by!{ |status| status['id'] }
......
...@@ -14,10 +14,6 @@ describe API::Deployments, api: true do ...@@ -14,10 +14,6 @@ describe API::Deployments, api: true do
describe 'GET /projects/:id/deployments' do describe 'GET /projects/:id/deployments' do
context 'as member of the project' do context 'as member of the project' do
it_behaves_like 'a paginated resources' do
let(:request) { get api("/projects/#{project.id}/deployments", user) }
end
it 'returns projects deployments' do it 'returns projects deployments' do
get api("/projects/#{project.id}/deployments", user) get api("/projects/#{project.id}/deployments", user)
......
...@@ -14,10 +14,6 @@ describe API::Environments, api: true do ...@@ -14,10 +14,6 @@ describe API::Environments, api: true do
describe 'GET /projects/:id/environments' do describe 'GET /projects/:id/environments' do
context 'as member of the project' do context 'as member of the project' do
it_behaves_like 'a paginated resources' do
let(:request) { get api("/projects/#{project.id}/environments", user) }
end
it 'returns project environments' do it 'returns project environments' do
get api("/projects/#{project.id}/environments", user) get api("/projects/#{project.id}/environments", user)
......
...@@ -117,14 +117,20 @@ describe API::Issues, api: true do ...@@ -117,14 +117,20 @@ describe API::Issues, api: true do
expect(json_response.first['labels']).to eq([label.title]) expect(json_response.first['labels']).to eq([label.title])
end end
it 'returns an array of labeled issues when at least one label matches' do it 'returns an array of labeled issues when all labels matches' do
get api("/issues?labels=#{label.title},foo,bar", user) 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 have_http_status(200)
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.length).to eq(1) 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 end
it 'returns an empty array if no issue matches labels' do it 'returns an empty array if no issue matches labels' do
...@@ -356,6 +362,21 @@ describe API::Issues, api: true do ...@@ -356,6 +362,21 @@ describe API::Issues, api: true do
expect(json_response.length).to eq(0) expect(json_response.length).to eq(0)
end 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 it 'returns an empty array if no group issue matches labels' do
get api("#{base_url}?labels=foo,bar", user) get api("#{base_url}?labels=foo,bar", user)
...@@ -549,14 +570,28 @@ describe API::Issues, api: true do ...@@ -549,14 +570,28 @@ describe API::Issues, api: true do
expect(json_response.first['labels']).to eq([label.title]) expect(json_response.first['labels']).to eq([label.title])
end end
it 'returns an array of labeled project issues where all labels match' do it 'returns an array of labeled issues when all labels matches' do
get api("#{base_url}/issues?labels=#{label.title},foo,bar", user) 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 have_http_status(200)
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.length).to eq(1) 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 end
it 'returns an empty array if no project issue matches labels' do it 'returns an empty array if no project issue matches labels' do
......
...@@ -32,10 +32,6 @@ describe API::Notes, api: true do ...@@ -32,10 +32,6 @@ describe API::Notes, api: true do
before { project.team << [user, :reporter] } before { project.team << [user, :reporter] }
describe "GET /projects/:id/noteable/:noteable_id/notes" do describe "GET /projects/:id/noteable/:noteable_id/notes" do
it_behaves_like 'a paginated resources' do
let(:request) { get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) }
end
context "when noteable is an Issue" do context "when noteable is an Issue" do
it "returns an array of issue notes" do it "returns an array of issue notes" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) get api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
......
...@@ -15,15 +15,12 @@ describe API::Pipelines, api: true do ...@@ -15,15 +15,12 @@ describe API::Pipelines, api: true do
before { project.team << [user, :master] } before { project.team << [user, :master] }
describe 'GET /projects/:id/pipelines ' do describe 'GET /projects/:id/pipelines ' do
it_behaves_like 'a paginated resources' do
let(:request) { get api("/projects/#{project.id}/pipelines", user) }
end
context 'authorized user' do context 'authorized user' do
it 'returns project pipelines' do it 'returns project pipelines' do
get api("/projects/#{project.id}/pipelines", user) get api("/projects/#{project.id}/pipelines", user)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.first['sha']).to match /\A\h{40}\z/ expect(json_response.first['sha']).to match /\A\h{40}\z/
expect(json_response.first['id']).to eq pipeline.id expect(json_response.first['id']).to eq pipeline.id
......
...@@ -1235,7 +1235,7 @@ describe API::Projects, api: true do ...@@ -1235,7 +1235,7 @@ describe API::Projects, api: true do
end end
end end
describe 'DELETE /projects/:id/star' do describe 'POST /projects/:id/unstar' do
context 'on a starred project' do context 'on a starred project' do
before do before do
user.toggle_star(project) user.toggle_star(project)
...@@ -1243,16 +1243,16 @@ describe API::Projects, api: true do ...@@ -1243,16 +1243,16 @@ describe API::Projects, api: true do
end end
it 'unstars the project' do it 'unstars the project' do
expect { delete api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1) expect { post api("/projects/#{project.id}/unstar", user) }.to change { project.reload.star_count }.by(-1)
expect(response).to have_http_status(200) expect(response).to have_http_status(201)
expect(json_response['star_count']).to eq(0) expect(json_response['star_count']).to eq(0)
end end
end end
context 'on an unstarred project' do context 'on an unstarred project' do
it 'does not modify the star count' do it 'does not modify the star count' do
expect { delete api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } expect { post api("/projects/#{project.id}/unstar", user) }.not_to change { project.reload.star_count }
expect(response).to have_http_status(304) expect(response).to have_http_status(304)
end end
......
...@@ -107,46 +107,47 @@ describe API::Todos, api: true do ...@@ -107,46 +107,47 @@ describe API::Todos, api: true do
end end
end end
describe 'DELETE /todos/:id' do describe 'POST /todos/:id/mark_as_done' do
context 'when unauthenticated' do context 'when unauthenticated' do
it 'returns authentication error' do it 'returns authentication error' do
delete api("/todos/#{pending_1.id}") post api("/todos/#{pending_1.id}/mark_as_done")
expect(response.status).to eq(401) expect(response).to have_http_status(401)
end end
end end
context 'when authenticated' do context 'when authenticated' do
it 'marks a todo as done' do it 'marks a todo as done' do
delete api("/todos/#{pending_1.id}", john_doe) post api("/todos/#{pending_1.id}/mark_as_done", john_doe)
expect(response.status).to eq(200) expect(response).to have_http_status(201)
expect(json_response['id']).to eq(pending_1.id)
expect(json_response['state']).to eq('done')
expect(pending_1.reload).to be_done expect(pending_1.reload).to be_done
end end
it 'updates todos cache' do it 'updates todos cache' do
expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
delete api("/todos/#{pending_1.id}", john_doe) post api("/todos/#{pending_1.id}/mark_as_done", john_doe)
end end
end end
end end
describe 'DELETE /todos' do describe 'POST /mark_as_done' do
context 'when unauthenticated' do context 'when unauthenticated' do
it 'returns authentication error' do it 'returns authentication error' do
delete api('/todos') post api('/todos/mark_as_done')
expect(response.status).to eq(401) expect(response).to have_http_status(401)
end end
end end
context 'when authenticated' do context 'when authenticated' do
it 'marks all todos as done' do it 'marks all todos as done' do
delete api('/todos', john_doe) post api('/todos/mark_as_done', john_doe)
expect(response.status).to eq(200) expect(response).to have_http_status(204)
expect(response.body).to eq('3')
expect(pending_1.reload).to be_done expect(pending_1.reload).to be_done
expect(pending_2.reload).to be_done expect(pending_2.reload).to be_done
expect(pending_3.reload).to be_done expect(pending_3.reload).to be_done
...@@ -155,7 +156,7 @@ describe API::Todos, api: true do ...@@ -155,7 +156,7 @@ describe API::Todos, api: true do
it 'updates todos cache' do it 'updates todos cache' do
expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
delete api("/todos", john_doe) post api("/todos/mark_as_done", john_doe)
end end
end end
end end
......
...@@ -1093,14 +1093,14 @@ describe API::Users, api: true do ...@@ -1093,14 +1093,14 @@ describe API::Users, api: true do
end end
context "as a user than can see the event's project" do context "as a user than can see the event's project" do
it_behaves_like 'a paginated resources' do
let(:request) { get api("/users/#{user.id}/events", user) }
end
context 'joined event' do context 'joined event' do
it 'returns the "joined" event' do it 'returns the "joined" event' do
get api("/users/#{user.id}/events", user) get api("/users/#{user.id}/events", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
comment_event = json_response.find { |e| e['action_name'] == 'commented on' } comment_event = json_response.find { |e| e['action_name'] == 'commented on' }
expect(comment_event['project_id'].to_i).to eq(project.id) expect(comment_event['project_id'].to_i).to eq(project.id)
......
require 'spec_helper'
describe API::V3::Todos, api: true do
include ApiHelpers
let(:project_1) { create(:empty_project) }
let(:project_2) { create(:empty_project) }
let(:author_1) { create(:user) }
let(:author_2) { create(:user) }
let(:john_doe) { create(:user, username: 'john_doe') }
let!(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe) }
let!(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe) }
let!(:pending_3) { create(:todo, project: project_1, author: author_2, user: john_doe) }
let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) }
before do
project_1.team << [john_doe, :developer]
project_2.team << [john_doe, :developer]
end
describe 'DELETE /todos/:id' do
context 'when unauthenticated' do
it 'returns authentication error' do
delete v3_api("/todos/#{pending_1.id}")
expect(response.status).to eq(401)
end
end
context 'when authenticated' do
it 'marks a todo as done' do
delete v3_api("/todos/#{pending_1.id}", john_doe)
expect(response.status).to eq(200)
expect(pending_1.reload).to be_done
end
it 'updates todos cache' do
expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
delete v3_api("/todos/#{pending_1.id}", john_doe)
end
end
end
describe 'DELETE /todos' do
context 'when unauthenticated' do
it 'returns authentication error' do
delete v3_api('/todos')
expect(response.status).to eq(401)
end
end
context 'when authenticated' do
it 'marks all todos as done' do
delete v3_api('/todos', john_doe)
expect(response.status).to eq(200)
expect(response.body).to eq('3')
expect(pending_1.reload).to be_done
expect(pending_2.reload).to be_done
expect(pending_3.reload).to be_done
end
it 'updates todos cache' do
expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
delete v3_api("/todos", john_doe)
end
end
end
end
...@@ -2,11 +2,11 @@ require 'spec_helper' ...@@ -2,11 +2,11 @@ require 'spec_helper'
describe Users::DestroyService, services: true do describe Users::DestroyService, services: true do
describe "Deletes a user and all their personal projects" do describe "Deletes a user and all their personal projects" do
let!(:user) { create(:user) } let!(:user) { create(:user) }
let!(:current_user) { create(:user) } let!(:admin) { create(:admin) }
let!(:namespace) { create(:namespace, owner: user) } let!(:namespace) { create(:namespace, owner: user) }
let!(:project) { create(:project, namespace: namespace) } let!(:project) { create(:project, namespace: namespace) }
let(:service) { described_class.new(current_user) } let(:service) { described_class.new(admin) }
context 'no options are given' do context 'no options are given' do
it 'deletes the user' do it 'deletes the user' do
...@@ -57,5 +57,26 @@ describe Users::DestroyService, services: true do ...@@ -57,5 +57,26 @@ describe Users::DestroyService, services: true do
expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end end
end end
context "deletion permission checks" do
it 'does not delete the user when user is not an admin' do
other_user = create(:user)
expect { described_class.new(other_user).execute(user) }.to raise_error(Gitlab::Access::AccessDeniedError)
expect(User.exists?(user.id)).to be(true)
end
it 'allows admins to delete anyone' do
described_class.new(admin).execute(user)
expect(User.exists?(user.id)).to be(false)
end
it 'allows users to delete their own account' do
described_class.new(user).execute(user)
expect(User.exists?(user.id)).to be(false)
end
end
end end
end end
# Specs for paginated resources.
#
# Requires an API request:
# let(:request) { get api("/projects/#{project.id}/repository/branches", user) }
shared_examples 'a paginated resources' do
before do
# Fires the request
request
end
it 'has pagination headers' do
expect(response.headers).to include('X-Total')
expect(response.headers).to include('X-Total-Pages')
expect(response.headers).to include('X-Per-Page')
expect(response.headers).to include('X-Page')
expect(response.headers).to include('X-Next-Page')
expect(response.headers).to include('X-Prev-Page')
expect(response.headers).to include('Link')
end
end
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