Commit c0ba747c authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into go-go-gadget-webpack

parents dc623cf4 e7fdb1aa
...@@ -220,8 +220,7 @@ gem 'chronic', '~> 0.10.2' ...@@ -220,8 +220,7 @@ gem 'chronic', '~> 0.10.2'
gem 'chronic_duration', '~> 0.10.6' gem 'chronic_duration', '~> 0.10.6'
gem 'webpack-rails', '~> 0.9.9' gem 'webpack-rails', '~> 0.9.9'
gem 'sassc-rails', '~> 1.3.0'
gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0' gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2' gem 'uglifier', '~> 2.7.2'
gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6' gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6'
...@@ -257,7 +256,6 @@ group :development do ...@@ -257,7 +256,6 @@ group :development do
gem 'brakeman', '~> 3.3.0', require: false gem 'brakeman', '~> 3.3.0', require: false
gem 'letter_opener_web', '~> 1.3.0' gem 'letter_opener_web', '~> 1.3.0'
gem 'rerun', '~> 0.11.0'
gem 'bullet', '~> 5.2.0', require: false gem 'bullet', '~> 5.2.0', require: false
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
gem 'web-console', '~> 2.0' gem 'web-console', '~> 2.0'
...@@ -288,7 +286,7 @@ group :development, :test do ...@@ -288,7 +286,7 @@ group :development, :test do
gem 'minitest', '~> 5.7.0' gem 'minitest', '~> 5.7.0'
# Generate Fake data # Generate Fake data
gem 'ffaker', '~> 2.0.0' gem 'ffaker', '~> 2.4'
gem 'capybara', '~> 2.6.2' gem 'capybara', '~> 2.6.2'
gem 'capybara-screenshot', '~> 1.0.0' gem 'capybara-screenshot', '~> 1.0.0'
......
...@@ -198,7 +198,7 @@ GEM ...@@ -198,7 +198,7 @@ GEM
faraday_middleware-multi_json (0.0.6) faraday_middleware-multi_json (0.0.6)
faraday_middleware faraday_middleware
multi_json multi_json
ffaker (2.0.0) ffaker (2.4.0)
ffi (1.9.10) ffi (1.9.10)
flay (2.6.1) flay (2.6.1)
ruby_parser (~> 3.0) ruby_parser (~> 3.0)
...@@ -407,9 +407,6 @@ GEM ...@@ -407,9 +407,6 @@ GEM
xml-simple xml-simple
licensee (8.0.0) licensee (8.0.0)
rugged (>= 0.24b) rugged (>= 0.24b)
listen (3.0.5)
rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9)
little-plugger (1.1.4) little-plugger (1.1.4)
logging (2.1.0) logging (2.1.0)
little-plugger (~> 1.1) little-plugger (~> 1.1)
...@@ -580,9 +577,6 @@ GEM ...@@ -580,9 +577,6 @@ GEM
rainbow (2.1.0) rainbow (2.1.0)
raindrops (0.17.0) raindrops (0.17.0)
rake (10.5.0) rake (10.5.0)
rb-fsevent (0.9.6)
rb-inotify (0.9.5)
ffi (>= 0.5.0)
rblineprof (0.3.6) rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3) debugger-ruby_core_source (~> 1.3)
rdoc (4.2.2) rdoc (4.2.2)
...@@ -611,8 +605,6 @@ GEM ...@@ -611,8 +605,6 @@ GEM
redis-store (1.2.0) redis-store (1.2.0)
redis (>= 2.2) redis (>= 2.2)
request_store (1.3.1) request_store (1.3.1)
rerun (0.11.0)
listen (~> 3.0)
responders (2.3.0) responders (2.3.0)
railties (>= 4.2.0, < 5.1) railties (>= 4.2.0, < 5.1)
rest-client (2.0.0) rest-client (2.0.0)
...@@ -675,12 +667,17 @@ GEM ...@@ -675,12 +667,17 @@ GEM
sanitize (2.1.0) sanitize (2.1.0)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
sass (3.4.22) sass (3.4.22)
sass-rails (5.0.6) sassc (1.11.1)
railties (>= 4.0.0, < 6) bundler
sass (~> 3.1) ffi (~> 1.9.6)
sprockets (>= 2.8, < 4.0) sass (>= 3.3.0)
sprockets-rails (>= 2.0, < 4.0) sassc-rails (1.3.0)
tilt (>= 1.1, < 3) railties (>= 4.0.0)
sass
sassc (~> 1.9)
sprockets (> 2.11)
sprockets-rails
tilt
sawyer (0.8.1) sawyer (0.8.1)
addressable (>= 2.3.5, < 2.6) addressable (>= 2.3.5, < 2.6)
faraday (~> 0.8, < 1.0) faraday (~> 0.8, < 1.0)
...@@ -873,7 +870,7 @@ DEPENDENCIES ...@@ -873,7 +870,7 @@ DEPENDENCIES
email_reply_trimmer (~> 0.1) email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0) email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0) factory_girl_rails (~> 4.7.0)
ffaker (~> 2.0.0) ffaker (~> 2.4)
flay (~> 2.6.1) flay (~> 2.6.1)
fog-aws (~> 0.9) fog-aws (~> 0.9)
fog-core (~> 1.40) fog-core (~> 1.40)
...@@ -964,7 +961,6 @@ DEPENDENCIES ...@@ -964,7 +961,6 @@ DEPENDENCIES
redis-namespace (~> 1.5.2) redis-namespace (~> 1.5.2)
redis-rails (~> 5.0.1) redis-rails (~> 5.0.1)
request_store (~> 1.3) request_store (~> 1.3)
rerun (~> 0.11.0)
responders (~> 2.0) responders (~> 2.0)
rouge (~> 2.0) rouge (~> 2.0)
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
...@@ -976,7 +972,7 @@ DEPENDENCIES ...@@ -976,7 +972,7 @@ DEPENDENCIES
ruby-prof (~> 0.16.2) ruby-prof (~> 0.16.2)
rugged (~> 0.24.0) rugged (~> 0.24.0)
sanitize (~> 2.0) sanitize (~> 2.0)
sass-rails (~> 5.0.6) sassc-rails (~> 1.3.0)
scss_lint (~> 0.47.0) scss_lint (~> 0.47.0)
seed-fu (~> 2.3.5) seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
.on('click', '.js-unfold', this.handleClickUnfold.bind(this)) .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
.on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this));
this.highlighSelectedLine(); this.openAnchoredDiff();
} }
handleClickUnfold(e) { handleClickUnfold(e) {
...@@ -61,13 +61,22 @@ ...@@ -61,13 +61,22 @@
$.get(link, params, response => $target.parent().replaceWith(response)); $.get(link, params, response => $target.parent().replaceWith(response));
} }
openAnchoredDiff(anchoredDiff, cb) { openAnchoredDiff(cb) {
const diffTitle = $(`#file-path-${anchoredDiff}`); const locationHash = gl.utils.getLocationHash();
const anchoredDiff = locationHash && locationHash.split('_')[0];
if (!anchoredDiff) return;
const diffTitle = $(`#${anchoredDiff}`);
const diffFile = diffTitle.closest('.diff-file'); const diffFile = diffTitle.closest('.diff-file');
const nothingHereBlock = $('.nothing-here-block:visible', diffFile); const nothingHereBlock = $('.nothing-here-block:visible', diffFile);
if (nothingHereBlock.length) { if (nothingHereBlock.length) {
diffFile.singleFileDiff(true, cb); const clickTarget = $('.file-title, .click-to-expand', diffFile);
} else { diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => {
this.highlighSelectedLine();
if (cb) cb();
});
} else if (cb) {
cb(); cb();
} }
} }
......
...@@ -476,7 +476,7 @@ ...@@ -476,7 +476,7 @@
this.removeArrayKeyEvent(); this.removeArrayKeyEvent();
$input = this.dropdown.find(".dropdown-input-field"); $input = this.dropdown.find(".dropdown-input-field");
if (this.options.filterable) { if (this.options.filterable) {
$input.blur().val(""); $input.blur();
} }
if (this.dropdown.find(".dropdown-toggle-page").length) { if (this.dropdown.find(".dropdown-toggle-page").length) {
$('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
......
...@@ -237,13 +237,8 @@ require('./flash'); ...@@ -237,13 +237,8 @@ require('./flash');
} }
this.diffsLoaded = true; this.diffsLoaded = true;
const diffPage = new gl.Diff(); new gl.Diff();
this.scrollToElement('#diffs');
const locationHash = gl.utils.getLocationHash();
const anchoredDiff = locationHash && locationHash.split('_')[0];
if (anchoredDiff) {
diffPage.openAnchoredDiff(anchoredDiff, () => this.scrollToElement('#diffs'));
}
}, },
}); });
} }
......
...@@ -12,6 +12,9 @@ ...@@ -12,6 +12,9 @@
selectable: true, selectable: true,
filterable: true, filterable: true,
fieldName: 'group_id', fieldName: 'group_id',
search: {
fields: ['name']
},
data: function(term, callback) { data: function(term, callback) {
return Api.groups(term, {}, function(data) { return Api.groups(term, {}, function(data) {
data.unshift({ data.unshift({
...@@ -40,6 +43,9 @@ ...@@ -40,6 +43,9 @@
selectable: true, selectable: true,
filterable: true, filterable: true,
fieldName: 'project_id', fieldName: 'project_id',
search: {
fields: ['name']
},
data: function(term, callback) { data: function(term, callback) {
return Api.projects(term, 'id', function(data) { return Api.projects(term, 'id', function(data) {
data.unshift({ data.unshift({
......
/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, padded-blocks, max-len */ /* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, padded-blocks, max-len */
(function() { (function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
...@@ -14,8 +14,7 @@ ...@@ -14,8 +14,7 @@
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>'; COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
function SingleFileDiff(file, forceLoad, cb) { function SingleFileDiff(file) {
var clickTarget;
this.file = file; this.file = file;
this.toggleDiff = bind(this.toggleDiff, this); this.toggleDiff = bind(this.toggleDiff, this);
this.content = $('.diff-content', this.file); this.content = $('.diff-content', this.file);
...@@ -33,14 +32,13 @@ ...@@ -33,14 +32,13 @@
this.content.after(this.collapsedContent); this.content.after(this.collapsedContent);
this.$toggleIcon.addClass('fa-caret-down'); this.$toggleIcon.addClass('fa-caret-down');
} }
clickTarget = $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff);
if (forceLoad) { $('.file-title, .click-to-expand', this.file).on('click', (function (e) {
this.toggleDiff({ target: clickTarget }, cb); this.toggleDiff($(e.target));
} }).bind(this));
} }
SingleFileDiff.prototype.toggleDiff = function(e, cb) { SingleFileDiff.prototype.toggleDiff = function($target, cb) {
var $target = $(e.target);
if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return; if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
this.isOpen = !this.isOpen; this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) { if (!this.isOpen && !this.hasError) {
...@@ -91,10 +89,10 @@ ...@@ -91,10 +89,10 @@
})(); })();
$.fn.singleFileDiff = function(forceLoad, cb) { $.fn.singleFileDiff = function() {
return this.each(function() { return this.each(function() {
if (!$.data(this, 'singleFileDiff') || forceLoad) { if (!$.data(this, 'singleFileDiff')) {
return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this, forceLoad, cb)); return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this));
} }
}); });
}; };
......
/* global Vue, Flash, gl */ /* global Vue, Flash, gl */
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign, no-bitwise */
((gl) => { ((gl) => {
gl.VueStage = Vue.extend({ gl.VueStage = Vue.extend({
data() { data() {
return { return {
request: false, count: 0,
builds: '', builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>', spinner: '<span class="fa fa-spinner fa-spin"></span>',
}; };
...@@ -13,29 +13,23 @@ ...@@ -13,29 +13,23 @@
props: ['stage', 'svgs', 'match'], props: ['stage', 'svgs', 'match'],
methods: { methods: {
fetchBuilds() { fetchBuilds() {
if (this.request) return this.clearBuilds(); if (this.count > 0) return null;
return this.$http.get(this.stage.dropdown_path) return this.$http.get(this.stage.dropdown_path)
.then((response) => { .then((response) => {
this.request = true; this.count += 1;
this.builds = JSON.parse(response.body).html; this.builds = JSON.parse(response.body).html;
}, () => { }, () => {
const flash = new Flash('Something went wrong on our end.'); const flash = new Flash('Something went wrong on our end.');
this.request = false;
return flash; return flash;
}); });
}, },
clearBuilds() {
this.builds = '';
this.request = false;
},
}, },
computed: { computed: {
buildsOrSpinner() { buildsOrSpinner() {
return this.request ? this.builds : this.spinner; return this.builds ? this.builds : this.spinner;
}, },
dropdownClass() { dropdownClass() {
if (this.request) return 'js-builds-dropdown-container'; if (this.builds) return 'js-builds-dropdown-container';
return 'js-builds-dropdown-loading builds-dropdown-loading'; return 'js-builds-dropdown-loading builds-dropdown-loading';
}, },
buildStatus() { buildStatus() {
...@@ -57,7 +51,6 @@ ...@@ -57,7 +51,6 @@
<div> <div>
<button <button
@click='fetchBuilds' @click='fetchBuilds'
@blur='fetchBuilds'
:class="triggerButtonClass" :class="triggerButtonClass"
:title='stage.title' :title='stage.title'
data-placement="top" data-placement="top"
......
...@@ -50,3 +50,77 @@ ...@@ -50,3 +50,77 @@
.pulse { .pulse {
@include webkit-prefix(animation-name, pulse); @include webkit-prefix(animation-name, pulse);
} }
/*
* General hover animations
*/
// Sass multiple transitions mixin | https://gist.github.com/tobiasahlin/7a421fb9306a4f518aab
// Usage: @include transition(width, height 0.3s ease-in-out);
// Output: -webkit-transition(width 0.2s, height 0.3s ease-in-out);
// transition(width 0.2s, height 0.3s ease-in-out);
//
// Pass in any number of transitions
@mixin transition($transitions...) {
$unfoldedTransitions: ();
@each $transition in $transitions {
$unfoldedTransitions: append($unfoldedTransitions, unfoldTransition($transition), comma);
}
transition: $unfoldedTransitions;
}
@function unfoldTransition ($transition) {
// Default values
$property: all;
$duration: $general-hover-transition-duration;
$easing: $general-hover-transition-curve; // Browser default is ease, which is what we want
$delay: null; // Browser default is 0, which is what we want
$defaultProperties: ($property, $duration, $easing, $delay);
// Grab transition properties if they exist
$unfoldedTransition: ();
@for $i from 1 through length($defaultProperties) {
$p: null;
@if $i <= length($transition) {
$p: nth($transition, $i);
} @else {
$p: nth($defaultProperties, $i);
}
$unfoldedTransition: append($unfoldedTransition, $p);
}
@return $unfoldedTransition;
}
.btn,
.side-nav-toggle {
@include transition(background-color, border-color, color, box-shadow);
}
.dropdown-menu-toggle,
.avatar-circle,
.header-user-avatar {
@include transition(border-color);
}
.note-action-button .link-highlight,
.toolbar-btn,
.dropdown-toggle-caret,
.fa:not(.fa-bell) {
@include transition(color);
}
a {
@include transition(background-color, color, border);
}
.tree-table td,
.well-list > li {
@include transition(background-color, border-color);
}
.stage-nav-item {
@include transition(background-color, box-shadow);
}
...@@ -52,6 +52,10 @@ ...@@ -52,6 +52,10 @@
border-radius: 0; border-radius: 0;
border: none; border: none;
} }
&:not([href]):hover {
border-color: rgba($avatar-border, .2);
}
} }
.identicon { .identicon {
......
...@@ -57,6 +57,14 @@ header { ...@@ -57,6 +57,14 @@ header {
&.header-user-dropdown-toggle { &.header-user-dropdown-toggle {
margin-left: 14px; margin-left: 14px;
&:hover,
&:focus,
&:active {
.header-user-avatar {
border-color: rgba($avatar-border, .2);
}
}
} }
&:hover, &:hover,
...@@ -104,6 +112,7 @@ header { ...@@ -104,6 +112,7 @@ header {
&:hover { &:hover {
background-color: $white-normal; background-color: $white-normal;
color: $gl-header-nav-hover-color;
} }
} }
} }
...@@ -180,6 +189,7 @@ header { ...@@ -180,6 +189,7 @@ header {
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
color: $gl-header-nav-hover-color;
} }
} }
...@@ -198,7 +208,7 @@ header { ...@@ -198,7 +208,7 @@ header {
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: darken($color: $gl-text-color, $amount: 30%); color: $gl-header-nav-hover-color;
} }
} }
...@@ -271,4 +281,5 @@ header { ...@@ -271,4 +281,5 @@ header {
float: left; float: left;
margin-right: 5px; margin-right: 5px;
border-radius: 50%; border-radius: 50%;
border: 1px solid $avatar-border;
} }
...@@ -101,7 +101,7 @@ ...@@ -101,7 +101,7 @@
&:hover, &:hover,
&:active, &:active,
&:focus { &:focus {
border-bottom: none; border-color: transparent;
} }
} }
} }
......
...@@ -102,6 +102,10 @@ $gl-text-red: #d12f19; ...@@ -102,6 +102,10 @@ $gl-text-red: #d12f19;
$gl-text-orange: #d90; $gl-text-orange: #d90;
$gl-link-color: #3777b0; $gl-link-color: #3777b0;
$gl-grayish-blue: #7f8fa4; $gl-grayish-blue: #7f8fa4;
$gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
$gl-header-color: #4c4e54;
$gl-header-nav-hover-color: #434343;
/* /*
* Lists * Lists
...@@ -172,6 +176,9 @@ $count-arrow-border: #dce0e5; ...@@ -172,6 +176,9 @@ $count-arrow-border: #dce0e5;
$save-project-loader-color: #555; $save-project-loader-color: #555;
$divergence-graph-bar-bg: #ccc; $divergence-graph-bar-bg: #ccc;
$divergence-graph-separator-bg: #ccc; $divergence-graph-separator-bg: #ccc;
$general-hover-transition-duration: 150ms;
$general-hover-transition-curve: linear;
/* /*
* Common component specific colors * Common component specific colors
......
...@@ -20,6 +20,10 @@ ...@@ -20,6 +20,10 @@
.fa { .fa {
color: $cycle-analytics-light-gray; color: $cycle-analytics-light-gray;
&:hover {
color: $gl-text-color;
}
} }
.stage-header { .stage-header {
......
...@@ -154,8 +154,8 @@ ...@@ -154,8 +154,8 @@
.edit-link { .edit-link {
color: $gl-text-color; color: $gl-text-color;
&:hover { &:not([href]):hover {
color: $md-link-color; color: rgba($avatar-border, .2);
} }
} }
} }
...@@ -332,6 +332,10 @@ ...@@ -332,6 +332,10 @@
&:hover { &:hover {
color: $md-link-color; color: $md-link-color;
text-decoration: none; text-decoration: none;
.avatar {
border-color: rgba($avatar-border, .2);
}
} }
} }
......
...@@ -203,6 +203,10 @@ ...@@ -203,6 +203,10 @@
z-index: 3; z-index: 3;
border-radius: $label-border-radius; border-radius: $label-border-radius;
padding: 6px 10px 6px 9px; padding: 6px 10px 6px 9px;
&:hover {
box-shadow: inset 0 0 0 80px $label-remove-border;
}
} }
.btn { .btn {
......
...@@ -216,8 +216,8 @@ ...@@ -216,8 +216,8 @@
} }
} }
.user-profile {
.user-profile {
.cover-controls a { .cover-controls a {
margin-left: 5px; margin-left: 5px;
} }
...@@ -231,8 +231,11 @@ ...@@ -231,8 +231,11 @@
} }
} }
@media (max-width: $screen-xs-max) { .user-profile-nav {
font-size: 0;
}
@media (max-width: $screen-xs-max) {
.cover-block { .cover-block {
padding-top: 20px; padding-top: 20px;
} }
...@@ -253,6 +256,12 @@ ...@@ -253,6 +256,12 @@
} }
} }
} }
.user-profile-nav {
a {
margin-right: 0;
}
}
} }
} }
......
...@@ -14,6 +14,20 @@ ...@@ -14,6 +14,20 @@
} }
} }
.search form:hover,
.file-finder-input:hover,
.issuable-search-form:hover,
.search-text-input:hover,
textarea:hover,
.form-control:hover {
border-color: lighten($dropdown-input-focus-border, 20%);
box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
}
input[type="checkbox"]:hover {
box-shadow: 0 0 2px 2px lighten($search-input-focus-shadow-color, 20%), 0 0 0 1px lighten($search-input-focus-shadow-color, 20%);
}
.search { .search {
margin-right: 10px; margin-right: 10px;
margin-left: 10px; margin-left: 10px;
......
...@@ -42,19 +42,16 @@ class ProjectsController < Projects::ApplicationController ...@@ -42,19 +42,16 @@ class ProjectsController < Projects::ApplicationController
end end
def update def update
status = ::Projects::UpdateService.new(@project, current_user, project_params).execute result = ::Projects::UpdateService.new(@project, current_user, project_params).execute
# Refresh the repo in case anything changed # Refresh the repo in case anything changed
@repository = project.repository @repository = @project.repository
respond_to do |format| respond_to do |format|
if status if result[:status] == :success
flash[:notice] = "Project '#{@project.name}' was successfully updated." flash[:notice] = "Project '#{@project.name}' was successfully updated."
format.html do format.html do
redirect_to( redirect_to(edit_project_path(@project))
edit_project_path(@project),
notice: "Project '#{@project.name}' was successfully updated."
)
end end
else else
format.html { render 'edit' } format.html { render 'edit' }
......
...@@ -1032,7 +1032,7 @@ class Project < ActiveRecord::Base ...@@ -1032,7 +1032,7 @@ class Project < ActiveRecord::Base
"refs/heads/#{branch}", "refs/heads/#{branch}",
force: true) force: true)
repository.copy_gitattributes(branch) repository.copy_gitattributes(branch)
repository.expire_avatar_cache repository.after_change_head
reload_default_branch reload_default_branch
end end
......
...@@ -439,6 +439,11 @@ class Repository ...@@ -439,6 +439,11 @@ class Repository
expire_content_cache expire_content_cache
end end
# Runs code after the HEAD of a repository is changed.
def after_change_head
expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys)
end
# Runs code after a repository has been forked/imported. # Runs code after a repository has been forked/imported.
def after_import def after_import
expire_content_cache expire_content_cache
......
...@@ -2,7 +2,7 @@ class BuildActionEntity < Grape::Entity ...@@ -2,7 +2,7 @@ class BuildActionEntity < Grape::Entity
include RequestAwareEntity include RequestAwareEntity
expose :name do |build| expose :name do |build|
build.name.humanize build.name
end end
expose :path do |build| expose :path do |build|
......
...@@ -9,7 +9,7 @@ module Projects ...@@ -9,7 +9,7 @@ module Projects
Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(project, new_visibility) deny_visibility_level(project, new_visibility)
return project return error('Visibility level unallowed')
end end
end end
...@@ -23,6 +23,10 @@ module Projects ...@@ -23,6 +23,10 @@ module Projects
if project.previous_changes.include?('path') if project.previous_changes.include?('path')
project.rename_repo project.rename_repo
end end
success
else
error('Project could not be updated')
end end
end end
end end
......
...@@ -26,8 +26,26 @@ module Users ...@@ -26,8 +26,26 @@ module Users
user.reload user.reload
end end
# This method returns the updated User object.
def execute def execute
lease_key = "refresh_authorized_projects:#{user.id}"
lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
until uuid = lease.try_obtain
# Keep trying until we obtain the lease. If we don't do so we may end up
# not updating the list of authorized projects properly. To prevent
# hammering Redis too much we'll wait for a bit between retries.
sleep(1)
end
begin
execute_without_lease
ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
end
end
# This method returns the updated User object.
def execute_without_lease
current = current_authorizations_per_project current = current_authorizations_per_project
fresh = fresh_access_levels_per_project fresh = fresh_access_levels_per_project
...@@ -47,26 +65,7 @@ module Users ...@@ -47,26 +65,7 @@ module Users
end end
end end
update_with_lease(remove, add)
end
# Updates the list of authorizations using an exclusive lease.
def update_with_lease(remove = [], add = [])
lease_key = "refresh_authorized_projects:#{user.id}"
lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
until uuid = lease.try_obtain
# Keep trying until we obtain the lease. If we don't do so we may end up
# not updating the list of authorized projects properly. To prevent
# hammering Redis too much we'll wait for a bit between retries.
sleep(1)
end
begin
update_authorizations(remove, add) update_authorizations(remove, add)
ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
end
end end
# Updates the list of authorizations for the current user. # Updates the list of authorizations for the current user.
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host} or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
.col-lg-9 .col-lg-9
.clearfix.avatar-image.append-bottom-default .clearfix.avatar-image.append-bottom-default
= link_to avatar_icon(@user, 400), target: '_blank' do
= image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160' = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
%h5.prepend-top-0 %h5.prepend-top-0
Upload new avatar Upload new avatar
......
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
- if @project.protected_branch? branch.name - if @project.protected_branch? branch.name
%span.label.label-success %span.label.label-success
%i.fa.fa-lock
protected protected
.controls.hidden-xs .controls.hidden-xs
- if merge_project && create_mr_button?(@repository.root_ref, branch.name) - if merge_project && create_mr_button?(@repository.root_ref, branch.name)
......
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
%li %li
= link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do = link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do
= custom_icon('icon_play') = custom_icon('icon_play')
%span= build.name.humanize %span= build.name
- if artifacts.present? - if artifacts.present?
.btn-group .btn-group
%button.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{ type: 'button', 'data-toggle' => 'dropdown' } %button.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{ type: 'button', 'data-toggle' => 'dropdown' }
......
.diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id) } .diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id) }
.file-title{ id: "file-path-#{hexdigest(diff_file.file_path)}" } .file-title
= render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}" = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}"
- unless diff_file.submodule? - unless diff_file.submodule?
......
---
title: Remove Lock Icon on Protected Tag
merge_request: 8513
author: Sergey Nikitin
---
title: Use original casing for build action text
merge_request: 8387
author:
---
title: Add various hover animations throughout the application
merge_request:
author:
---
title: Fix search group/project filtering to show results
merge_request:
author:
---
title: Allow API query to find projects with dots in their name
merge_request:
author: Bruno Melli
---
title: Expire related caches after changing HEAD
merge_request:
author: Minqi Pan
---
title: Synchronize all project authorization refreshing work to prevent race conditions
merge_request:
author:
---
title: Ensure updating project settings shows a flash message on success
merge_request: 8579
author: Sandish Chen
---
title: Switch to sassc-rails for faster stylesheet compilation
merge_request: 8556
author: Richard Macklin
...@@ -14,9 +14,11 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration ...@@ -14,9 +14,11 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
namespace_id = user['namespace_id'] namespace_id = user['namespace_id']
path_was = user['username'] path_was = user['username']
path_was_wildcard = quote_string("#{path_was}/%") path_was_wildcard = quote_string("#{path_was}/%")
path = quote_string(new_path(path_was))
path = move_namespace(namespace_id, path_was, path) move_namespace(namespace_id, path_was, path)
begin
execute "UPDATE routes SET path = '#{path}' WHERE source_type = 'Namespace' AND source_id = #{namespace_id}" execute "UPDATE routes SET path = '#{path}' WHERE source_type = 'Namespace' AND source_id = #{namespace_id}"
execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{namespace_id}" execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{namespace_id}"
execute "UPDATE users SET username = '#{path}' WHERE id = #{id}" execute "UPDATE users SET username = '#{path}' WHERE id = #{id}"
...@@ -25,6 +27,13 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration ...@@ -25,6 +27,13 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
new_path = "#{path}/#{route['path'].split('/').last}" new_path = "#{path}/#{route['path'].split('/').last}"
execute "UPDATE routes SET path = '#{new_path}' WHERE id = #{route['id']}" execute "UPDATE routes SET path = '#{new_path}' WHERE id = #{route['id']}"
end end
rescue => e
say("Couldn't update routes for path #{path_was} to #{path}")
# Move namespace back
move_namespace(namespace_id, path, path_was)
raise e
end
end end
end end
...@@ -44,23 +53,30 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration ...@@ -44,23 +53,30 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present? select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present?
end end
def path_exists?(repository_storage_path, path) def path_exists?(path, repository_storage_path)
gitlab_shell.exists?(repository_storage_path, path) repository_storage_path && gitlab_shell.exists?(repository_storage_path, path)
end end
# Accepts invalid path like test.git and returns test_git or # Accepts invalid path like test.git and returns test_git or
# test_git1 if test_git already taken # test_git1 if test_git already taken
def rename_path(repository_storage_path, path) def new_path(path)
# To stay closer with original name and reduce risk of duplicates # To stay closer with original name and reduce risk of duplicates
# we rename suffix instead of removing it # we rename suffix instead of removing it
path = path.sub(/\.git\z/, '_git') path = path.sub(/\.git\z/, '_git')
counter = 0 check_routes(path.dup, 0, path)
base = path end
def check_routes(base, counter, path)
route_exists = route_exists?(path)
while route_exists?(path) || path_exists?(repository_storage_path, path) Gitlab.config.repositories.storages.each_value do |storage|
if route_exists || path_exists?(path, storage)
counter += 1 counter += 1
path = "#{base}#{counter}" path = "#{base}#{counter}"
return check_routes(base, counter, path)
end
end end
path path
...@@ -76,8 +92,6 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration ...@@ -76,8 +92,6 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
# Ensure old directory exists before moving it # Ensure old directory exists before moving it
gitlab_shell.add_namespace(repository_storage_path, path_was) gitlab_shell.add_namespace(repository_storage_path, path_was)
path = quote_string(rename_path(repository_storage_path, path_was))
unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path) unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}" Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
...@@ -87,8 +101,14 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration ...@@ -87,8 +101,14 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
end end
end end
begin
Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
rescue => e
path if path.nil?
say("Couldn't find a storage path for #{namespace_id}, #{path_was} -- skipping")
else
raise e
end
end
end end
end end
@admin
Feature: Admin Groups
Background:
Given I sign in as an admin
And I have group with projects
And User "John Doe" exists
And I visit admin groups page
Scenario: See group list
Then I should be all groups
Scenario: Create a group
When I click new group link
And submit form with new group info
Then I should be redirected to group page
And I should see newly created group
@javascript
Scenario: Add user into projects in group
When I visit admin group page
When I select user "John Doe" from user list as "Reporter"
Then I should see "John Doe" in team list in every project as "Reporter"
Scenario: Shared projects
Given group has shared projects
When I visit group page
Then I should see project shared with group
@javascript
Scenario: Invite user to a group by e-mail
When I visit admin group page
When I select user "johndoe@gitlab.com" from user list as "Reporter"
Then I should see "johndoe@gitlab.com" in team list in every project as "Reporter"
@javascript
Scenario: Signed in admin should be able to add himself to a group
Given "John Doe" is owner of group "Owned"
When I visit group "Owned" members page
When I select current user as "Developer"
Then I should see current user as "Developer"
@javascript
Scenario: Signed in admin should be able to remove himself from group
Given current user is developer of group "Owned"
When I visit group "Owned" members page
Then I should see current user as "Developer"
When I click on the "Remove User From Group" button for current user
When I visit group "Owned" members page
Then I should not see current user as "Developer"
class Spinach::Features::AdminGroups < Spinach::FeatureSteps
include SharedAuthentication
include SharedGroup
include SharedPaths
include SharedUser
include SharedActiveTab
include Select2Helper
When 'I visit admin group page' do
visit admin_group_path(current_group)
end
When 'I click new group link' do
click_link "New Group"
end
step 'I have group with projects' do
@group = create(:group)
@project = create(:project, group: @group)
@event = create(:closed_issue_event, project: @project)
@project.team << [current_user, :master]
end
step 'submit form with new group info' do
fill_in 'group_path', with: 'gitlab'
fill_in 'group_description', with: 'Group description'
click_button "Create group"
end
step 'I should see newly created group' do
expect(page).to have_content "Group: gitlab"
expect(page).to have_content "Group description"
end
step 'I should be redirected to group page' do
expect(current_path).to eq admin_group_path(Group.find_by(path: 'gitlab'))
end
When 'I select user "John Doe" from user list as "Reporter"' do
select2(user_john.id, from: "#user_ids", multiple: true)
page.within "#new_project_member" do
select "Reporter", from: "access_level"
end
click_button "Add users to group"
end
When 'I select user "johndoe@gitlab.com" from user list as "Reporter"' do
select2('johndoe@gitlab.com', from: "#user_ids", multiple: true)
page.within "#new_project_member" do
select "Reporter", from: "access_level"
end
click_button "Add users to group"
end
step 'I should see "John Doe" in team list in every project as "Reporter"' do
page.within ".group-users-list" do
expect(page).to have_content "John Doe"
expect(page).to have_content "Reporter"
end
end
step 'I should see "johndoe@gitlab.com" in team list in every project as "Reporter"' do
page.within ".group-users-list" do
expect(page).to have_content "johndoe@gitlab.com"
expect(page).to have_content "Invited by"
expect(page).to have_content "Reporter"
end
end
step 'I should be all groups' do
Group.all.each do |group|
expect(page).to have_content group.name
end
end
step 'group has shared projects' do
share_link = shared_project.project_group_links.new(group_access: Gitlab::Access::MASTER)
share_link.group_id = current_group.id
share_link.save!
end
step 'I visit group page' do
visit admin_group_path(current_group)
end
step 'I should see project shared with group' do
expect(page).to have_content(shared_project.name_with_namespace)
expect(page).to have_content "Projects shared with"
end
step 'we have user "John Doe" in group' do
current_group.add_reporter(user_john)
end
step 'I should not see "John Doe" in team list' do
page.within ".group-users-list" do
expect(page).not_to have_content "John Doe"
end
end
step 'I select current user as "Developer"' do
page.within ".users-group-form" do
select2(current_user.id, from: "#user_ids", multiple: true)
select "Developer", from: "access_level"
end
click_button "Add to group"
end
step 'I should see current user as "Developer"' do
page.within '.content-list' do
expect(page).to have_content(current_user.name)
expect(page).to have_content('Developer')
end
end
step 'I click on the "Remove User From Group" button for current user' do
find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click
# poltergeist always confirms popups.
end
step 'I should not see current user as "Developer"' do
page.within '.content-list' do
expect(page).not_to have_content(current_user.name)
expect(page).not_to have_content('Developer')
end
end
protected
def current_group
@group ||= Group.first
end
def shared_project
@shared_project ||= create(:empty_project)
end
def user_john
@user_john ||= User.find_by(name: "John Doe")
end
end
...@@ -191,10 +191,6 @@ module SharedPaths ...@@ -191,10 +191,6 @@ module SharedPaths
visit admin_background_jobs_path visit admin_background_jobs_path
end end
step 'I visit admin groups page' do
visit admin_groups_path
end
step 'I visit admin teams page' do step 'I visit admin teams page' do
visit admin_teams_path visit admin_teams_path
end end
......
...@@ -294,7 +294,7 @@ module API ...@@ -294,7 +294,7 @@ module API
header['X-Sendfile'] = path header['X-Sendfile'] = path
body body
else else
file FileStreamer.new(path) path
end end
end end
......
...@@ -159,7 +159,7 @@ module API ...@@ -159,7 +159,7 @@ module API
use :sort_params use :sort_params
use :pagination use :pagination
end end
get "/search/:query" do get "/search/:query", requirements: { query: /[^\/]+/ } do
search_service = Search::GlobalService.new(current_user, search: params[:query]).execute search_service = Search::GlobalService.new(current_user, search: params[:query]).execute
projects = search_service.objects('projects', params[:page]) projects = search_service.objects('projects', params[:page])
projects = projects.reorder(params[:order_by] => params[:sort]) projects = projects.reorder(params[:order_by] => params[:sort])
...@@ -295,13 +295,13 @@ module API ...@@ -295,13 +295,13 @@ module API
authorize! :rename_project, user_project if attrs[:name].present? authorize! :rename_project, user_project if attrs[:name].present?
authorize! :change_visibility_level, user_project if attrs[:visibility_level].present? authorize! :change_visibility_level, user_project if attrs[:visibility_level].present?
::Projects::UpdateService.new(user_project, current_user, attrs).execute result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
if user_project.errors.any? if result[:status] == :success
render_validation_error!(user_project)
else
present user_project, with: Entities::Project, present user_project, with: Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, user_project) user_can_admin_project: can?(current_user, :admin_project, user_project)
else
render_validation_error!(user_project)
end end
end end
......
...@@ -7,9 +7,4 @@ namespace :dev do ...@@ -7,9 +7,4 @@ namespace :dev do
Rake::Task["gitlab:setup"].invoke Rake::Task["gitlab:setup"].invoke
Rake::Task["gitlab:shell:setup"].invoke Rake::Task["gitlab:shell:setup"].invoke
end end
desc 'GitLab | Start/restart foreman and watch for changes'
task :foreman => :environment do
sh 'rerun --dir app,config,lib -- foreman start'
end
end end
...@@ -245,7 +245,7 @@ describe ProjectsController do ...@@ -245,7 +245,7 @@ describe ProjectsController do
expect(project.repository.path).to include(new_path) expect(project.repository.path).to include(new_path)
expect(assigns(:repository).path).to eq(project.repository.path) expect(assigns(:repository).path).to eq(project.repository.path)
expect(response).to have_http_status(200) expect(response).to have_http_status(302)
end end
end end
......
require 'spec_helper' require 'spec_helper'
feature 'Admin Groups', feature: true do feature 'Admin Groups', feature: true do
include Select2Helper
let(:internal) { Gitlab::VisibilityLevel::INTERNAL } let(:internal) { Gitlab::VisibilityLevel::INTERNAL }
let(:user) { create :user }
let!(:group) { create :group }
let!(:current_user) { login_as :admin }
before do before do
login_as(:admin)
stub_application_setting(default_group_visibility: internal) stub_application_setting(default_group_visibility: internal)
end end
describe 'list' do
it 'renders groups' do
visit admin_groups_path
expect(page).to have_content(group.name)
end
end
describe 'create a group' do describe 'create a group' do
it 'creates new group' do
visit admin_groups_path
click_link "New Group"
fill_in 'group_path', with: 'gitlab'
fill_in 'group_description', with: 'Group description'
click_button "Create group"
expect(current_path).to eq admin_group_path(Group.find_by(path: 'gitlab'))
expect(page).to have_content('Group: gitlab')
expect(page).to have_content('Group description')
end
scenario 'shows the visibility level radio populated with the default value' do scenario 'shows the visibility level radio populated with the default value' do
visit new_admin_group_path visit new_admin_group_path
...@@ -37,6 +61,91 @@ feature 'Admin Groups', feature: true do ...@@ -37,6 +61,91 @@ feature 'Admin Groups', feature: true do
end end
end end
describe 'add user into a group', js: true do
shared_context 'adds user into a group' do
it do
visit admin_group_path(group)
select2(user_selector, from: '#user_ids', multiple: true)
page.within '#new_project_member' do
select2(Gitlab::Access::REPORTER, from: '#access_level')
end
click_button "Add users to group"
page.within ".group-users-list" do
expect(page).to have_content(user.name)
expect(page).to have_content('Reporter')
end
end
end
it_behaves_like 'adds user into a group' do
let(:user_selector) { user.id }
end
it_behaves_like 'adds user into a group' do
let(:user_selector) { user.email }
end
end
describe 'add admin himself to a group' do
before do
group.add_user(:user, Gitlab::Access::OWNER)
end
it 'adds admin a to a group as developer', js: true do
visit group_group_members_path(group)
page.within '.users-group-form' do
select2(current_user.id, from: '#user_ids', multiple: true)
select 'Developer', from: 'access_level'
end
click_button 'Add to group'
page.within '.content-list' do
expect(page).to have_content(current_user.name)
expect(page).to have_content('Developer')
end
end
end
describe 'admin remove himself from a group', js: true do
it 'removes admin from the group' do
group.add_user(current_user, Gitlab::Access::DEVELOPER)
visit group_group_members_path(group)
page.within '.content-list' do
expect(page).to have_content(current_user.name)
expect(page).to have_content('Developer')
end
find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click
visit group_group_members_path(group)
page.within '.content-list' do
expect(page).not_to have_content(current_user.name)
expect(page).not_to have_content('Developer')
end
end
end
describe 'shared projects' do
it 'renders shared project' do
empty_project = create(:empty_project)
empty_project.project_group_links.create!(
group_access: Gitlab::Access::MASTER,
group: group
)
visit admin_group_path(group)
expect(page).to have_content(empty_project.name_with_namespace)
expect(page).to have_content('Projects shared with')
end
end
def expect_selected_visibility(level) def expect_selected_visibility(level)
selector = "#group_visibility_level_#{level}[checked=checked]" selector = "#group_visibility_level_#{level}[checked=checked]"
......
...@@ -125,7 +125,9 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -125,7 +125,9 @@ describe 'Issue Boards', feature: true, js: true do
first('.card').click first('.card').click
end end
page.within('.assignee') do page.within(find('.assignee')) do
expect(page).to have_content('No assignee')
click_link 'assign yourself' click_link 'assign yourself'
wait_for_vue_resource wait_for_vue_resource
......
...@@ -4,10 +4,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do ...@@ -4,10 +4,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do
include WaitForAjax include WaitForAjax
let(:branch) { 'expand-collapse-diffs' } let(:branch) { 'expand-collapse-diffs' }
let(:project) { create(:project) }
before do before do
login_as :admin login_as :admin
project = create(:project)
# Ensure that undiffable.md is in .gitattributes # Ensure that undiffable.md is in .gitattributes
project.repository.copy_gitattributes(branch) project.repository.copy_gitattributes(branch)
...@@ -31,6 +31,33 @@ feature 'Expand and collapse diffs', js: true, feature: true do ...@@ -31,6 +31,33 @@ feature 'Expand and collapse diffs', js: true, feature: true do
define_method(file.split('.').first) { file_container(file) } define_method(file.split('.').first) { file_container(file) }
end end
it 'should show the diff content with a highlighted line when linking to line' do
expect(large_diff).not_to have_selector('.code')
expect(large_diff).to have_selector('.nothing-here-block')
visit namespace_project_commit_path(project.namespace, project, project.commit(branch), anchor: "#{large_diff[:id]}_0_1")
execute_script('window.location.reload()')
wait_for_ajax
expect(large_diff).to have_selector('.code')
expect(large_diff).not_to have_selector('.nothing-here-block')
expect(large_diff).to have_selector('.hll')
end
it 'should show the diff content when linking to file' do
expect(large_diff).not_to have_selector('.code')
expect(large_diff).to have_selector('.nothing-here-block')
visit namespace_project_commit_path(project.namespace, project, project.commit(branch), anchor: large_diff[:id])
execute_script('window.location.reload()')
wait_for_ajax
expect(large_diff).to have_selector('.code')
expect(large_diff).not_to have_selector('.nothing-here-block')
end
context 'visiting a commit with collapsed diffs' do context 'visiting a commit with collapsed diffs' do
it 'shows small diffs immediately' do it 'shows small diffs immediately' do
expect(small_diff).to have_selector('.code') expect(small_diff).to have_selector('.code')
......
...@@ -128,13 +128,13 @@ describe 'Pipelines', :feature, :js do ...@@ -128,13 +128,13 @@ describe 'Pipelines', :feature, :js do
it 'has link to the manual action' do it 'has link to the manual action' do
find('.js-pipeline-dropdown-manual-actions').click find('.js-pipeline-dropdown-manual-actions').click
expect(page).to have_link('Manual build') expect(page).to have_link('manual build')
end end
context 'when manual action was played' do context 'when manual action was played' do
before do before do
find('.js-pipeline-dropdown-manual-actions').click find('.js-pipeline-dropdown-manual-actions').click
click_link('Manual build') click_link('manual build')
end end
it 'enqueues manual action job' do it 'enqueues manual action job' do
......
...@@ -21,6 +21,16 @@ describe 'Edit Project Settings', feature: true do ...@@ -21,6 +21,16 @@ describe 'Edit Project Settings', feature: true do
expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'." expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
expect(page).to have_button 'Save changes' expect(page).to have_button 'Save changes'
end end
scenario 'shows a successful notice when the project is updated' do
visit edit_namespace_project_path(project.namespace, project)
fill_in 'project_name_edit', with: 'hello world'
click_button 'Save changes'
expect(page).to have_content "Project 'hello world' was successfully updated."
end
end end
describe 'Rename repository' do describe 'Rename repository' do
......
...@@ -49,6 +49,9 @@ require('~/lib/utils/type_utility'); ...@@ -49,6 +49,9 @@ require('~/lib/utils/type_utility');
selectable: true, selectable: true,
filterable: isFilterable, filterable: isFilterable,
data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData, data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
search: {
fields: ['name']
},
text: (project) => { text: (project) => {
(project.name_with_namespace || project.name); (project.name_with_namespace || project.name);
}, },
...@@ -166,5 +169,21 @@ require('~/lib/utils/type_utility'); ...@@ -166,5 +169,21 @@ require('~/lib/utils/type_utility');
expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
}); });
}); });
it('should still have input value on close and restore', () => {
let $searchInput = $(SEARCH_INPUT_SELECTOR);
initDropDown.call(this, false, true);
$searchInput
.trigger('focus')
.val('g')
.trigger('input');
expect($searchInput.val()).toEqual('g');
this.dropdownButtonElement.trigger('hidden.bs.dropdown');
$searchInput
.trigger('blur')
.trigger('focus');
expect($searchInput.val()).toEqual('g');
});
}); });
})(); })();
...@@ -5,17 +5,11 @@ require Rails.root.join('db', 'migrate', '20161226122833_remove_dot_git_from_use ...@@ -5,17 +5,11 @@ require Rails.root.join('db', 'migrate', '20161226122833_remove_dot_git_from_use
describe RemoveDotGitFromUsernames do describe RemoveDotGitFromUsernames do
let(:user) { create(:user) } let(:user) { create(:user) }
describe '#up' do
let(:migration) { described_class.new } let(:migration) { described_class.new }
describe '#up' do
before do before do
namespace = user.namespace update_namespace(user, 'test.git')
namespace.path = 'test.git'
namespace.save!(validate: false)
user.username = 'test.git'
user.save!(validate: false)
end end
it 'renames user with .git in username' do it 'renames user with .git in username' do
...@@ -26,4 +20,38 @@ describe RemoveDotGitFromUsernames do ...@@ -26,4 +20,38 @@ describe RemoveDotGitFromUsernames do
expect(user.namespace.route.path).to eq('test_git') expect(user.namespace.route.path).to eq('test_git')
end end
end end
context 'when new path exists already' do
describe '#up' do
let(:user2) { create(:user) }
before do
update_namespace(user, 'test.git')
update_namespace(user2, 'test_git')
storages = { 'default' => 'tmp/tests/custom_repositories' }
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
allow(migration).to receive(:route_exists?).with('test_git').and_return(true)
allow(migration).to receive(:route_exists?).with('test_git1').and_return(false)
end
it 'renames user with .git in username' do
migration.up
expect(user.reload.username).to eq('test_git1')
expect(user.namespace.reload.path).to eq('test_git1')
expect(user.namespace.route.path).to eq('test_git1')
end
end
end
def update_namespace(user, path)
namespace = user.namespace
namespace.path = path
namespace.save!(validate: false)
user.username = path
user.save!(validate: false)
end
end end
...@@ -1545,11 +1545,13 @@ describe Project, models: true do ...@@ -1545,11 +1545,13 @@ describe Project, models: true do
end end
end end
describe 'change_head' do describe '#change_head' do
let(:project) { create(:project) } let(:project) { create(:project) }
it 'calls the before_change_head method' do it 'calls the before_change_head and after_change_head methods' do
expect(project.repository).to receive(:before_change_head) expect(project.repository).to receive(:before_change_head)
expect(project.repository).to receive(:after_change_head)
project.change_head(project.default_branch) project.change_head(project.default_branch)
end end
...@@ -1565,11 +1567,6 @@ describe Project, models: true do ...@@ -1565,11 +1567,6 @@ describe Project, models: true do
project.change_head(project.default_branch) project.change_head(project.default_branch)
end end
it 'expires the avatar cache' do
expect(project.repository).to receive(:expire_avatar_cache)
project.change_head(project.default_branch)
end
it 'reloads the default branch' do it 'reloads the default branch' do
expect(project).to receive(:reload_default_branch) expect(project).to receive(:reload_default_branch)
project.change_head(project.default_branch) project.change_head(project.default_branch)
......
...@@ -1150,6 +1150,24 @@ describe Repository, models: true do ...@@ -1150,6 +1150,24 @@ describe Repository, models: true do
end end
end end
describe '#after_change_head' do
it 'flushes the readme cache' do
expect(repository).to receive(:expire_method_caches).with([
:readme,
:changelog,
:license,
:contributing,
:version,
:gitignore,
:koding,
:gitlab_ci,
:avatar
])
repository.after_change_head
end
end
describe '#before_push_tag' do describe '#before_push_tag' do
it 'flushes the cache' do it 'flushes the cache' do
expect(repository).to receive(:expire_statistics_caches) expect(repository).to receive(:expire_statistics_caches)
...@@ -1513,14 +1531,6 @@ describe Repository, models: true do ...@@ -1513,14 +1531,6 @@ describe Repository, models: true do
end end
end end
describe '#expire_avatar_cache' do
it 'expires the cache' do
expect(repository).to receive(:expire_method_caches).with(%i(avatar))
repository.expire_avatar_cache
end
end
describe '#file_on_head' do describe '#file_on_head' do
context 'with a non-existing repository' do context 'with a non-existing repository' do
it 'returns nil' do it 'returns nil' do
......
...@@ -1095,32 +1095,37 @@ describe API::Projects, api: true do ...@@ -1095,32 +1095,37 @@ describe API::Projects, api: true do
let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') } let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') }
let!(:public) { create(:empty_project, :public, name: "public #{query}") } let!(:public) { create(:empty_project, :public, name: "public #{query}") }
let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') } let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') }
let!(:one_dot_two) { create(:empty_project, :public, name: "one.dot.two") }
shared_examples_for 'project search response' do |args = {}| shared_examples_for 'project search response' do |args = {}|
it 'returns project search responses' do it 'returns project search responses' do
get api("/projects/search/#{query}", current_user) get api("/projects/search/#{args[:query]}", current_user)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.size).to eq(args[:results]) expect(json_response.size).to eq(args[:results])
json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*query.*/) } json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) }
end end
end end
context 'when unauthenticated' do context 'when unauthenticated' do
it_behaves_like 'project search response', results: 1 do it_behaves_like 'project search response', query: 'query', results: 1 do
let(:current_user) { nil } let(:current_user) { nil }
end end
end end
context 'when authenticated' do context 'when authenticated' do
it_behaves_like 'project search response', results: 6 do it_behaves_like 'project search response', query: 'query', results: 6 do
let(:current_user) { user } let(:current_user) { user }
end end
it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do
let(:current_user) { user }
end
end end
context 'when authenticated as a different user' do context 'when authenticated as a different user' do
it_behaves_like 'project search response', results: 2, match_regex: /(internal|public) query/ do it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do
let(:current_user) { user2 } let(:current_user) { user2 }
end end
end end
......
...@@ -10,8 +10,8 @@ describe BuildActionEntity do ...@@ -10,8 +10,8 @@ describe BuildActionEntity do
describe '#as_json' do describe '#as_json' do
subject { entity.as_json } subject { entity.as_json }
it 'contains humanized build name' do it 'contains original build name' do
expect(subject[:name]).to eq 'Test build' expect(subject[:name]).to eq 'test_build'
end end
it 'contains path to the action play' do it 'contains path to the action play' do
......
require 'spec_helper' require 'spec_helper'
describe Projects::UpdateService, services: true do describe Projects::UpdateService, services: true do
describe :update_by_user do let(:user) { create(:user) }
before do let(:admin) { create(:admin) }
@user = create :user let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
@admin = create :user, admin: true
@project = create :project, creator_id: @user.id, namespace: @user.namespace
@opts = {}
end
context 'is private when updated to private' do describe 'update_by_user' do
before do context 'when visibility_level is INTERNAL' do
@created_private = @project.private? it 'updates the project to internal' do
result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL)
@opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) expect(result).to eq({ status: :success })
update_project(@project, @user, @opts) expect(project).to be_internal
end end
it { expect(@created_private).to be_truthy }
it { expect(@project.private?).to be_truthy }
end end
context 'is internal when updated to internal' do context 'when visibility_level is PUBLIC' do
before do it 'updates the project to public' do
@created_private = @project.private? result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
expect(result).to eq({ status: :success })
@opts.merge!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) expect(project).to be_public
update_project(@project, @user, @opts)
end end
it { expect(@created_private).to be_truthy }
it { expect(@project.internal?).to be_truthy }
end end
context 'is public when updated to public' do context 'when visibility levels are restricted to PUBLIC only' do
before do before do
@created_private = @project.private?
@opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
update_project(@project, @user, @opts)
end
it { expect(@created_private).to be_truthy }
it { expect(@project.public?).to be_truthy }
end
context 'respect configured visibility restrictions setting' do
before(:each) do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end end
context 'is private when updated to private' do context 'when visibility_level is INTERNAL' do
before do it 'updates the project to internal' do
@created_private = @project.private? result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL)
expect(result).to eq({ status: :success })
@opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) expect(project).to be_internal
update_project(@project, @user, @opts)
end
it { expect(@created_private).to be_truthy }
it { expect(@project.private?).to be_truthy }
end
context 'is internal when updated to internal' do
before do
@created_private = @project.private?
@opts.merge!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
update_project(@project, @user, @opts)
end end
it { expect(@created_private).to be_truthy }
it { expect(@project.internal?).to be_truthy }
end end
context 'is private when updated to public' do context 'when visibility_level is PUBLIC' do
before do it 'does not update the project to public' do
@created_private = @project.private? result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
@opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) expect(result).to eq({ status: :error, message: 'Visibility level unallowed' })
update_project(@project, @user, @opts) expect(project).to be_private
end end
it { expect(@created_private).to be_truthy } context 'when updated by an admin' do
it { expect(@project.private?).to be_truthy } it 'updates the project to public' do
result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
expect(result).to eq({ status: :success })
expect(project).to be_public
end end
context 'is public when updated to public by admin' do
before do
@created_private = @project.private?
@opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
update_project(@project, @admin, @opts)
end end
it { expect(@created_private).to be_truthy }
it { expect(@project.public?).to be_truthy }
end end
end end
end end
describe :visibility_level do describe 'visibility_level' do
let(:user) { create :user, admin: true }
let(:project) { create(:project, :internal) } let(:project) { create(:project, :internal) }
let(:forked_project) { create(:forked_project_with_submodules, :internal) } let(:forked_project) { create(:forked_project_with_submodules, :internal) }
let(:opts) { {} }
before do before do
forked_project.build_forked_project_link(forked_to_project_id: forked_project.id, forked_from_project_id: project.id) forked_project.build_forked_project_link(forked_to_project_id: forked_project.id, forked_from_project_id: project.id)
forked_project.save forked_project.save
@created_internal = project.internal?
@fork_created_internal = forked_project.internal?
end end
context 'updates forks visibility level when parent set to more restrictive' do it 'updates forks visibility level when parent set to more restrictive' do
before do opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE }
opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
update_project(project, user, opts).inspect
end
it { expect(@created_internal).to be_truthy } expect(project).to be_internal
it { expect(@fork_created_internal).to be_truthy } expect(forked_project).to be_internal
it { expect(project.private?).to be_truthy }
it { expect(project.forks.first.private?).to be_truthy }
end
context 'does not update forks visibility level when parent set to less restrictive' do expect(update_project(project, admin, opts)).to eq({ status: :success })
before do
opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) expect(project).to be_private
update_project(project, user, opts).inspect expect(forked_project.reload).to be_private
end end
it { expect(@created_internal).to be_truthy } it 'does not update forks visibility level when parent set to less restrictive' do
it { expect(@fork_created_internal).to be_truthy } opts = { visibility_level: Gitlab::VisibilityLevel::PUBLIC }
it { expect(project.public?).to be_truthy }
it { expect(project.forks.first.internal?).to be_truthy } expect(project).to be_internal
expect(forked_project).to be_internal
expect(update_project(project, admin, opts)).to eq({ status: :success })
expect(project).to be_public
expect(forked_project.reload).to be_internal
end
end end
it 'returns an error result when record cannot be updated' do
result = update_project(project, admin, { name: 'foo&bar' })
expect(result).to eq({ status: :error, message: 'Project could not be updated' })
end end
def update_project(project, user, opts) def update_project(project, user, opts)
Projects::UpdateService.new(project, user, opts).execute described_class.new(project, user, opts).execute
end end
end end
...@@ -10,7 +10,21 @@ describe Users::RefreshAuthorizedProjectsService do ...@@ -10,7 +10,21 @@ describe Users::RefreshAuthorizedProjectsService do
create!(project: project, user: user, access_level: access_level) create!(project: project, user: user, access_level: access_level)
end end
describe '#execute' do describe '#execute', :redis do
it 'refreshes the authorizations using a lease' do
expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
and_return('foo')
expect(Gitlab::ExclusiveLease).to receive(:cancel).
with(an_instance_of(String), 'foo')
expect(service).to receive(:execute_without_lease)
service.execute
end
end
describe '#execute_without_lease' do
before do before do
user.project_authorizations.delete_all user.project_authorizations.delete_all
end end
...@@ -19,37 +33,23 @@ describe Users::RefreshAuthorizedProjectsService do ...@@ -19,37 +33,23 @@ describe Users::RefreshAuthorizedProjectsService do
project2 = create(:empty_project) project2 = create(:empty_project)
to_remove = create_authorization(project2, user) to_remove = create_authorization(project2, user)
expect(service).to receive(:update_with_lease). expect(service).to receive(:update_authorizations).
with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]])
service.execute service.execute_without_lease
end end
it 'sets the access level of a project to the highest available level' do it 'sets the access level of a project to the highest available level' do
to_remove = create_authorization(project, user, Gitlab::Access::DEVELOPER) to_remove = create_authorization(project, user, Gitlab::Access::DEVELOPER)
expect(service).to receive(:update_with_lease). expect(service).to receive(:update_authorizations).
with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]])
service.execute service.execute_without_lease
end end
it 'returns a User' do it 'returns a User' do
expect(service.execute).to be_an_instance_of(User) expect(service.execute_without_lease).to be_an_instance_of(User)
end
end
describe '#update_with_lease', :redis do
it 'refreshes the authorizations using a lease' do
expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
and_return('foo')
expect(Gitlab::ExclusiveLease).to receive(:cancel).
with(an_instance_of(String), 'foo')
expect(service).to receive(:update_authorizations).with([1], [])
service.update_with_lease([1])
end end
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