Commit 93f0eb07 authored by Phil Hughes's avatar Phil Hughes

Merge branch '44846-improve-web-ide-left-panel-and-modes' into ide-sidebar-commit-box

parents efed5aed 9b8332d3
...@@ -2,6 +2,28 @@ ...@@ -2,6 +2,28 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.7.3 (2018-05-02)
### Fixed (8 changes)
- Fixed wrong avatar URL when the avatar is on object storage. !18092
- Fix errors on pushing to an empty repository. !18462
- Update doorkeeper to 4.3.2 to fix GitLab OAuth authentication. !18543
- Ports omniauth-jwt gem onto GitLab OmniAuth Strategies suite. !18580
- Fix redirection error for applications using OpenID. !18599
- Fix commit trailer rendering when Gravatar is disabled.
- Fix file_store for artifacts and lfs when saving.
- Fix users not seeing labels from private groups when being a member of a child project.
## 10.7.2 (2018-04-25)
### Security (2 changes)
- Serve archive requests with the correct file in all cases.
- Sanitizes user name to avoid XSS attacks.
## 10.7.1 (2018-04-23) ## 10.7.1 (2018-04-23)
### Fixed (11 changes) ### Fixed (11 changes)
...@@ -237,6 +259,13 @@ entry. ...@@ -237,6 +259,13 @@ entry.
- Upgrade Gitaly to upgrade its charlock_holmes. - Upgrade Gitaly to upgrade its charlock_holmes.
## 10.6.5 (2018-04-24)
### Security (1 change)
- Sanitizes user name to avoid XSS attacks.
## 10.6.4 (2018-04-09) ## 10.6.4 (2018-04-09)
### Fixed (8 changes, 1 of them is from the community) ### Fixed (8 changes, 1 of them is from the community)
...@@ -478,6 +507,13 @@ entry. ...@@ -478,6 +507,13 @@ entry.
- Use host URL to build JIRA remote link icon. - Use host URL to build JIRA remote link icon.
## 10.5.8 (2018-04-24)
### Security (1 change)
- Sanitizes user name to avoid XSS attacks.
## 10.5.7 (2018-04-03) ## 10.5.7 (2018-04-03)
### Security (2 changes) ### Security (2 changes)
......
...@@ -51,7 +51,6 @@ gem 'omniauth-shibboleth', '~> 1.2.0' ...@@ -51,7 +51,6 @@ gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.4' gem 'omniauth-twitter', '~> 1.4'
gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.1' gem 'omniauth-authentiq', '~> 0.3.1'
gem 'omniauth-jwt', '~> 0.0.2'
gem 'rack-oauth2', '~> 1.2.1' gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt', '~> 1.5.6' gem 'jwt', '~> 1.5.6'
...@@ -185,6 +184,9 @@ gem 're2', '~> 1.1.1' ...@@ -185,6 +184,9 @@ gem 're2', '~> 1.1.1'
gem 'version_sorter', '~> 2.1.0' gem 'version_sorter', '~> 2.1.0'
# User agent parsing
gem 'device_detector'
# Cache # Cache
gem 'redis-rails', '~> 5.0.2' gem 'redis-rails', '~> 5.0.2'
...@@ -415,7 +417,7 @@ end ...@@ -415,7 +417,7 @@ end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.97.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.97.0', require: 'gitaly'
gem 'grpc', '~> 1.10.0' gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1' gem 'google-protobuf', '= 3.5.1'
......
...@@ -161,6 +161,7 @@ GEM ...@@ -161,6 +161,7 @@ GEM
activerecord (>= 3.2.0, < 5.1) activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
device_detector (1.0.0)
devise (4.2.0) devise (4.2.0)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
...@@ -374,7 +375,7 @@ GEM ...@@ -374,7 +375,7 @@ GEM
rake rake
grape_logging (1.7.0) grape_logging (1.7.0)
grape grape
grpc (1.10.0) grpc (1.11.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0) googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7) googleauth (>= 0.5.1, < 0.7)
...@@ -555,9 +556,6 @@ GEM ...@@ -555,9 +556,6 @@ GEM
jwt (>= 1.5) jwt (>= 1.5)
omniauth (>= 1.1.1) omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.5) omniauth-oauth2 (>= 1.5)
omniauth-jwt (0.0.2)
jwt
omniauth (~> 1.1)
omniauth-kerberos (0.3.0) omniauth-kerberos (0.3.0)
omniauth-multipassword omniauth-multipassword
timfel-krb5-auth (~> 0.8) timfel-krb5-auth (~> 0.8)
...@@ -1029,6 +1027,7 @@ DEPENDENCIES ...@@ -1029,6 +1027,7 @@ DEPENDENCIES
database_cleaner (~> 1.5.0) database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0) deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.0) default_value_for (~> 3.0.0)
device_detector
devise (~> 4.2) devise (~> 4.2)
devise-two-factor (~> 3.0.0) devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0) diffy (~> 3.1.0)
...@@ -1076,7 +1075,7 @@ DEPENDENCIES ...@@ -1076,7 +1075,7 @@ DEPENDENCIES
grape-entity (~> 0.6.0) grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0) grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7) grape_logging (~> 1.7)
grpc (~> 1.10.0) grpc (~> 1.11.0)
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
hamlit (~> 2.6.1) hamlit (~> 2.6.1)
hashie-forbidden_attributes hashie-forbidden_attributes
...@@ -1117,7 +1116,6 @@ DEPENDENCIES ...@@ -1117,7 +1116,6 @@ DEPENDENCIES
omniauth-github (~> 1.1.1) omniauth-github (~> 1.1.1)
omniauth-gitlab (~> 1.0.2) omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.5.3) omniauth-google-oauth2 (~> 0.5.3)
omniauth-jwt (~> 0.0.2)
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2) omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10) omniauth-saml (~> 1.10)
......
...@@ -162,6 +162,7 @@ GEM ...@@ -162,6 +162,7 @@ GEM
activerecord (>= 3.2.0, < 5.2) activerecord (>= 3.2.0, < 5.2)
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
device_detector (1.0.1)
devise (4.4.1) devise (4.4.1)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
...@@ -304,12 +305,12 @@ GEM ...@@ -304,12 +305,12 @@ GEM
flowdock (~> 0.7) flowdock (~> 0.7)
gitlab-grit (>= 2.4.1) gitlab-grit (>= 2.4.1)
multi_json multi_json
gitlab-gollum-lib (4.2.7.1) gitlab-gollum-lib (4.2.7.2)
gemojione (~> 3.2) gemojione (~> 3.2)
github-markup (~> 1.6) github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0) gollum-grit_adapter (~> 1.0)
nokogiri (>= 1.6.1, < 2.0) nokogiri (>= 1.6.1, < 2.0)
rouge (~> 2.1) rouge (~> 3.1)
sanitize (~> 2.1) sanitize (~> 2.1)
stringex (~> 2.6) stringex (~> 2.6)
gitlab-gollum-rugged_adapter (0.4.4) gitlab-gollum-rugged_adapter (0.4.4)
...@@ -375,7 +376,7 @@ GEM ...@@ -375,7 +376,7 @@ GEM
rake rake
grape_logging (1.7.0) grape_logging (1.7.0)
grape grape
grpc (1.10.0) grpc (1.11.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
googleapis-common-protos-types (~> 1.0.0) googleapis-common-protos-types (~> 1.0.0)
googleauth (>= 0.5.1, < 0.7) googleauth (>= 0.5.1, < 0.7)
...@@ -554,9 +555,6 @@ GEM ...@@ -554,9 +555,6 @@ GEM
jwt (>= 1.5) jwt (>= 1.5)
omniauth (>= 1.1.1) omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.5) omniauth-oauth2 (>= 1.5)
omniauth-jwt (0.0.2)
jwt
omniauth (~> 1.1)
omniauth-kerberos (0.3.0) omniauth-kerberos (0.3.0)
omniauth-multipassword omniauth-multipassword
timfel-krb5-auth (~> 0.8) timfel-krb5-auth (~> 0.8)
...@@ -602,8 +600,6 @@ GEM ...@@ -602,8 +600,6 @@ GEM
atomic (>= 1.0.0) atomic (>= 1.0.0)
mysql2 mysql2
peek peek
peek-performance_bar (1.3.1)
peek (>= 0.1.0)
peek-pg (1.3.0) peek-pg (1.3.0)
concurrent-ruby concurrent-ruby
concurrent-ruby-ext concurrent-ruby-ext
...@@ -752,7 +748,7 @@ GEM ...@@ -752,7 +748,7 @@ GEM
retriable (3.1.1) retriable (3.1.1)
rinku (2.0.4) rinku (2.0.4)
rotp (2.1.2) rotp (2.1.2)
rouge (2.2.1) rouge (3.1.1)
rqrcode (0.10.1) rqrcode (0.10.1)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode-rails3 (0.1.7) rqrcode-rails3 (0.1.7)
...@@ -1035,6 +1031,7 @@ DEPENDENCIES ...@@ -1035,6 +1031,7 @@ DEPENDENCIES
database_cleaner (~> 1.5.0) database_cleaner (~> 1.5.0)
deckar01-task_list (= 2.0.0) deckar01-task_list (= 2.0.0)
default_value_for (~> 3.0.5) default_value_for (~> 3.0.5)
device_detector
devise (~> 4.2) devise (~> 4.2)
devise-two-factor (~> 3.0.0) devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0) diffy (~> 3.1.0)
...@@ -1082,7 +1079,7 @@ DEPENDENCIES ...@@ -1082,7 +1079,7 @@ DEPENDENCIES
grape-entity (~> 0.6.0) grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0) grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7) grape_logging (~> 1.7)
grpc (~> 1.10.0) grpc (~> 1.11.0)
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
hamlit (~> 2.6.1) hamlit (~> 2.6.1)
hashie-forbidden_attributes hashie-forbidden_attributes
...@@ -1123,7 +1120,6 @@ DEPENDENCIES ...@@ -1123,7 +1120,6 @@ DEPENDENCIES
omniauth-github (~> 1.1.1) omniauth-github (~> 1.1.1)
omniauth-gitlab (~> 1.0.2) omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.5.3) omniauth-google-oauth2 (~> 0.5.3)
omniauth-jwt (~> 0.0.2)
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2) omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.10) omniauth-saml (~> 1.10)
...@@ -1134,7 +1130,6 @@ DEPENDENCIES ...@@ -1134,7 +1130,6 @@ DEPENDENCIES
peek (~> 1.0.1) peek (~> 1.0.1)
peek-gc (~> 0.0.2) peek-gc (~> 0.0.2)
peek-mysql2 (~> 1.1.0) peek-mysql2 (~> 1.1.0)
peek-performance_bar (~> 1.3.0)
peek-pg (~> 1.3.0) peek-pg (~> 1.3.0)
peek-rblineprof (~> 0.2.0) peek-rblineprof (~> 0.2.0)
peek-redis (~> 1.2.0) peek-redis (~> 1.2.0)
...@@ -1166,7 +1161,7 @@ DEPENDENCIES ...@@ -1166,7 +1161,7 @@ DEPENDENCIES
redis-rails (~> 5.0.2) redis-rails (~> 5.0.2)
request_store (~> 1.3) request_store (~> 1.3)
responders (~> 2.0) responders (~> 2.0)
rouge (~> 2.0) rouge (~> 3.1)
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-parameterized rspec-parameterized
rspec-rails (~> 3.6.0) rspec-rails (~> 3.6.0)
......
...@@ -7,27 +7,24 @@ export default function installGlEmojiElement() { ...@@ -7,27 +7,24 @@ export default function installGlEmojiElement() {
const GlEmojiElementProto = Object.create(HTMLElement.prototype); const GlEmojiElementProto = Object.create(HTMLElement.prototype);
GlEmojiElementProto.createdCallback = function createdCallback() { GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim(); const emojiUnicode = this.textContent.trim();
const { const { name, unicodeVersion, fallbackSrc, fallbackSpriteClass } = this.dataset;
name,
unicodeVersion,
fallbackSrc,
fallbackSpriteClass,
} = this.dataset;
const isEmojiUnicode = this.childNodes && Array.prototype.every.call( const isEmojiUnicode =
this.childNodes, this.childNodes &&
childNode => childNode.nodeType === 3, Array.prototype.every.call(this.childNodes, childNode => childNode.nodeType === 3);
);
const hasImageFallback = fallbackSrc && fallbackSrc.length > 0; const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0; const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
if ( if (emojiUnicode && isEmojiUnicode && !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)) {
emojiUnicode &&
isEmojiUnicode &&
!isEmojiUnicodeSupported(emojiUnicode, unicodeVersion)
) {
// CSS sprite fallback takes precedence over image fallback // CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) { if (hasCssSpriteFalback) {
if (!gon.emoji_sprites_css_added && gon.emoji_sprites_css_path) {
const emojiSpriteLinkTag = document.createElement('link');
emojiSpriteLinkTag.setAttribute('rel', 'stylesheet');
emojiSpriteLinkTag.setAttribute('href', gon.emoji_sprites_css_path);
document.head.appendChild(emojiSpriteLinkTag);
gon.emoji_sprites_css_added = true;
}
// IE 11 doesn't like adding multiple at once :( // IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon'); this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass); this.classList.add(fallbackSpriteClass);
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */
import $ from 'jquery';
import { localTimeAgo } from './lib/utils/datetime_utility';
import axios from './lib/utils/axios_utils';
export default class Compare {
constructor(opts) {
this.opts = opts;
this.source_loading = $(".js-source-loading");
this.target_loading = $(".js-target-loading");
$('.js-compare-dropdown').each((function(_this) {
return function(i, dropdown) {
var $dropdown;
$dropdown = $(dropdown);
return $dropdown.glDropdown({
selectable: true,
fieldName: $dropdown.data('fieldName'),
filterable: true,
id: function(obj, $el) {
return $el.data('id');
},
toggleLabel: function(obj, $el) {
return $el.text().trim();
},
clicked: function(e, el) {
if ($dropdown.is('.js-target-branch')) {
return _this.getTargetHtml();
} else if ($dropdown.is('.js-source-branch')) {
return _this.getSourceHtml();
} else if ($dropdown.is('.js-target-project')) {
return _this.getTargetProject();
}
}
});
};
})(this));
this.initialState();
}
initialState() {
this.getSourceHtml();
this.getTargetHtml();
}
getTargetProject() {
$('.mr_target_commit').empty();
return axios.get(this.opts.targetProjectUrl, {
params: {
target_project_id: $("input[name='merge_request[target_project_id]']").val(),
},
}).then(({ data }) => {
$('.js-target-branch-dropdown .dropdown-content').html(data);
});
}
getSourceHtml() {
return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
ref: $("input[name='merge_request[source_branch]']").val()
});
}
getTargetHtml() {
return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
target_project_id: $("input[name='merge_request[target_project_id]']").val(),
ref: $("input[name='merge_request[target_branch]']").val()
});
}
static sendAjax(url, loading, target, params) {
const $target = $(target);
loading.show();
$target.empty();
return axios.get(url, {
params,
}).then(({ data }) => {
loading.hide();
$target.html(data);
const className = '.' + $target[0].className.replace(' ', '.');
localTimeAgo($('.js-timeago', className));
});
}
}
...@@ -4,8 +4,9 @@ import $ from 'jquery'; ...@@ -4,8 +4,9 @@ import $ from 'jquery';
import { __ } from './locale'; import { __ } from './locale';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import flash from './flash'; import flash from './flash';
import { capitalizeFirstCharacter } from './lib/utils/text_utility';
export default function initCompareAutocomplete() { export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) {
$('.js-compare-dropdown').each(function() { $('.js-compare-dropdown').each(function() {
var $dropdown, selected; var $dropdown, selected;
$dropdown = $(this); $dropdown = $(this);
...@@ -15,14 +16,27 @@ export default function initCompareAutocomplete() { ...@@ -15,14 +16,27 @@ export default function initCompareAutocomplete() {
const $filterInput = $('input[type="search"]', $dropdownContainer); const $filterInput = $('input[type="search"]', $dropdownContainer);
$dropdown.glDropdown({ $dropdown.glDropdown({
data: function(term, callback) { data: function(term, callback) {
axios.get($dropdown.data('refsUrl'), { const params = {
params: { ref: $dropdown.data('ref'),
ref: $dropdown.data('ref'), search: term,
search: term, };
},
}).then(({ data }) => { if (limitTo) {
callback(data); params.find = limitTo;
}).catch(() => flash(__('Error fetching refs'))); }
axios
.get($dropdown.data('refsUrl'), {
params,
})
.then(({ data }) => {
if (limitTo) {
callback(data[capitalizeFirstCharacter(limitTo)] || []);
} else {
callback(data);
}
})
.catch(() => flash(__('Error fetching refs')));
}, },
selectable: true, selectable: true,
filterable: true, filterable: true,
...@@ -32,9 +46,15 @@ export default function initCompareAutocomplete() { ...@@ -32,9 +46,15 @@ export default function initCompareAutocomplete() {
renderRow: function(ref) { renderRow: function(ref) {
var link; var link;
if (ref.header != null) { if (ref.header != null) {
return $('<li />').addClass('dropdown-header').text(ref.header); return $('<li />')
.addClass('dropdown-header')
.text(ref.header);
} else { } else {
link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref)); link = $('<a />')
.attr('href', '#')
.addClass(ref === selected ? 'is-active' : '')
.text(ref)
.attr('data-ref', escape(ref));
return $('<li />').append(link); return $('<li />').append(link);
} }
}, },
...@@ -43,9 +63,10 @@ export default function initCompareAutocomplete() { ...@@ -43,9 +63,10 @@ export default function initCompareAutocomplete() {
}, },
toggleLabel: function(obj, $el) { toggleLabel: function(obj, $el) {
return $el.text().trim(); return $el.text().trim();
} },
clicked: () => clickHandler($dropdown),
}); });
$filterInput.on('keyup', (e) => { $filterInput.on('keyup', e => {
const keyCode = e.keyCode || e.which; const keyCode = e.keyCode || e.which;
if (keyCode !== 13) return; if (keyCode !== 13) return;
const text = $filterInput.val(); const text = $filterInput.val();
...@@ -54,7 +75,7 @@ export default function initCompareAutocomplete() { ...@@ -54,7 +75,7 @@ export default function initCompareAutocomplete() {
$dropdownContainer.removeClass('open'); $dropdownContainer.removeClass('open');
}); });
$dropdownContainer.on('click', '.dropdown-content a', (e) => { $dropdownContainer.on('click', '.dropdown-content a', e => {
$dropdown.prop('title', e.target.text.replace(/_+?/g, '-')); $dropdown.prop('title', e.target.text.replace(/_+?/g, '-'));
if ($dropdown.hasClass('has-tooltip')) { if ($dropdown.hasClass('has-tooltip')) {
$dropdown.tooltip('fixTitle'); $dropdown.tooltip('fixTitle');
......
...@@ -34,7 +34,7 @@ export function getEmojiCategoryMap() { ...@@ -34,7 +34,7 @@ export function getEmojiCategoryMap() {
symbols: [], symbols: [],
flags: [], flags: [],
}; };
Object.keys(emojiMap).forEach((name) => { Object.keys(emojiMap).forEach(name => {
const emoji = emojiMap[name]; const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.category]) { if (emojiCategoryMap[emoji.category]) {
emojiCategoryMap[emoji.category].push(name); emojiCategoryMap[emoji.category].push(name);
...@@ -79,7 +79,9 @@ export function glEmojiTag(inputName, options) { ...@@ -79,7 +79,9 @@ export function glEmojiTag(inputName, options) {
classList.push(fallbackSpriteClass); classList.push(fallbackSpriteClass);
} }
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : ''; const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : ''; const fallbackSpriteAttribute = opts.sprite
? `data-fallback-sprite-class="${fallbackSpriteClass}"`
: '';
let contents = emojiInfo.moji; let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) { if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc); contents = emojiImageTag(name, fallbackImageSrc);
......
...@@ -54,7 +54,8 @@ const unicodeSupportTestMap = { ...@@ -54,7 +54,8 @@ const unicodeSupportTestMap = {
function checkPixelInImageDataArray(pixelOffset, imageDataArray) { function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
// `4 *` because RGBA // `4 *` because RGBA
const indexOffset = 4 * pixelOffset; const indexOffset = 4 * pixelOffset;
const hasColor = imageDataArray[indexOffset + 0] || const hasColor =
imageDataArray[indexOffset + 0] ||
imageDataArray[indexOffset + 1] || imageDataArray[indexOffset + 1] ||
imageDataArray[indexOffset + 2]; imageDataArray[indexOffset + 2];
const isVisible = imageDataArray[indexOffset + 3]; const isVisible = imageDataArray[indexOffset + 3];
...@@ -75,23 +76,23 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche ...@@ -75,23 +76,23 @@ const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatche
const fontSize = 16; const fontSize = 16;
function generateUnicodeSupportMap(testMap) { function generateUnicodeSupportMap(testMap) {
const testMapKeys = Object.keys(testMap); const testMapKeys = Object.keys(testMap);
const numTestEntries = testMapKeys const numTestEntries = testMapKeys.reduce((list, testKey) => list.concat(testMap[testKey]), [])
.reduce((list, testKey) => list.concat(testMap[testKey]), []).length; .length;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
(window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas; (window.gl || window).testEmojiUnicodeSupportMapCanvas = canvas;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
canvas.width = (2 * fontSize); canvas.width = 2 * fontSize;
canvas.height = (numTestEntries * fontSize); canvas.height = numTestEntries * fontSize;
ctx.fillStyle = '#000000'; ctx.fillStyle = '#000000';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`; ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
// Write each emoji to the canvas vertically // Write each emoji to the canvas vertically
let writeIndex = 0; let writeIndex = 0;
testMapKeys.forEach((testKey) => { testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey]; const testEntry = testMap[testKey];
[].concat(testEntry).forEach((emojiUnicode) => { [].concat(testEntry).forEach(emojiUnicode => {
ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2)); ctx.fillText(emojiUnicode, 0, writeIndex * fontSize + fontSize / 2);
writeIndex += 1; writeIndex += 1;
}); });
}); });
...@@ -99,29 +100,25 @@ function generateUnicodeSupportMap(testMap) { ...@@ -99,29 +100,25 @@ function generateUnicodeSupportMap(testMap) {
// Read from the canvas // Read from the canvas
const resultMap = {}; const resultMap = {};
let readIndex = 0; let readIndex = 0;
testMapKeys.forEach((testKey) => { testMapKeys.forEach(testKey => {
const testEntry = testMap[testKey]; const testEntry = testMap[testKey];
// This needs to be a `reduce` instead of `every` because we need to // This needs to be a `reduce` instead of `every` because we need to
// keep the `readIndex` in sync from the writes by running all entries // keep the `readIndex` in sync from the writes by running all entries
const isTestSatisfied = [].concat(testEntry).reduce((isSatisfied) => { const isTestSatisfied = [].concat(testEntry).reduce(isSatisfied => {
// Sample along the vertical-middle for a couple of characters // Sample along the vertical-middle for a couple of characters
const imageData = ctx.getImageData( const imageData = ctx.getImageData(0, readIndex * fontSize + fontSize / 2, 2 * fontSize, 1)
0, .data;
(readIndex * fontSize) + (fontSize / 2),
2 * fontSize,
1,
).data;
let isValidEmoji = false; let isValidEmoji = false;
for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) { for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
const isLookingAtFirstChar = currentPixel < fontSize; const isLookingAtFirstChar = currentPixel < fontSize;
const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2)); const isLookingAtSecondChar = currentPixel >= fontSize + fontSize / 2;
// Check for the emoji somewhere along the row // Check for the emoji somewhere along the row
if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) { if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = true; isValidEmoji = true;
// Check to see that nothing is rendered next to the first character // Check to see that nothing is rendered next to the first character
// to ensure that the ZWJ sequence rendered as one piece // to ensure that the ZWJ sequence rendered as one piece
} else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) { } else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = false; isValidEmoji = false;
break; break;
...@@ -170,7 +167,10 @@ export default function getUnicodeSupportMap() { ...@@ -170,7 +167,10 @@ export default function getUnicodeSupportMap() {
if (isLocalStorageAvailable) { if (isLocalStorageAvailable) {
window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION); window.localStorage.setItem('gl-emoji-version', GL_EMOJI_VERSION);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent); window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap)); window.localStorage.setItem(
'gl-emoji-unicode-support-map',
JSON.stringify(unicodeSupportMap),
);
} }
} }
......
<script> <script>
import playIconSvg from 'icons/_icon_play.svg'; import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
...@@ -8,9 +8,9 @@ ...@@ -8,9 +8,9 @@
directives: { directives: {
tooltip, tooltip,
}, },
components: { components: {
loadingIcon, loadingIcon,
Icon,
}, },
props: { props: {
actions: { actions: {
...@@ -19,20 +19,16 @@ ...@@ -19,20 +19,16 @@
default: () => [], default: () => [],
}, },
}, },
data() { data() {
return { return {
playIconSvg,
isLoading: false, isLoading: false,
}; };
}, },
computed: { computed: {
title() { title() {
return 'Deploy to...'; return 'Deploy to...';
}, },
}, },
methods: { methods: {
onClickAction(endpoint) { onClickAction(endpoint) {
this.isLoading = true; this.isLoading = true;
...@@ -65,7 +61,10 @@ ...@@ -65,7 +61,10 @@
:disabled="isLoading" :disabled="isLoading"
> >
<span> <span>
<span v-html="playIconSvg"></span> <icon
name="play"
:size="12"
/>
<i <i
class="fa fa-caret-down" class="fa fa-caret-down"
aria-hidden="true" aria-hidden="true"
...@@ -86,7 +85,10 @@ ...@@ -86,7 +85,10 @@
:class="{ disabled: isActionDisabled(action) }" :class="{ disabled: isActionDisabled(action) }"
:disabled="isActionDisabled(action)" :disabled="isActionDisabled(action)"
> >
<span v-html="playIconSvg"></span> <icon
name="play"
:size="12"
/>
<span> <span>
{{ action.name }} {{ action.name }}
</span> </span>
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
...@@ -6,6 +7,9 @@ ...@@ -6,6 +7,9 @@
* Renders the external url link in environments table. * Renders the external url link in environments table.
*/ */
export default { export default {
components: {
Icon,
},
directives: { directives: {
tooltip, tooltip,
}, },
...@@ -15,7 +19,6 @@ ...@@ -15,7 +19,6 @@
required: true, required: true,
}, },
}, },
computed: { computed: {
title() { title() {
return s__('Environments|Open'); return s__('Environments|Open');
...@@ -34,10 +37,9 @@ ...@@ -34,10 +37,9 @@
:aria-label="title" :aria-label="title"
:href="externalUrl" :href="externalUrl"
> >
<i <icon
class="fa fa-external-link" name="external-link"
aria-hidden="true" :size="12"
> />
</i>
</a> </a>
</template> </template>
...@@ -2,20 +2,22 @@ ...@@ -2,20 +2,22 @@
/** /**
* Renders the Monitoring (Metrics) link in environments table. * Renders the Monitoring (Metrics) link in environments table.
*/ */
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
components: {
Icon,
},
directives: { directives: {
tooltip, tooltip,
}, },
props: { props: {
monitoringUrl: { monitoringUrl: {
type: String, type: String,
required: true, required: true,
}, },
}, },
computed: { computed: {
title() { title() {
return 'Monitoring'; return 'Monitoring';
...@@ -33,10 +35,9 @@ ...@@ -33,10 +35,9 @@
:title="title" :title="title"
:aria-label="title" :aria-label="title"
> >
<i <icon
class="fa fa-area-chart" name="chart"
aria-hidden="true" :size="12"
> />
</i>
</a> </a>
</template> </template>
...@@ -12,7 +12,6 @@ ...@@ -12,7 +12,6 @@
components: { components: {
loadingIcon, loadingIcon,
}, },
props: { props: {
retryUrl: { retryUrl: {
type: String, type: String,
...@@ -24,13 +23,11 @@ ...@@ -24,13 +23,11 @@
default: true, default: true,
}, },
}, },
data() { data() {
return { return {
isLoading: false, isLoading: false,
}; };
}, },
methods: { methods: {
onClick() { onClick() {
this.isLoading = true; this.isLoading = true;
......
...@@ -3,14 +3,16 @@ ...@@ -3,14 +3,16 @@
* Renders a terminal button to open a web terminal. * Renders a terminal button to open a web terminal.
* Used in environments table. * Used in environments table.
*/ */
import terminalIconSvg from 'icons/_icon_terminal.svg'; import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
components: {
Icon,
},
directives: { directives: {
tooltip, tooltip,
}, },
props: { props: {
terminalPath: { terminalPath: {
type: String, type: String,
...@@ -18,13 +20,6 @@ ...@@ -18,13 +20,6 @@
default: '', default: '',
}, },
}, },
data() {
return {
terminalIconSvg,
};
},
computed: { computed: {
title() { title() {
return 'Terminal'; return 'Terminal';
...@@ -40,7 +35,10 @@ ...@@ -40,7 +35,10 @@
:title="title" :title="title"
:aria-label="title" :aria-label="title"
:href="terminalPath" :href="terminalPath"
v-html="terminalIconSvg"
> >
<icon
name="terminal"
:size="12"
/>
</a> </a>
</template> </template>
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
components: {
Icon,
},
directives: {
tooltip,
},
computed: { computed: {
...mapState(['lastCommitMsg', 'noChangesStateSvgPath', 'committedStateSvgPath']), ...mapState(['lastCommitMsg', 'noChangesStateSvgPath']),
statusSvg() {
return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
},
}, },
}; };
</script> </script>
<template> <template>
<div <div
v-if="!lastCommitMsg"
class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state" class="multi-file-commit-panel-section ide-commit-empty-state js-empty-state"
> >
<div <div
class="ide-commit-empty-state-container" class="ide-commit-empty-state-container"
> >
<div class="svg-content svg-80"> <div class="svg-content svg-80">
<img :src="statusSvg" /> <img :src="noChangesStateSvgPath" />
</div> </div>
<div class="append-right-default prepend-left-default"> <div class="append-right-default prepend-left-default">
<div <div
class="text-content text-center" class="text-content text-center"
v-if="!lastCommitMsg"
> >
<h4> <h4>
{{ __('No changes') }} {{ __('No changes') }}
...@@ -41,15 +30,6 @@ export default { ...@@ -41,15 +30,6 @@ export default {
{{ __('Edit files in the editor and commit changes here') }} {{ __('Edit files in the editor and commit changes here') }}
</p> </p>
</div> </div>
<div
class="text-content text-center"
v-else
>
<h4>
{{ __('All changes are committed') }}
</h4>
<p v-html="lastCommitMsg"></p>
</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -4,6 +4,7 @@ import { sprintf, __ } from '~/locale'; ...@@ -4,6 +4,7 @@ import { sprintf, __ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitMessageField from './message_field.vue'; import CommitMessageField from './message_field.vue';
import Actions from './actions.vue'; import Actions from './actions.vue';
import SuccessMessage from './success_message.vue';
import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT, COMMIT_ITEM_PADDING } from '../../constants'; import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT, COMMIT_ITEM_PADDING } from '../../constants';
export default { export default {
...@@ -11,6 +12,7 @@ export default { ...@@ -11,6 +12,7 @@ export default {
Actions, Actions,
LoadingButton, LoadingButton,
CommitMessageField, CommitMessageField,
SuccessMessage,
}, },
data() { data() {
return { return {
...@@ -19,10 +21,13 @@ export default { ...@@ -19,10 +21,13 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['changedFiles', 'stagedFiles', 'currentActivityView']), ...mapState(['lastCommitMsg', 'changedFiles', 'stagedFiles', 'currentActivityView']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']), ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['hasChanges']), ...mapGetters(['hasChanges']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']), ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
someUncommitedChanges() {
return !!(this.changedFiles.length || this.stagedFiles.length);
},
overviewText() { overviewText() {
return sprintf( return sprintf(
__( __(
...@@ -120,6 +125,10 @@ export default { ...@@ -120,6 +125,10 @@ export default {
@submit.prevent.stop="commitChanges" @submit.prevent.stop="commitChanges"
ref="formEl" ref="formEl"
> >
<success-message
v-if="lastCommitMsg && !someUncommitedChanges"
:committed-state-svg-path="committedStateSvgPath"
/>
<commit-message-field <commit-message-field
:text="commitMessage" :text="commitMessage"
@input="updateCommitMessage" @input="updateCommitMessage"
......
<script>
import { mapState } from 'vuex';
export default {
props: {
committedStateSvgPath: {
type: String,
required: true,
},
},
computed: {
...mapState(['lastCommitMsg']),
},
};
</script>
<template>
<div
class="multi-file-commit-panel-success-message"
aria-live="assertive"
>
<div class="svg-content svg-80">
<img
:src="committedStateSvgPath"
alt=""
/>
</div>
<div class="append-right-default prepend-left-default">
<div
class="text-content text-center"
>
<h4>
{{ __('All changes are committed') }}
</h4>
<p v-html="lastCommitMsg"></p>
</div>
</div>
</div>
</template>
...@@ -19,8 +19,22 @@ export default { ...@@ -19,8 +19,22 @@ export default {
tooltip, tooltip,
}, },
computed: { computed: {
...mapState(['changedFiles', 'stagedFiles']), ...mapState([
'changedFiles',
'stagedFiles',
'rightPanelCollapsed',
'lastCommitMsg',
'unusedSeal',
]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['lastOpenedFile', 'hasChanges']), ...mapGetters(['lastOpenedFile', 'hasChanges']),
...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']),
showStageUnstageArea() {
return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal);
},
someUncommitedChanges() {
return !!(this.changedFiles.length || this.stagedFiles.length);
},
}, },
watch: { watch: {
hasChanges() { hasChanges() {
...@@ -71,7 +85,7 @@ export default { ...@@ -71,7 +85,7 @@ export default {
</template> </template>
</deprecated-modal> </deprecated-modal>
<template <template
v-if="changedFiles.length || stagedFiles.length" v-if="showStageUnstageArea"
> >
<commit-files-list <commit-files-list
class="is-first" class="is-first"
...@@ -93,7 +107,7 @@ export default { ...@@ -93,7 +107,7 @@ export default {
/> />
</template> </template>
<empty-state <empty-state
v-else v-if="unusedSeal"
/> />
</div> </div>
</template> </template>
...@@ -161,6 +161,12 @@ export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, temp ...@@ -161,6 +161,12 @@ export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, temp
export const toggleFileFinder = ({ commit }, fileFindVisible) => export const toggleFileFinder = ({ commit }, fileFindVisible) =>
commit(types.TOGGLE_FILE_FINDER, fileFindVisible); commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
export const burstUnusedSeal = ({ state, commit }) => {
if (state.unusedSeal) {
commit(types.BURST_UNUSED_SEAL);
}
};
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
......
...@@ -116,7 +116,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) = ...@@ -116,7 +116,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
}); });
}; };
export const changeFileContent = ({ state, commit }, { path, content }) => { export const changeFileContent = ({ commit, dispatch, state }, { path, content }) => {
const file = state.entries[path]; const file = state.entries[path];
commit(types.UPDATE_FILE_CONTENT, { path, content }); commit(types.UPDATE_FILE_CONTENT, { path, content });
...@@ -127,6 +127,8 @@ export const changeFileContent = ({ state, commit }, { path, content }) => { ...@@ -127,6 +127,8 @@ export const changeFileContent = ({ state, commit }, { path, content }) => {
} else if (!file.changed && indexOfChangedFile !== -1) { } else if (!file.changed && indexOfChangedFile !== -1) {
commit(types.REMOVE_FILE_FROM_CHANGED, path); commit(types.REMOVE_FILE_FROM_CHANGED, path);
} }
dispatch('burstUnusedSeal', {}, { root: true });
}; };
export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => { export const setFileLanguage = ({ getters, commit }, { fileLanguage }) => {
......
...@@ -177,6 +177,10 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ...@@ -177,6 +177,10 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
} }
commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true }); commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true });
setTimeout(() => {
commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true });
}, 5000);
}) })
.then(() => { .then(() => {
if (rootGetters.lastOpenedFile) { if (rootGetters.lastOpenedFile) {
......
...@@ -63,3 +63,4 @@ export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB'; ...@@ -63,3 +63,4 @@ export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW'; export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW';
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG'; export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
...@@ -143,6 +143,11 @@ export default { ...@@ -143,6 +143,11 @@ export default {
}), }),
}); });
}, },
[types.BURST_UNUSED_SEAL](state) {
Object.assign(state, {
unusedSeal: false,
});
},
...projectMutations, ...projectMutations,
...mergeRequestMutation, ...mergeRequestMutation,
...fileMutations, ...fileMutations,
......
...@@ -21,5 +21,6 @@ export default () => ({ ...@@ -21,5 +21,6 @@ export default () => ({
viewer: 'editor', viewer: 'editor',
delayViewerUpdated: false, delayViewerUpdated: false,
currentActivityView: activityBarViews.edit, currentActivityView: activityBarViews.edit,
unusedSeal: true,
fileFindVisible: false, fileFindVisible: false,
}); });
...@@ -74,7 +74,11 @@ export function capitalizeFirstCharacter(text) { ...@@ -74,7 +74,11 @@ export function capitalizeFirstCharacter(text) {
* @param {*} replace * @param {*} replace
* @returns {String} * @returns {String}
*/ */
export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace); export const stripHtml = (string, replace = '') => {
if (!string) return string;
return string.replace(/<[^>]*>/g, replace);
};
/** /**
* Converts snake_case string to camelCase * Converts snake_case string to camelCase
......
...@@ -86,7 +86,7 @@ export default { ...@@ -86,7 +86,7 @@ export default {
v-html="resolveSvg" v-html="resolveSvg"
></span> ></span>
</span> </span>
<span class=".line-resolve-text"> <span class="line-resolve-text">
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved
</span> </span>
</div> </div>
......
import initCompareAutocomplete from '~/compare_autocomplete'; import initCompareAutocomplete from '~/compare_autocomplete';
document.addEventListener('DOMContentLoaded', initCompareAutocomplete); document.addEventListener('DOMContentLoaded', () => initCompareAutocomplete());
import $ from 'jquery';
import { localTimeAgo } from '~/lib/utils/datetime_utility';
import axios from '~/lib/utils/axios_utils';
import initCompareAutocomplete from '~/compare_autocomplete';
import initTargetProjectDropdown from './target_project_dropdown';
const updateCommitList = (url, $loadingIndicator, $commitList, params) => {
$loadingIndicator.show();
$commitList.empty();
return axios
.get(url, {
params,
})
.then(({ data }) => {
$loadingIndicator.hide();
$commitList.html(data);
localTimeAgo($('.js-timeago', $commitList));
});
};
export default mrNewCompareNode => {
const { sourceBranchUrl, targetBranchUrl } = mrNewCompareNode.dataset;
initTargetProjectDropdown();
const updateSourceBranchCommitList = () =>
updateCommitList(
sourceBranchUrl,
$(mrNewCompareNode).find('.js-source-loading'),
$(mrNewCompareNode).find('.mr_source_commit'),
{
ref: $(mrNewCompareNode)
.find("input[name='merge_request[source_branch]']")
.val(),
},
);
const updateTargetBranchCommitList = () =>
updateCommitList(
targetBranchUrl,
$(mrNewCompareNode).find('.js-target-loading'),
$(mrNewCompareNode).find('.mr_target_commit'),
{
target_project_id: $(mrNewCompareNode)
.find("input[name='merge_request[target_project_id]']")
.val(),
ref: $(mrNewCompareNode)
.find("input[name='merge_request[target_branch]']")
.val(),
},
);
initCompareAutocomplete('branches', $dropdown => {
if ($dropdown.is('.js-target-branch')) {
updateTargetBranchCommitList();
} else if ($dropdown.is('.js-source-branch')) {
updateSourceBranchCommitList();
}
});
updateSourceBranchCommitList();
updateTargetBranchCommitList();
};
import Compare from '~/compare';
import MergeRequest from '~/merge_request'; import MergeRequest from '~/merge_request';
import initPipelines from '~/commit/pipelines/pipelines_bundle'; import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initCompare from './compare';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
if (mrNewCompareNode) { if (mrNewCompareNode) {
new Compare({ // eslint-disable-line no-new initCompare(mrNewCompareNode);
targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl,
sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl,
targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl,
});
} else { } else {
const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit'); const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
new MergeRequest({ // eslint-disable-line no-new // eslint-disable-next-line no-new
new MergeRequest({
action: mrNewSubmitNode.dataset.mrSubmitAction, action: mrNewSubmitNode.dataset.mrSubmitAction,
}); });
initPipelines(); initPipelines();
......
import $ from 'jquery';
export default () => {
const $targetProjectDropdown = $('.js-target-project');
$targetProjectDropdown.glDropdown({
selectable: true,
fieldName: $targetProjectDropdown.data('fieldName'),
filterable: true,
id(obj, $el) {
return $el.data('id');
},
toggleLabel(obj, $el) {
return $el.text().trim();
},
clicked({ $el }) {
$('.mr_target_commit').empty();
const $targetBranchDropdown = $('.js-target-branch');
$targetBranchDropdown.data('refsUrl', $el.data('refsUrl'));
$targetBranchDropdown.data('glDropdown').clearMenu();
},
});
};
export default {
name: 'time-tracking-no-tracking-pane',
template: `
<div class="time-tracking-no-tracking-pane">
<span class="no-value">
{{ __('No estimate or time spent') }}
</span>
</div>
`,
};
<script>
export default {
name: 'TimeTrackingNoTrackingPane',
};
</script>
<template>
<div class="time-tracking-no-tracking-pane">
<span class="no-value">
{{ __('No estimate or time spent') }}
</span>
</div>
</template>
<script>
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
...@@ -10,14 +11,17 @@ import Mediator from '../../sidebar_mediator'; ...@@ -10,14 +11,17 @@ import Mediator from '../../sidebar_mediator';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
export default { export default {
components: {
IssuableTimeTracker,
},
data() { data() {
return { return {
mediator: new Mediator(), mediator: new Mediator(),
store: new Store(), store: new Store(),
}; };
}, },
components: { mounted() {
IssuableTimeTracker, this.listenForQuickActions();
}, },
methods: { methods: {
listenForQuickActions() { listenForQuickActions() {
...@@ -41,18 +45,17 @@ export default { ...@@ -41,18 +45,17 @@ export default {
} }
}, },
}, },
mounted() {
this.listenForQuickActions();
},
template: `
<div class="block">
<issuable-time-tracker
:time_estimate="store.timeEstimate"
:time_spent="store.totalTimeSpent"
:human_time_estimate="store.humanTimeEstimate"
:human_time_spent="store.humanTotalTimeSpent"
:rootPath="store.rootPath"
/>
</div>
`,
}; };
</script>
<template>
<div class="block">
<issuable-time-tracker
:time_estimate="store.timeEstimate"
:time_spent="store.totalTimeSpent"
:human_time_estimate="store.humanTimeEstimate"
:human_time_spent="store.humanTotalTimeSpent"
:root-path="store.rootPath"
/>
</div>
</template>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import TimeTrackingHelpState from './help_state.vue'; import TimeTrackingHelpState from './help_state.vue';
import TimeTrackingCollapsedState from './collapsed_state.vue'; import TimeTrackingCollapsedState from './collapsed_state.vue';
import timeTrackingSpentOnlyPane from './spent_only_pane'; import timeTrackingSpentOnlyPane from './spent_only_pane';
import timeTrackingNoTrackingPane from './no_tracking_pane'; import TimeTrackingNoTrackingPane from './no_tracking_pane.vue';
import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue'; import TimeTrackingEstimateOnlyPane from './estimate_only_pane.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue'; import TimeTrackingComparisonPane from './comparison_pane.vue';
...@@ -14,7 +14,7 @@ export default { ...@@ -14,7 +14,7 @@ export default {
TimeTrackingCollapsedState, TimeTrackingCollapsedState,
TimeTrackingEstimateOnlyPane, TimeTrackingEstimateOnlyPane,
'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane, 'time-tracking-spent-only-pane': timeTrackingSpentOnlyPane,
'time-tracking-no-tracking-pane': timeTrackingNoTrackingPane, TimeTrackingNoTrackingPane,
TimeTrackingComparisonPane, TimeTrackingComparisonPane,
TimeTrackingHelpState, TimeTrackingHelpState,
}, },
......
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore';
function isValidProjectId(id) { function isValidProjectId(id) {
return id > 0; return id > 0;
...@@ -43,7 +44,7 @@ class SidebarMoveIssue { ...@@ -43,7 +44,7 @@ class SidebarMoveIssue {
renderRow: project => ` renderRow: project => `
<li> <li>
<a href="#" class="js-move-issue-dropdown-item"> <a href="#" class="js-move-issue-dropdown-item">
${project.name_with_namespace} ${_.escape(project.name_with_namespace)}
</a> </a>
</li> </li>
`, `,
......
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking'; import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue'; import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
import SidebarMoveIssue from './lib/sidebar_move_issue'; import SidebarMoveIssue from './lib/sidebar_move_issue';
......
<script>
import $ from 'jquery'; import $ from 'jquery';
import statusIcon from '../mr_widget_status_icon.vue'; import statusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
export default { export default {
name: 'MRWidgetWIP', name: 'WorkInProgress',
props: { components: {
mr: { type: Object, required: true }, statusIcon,
service: { type: Object, required: true },
}, },
directives: { directives: {
tooltip, tooltip,
}, },
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
data() { data() {
return { return {
isMakingRequest: false, isMakingRequest: false,
}; };
}, },
components: {
statusIcon,
},
methods: { methods: {
removeWIP() { removeWIP() {
this.isMakingRequest = true; this.isMakingRequest = true;
...@@ -36,32 +37,40 @@ export default { ...@@ -36,32 +37,40 @@ export default {
}); });
}, },
}, },
template: `
<div class="mr-widget-body media">
<status-icon status="warning" :show-disabled-button="Boolean(mr.removeWIPPath)" />
<div class="media-body space-children">
<span class="bold">
This is a Work in Progress
<i
v-tooltip
class="fa fa-question-circle"
title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged"
aria-label="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged">
</i>
</span>
<button
v-if="mr.removeWIPPath"
@click="removeWIP"
:disabled="isMakingRequest"
type="button"
class="btn btn-default btn-xs js-remove-wip">
<i
v-if="isMakingRequest"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
Resolve WIP status
</button>
</div>
</div>
`,
}; };
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="warning"
:show-disabled-button="Boolean(mr.removeWIPPath)"
/>
<div class="media-body space-children">
<span class="bold">
This is a Work in Progress
<i
v-tooltip
class="fa fa-question-circle"
title="When this merge request is ready,
remove the WIP: prefix from the title to allow it to be merged"
aria-label="When this merge request is ready,
remove the WIP: prefix from the title to allow it to be merged">
</i>
</span>
<button
v-if="mr.removeWIPPath"
@click="removeWIP"
:disabled="isMakingRequest"
type="button"
class="btn btn-default btn-xs js-remove-wip">
<i
v-if="isMakingRequest"
class="fa fa-spinner fa-spin"
aria-hidden="true">
</i>
Resolve WIP status
</button>
</div>
</div>
</template>
...@@ -21,7 +21,7 @@ export { default as MergedState } from './components/states/mr_widget_merged.vue ...@@ -21,7 +21,7 @@ export { default as MergedState } from './components/states/mr_widget_merged.vue
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue'; export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
export { default as ClosedState } from './components/states/mr_widget_closed.vue'; export { default as ClosedState } from './components/states/mr_widget_closed.vue';
export { default as MergingState } from './components/states/mr_widget_merging.vue'; export { default as MergingState } from './components/states/mr_widget_merging.vue';
export { default as WipState } from './components/states/mr_widget_wip'; export { default as WorkInProgressState } from './components/states/work_in_progress.vue';
export { default as ArchivedState } from './components/states/mr_widget_archived.vue'; export { default as ArchivedState } from './components/states/mr_widget_archived.vue';
export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue'; export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue';
export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue'; export { default as NothingToMergeState } from './components/states/nothing_to_merge.vue';
......
...@@ -12,7 +12,7 @@ import { ...@@ -12,7 +12,7 @@ import {
ClosedState, ClosedState,
MergingState, MergingState,
RebaseState, RebaseState,
WipState, WorkInProgressState,
ArchivedState, ArchivedState,
ConflictsState, ConflictsState,
NothingToMergeState, NothingToMergeState,
...@@ -220,7 +220,7 @@ export default { ...@@ -220,7 +220,7 @@ export default {
'mr-widget-closed': ClosedState, 'mr-widget-closed': ClosedState,
'mr-widget-merging': MergingState, 'mr-widget-merging': MergingState,
'mr-widget-failed-to-merge': FailedToMerge, 'mr-widget-failed-to-merge': FailedToMerge,
'mr-widget-wip': WipState, 'mr-widget-wip': WorkInProgressState,
'mr-widget-archived': ArchivedState, 'mr-widget-archived': ArchivedState,
'mr-widget-conflicts': ConflictsState, 'mr-widget-conflicts': ConflictsState,
'mr-widget-nothing-to-merge': NothingToMergeState, 'mr-widget-nothing-to-merge': NothingToMergeState,
......
...@@ -17,7 +17,7 @@ export default { ...@@ -17,7 +17,7 @@ export default {
}, },
computed: { computed: {
/** /**
* This method is based on app/helpers/application_helper.rb#project_identicon * This method is based on app/helpers/avatars_helper.rb#project_identicon
*/ */
identiconStyles() { identiconStyles() {
const allowedColors = [ const allowedColors = [
......
This diff is collapsed.
@import "framework/variables"; @import 'framework/variables';
@import "framework/mixins"; @import 'framework/mixins';
@import 'framework/tw_bootstrap_variables'; @import 'framework/tw_bootstrap_variables';
@import 'framework/tw_bootstrap'; @import 'framework/tw_bootstrap';
@import "framework/layout"; @import 'framework/layout';
@import "framework/animations"; @import 'framework/animations';
@import "framework/vue_transitions"; @import 'framework/vue_transitions';
@import "framework/avatar"; @import 'framework/avatar';
@import "framework/asciidoctor"; @import 'framework/asciidoctor';
@import "framework/banner"; @import 'framework/banner';
@import "framework/blocks"; @import 'framework/blocks';
@import "framework/buttons"; @import 'framework/buttons';
@import "framework/badges"; @import 'framework/badges';
@import "framework/calendar"; @import 'framework/calendar';
@import "framework/callout"; @import 'framework/callout';
@import "framework/common"; @import 'framework/common';
@import "framework/dropdowns"; @import 'framework/dropdowns';
@import "framework/files"; @import 'framework/files';
@import "framework/filters"; @import 'framework/filters';
@import "framework/flash"; @import 'framework/flash';
@import "framework/forms"; @import 'framework/forms';
@import "framework/gfm"; @import 'framework/gfm';
@import "framework/gitlab_theme"; @import 'framework/gitlab_theme';
@import "framework/header"; @import 'framework/header';
@import "framework/highlight"; @import 'framework/highlight';
@import "framework/issue_box"; @import 'framework/issue_box';
@import "framework/jquery"; @import 'framework/jquery';
@import "framework/lists"; @import 'framework/lists';
@import "framework/logo"; @import 'framework/logo';
@import "framework/markdown_area"; @import 'framework/markdown_area';
@import "framework/media_object"; @import 'framework/media_object';
@import "framework/mobile"; @import 'framework/mobile';
@import "framework/modal"; @import 'framework/modal';
@import "framework/pagination"; @import 'framework/pagination';
@import "framework/panels"; @import 'framework/panels';
@import "framework/popup"; @import 'framework/popup';
@import "framework/secondary_navigation_elements"; @import 'framework/secondary_navigation_elements';
@import "framework/selects"; @import 'framework/selects';
@import "framework/sidebar"; @import 'framework/sidebar';
@import "framework/contextual_sidebar"; @import 'framework/contextual_sidebar';
@import "framework/tables"; @import 'framework/tables';
@import "framework/notes"; @import 'framework/notes';
@import "framework/tabs"; @import 'framework/tabs';
@import "framework/timeline"; @import 'framework/timeline';
@import "framework/tooltips"; @import 'framework/tooltips';
@import "framework/toggle"; @import 'framework/toggle';
@import "framework/typography"; @import 'framework/typography';
@import "framework/zen"; @import 'framework/zen';
@import "framework/blank"; @import 'framework/blank';
@import "framework/wells"; @import 'framework/wells';
@import "framework/page_header"; @import 'framework/page_header';
@import "framework/awards"; @import 'framework/awards';
@import "framework/images"; @import 'framework/images';
@import "framework/broadcast_messages"; @import 'framework/broadcast_messages';
@import "framework/emojis"; @import 'framework/emojis';
@import "framework/emoji_sprites"; @import 'framework/icons';
@import "framework/icons"; @import 'framework/snippets';
@import "framework/snippets"; @import 'framework/memory_graph';
@import "framework/memory_graph"; @import 'framework/responsive_tables';
@import "framework/responsive_tables"; @import 'framework/stacked_progress_bar';
@import "framework/stacked_progress_bar"; @import 'framework/ci_variable_list';
@import "framework/ci_variable_list"; @import 'framework/feature_highlight';
@import "framework/feature_highlight";
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
} }
&.middle-block { &.middle-block {
margin-top: 0; margin-top: $gl-padding-24;
margin-bottom: 0; margin-bottom: 0;
} }
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
} }
&.footer-block { &.footer-block {
margin-top: 0; margin-top: $gl-padding-24;
border-bottom: 0; border-bottom: 0;
margin-bottom: -$gl-padding; margin-bottom: -$gl-padding;
} }
......
...@@ -452,6 +452,7 @@ img.emoji { ...@@ -452,6 +452,7 @@ img.emoji {
/** COMMON CLASSES **/ /** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; } .prepend-top-0 { margin-top: 0; }
.prepend-top-2 { margin-top: 2px; }
.prepend-top-5 { margin-top: 5px; } .prepend-top-5 { margin-top: 5px; }
.prepend-top-8 { margin-top: $grid-size; } .prepend-top-8 { margin-top: $grid-size; }
.prepend-top-10 { margin-top: 10px; } .prepend-top-10 { margin-top: 10px; }
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -39,35 +39,10 @@ ...@@ -39,35 +39,10 @@
svg { svg {
fill: currentColor; fill: currentColor;
&.s8 { $svg-sizes: 8 12 16 18 24 32 48 72;
@include svg-size(8px); @each $svg-size in $svg-sizes {
} &.s#{$svg-size} {
@include svg-size(#{$svg-size}px);
&.s12 { }
@include svg-size(12px);
}
&.s16 {
@include svg-size(16px);
}
&.s18 {
@include svg-size(18px);
}
&.s24 {
@include svg-size(24px);
}
&.s32 {
@include svg-size(32px);
}
&.s48 {
@include svg-size(48px);
}
&.s72 {
@include svg-size(72px);
} }
} }
...@@ -107,6 +107,16 @@ ...@@ -107,6 +107,16 @@
padding-top: 10px; padding-top: 10px;
} }
.referenced-commands {
background: $blue-50;
padding: $gl-padding-8 $gl-padding;
border-radius: $border-radius-default;
p {
margin: 0;
}
}
.md-preview-holder { .md-preview-holder {
min-height: 167px; min-height: 167px;
padding: 10px 0; padding: 10px 0;
......
...@@ -212,6 +212,7 @@ $tooltip-font-size: 12px; ...@@ -212,6 +212,7 @@ $tooltip-font-size: 12px;
/* /*
* Padding * Padding
*/ */
$gl-padding-24: 24px;
$gl-padding: 16px; $gl-padding: 16px;
$gl-padding-8: 8px; $gl-padding-8: 8px;
$gl-padding-4: 4px; $gl-padding-4: 4px;
......
...@@ -44,6 +44,12 @@ ...@@ -44,6 +44,12 @@
} }
} }
.note-text {
table {
font-family: $font-family-sans-serif;
}
}
table { table {
width: 100%; width: 100%;
font-family: $monospace_font; font-family: $monospace_font;
......
...@@ -772,7 +772,3 @@ ul.notes { ...@@ -772,7 +772,3 @@ ul.notes {
height: auto; height: auto;
} }
} }
.line-resolve-text {
vertical-align: middle;
}
...@@ -485,6 +485,7 @@ ...@@ -485,6 +485,7 @@
margin-bottom: 0; margin-bottom: 0;
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
padding: $gl-btn-padding $gl-padding; padding: $gl-btn-padding $gl-padding;
min-height: 56px;
} }
.multi-file-commit-panel-header-title { .multi-file-commit-panel-header-title {
...@@ -613,6 +614,24 @@ ...@@ -613,6 +614,24 @@
} }
} }
.multi-file-commit-panel-bottom {
position: relative;
.multi-file-commit-panel-success-message {
position: absolute;
top: 1px;
left: 3px;
bottom: 0;
right: 0;
z-index: 10;
background: $gray-light;
overflow: auto;
display: flex;
flex-direction: column;
justify-content: center;
}
}
.dirty-diff { .dirty-diff {
// !important need to override monaco inline style // !important need to override monaco inline style
width: 4px !important; width: 4px !important;
......
This diff is collapsed.
...@@ -110,7 +110,8 @@ class ApplicationController < ActionController::Base ...@@ -110,7 +110,8 @@ class ApplicationController < ActionController::Base
def log_exception(exception) def log_exception(exception)
Raven.capture_exception(exception) if sentry_enabled? Raven.capture_exception(exception) if sentry_enabled?
application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace backtrace_cleaner = Gitlab.rails5? ? env["action_dispatch.backtrace_cleaner"] : env
application_trace = ActionDispatch::ExceptionWrapper.new(backtrace_cleaner, exception).application_trace
application_trace.map! { |t| " #{t}\n" } application_trace.map! { |t| " #{t}\n" }
logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}" logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}"
end end
......
...@@ -57,7 +57,7 @@ module IssuableCollections ...@@ -57,7 +57,7 @@ module IssuableCollections
out_of_range = @issuables.current_page > total_pages # rubocop:disable Gitlab/ModuleWithInstanceVariables out_of_range = @issuables.current_page > total_pages # rubocop:disable Gitlab/ModuleWithInstanceVariables
if out_of_range if out_of_range
redirect_to(url_for(params.merge(page: total_pages, only_path: true))) redirect_to(url_for(safe_params.merge(page: total_pages, only_path: true)))
end end
out_of_range out_of_range
......
...@@ -33,6 +33,6 @@ class Groups::ApplicationController < ApplicationController ...@@ -33,6 +33,6 @@ class Groups::ApplicationController < ApplicationController
def build_canonical_path(group) def build_canonical_path(group)
params[:group_id] = group.to_param params[:group_id] = group.to_param
url_for(params) url_for(safe_params)
end end
end end
...@@ -8,8 +8,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -8,8 +8,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
omniauth_flow(Gitlab::Auth::OAuth) omniauth_flow(Gitlab::Auth::OAuth)
end end
Gitlab.config.omniauth.providers.each do |provider| AuthHelper.providers_for_base_controller.each do |provider|
alias_method provider['name'], :handle_omniauth alias_method provider, :handle_omniauth
end end
# Extend the standard implementation to also increment # Extend the standard implementation to also increment
......
class Profiles::ActiveSessionsController < Profiles::ApplicationController
def index
@sessions = ActiveSession.list(current_user)
end
def destroy
ActiveSession.destroy(current_user, params[:id])
respond_to do |format|
format.html { redirect_to profile_active_sessions_url, status: 302 }
format.js { head :ok }
end
end
end
...@@ -25,7 +25,7 @@ class Projects::ApplicationController < ApplicationController ...@@ -25,7 +25,7 @@ class Projects::ApplicationController < ApplicationController
params[:namespace_id] = project.namespace.to_param params[:namespace_id] = project.namespace.to_param
params[:project_id] = project.to_param params[:project_id] = project.to_param
url_for(params) url_for(safe_params)
end end
def repository def repository
......
...@@ -77,8 +77,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController ...@@ -77,8 +77,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
def link_to_project!(object) def link_to_project!(object)
if object && !object.projects.exists?(storage_project.id) if object && !object.projects.exists?(storage_project.id)
object.projects << storage_project object.lfs_objects_projects.create!(project: storage_project)
object.save!
end end
end end
end end
...@@ -83,13 +83,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -83,13 +83,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
render layout: false render layout: false
end end
def update_branches
@target_project = selected_target_project
@target_branches = @target_project ? @target_project.repository.branch_names : []
render layout: false
end
private private
def build_merge_request def build_merge_request
......
...@@ -33,9 +33,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -33,9 +33,7 @@ class Projects::NotesController < Projects::ApplicationController
def resolve def resolve
return render_404 unless note.resolvable? return render_404 unless note.resolvable?
note.resolve!(current_user) Notes::ResolveService.new(project, current_user).execute(note)
MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(note.noteable)
discussion = note.discussion discussion = note.discussion
......
...@@ -39,7 +39,7 @@ class GroupsFinder < UnionFinder ...@@ -39,7 +39,7 @@ class GroupsFinder < UnionFinder
def all_groups def all_groups
return [owned_groups] if params[:owned] return [owned_groups] if params[:owned]
return [Group.all] if current_user&.full_private_access? return [Group.all] if current_user&.full_private_access? && all_available?
groups = [] groups = []
groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups if current_user groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups if current_user
...@@ -67,6 +67,10 @@ class GroupsFinder < UnionFinder ...@@ -67,6 +67,10 @@ class GroupsFinder < UnionFinder
end end
def include_public_groups? def include_public_groups?
current_user.nil? || params.fetch(:all_available, true) current_user.nil? || all_available?
end
def all_available?
params.fetch(:all_available, true)
end end
end end
module ActiveSessionsHelper
# Maps a device type as defined in `ActiveSession` to an svg icon name and
# outputs the icon html.
#
# see `DeviceDetector::Device::DEVICE_NAMES` about the available device types
def active_session_device_type_icon(active_session)
icon_name =
case active_session.device_type
when 'smartphone', 'feature phone', 'phablet'
'mobile'
when 'tablet'
'tablet'
when 'tv', 'smart display', 'camera', 'portable media player', 'console'
'media'
when 'car browser'
'car'
else
'monitor-o'
end
sprite_icon(icon_name, size: 16, css_class: 'prepend-top-2')
end
end
...@@ -32,80 +32,6 @@ module ApplicationHelper ...@@ -32,80 +32,6 @@ module ApplicationHelper
args.any? { |v| v.to_s.downcase == action_name } args.any? { |v| v.to_s.downcase == action_name }
end end
def project_icon(project_id, options = {})
project =
if project_id.respond_to?(:avatar_url)
project_id
else
Project.find_by_full_path(project_id)
end
if project.avatar_url
image_tag project.avatar_url, options
else # generated icon
project_identicon(project, options)
end
end
def project_identicon(project, options = {})
allowed_colors = {
red: 'FFEBEE',
purple: 'F3E5F5',
indigo: 'E8EAF6',
blue: 'E3F2FD',
teal: 'E0F2F1',
orange: 'FBE9E7',
gray: 'EEEEEE'
}
options[:class] ||= ''
options[:class] << ' identicon'
bg_key = project.id % 7
style = "background-color: ##{allowed_colors.values[bg_key]}; color: #555"
content_tag(:div, class: options[:class], style: style) do
project.name[0, 1].upcase
end
end
# Takes both user and email and returns the avatar_icon by
# user (preferred) or email.
def avatar_icon_for(user = nil, email = nil, size = nil, scale = 2, only_path: true)
if user
avatar_icon_for_user(user, size, scale, only_path: only_path)
elsif email
avatar_icon_for_email(email, size, scale, only_path: only_path)
else
default_avatar
end
end
def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true)
user = User.find_by_any_email(email.try(:downcase))
if user
avatar_icon_for_user(user, size, scale, only_path: only_path)
else
gravatar_icon(email, size, scale)
end
end
def avatar_icon_for_user(user = nil, size = nil, scale = 2, only_path: true)
if user
user.avatar_url(size: size, only_path: only_path) || default_avatar
else
gravatar_icon(nil, size, scale)
end
end
def gravatar_icon(user_email = '', size = nil, scale = 2)
GravatarService.new.execute(user_email, size, scale) ||
default_avatar
end
def default_avatar
asset_path('no_avatar.png')
end
def last_commit(project) def last_commit(project)
if project.repo_exists? if project.repo_exists?
time_ago_with_tooltip(project.repository.commit.committed_date) time_ago_with_tooltip(project.repository.commit.committed_date)
......
module AuthHelper module AuthHelper
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze LDAP_PROVIDER = /\Aldap/
def ldap_enabled? def ldap_enabled?
Gitlab::Auth::LDAP::Config.enabled? Gitlab::Auth::LDAP::Config.enabled?
...@@ -23,7 +23,7 @@ module AuthHelper ...@@ -23,7 +23,7 @@ module AuthHelper
end end
def form_based_provider?(name) def form_based_provider?(name)
FORM_BASED_PROVIDERS.any? { |pattern| pattern === name.to_s } [LDAP_PROVIDER, 'crowd'].any? { |pattern| pattern === name.to_s }
end end
def form_based_providers def form_based_providers
...@@ -38,6 +38,10 @@ module AuthHelper ...@@ -38,6 +38,10 @@ module AuthHelper
auth_providers.reject { |provider| form_based_provider?(provider) } auth_providers.reject { |provider| form_based_provider?(provider) }
end end
def providers_for_base_controller
auth_providers.reject { |provider| LDAP_PROVIDER === provider }
end
def enabled_button_based_providers def enabled_button_based_providers
disabled_providers = Gitlab::CurrentSettings.disabled_oauth_sign_in_sources || [] disabled_providers = Gitlab::CurrentSettings.disabled_oauth_sign_in_sources || []
......
module AvatarsHelper module AvatarsHelper
def project_icon(project_id, options = {})
project =
if project_id.respond_to?(:avatar_url)
project_id
else
Project.find_by_full_path(project_id)
end
if project.avatar_url
image_tag project.avatar_url, options
else # generated icon
project_identicon(project, options)
end
end
def project_identicon(project, options = {})
allowed_colors = {
red: 'FFEBEE',
purple: 'F3E5F5',
indigo: 'E8EAF6',
blue: 'E3F2FD',
teal: 'E0F2F1',
orange: 'FBE9E7',
gray: 'EEEEEE'
}
options[:class] ||= ''
options[:class] << ' identicon'
bg_key = project.id % 7
style = "background-color: ##{allowed_colors.values[bg_key]}; color: #555"
content_tag(:div, class: options[:class], style: style) do
project.name[0, 1].upcase
end
end
# Takes both user and email and returns the avatar_icon by
# user (preferred) or email.
def avatar_icon_for(user = nil, email = nil, size = nil, scale = 2, only_path: true)
if user
avatar_icon_for_user(user, size, scale, only_path: only_path)
elsif email
avatar_icon_for_email(email, size, scale, only_path: only_path)
else
default_avatar
end
end
def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true)
user = User.find_by_any_email(email.try(:downcase))
if user
avatar_icon_for_user(user, size, scale, only_path: only_path)
else
gravatar_icon(email, size, scale)
end
end
def avatar_icon_for_user(user = nil, size = nil, scale = 2, only_path: true)
if user
user.avatar_url(size: size, only_path: only_path) || default_avatar
else
gravatar_icon(nil, size, scale)
end
end
def gravatar_icon(user_email = '', size = nil, scale = 2)
GravatarService.new.execute(user_email, size, scale) ||
default_avatar
end
def default_avatar
ActionController::Base.helpers.image_path('no_avatar.png')
end
def author_avatar(commit_or_event, options = {}) def author_avatar(commit_or_event, options = {})
user_avatar(options.merge({ user_avatar(options.merge({
user: commit_or_event.author, user: commit_or_event.author,
......
...@@ -41,7 +41,7 @@ module DropdownsHelper ...@@ -41,7 +41,7 @@ module DropdownsHelper
def dropdown_toggle(toggle_text, data_attr, options = {}) def dropdown_toggle(toggle_text, data_attr, options = {})
default_label = data_attr[:default_label] default_label = data_attr[:default_label]
content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.key?(:toggle_class)}", id: (options[:id] if options.key?(:id)), type: "button", data: data_attr) do content_tag(:button, disabled: options[:disabled], class: "dropdown-menu-toggle #{options[:toggle_class] if options.key?(:toggle_class)}", id: (options[:id] if options.key?(:id)), type: "button", data: data_attr) do
output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}") output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}")
output << icon('chevron-down') output << icon('chevron-down')
output.html_safe output.html_safe
......
...@@ -442,7 +442,7 @@ module ProjectsHelper ...@@ -442,7 +442,7 @@ module ProjectsHelper
visibilityHelpPath: help_page_path('public_access/public_access'), visibilityHelpPath: help_page_path('public_access/public_access'),
registryAvailable: Gitlab.config.registry.enabled, registryAvailable: Gitlab.config.registry.enabled,
registryHelpPath: help_page_path('user/project/container_registry'), registryHelpPath: help_page_path('user/project/container_registry'),
lfsAvailable: Gitlab.config.lfs.enabled && current_user.admin?, lfsAvailable: Gitlab.config.lfs.enabled,
lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
} }
......
module SystemNoteHelper module SystemNoteHelper
ICON_NAMES_BY_ACTION = { ICON_NAMES_BY_ACTION = {
'commit' => 'commit', 'commit' => 'commit',
'description' => 'pencil', 'description' => 'pencil-square',
'merge' => 'git-merge', 'merge' => 'git-merge',
'merged' => 'git-merge', 'merged' => 'git-merge',
'opened' => 'issue-open', 'opened' => 'issue-open',
'closed' => 'issue-close', 'closed' => 'issue-close',
'time_tracking' => 'timer', 'time_tracking' => 'timer',
'assignee' => 'user', 'assignee' => 'user',
'title' => 'pencil', 'title' => 'pencil-square',
'task' => 'task-done', 'task' => 'task-done',
'label' => 'label', 'label' => 'label',
'cross_reference' => 'comment-dots', 'cross_reference' => 'comment-dots',
...@@ -18,7 +18,7 @@ module SystemNoteHelper ...@@ -18,7 +18,7 @@ module SystemNoteHelper
'milestone' => 'clock', 'milestone' => 'clock',
'discussion' => 'comment', 'discussion' => 'comment',
'moved' => 'arrow-right', 'moved' => 'arrow-right',
'outdated' => 'pencil', 'outdated' => 'pencil-square',
'duplicate' => 'issue-duplicate', 'duplicate' => 'issue-duplicate',
'locked' => 'lock', 'locked' => 'lock',
'unlocked' => 'lock-open' 'unlocked' => 'lock-open'
......
...@@ -16,6 +16,7 @@ class Notify < BaseMailer ...@@ -16,6 +16,7 @@ class Notify < BaseMailer
helper BlobHelper helper BlobHelper
helper EmailsHelper helper EmailsHelper
helper MembersHelper helper MembersHelper
helper AvatarsHelper
helper GitlabRoutingHelper helper GitlabRoutingHelper
def test_email(recipient_email, subject, body) def test_email(recipient_email, subject, body)
......
class ActiveSession
include ActiveModel::Model
attr_accessor :created_at, :updated_at,
:session_id, :ip_address,
:browser, :os, :device_name, :device_type
def current?(session)
return false if session_id.nil? || session.id.nil?
session_id == session.id
end
def human_device_type
device_type&.titleize
end
def self.set(user, request)
Gitlab::Redis::SharedState.with do |redis|
session_id = request.session.id
client = DeviceDetector.new(request.user_agent)
timestamp = Time.current
active_user_session = new(
ip_address: request.ip,
browser: client.name,
os: client.os_name,
device_name: client.device_name,
device_type: client.device_type,
created_at: user.current_sign_in_at || timestamp,
updated_at: timestamp,
session_id: session_id
)
redis.pipelined do
redis.setex(
key_name(user.id, session_id),
Settings.gitlab['session_expire_delay'] * 60,
Marshal.dump(active_user_session)
)
redis.sadd(
lookup_key_name(user.id),
session_id
)
end
end
end
def self.list(user)
Gitlab::Redis::SharedState.with do |redis|
cleaned_up_lookup_entries(redis, user.id).map do |entry|
# rubocop:disable Security/MarshalLoad
Marshal.load(entry)
# rubocop:enable Security/MarshalLoad
end
end
end
def self.destroy(user, session_id)
Gitlab::Redis::SharedState.with do |redis|
redis.srem(lookup_key_name(user.id), session_id)
deleted_keys = redis.del(key_name(user.id, session_id))
# only allow deleting the devise session if we could actually find a
# related active session. this prevents another user from deleting
# someone else's session.
if deleted_keys > 0
redis.del("#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}")
end
end
end
def self.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
cleaned_up_lookup_entries(redis, user.id)
end
end
def self.key_name(user_id, session_id = '*')
"#{Gitlab::Redis::SharedState::USER_SESSIONS_NAMESPACE}:#{user_id}:#{session_id}"
end
def self.lookup_key_name(user_id)
"#{Gitlab::Redis::SharedState::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user_id}"
end
def self.cleaned_up_lookup_entries(redis, user_id)
lookup_key = lookup_key_name(user_id)
session_ids = redis.smembers(lookup_key)
entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
return [] if entry_keys.empty?
entries = redis.mget(entry_keys)
session_ids_and_entries = session_ids.zip(entries)
# remove expired keys.
# only the single key entries are automatically expired by redis, the
# lookup entries in the set need to be removed manually.
session_ids_and_entries.reject { |_session_id, entry| entry }.each do |session_id, _entry|
redis.srem(lookup_key, session_id)
end
session_ids_and_entries.select { |_session_id, entry| entry }.map { |_session_id, entry| entry }
end
end
...@@ -13,7 +13,7 @@ module Ci ...@@ -13,7 +13,7 @@ module Ci
after_save :update_project_statistics_after_save, if: :size_changed? after_save :update_project_statistics_after_save, if: :size_changed?
after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed? after_destroy :update_project_statistics_after_destroy, unless: :project_destroyed?
after_save :update_file_store after_save :update_file_store, if: :file_changed?
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) } scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
......
...@@ -530,6 +530,17 @@ module Ci ...@@ -530,6 +530,17 @@ module Ci
@latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a @latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a
end end
# Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil.
# They always return `false`.
# These methods overwrite autogenerated ones to return correct results.
def unknown?
Gitlab.rails5? ? source.nil? : super
end
def unknown_source?
Gitlab.rails5? ? config_source.nil? : super
end
private private
def ci_yaml_from_repo def ci_yaml_from_repo
......
...@@ -13,14 +13,27 @@ module Ci ...@@ -13,14 +13,27 @@ module Ci
has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id
has_many :builds, foreign_key: :stage_id has_many :builds, foreign_key: :stage_id
validates :project, presence: true, unless: :importing? with_options unless: :importing? do
validates :pipeline, presence: true, unless: :importing? validates :project, presence: true
validates :name, presence: true, unless: :importing? validates :pipeline, presence: true
validates :name, presence: true
validates :position, presence: true
end
after_initialize do |stage| after_initialize do
self.status = DEFAULT_STATUS if self.status.nil? self.status = DEFAULT_STATUS if self.status.nil?
end end
before_validation unless: :importing? do
next if position.present?
self.position = statuses.select(:stage_idx)
.where('stage_idx IS NOT NULL')
.group(:stage_idx)
.order('COUNT(*) DESC')
.first&.stage_idx.to_i
end
state_machine :status, initial: :created do state_machine :status, initial: :created do
event :enqueue do event :enqueue do
transition created: :pending transition created: :pending
......
...@@ -105,6 +105,10 @@ class Commit ...@@ -105,6 +105,10 @@ class Commit
end end
end end
end end
def parent_class
::Project
end
end end
attr_accessor :raw attr_accessor :raw
...@@ -420,6 +424,12 @@ class Commit ...@@ -420,6 +424,12 @@ class Commit
# no-op but needs to be defined since #persisted? is defined # no-op but needs to be defined since #persisted? is defined
end end
def touch_later
# No-op.
# This method is called by ActiveRecord.
# We don't want to do anything for `Commit` model, so this is empty.
end
WIP_REGEX = /\A\s*(((?i)(\[WIP\]|WIP:|WIP)\s|WIP$))|(fixup!|squash!)\s/.freeze WIP_REGEX = /\A\s*(((?i)(\[WIP\]|WIP:|WIP)\s|WIP$))|(fixup!|squash!)\s/.freeze
def work_in_progress? def work_in_progress?
......
...@@ -189,4 +189,11 @@ class CommitStatus < ActiveRecord::Base ...@@ -189,4 +189,11 @@ class CommitStatus < ActiveRecord::Base
v =~ /\d+/ ? v.to_i : v v =~ /\d+/ ? v.to_i : v
end end
end end
# Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil.
# They always return `false`.
# This method overwrites the autogenerated one to return correct result.
def unknown_failure?
Gitlab.rails5? ? failure_reason.nil? : super
end
end end
...@@ -54,7 +54,20 @@ class DiffNote < Note ...@@ -54,7 +54,20 @@ class DiffNote < Note
end end
def diff_file def diff_file
@diff_file ||= self.original_position.diff_file(self.project.repository) @diff_file ||=
begin
if created_at_diff?(noteable.diff_refs)
# We're able to use the already persisted diffs (Postgres) if we're
# presenting a "current version" of the MR discussion diff.
# So no need to make an extra Gitaly diff request for it.
# As an extra benefit, the returned `diff_file` already
# has `highlighted_diff_lines` data set from Redis on
# `Diff::FileCollection::MergeRequestDiff`.
noteable.diffs(paths: original_position.paths, expanded: true).diff_files.first
else
original_position.diff_file(self.project.repository)
end
end
end end
def diff_line def diff_line
......
class Identity < ActiveRecord::Base class Identity < ActiveRecord::Base
def self.uniqueness_scope
:provider
end
include Sortable include Sortable
include CaseSensitivity include CaseSensitivity
belongs_to :user belongs_to :user
validates :provider, presence: true validates :provider, presence: true
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider, case_sensitive: false } validates :extern_uid, allow_blank: true, uniqueness: { scope: uniqueness_scope, case_sensitive: false }
validates :user_id, uniqueness: { scope: :provider } validates :user_id, uniqueness: { scope: uniqueness_scope }
before_save :ensure_normalized_extern_uid, if: :extern_uid_changed? before_save :ensure_normalized_extern_uid, if: :extern_uid_changed?
after_destroy :clear_user_synced_attributes, if: :user_synced_attributes_metadata_from_provider? after_destroy :clear_user_synced_attributes, if: :user_synced_attributes_metadata_from_provider?
......
...@@ -11,7 +11,7 @@ class LfsObject < ActiveRecord::Base ...@@ -11,7 +11,7 @@ class LfsObject < ActiveRecord::Base
mount_uploader :file, LfsObjectUploader mount_uploader :file, LfsObjectUploader
after_save :update_file_store after_save :update_file_store, if: :file_changed?
def update_file_store def update_file_store
# The file.object_store is set during `uploader.store!` # The file.object_store is set during `uploader.store!`
......
...@@ -37,20 +37,20 @@ class GroupMember < Member ...@@ -37,20 +37,20 @@ class GroupMember < Member
private private
def send_invite def send_invite
notification_service.invite_group_member(self, @raw_invite_token) run_after_commit_or_now { notification_service.invite_group_member(self, @raw_invite_token) }
super super
end end
def post_create_hook def post_create_hook
notification_service.new_group_member(self) run_after_commit_or_now { notification_service.new_group_member(self) }
super super
end end
def post_update_hook def post_update_hook
if access_level_changed? if access_level_changed?
notification_service.update_group_member(self) run_after_commit { notification_service.update_group_member(self) }
end end
super super
......
...@@ -92,7 +92,7 @@ class ProjectMember < Member ...@@ -92,7 +92,7 @@ class ProjectMember < Member
private private
def send_invite def send_invite
notification_service.invite_project_member(self, @raw_invite_token) run_after_commit_or_now { notification_service.invite_project_member(self, @raw_invite_token) }
super super
end end
...@@ -100,7 +100,7 @@ class ProjectMember < Member ...@@ -100,7 +100,7 @@ class ProjectMember < Member
def post_create_hook def post_create_hook
unless owner? unless owner?
event_service.join_project(self.project, self.user) event_service.join_project(self.project, self.user)
notification_service.new_project_member(self) run_after_commit_or_now { notification_service.new_project_member(self) }
end end
super super
...@@ -108,7 +108,7 @@ class ProjectMember < Member ...@@ -108,7 +108,7 @@ class ProjectMember < Member
def post_update_hook def post_update_hook
if access_level_changed? if access_level_changed?
notification_service.update_project_member(self) run_after_commit { notification_service.update_project_member(self) }
end end
super super
......
...@@ -323,7 +323,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -323,7 +323,7 @@ class MergeRequest < ActiveRecord::Base
# updates `merge_jid` with the MergeWorker#jid. # updates `merge_jid` with the MergeWorker#jid.
# This helps tracking enqueued and ongoing merge jobs. # This helps tracking enqueued and ongoing merge jobs.
def merge_async(user_id, params) def merge_async(user_id, params)
jid = MergeWorker.perform_async(id, user_id, params) jid = MergeWorker.perform_async(id, user_id, params.to_h)
update_column(:merge_jid, jid) update_column(:merge_jid, jid)
end end
......
...@@ -910,7 +910,7 @@ class User < ActiveRecord::Base ...@@ -910,7 +910,7 @@ class User < ActiveRecord::Base
def delete_async(deleted_by:, params: {}) def delete_async(deleted_by:, params: {})
block if params[:hard_delete] block if params[:hard_delete]
DeleteUserWorker.perform_async(deleted_by.id, id, params) DeleteUserWorker.perform_async(deleted_by.id, id, params.to_h)
end end
def notification_service def notification_service
......
...@@ -42,6 +42,7 @@ module Ci ...@@ -42,6 +42,7 @@ module Ci
def create_stage def create_stage
Ci::Stage.create!(name: @build.stage, Ci::Stage.create!(name: @build.stage,
position: @build.stage_idx,
pipeline: @build.pipeline, pipeline: @build.pipeline,
project: @build.project) project: @build.project)
end end
......
...@@ -26,7 +26,7 @@ module Issues ...@@ -26,7 +26,7 @@ module Issues
issue.update(closed_by: current_user) issue.update(closed_by: current_user)
event_service.close_issue(issue, current_user) event_service.close_issue(issue, current_user)
create_note(issue, commit) if system_note create_note(issue, commit) if system_note
notification_service.close_issue(issue, current_user) if notifications notification_service.async.close_issue(issue, current_user) if notifications
todo_service.close_issue(issue, current_user) todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close') execute_hooks(issue, 'close')
invalidate_cache_counts(issue, users: issue.assignees) invalidate_cache_counts(issue, users: issue.assignees)
......
...@@ -139,7 +139,7 @@ module Issues ...@@ -139,7 +139,7 @@ module Issues
end end
def notify_participants def notify_participants
notification_service.issue_moved(@old_issue, @new_issue, @current_user) notification_service.async.issue_moved(@old_issue, @new_issue, @current_user)
end end
end end
end end
...@@ -6,7 +6,7 @@ module Issues ...@@ -6,7 +6,7 @@ module Issues
if issue.reopen if issue.reopen
event_service.reopen_issue(issue, current_user) event_service.reopen_issue(issue, current_user)
create_note(issue, 'reopened') create_note(issue, 'reopened')
notification_service.reopen_issue(issue, current_user) notification_service.async.reopen_issue(issue, current_user)
execute_hooks(issue, 'reopen') execute_hooks(issue, 'reopen')
invalidate_cache_counts(issue, users: issue.assignees) invalidate_cache_counts(issue, users: issue.assignees)
issue.update_project_counter_caches issue.update_project_counter_caches
......
...@@ -30,7 +30,7 @@ module Issues ...@@ -30,7 +30,7 @@ module Issues
if issue.assignees != old_assignees if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees) create_assignee_note(issue, old_assignees)
notification_service.reassigned_issue(issue, current_user, old_assignees) notification_service.async.reassigned_issue(issue, current_user, old_assignees)
todo_service.reassigned_issue(issue, current_user, old_assignees) todo_service.reassigned_issue(issue, current_user, old_assignees)
end end
...@@ -41,13 +41,13 @@ module Issues ...@@ -41,13 +41,13 @@ module Issues
added_labels = issue.labels - old_labels added_labels = issue.labels - old_labels
if added_labels.present? if added_labels.present?
notification_service.relabeled_issue(issue, added_labels, current_user) notification_service.async.relabeled_issue(issue, added_labels, current_user)
end end
added_mentions = issue.mentioned_users - old_mentioned_users added_mentions = issue.mentioned_users - old_mentioned_users
if added_mentions.present? if added_mentions.present?
notification_service.new_mentions_in_issue(issue, added_mentions, current_user) notification_service.async.new_mentions_in_issue(issue, added_mentions, current_user)
end end
end end
......
...@@ -10,7 +10,7 @@ module MergeRequests ...@@ -10,7 +10,7 @@ module MergeRequests
if merge_request.close if merge_request.close
create_event(merge_request) create_event(merge_request)
create_note(merge_request) create_note(merge_request)
notification_service.close_mr(merge_request, current_user) notification_service.async.close_mr(merge_request, current_user)
todo_service.close_merge_request(merge_request, current_user) todo_service.close_merge_request(merge_request, current_user)
execute_hooks(merge_request, 'close') execute_hooks(merge_request, 'close')
invalidate_cache_counts(merge_request, users: merge_request.assignees) invalidate_cache_counts(merge_request, users: merge_request.assignees)
......
...@@ -50,21 +50,30 @@ module MergeRequests ...@@ -50,21 +50,30 @@ module MergeRequests
end end
def commit def commit
message = params[:commit_message] || merge_request.merge_commit_message
log_info("Git merge started on JID #{merge_jid}") log_info("Git merge started on JID #{merge_jid}")
commit_id = repository.merge(current_user, source, merge_request, message) commit_id = try_merge
log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
if commit_id
log_info("Git merge finished on JID #{merge_jid} commit #{commit_id}")
else
raise MergeError, 'Conflicts detected during merge'
end
raise MergeError, 'Conflicts detected during merge' unless commit_id merge_request.update!(merge_commit_sha: commit_id)
end
def try_merge
message = params[:commit_message] || merge_request.merge_commit_message
merge_request.update(merge_commit_sha: commit_id) repository.merge(current_user, source, merge_request, message)
rescue Gitlab::Git::HooksService::PreReceiveError => e rescue Gitlab::Git::HooksService::PreReceiveError => e
raise MergeError, e.message handle_merge_error(log_message: e.message)
rescue StandardError => e raise MergeError, 'Something went wrong during merge pre-receive hook'
raise MergeError, "Something went wrong during merge: #{e.message}" rescue => e
handle_merge_error(log_message: e.message)
raise MergeError, 'Something went wrong during merge'
ensure ensure
merge_request.update(in_progress_merge_commit_sha: nil) merge_request.update!(in_progress_merge_commit_sha: nil)
end end
def after_merge def after_merge
......
...@@ -6,7 +6,7 @@ module MergeRequests ...@@ -6,7 +6,7 @@ module MergeRequests
if merge_request.reopen if merge_request.reopen
create_event(merge_request) create_event(merge_request)
create_note(merge_request, 'reopened') create_note(merge_request, 'reopened')
notification_service.reopen_mr(merge_request, current_user) notification_service.async.reopen_mr(merge_request, current_user)
execute_hooks(merge_request, 'reopen') execute_hooks(merge_request, 'reopen')
merge_request.reload_diff(current_user) merge_request.reload_diff(current_user)
merge_request.mark_as_unchecked merge_request.mark_as_unchecked
......
...@@ -4,7 +4,7 @@ module MergeRequests ...@@ -4,7 +4,7 @@ module MergeRequests
return unless merge_request.discussions_resolved? return unless merge_request.discussions_resolved?
SystemNoteService.resolve_all_discussions(merge_request, project, current_user) SystemNoteService.resolve_all_discussions(merge_request, project, current_user)
notification_service.resolve_all_discussions(merge_request, current_user) notification_service.async.resolve_all_discussions(merge_request, current_user)
end end
end end
end end
...@@ -21,6 +21,7 @@ module MergeRequests ...@@ -21,6 +21,7 @@ module MergeRequests
update(merge_request) update(merge_request)
end end
# rubocop:disable Metrics/AbcSize
def handle_changes(merge_request, options) def handle_changes(merge_request, options)
old_associations = options.fetch(:old_associations, {}) old_associations = options.fetch(:old_associations, {})
old_labels = old_associations.fetch(:labels, []) old_labels = old_associations.fetch(:labels, [])
...@@ -42,8 +43,11 @@ module MergeRequests ...@@ -42,8 +43,11 @@ module MergeRequests
end end
if merge_request.previous_changes.include?('assignee_id') if merge_request.previous_changes.include?('assignee_id')
old_assignee_id = merge_request.previous_changes['assignee_id'].first
old_assignee = User.find(old_assignee_id) if old_assignee_id
create_assignee_note(merge_request) create_assignee_note(merge_request)
notification_service.reassigned_merge_request(merge_request, current_user) notification_service.async.reassigned_merge_request(merge_request, current_user, old_assignee)
todo_service.reassigned_merge_request(merge_request, current_user) todo_service.reassigned_merge_request(merge_request, current_user)
end end
...@@ -54,7 +58,7 @@ module MergeRequests ...@@ -54,7 +58,7 @@ module MergeRequests
added_labels = merge_request.labels - old_labels added_labels = merge_request.labels - old_labels
if added_labels.present? if added_labels.present?
notification_service.relabeled_merge_request( notification_service.async.relabeled_merge_request(
merge_request, merge_request,
added_labels, added_labels,
current_user current_user
...@@ -63,13 +67,14 @@ module MergeRequests ...@@ -63,13 +67,14 @@ module MergeRequests
added_mentions = merge_request.mentioned_users - old_mentioned_users added_mentions = merge_request.mentioned_users - old_mentioned_users
if added_mentions.present? if added_mentions.present?
notification_service.new_mentions_in_merge_request( notification_service.async.new_mentions_in_merge_request(
merge_request, merge_request,
added_mentions, added_mentions,
current_user current_user
) )
end end
end end
# rubocop:enable Metrics/AbcSize
def merge_from_quick_action(merge_request) def merge_from_quick_action(merge_request)
last_diff_sha = params.delete(:merge) last_diff_sha = params.delete(:merge)
......
module Notes
class ResolveService < ::BaseService
def execute(note)
note.resolve!(current_user)
::MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(note.noteable)
end
end
end
...@@ -7,7 +7,32 @@ ...@@ -7,7 +7,32 @@
# Ex. # Ex.
# NotificationService.new.new_issue(issue, current_user) # NotificationService.new.new_issue(issue, current_user)
# #
# When calculating the recipients of a notification is expensive (for instance,
# in the new issue case), `#async` will make that calculation happen in Sidekiq
# instead:
#
# NotificationService.new.async.new_issue(issue, current_user)
#
class NotificationService class NotificationService
class Async
attr_reader :parent
delegate :respond_to_missing, to: :parent
def initialize(parent)
@parent = parent
end
def method_missing(meth, *args)
return super unless parent.respond_to?(meth)
MailScheduler::NotificationServiceWorker.perform_async(meth.to_s, *args)
end
end
def async
@async ||= Async.new(self)
end
# Always notify user about ssh key added # Always notify user about ssh key added
# only if ssh key is not deploy key # only if ssh key is not deploy key
# #
...@@ -142,8 +167,23 @@ class NotificationService ...@@ -142,8 +167,23 @@ class NotificationService
# * merge_request assignee if their notification level is not Disabled # * merge_request assignee if their notification level is not Disabled
# * users with custom level checked with "reassign merge request" # * users with custom level checked with "reassign merge request"
# #
def reassigned_merge_request(merge_request, current_user) def reassigned_merge_request(merge_request, current_user, previous_assignee)
reassign_resource_email(merge_request, current_user, :reassigned_merge_request_email) recipients = NotificationRecipientService.build_recipients(
merge_request,
current_user,
action: "reassign",
previous_assignee: previous_assignee
)
recipients.each do |recipient|
mailer.reassigned_merge_request_email(
recipient.user.id,
merge_request.id,
previous_assignee&.id,
current_user.id,
recipient.reason
).deliver_later
end
end end
# When we add labels to a merge request we should send an email to: # When we add labels to a merge request we should send an email to:
...@@ -421,29 +461,6 @@ class NotificationService ...@@ -421,29 +461,6 @@ class NotificationService
end end
end end
def reassign_resource_email(target, current_user, method)
previous_assignee_id = previous_record(target, 'assignee_id')
previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
recipients = NotificationRecipientService.build_recipients(
target,
current_user,
action: "reassign",
previous_assignee: previous_assignee
)
recipients.each do |recipient|
mailer.send(
method,
recipient.user.id,
target.id,
previous_assignee_id,
current_user.id,
recipient.reason
).deliver_later
end
end
def relabeled_resource_email(target, labels, current_user, method) def relabeled_resource_email(target, labels, current_user, method)
recipients = labels.flat_map { |l| l.subscribers(target.project) }.uniq recipients = labels.flat_map { |l| l.subscribers(target.project) }.uniq
recipients = notifiable_users( recipients = notifiable_users(
...@@ -471,14 +488,6 @@ class NotificationService ...@@ -471,14 +488,6 @@ class NotificationService
Notify Notify
end end
def previous_record(object, attribute)
return unless object && attribute
if object.previous_changes.include?(attribute)
object.previous_changes[attribute].first
end
end
private private
def recipients_for_pages_domain(domain) def recipients_for_pages_domain(domain)
......
module Projects module Projects
class UpdatePagesService < BaseService class UpdatePagesService < BaseService
InvaildStateError = Class.new(StandardError) InvalidStateError = Class.new(StandardError)
FailedToExtractError = Class.new(StandardError) FailedToExtractError = Class.new(StandardError)
BLOCK_SIZE = 32.kilobytes BLOCK_SIZE = 32.kilobytes
...@@ -21,8 +21,8 @@ module Projects ...@@ -21,8 +21,8 @@ module Projects
@status.enqueue! @status.enqueue!
@status.run! @status.run!
raise InvaildStateError, 'missing pages artifacts' unless build.artifacts? raise InvalidStateError, 'missing pages artifacts' unless build.artifacts?
raise InvaildStateError, 'pages are outdated' unless latest? raise InvalidStateError, 'pages are outdated' unless latest?
# Create temporary directory in which we will extract the artifacts # Create temporary directory in which we will extract the artifacts
FileUtils.mkdir_p(tmp_path) FileUtils.mkdir_p(tmp_path)
...@@ -31,16 +31,16 @@ module Projects ...@@ -31,16 +31,16 @@ module Projects
# Check if we did extract public directory # Check if we did extract public directory
archive_public_path = File.join(archive_path, 'public') archive_public_path = File.join(archive_path, 'public')
raise InvaildStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path) raise InvalidStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
raise InvaildStateError, 'pages are outdated' unless latest? raise InvalidStateError, 'pages are outdated' unless latest?
deploy_page!(archive_public_path) deploy_page!(archive_public_path)
success success
end end
rescue InvaildStateError => e rescue InvalidStateError => e
error(e.message) error(e.message)
rescue => e rescue => e
error(e.message, false) error(e.message)
raise e raise e
end end
...@@ -48,17 +48,15 @@ module Projects ...@@ -48,17 +48,15 @@ module Projects
def success def success
@status.success @status.success
delete_artifact!
super super
end end
def error(message, allow_delete_artifact = true) def error(message)
register_failure register_failure
log_error("Projects::UpdatePagesService: #{message}") log_error("Projects::UpdatePagesService: #{message}")
@status.allow_failure = !latest? @status.allow_failure = !latest?
@status.description = message @status.description = message
@status.drop(:script_failure) @status.drop(:script_failure)
delete_artifact! if allow_delete_artifact
super super
end end
...@@ -77,18 +75,18 @@ module Projects ...@@ -77,18 +75,18 @@ module Projects
if artifacts.ends_with?('.zip') if artifacts.ends_with?('.zip')
extract_zip_archive!(temp_path) extract_zip_archive!(temp_path)
else else
raise InvaildStateError, 'unsupported artifacts format' raise InvalidStateError, 'unsupported artifacts format'
end end
end end
def extract_zip_archive!(temp_path) def extract_zip_archive!(temp_path)
raise InvaildStateError, 'missing artifacts metadata' unless build.artifacts_metadata? raise InvalidStateError, 'missing artifacts metadata' unless build.artifacts_metadata?
# Calculate page size after extract # Calculate page size after extract
public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true) public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true)
if public_entry.total_size > max_size if public_entry.total_size > max_size
raise InvaildStateError, "artifacts for pages are too large: #{public_entry.total_size}" raise InvalidStateError, "artifacts for pages are too large: #{public_entry.total_size}"
end end
# Requires UnZip at least 6.00 Info-ZIP. # Requires UnZip at least 6.00 Info-ZIP.
...@@ -162,11 +160,6 @@ module Projects ...@@ -162,11 +160,6 @@ module Projects
build.artifacts_file.path build.artifacts_file.path
end end
def delete_artifact!
build.reload # Reload stable object to prevent erase artifacts with old state
build.erase_artifacts! unless build.has_expiring_artifacts?
end
def latest_sha def latest_sha
project.commit(build.ref).try(:sha).to_s project.commit(build.ref).try(:sha).to_s
ensure ensure
......
...@@ -20,11 +20,12 @@ class RepositoryArchiveCleanUpService ...@@ -20,11 +20,12 @@ class RepositoryArchiveCleanUpService
private private
def clean_up_old_archives def clean_up_old_archives
run(%W(find #{path} -not -path #{path} -type f \( -name \*.tar -o -name \*.bz2 -o -name \*.tar.gz -o -name \*.zip \) -maxdepth 2 -mmin +#{mmin} -delete)) run(%W(find #{path} -mindepth 1 -maxdepth 3 -type f \( -name \*.tar -o -name \*.bz2 -o -name \*.tar.gz -o -name \*.zip \) -mmin +#{mmin} -delete))
end end
def clean_up_empty_directories def clean_up_empty_directories
run(%W(find #{path} -not -path #{path} -type d -empty -name \*.git -maxdepth 1 -delete)) run(%W(find #{path} -mindepth 2 -maxdepth 2 -type d -empty -delete))
run(%W(find #{path} -mindepth 1 -maxdepth 1 -type d -empty -delete))
end end
def run(cmd) def run(cmd)
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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