Commit 3d22e7bd authored by Nick Thomas's avatar Nick Thomas

Merge remote-tracking branch 'ce/master' into ce-to-ee-2017-10-05

parents 7ed95992 eb4296ba
...@@ -10,3 +10,4 @@ app/policies/project_policy.rb ...@@ -10,3 +10,4 @@ app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb app/models/concerns/relative_positioning.rb
app/workers/stuck_merge_jobs_worker.rb app/workers/stuck_merge_jobs_worker.rb
lib/gitlab/redis/*.rb lib/gitlab/redis/*.rb
lib/gitlab/gitaly_client/operation_service.rb
...@@ -64,4 +64,5 @@ eslint-report.html ...@@ -64,4 +64,5 @@ eslint-report.html
/.gitlab_workhorse_secret /.gitlab_workhorse_secret
/webpack-report/ /webpack-report/
/locale/**/LC_MESSAGES /locale/**/LC_MESSAGES
/locale/**/*.time_stamp
/.rspec /.rspec
...@@ -108,7 +108,7 @@ gem 'fog-rackspace', '~> 0.1.1' ...@@ -108,7 +108,7 @@ gem 'fog-rackspace', '~> 0.1.1'
gem 'fog-aliyun', '~> 0.1.0' gem 'fog-aliyun', '~> 0.1.0'
# for Google storage # for Google storage
gem 'google-api-client', '~> 0.8.6' gem 'google-api-client', '~> 0.13.6'
# for aws storage # for aws storage
gem 'unf', '~> 0.1.4' gem 'unf', '~> 0.1.4'
...@@ -249,7 +249,7 @@ gem 'rack-proxy', '~> 0.6.0' ...@@ -249,7 +249,7 @@ gem 'rack-proxy', '~> 0.6.0'
gem 'sass-rails', '~> 5.0.6' gem 'sass-rails', '~> 5.0.6'
gem 'uglifier', '~> 2.7.2' gem 'uglifier', '~> 2.7.2'
gem 'addressable', '~> 2.3.8' gem 'addressable', '~> 2.5.2'
gem 'bootstrap-sass', '~> 3.3.0' gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.7' gem 'font-awesome-rails', '~> 4.7'
gem 'gemojione', '~> 3.3' gem 'gemojione', '~> 3.3'
...@@ -368,7 +368,7 @@ end ...@@ -368,7 +368,7 @@ end
group :test do group :test do
gem 'shoulda-matchers', '~> 3.1.2', require: false gem 'shoulda-matchers', '~> 3.1.2', require: false
gem 'email_spec', '~> 1.6.0' gem 'email_spec', '~> 1.6.0'
gem 'json-schema', '~> 2.6.2' gem 'json-schema', '~> 2.8.0'
gem 'webmock', '~> 2.3.2' gem 'webmock', '~> 2.3.2'
gem 'test_after_commit', '~> 1.1' gem 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6' gem 'sham_rack', '~> 1.3.6'
...@@ -414,7 +414,7 @@ group :ed25519 do ...@@ -414,7 +414,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.38.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.39.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -45,7 +45,8 @@ GEM ...@@ -45,7 +45,8 @@ GEM
adamantium (0.2.0) adamantium (0.2.0)
ice_nine (~> 0.11.0) ice_nine (~> 0.11.0)
memoizable (~> 0.4.0) memoizable (~> 0.4.0)
addressable (2.3.8) addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
akismet (2.0.0) akismet (2.0.0)
allocations (1.0.5) allocations (1.0.5)
arel (6.0.4) arel (6.0.4)
...@@ -62,10 +63,6 @@ GEM ...@@ -62,10 +63,6 @@ GEM
attr_encrypted (3.0.3) attr_encrypted (3.0.3)
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
attr_required (1.0.0) attr_required (1.0.0)
autoparse (0.3.3)
addressable (>= 2.3.1)
extlib (>= 0.9.15)
multi_json (>= 1.0.0)
autoprefixer-rails (6.2.3) autoprefixer-rails (6.2.3)
execjs execjs
json json
...@@ -154,6 +151,8 @@ GEM ...@@ -154,6 +151,8 @@ GEM
debugger-ruby_core_source (1.3.8) debugger-ruby_core_source (1.3.8)
deckar01-task_list (2.0.0) deckar01-task_list (2.0.0)
html-pipeline html-pipeline
declarative (0.0.10)
declarative-option (0.1.0)
default_value_for (3.0.2) default_value_for (3.0.2)
activerecord (>= 3.2.0, < 5.1) activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
...@@ -209,7 +208,6 @@ GEM ...@@ -209,7 +208,6 @@ GEM
excon (0.57.1) excon (0.57.1)
execjs (2.6.0) execjs (2.6.0)
expression_parser (0.9.0) expression_parser (0.9.0)
extlib (0.9.16)
factory_girl (4.7.0) factory_girl (4.7.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
factory_girl_rails (4.7.0) factory_girl_rails (4.7.0)
...@@ -299,7 +297,7 @@ GEM ...@@ -299,7 +297,7 @@ GEM
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly-proto (0.38.0) gitaly-proto (0.39.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -312,10 +310,10 @@ GEM ...@@ -312,10 +310,10 @@ GEM
flowdock (~> 0.7) flowdock (~> 0.7)
gitlab-grit (>= 2.4.1) gitlab-grit (>= 2.4.1)
multi_json multi_json
gitlab-grit (2.8.1) gitlab-grit (2.8.2)
charlock_holmes (~> 0.6) charlock_holmes (~> 0.6)
diff-lcs (~> 1.1) diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3) mime-types (>= 1.16)
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab-license (1.0.0) gitlab-license (1.0.0)
gitlab-markup (1.6.2) gitlab-markup (1.6.2)
...@@ -344,20 +342,16 @@ GEM ...@@ -344,20 +342,16 @@ GEM
json json
multi_json multi_json
request_store (>= 1.0) request_store (>= 1.0)
google-api-client (0.8.7) google-api-client (0.13.6)
activesupport (>= 3.2, < 5.0) addressable (~> 2.5, >= 2.5.1)
addressable (~> 2.3) googleauth (~> 0.5)
autoparse (~> 0.3) httpclient (>= 2.8.1, < 3.0)
extlib (~> 0.9) mime-types (~> 3.0)
faraday (~> 0.9) representable (~> 3.0)
googleauth (~> 0.3) retriable (>= 2.0, < 4.0)
launchy (~> 2.4)
multi_json (~> 1.10)
retriable (~> 1.4)
signet (~> 0.6)
google-protobuf (3.4.0.2) google-protobuf (3.4.0.2)
googleauth (0.5.1) googleauth (0.5.3)
faraday (~> 0.9) faraday (~> 0.12)
jwt (~> 1.4) jwt (~> 1.4)
logging (~> 2.0) logging (~> 2.0)
memoist (~> 0.12) memoist (~> 0.12)
...@@ -450,8 +444,8 @@ GEM ...@@ -450,8 +444,8 @@ GEM
multi_json (>= 1.3) multi_json (>= 1.3)
securecompare securecompare
url_safe_base64 url_safe_base64
json-schema (2.6.2) json-schema (2.8.0)
addressable (~> 2.3.8) addressable (>= 2.4)
jwt (1.5.6) jwt (1.5.6)
kaminari (1.0.1) kaminari (1.0.1)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
...@@ -503,18 +497,20 @@ GEM ...@@ -503,18 +497,20 @@ GEM
mail (2.6.6) mail (2.6.6)
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
mail_room (0.9.1) mail_room (0.9.1)
memoist (0.15.0) memoist (0.16.0)
memoizable (0.4.2) memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2) method_source (0.8.2)
mime-types (2.99.3) mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mimemagic (0.3.0) mimemagic (0.3.0)
mini_mime (0.1.4) mini_mime (0.1.4)
mini_portile2 (2.3.0) mini_portile2 (2.3.0)
minitest (5.7.0) minitest (5.7.0)
mmap2 (2.2.7) mmap2 (2.2.7)
mousetrap-rails (1.4.6) mousetrap-rails (1.4.6)
multi_json (1.12.1) multi_json (1.12.2)
multi_xml (0.6.0) multi_xml (0.6.0)
multipart-post (2.0.0) multipart-post (2.0.0)
mustermann (1.0.0) mustermann (1.0.0)
...@@ -664,6 +660,7 @@ GEM ...@@ -664,6 +660,7 @@ GEM
pry (~> 0.10) pry (~> 0.10)
pry-rails (0.3.5) pry-rails (0.3.5)
pry (>= 0.9.10) pry (>= 0.9.10)
public_suffix (3.0.0)
pyu-ruby-sasl (0.0.3.3) pyu-ruby-sasl (0.0.3.3)
rack (1.6.8) rack (1.6.8)
rack-accept (0.4.5) rack-accept (0.4.5)
...@@ -746,6 +743,10 @@ GEM ...@@ -746,6 +743,10 @@ GEM
redis-store (~> 1.2.0) redis-store (~> 1.2.0)
redis-store (1.2.0) redis-store (1.2.0)
redis (>= 2.2) redis (>= 2.2)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
request_store (1.3.1) request_store (1.3.1)
responders (2.3.0) responders (2.3.0)
railties (>= 4.2.0, < 5.1) railties (>= 4.2.0, < 5.1)
...@@ -753,7 +754,7 @@ GEM ...@@ -753,7 +754,7 @@ GEM
http-cookie (>= 1.0.2, < 2.0) http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0) mime-types (>= 1.16, < 4.0)
netrc (~> 0.8) netrc (~> 0.8)
retriable (1.4.1) retriable (3.1.1)
rinku (2.0.0) rinku (2.0.0)
rotp (2.1.2) rotp (2.1.2)
rouge (2.2.1) rouge (2.2.1)
...@@ -933,12 +934,13 @@ GEM ...@@ -933,12 +934,13 @@ GEM
tzinfo (1.2.3) tzinfo (1.2.3)
thread_safe (~> 0.1) thread_safe (~> 0.1)
u2f (0.2.1) u2f (0.2.1)
uber (0.1.0)
uglifier (2.7.2) uglifier (2.7.2)
execjs (>= 0.3.0) execjs (>= 0.3.0)
json (>= 1.8.0) json (>= 1.8.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.2) unf_ext (0.0.7.4)
unicode-display_width (1.3.0) unicode-display_width (1.3.0)
unicorn (5.1.0) unicorn (5.1.0)
kgio (~> 2.6) kgio (~> 2.6)
...@@ -993,7 +995,7 @@ DEPENDENCIES ...@@ -993,7 +995,7 @@ DEPENDENCIES
ace-rails-ap (~> 4.1.0) ace-rails-ap (~> 4.1.0)
activerecord_sane_schema_dumper (= 0.2) activerecord_sane_schema_dumper (= 0.2)
acts-as-taggable-on (~> 4.0) acts-as-taggable-on (~> 4.0)
addressable (~> 2.3.8) addressable (~> 2.5.2)
akismet (~> 2.0) akismet (~> 2.0)
allocations (~> 1.0) allocations (~> 1.0)
asana (~> 0.6.0) asana (~> 0.6.0)
...@@ -1060,7 +1062,7 @@ DEPENDENCIES ...@@ -1060,7 +1062,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0) gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.38.0) gitaly-proto (~> 0.39.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0) gitlab-license (~> 1.0)
...@@ -1069,7 +1071,7 @@ DEPENDENCIES ...@@ -1069,7 +1071,7 @@ DEPENDENCIES
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4) gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0) gon (~> 6.1.0)
google-api-client (~> 0.8.6) google-api-client (~> 0.13.6)
gpgme gpgme
grape (~> 1.0) grape (~> 1.0)
grape-entity (~> 0.6.0) grape-entity (~> 0.6.0)
...@@ -1088,7 +1090,7 @@ DEPENDENCIES ...@@ -1088,7 +1090,7 @@ DEPENDENCIES
jira-ruby (~> 1.4) jira-ruby (~> 1.4)
jquery-atwho-rails (~> 1.3.2) jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.1.0) jquery-rails (~> 4.1.0)
json-schema (~> 2.6.2) json-schema (~> 2.8.0)
jwt (~> 1.5.6) jwt (~> 1.5.6)
kaminari (~> 1.0) kaminari (~> 1.0)
knapsack (~> 1.11.0) knapsack (~> 1.11.0)
......
...@@ -197,6 +197,11 @@ month. When we say 'the most recent monthly release', this can refer to either ...@@ -197,6 +197,11 @@ month. When we say 'the most recent monthly release', this can refer to either
the version currently running on GitLab.com, or the most recent version the version currently running on GitLab.com, or the most recent version
available in the package repositories. available in the package repositories.
A regression issue should be labeled with the appropriate [subject label](../CONTRIBUTING.md#subject-labels-wiki-container-registry-ldap-api-etc)
and [team label](../CONTRIBUTING.md#team-labels-ci-discussion-edge-platform-etc),
just like any other issue, to help GitLab team members focus on issues that are
relevant to [their area of responsibility](https://about.gitlab.com/handbook/engineering/workflow/#choosing-something-to-work-on).
## Release retrospective and kickoff ## Release retrospective and kickoff
- [Retrospective](https://about.gitlab.com/handbook/engineering/workflow/#retrospective) - [Retrospective](https://about.gitlab.com/handbook/engineering/workflow/#retrospective)
......
...@@ -298,7 +298,7 @@ class CopyAsGFM { ...@@ -298,7 +298,7 @@ class CopyAsGFM {
const documentFragment = getSelectedFragment(); const documentFragment = getSelectedFragment();
if (!documentFragment) return; if (!documentFragment) return;
const el = transformer(documentFragment.cloneNode(true)); const el = transformer(documentFragment.cloneNode(true), e.currentTarget);
if (!el) return; if (!el) return;
e.preventDefault(); e.preventDefault();
...@@ -338,55 +338,64 @@ class CopyAsGFM { ...@@ -338,55 +338,64 @@ class CopyAsGFM {
} }
static transformGFMSelection(documentFragment) { static transformGFMSelection(documentFragment) {
const gfmEls = documentFragment.querySelectorAll('.md, .wiki'); const gfmElements = documentFragment.querySelectorAll('.md, .wiki');
switch (gfmEls.length) { switch (gfmElements.length) {
case 0: { case 0: {
return documentFragment; return documentFragment;
} }
case 1: { case 1: {
return gfmEls[0]; return gfmElements[0];
} }
default: { default: {
const allGfmEl = document.createElement('div'); const allGfmElement = document.createElement('div');
for (let i = 0; i < gfmEls.length; i += 1) { for (let i = 0; i < gfmElements.length; i += 1) {
const lineEl = gfmEls[i]; const gfmElement = gfmElements[i];
allGfmEl.appendChild(lineEl); allGfmElement.appendChild(gfmElement);
allGfmEl.appendChild(document.createTextNode('\n\n')); allGfmElement.appendChild(document.createTextNode('\n\n'));
} }
return allGfmEl; return allGfmElement;
} }
} }
} }
static transformCodeSelection(documentFragment) { static transformCodeSelection(documentFragment, target) {
const lineEls = documentFragment.querySelectorAll('.line'); let lineSelector = '.line';
let codeEl; if (target) {
if (lineEls.length > 1) { const lineClass = ['left-side', 'right-side'].filter(name => target.classList.contains(name))[0];
codeEl = document.createElement('pre'); if (lineClass) {
codeEl.className = 'code highlight'; lineSelector = `.line_content.${lineClass} ${lineSelector}`;
}
}
const lineElements = documentFragment.querySelectorAll(lineSelector);
let codeElement;
if (lineElements.length > 1) {
codeElement = document.createElement('pre');
codeElement.className = 'code highlight';
const lang = lineEls[0].getAttribute('lang'); const lang = lineElements[0].getAttribute('lang');
if (lang) { if (lang) {
codeEl.setAttribute('lang', lang); codeElement.setAttribute('lang', lang);
} }
} else { } else {
codeEl = document.createElement('code'); codeElement = document.createElement('code');
} }
if (lineEls.length > 0) { if (lineElements.length > 0) {
for (let i = 0; i < lineEls.length; i += 1) { for (let i = 0; i < lineElements.length; i += 1) {
const lineEl = lineEls[i]; const lineElement = lineElements[i];
codeEl.appendChild(lineEl); codeElement.appendChild(lineElement);
codeEl.appendChild(document.createTextNode('\n')); codeElement.appendChild(document.createTextNode('\n'));
} }
} else { } else {
codeEl.appendChild(documentFragment); codeElement.appendChild(documentFragment);
} }
return codeEl; return codeElement;
} }
static nodeToGFM(node, respectWhitespaceParam = false) { static nodeToGFM(node, respectWhitespaceParam = false) {
......
...@@ -24,7 +24,8 @@ class Diff { ...@@ -24,7 +24,8 @@ class Diff {
if (!isBound) { if (!isBound) {
$(document) $(document)
.on('click', '.js-unfold', this.handleClickUnfold.bind(this)) .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
.on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this))
.on('mousedown', 'td.line_content.parallel', this.handleParallelLineDown.bind(this));
isBound = true; isBound = true;
} }
...@@ -100,6 +101,18 @@ class Diff { ...@@ -100,6 +101,18 @@ class Diff {
this.highlightSelectedLine(); this.highlightSelectedLine();
} }
handleParallelLineDown(e) {
const line = $(e.currentTarget);
const table = line.closest('table');
table.removeClass('left-side-selected right-side-selected');
const lineClass = ['left-side', 'right-side'].filter(name => line.hasClass(name))[0];
if (lineClass) {
table.addClass(`${lineClass}-selected`);
}
}
diffViewType() { diffViewType() {
return $('.inline-parallel-buttons a.active').data('view-type'); return $('.inline-parallel-buttons a.active').data('view-type');
} }
......
...@@ -548,6 +548,7 @@ GitLabDropdown = (function() { ...@@ -548,6 +548,7 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.positionMenuAbove = function() { GitLabDropdown.prototype.positionMenuAbove = function() {
var $menu = this.dropdown.find('.dropdown-menu'); var $menu = this.dropdown.find('.dropdown-menu');
$menu.addClass('dropdown-open-top');
$menu.css('top', 'initial'); $menu.css('top', 'initial');
$menu.css('bottom', '100%'); $menu.css('bottom', '100%');
}; };
...@@ -753,7 +754,7 @@ GitLabDropdown = (function() { ...@@ -753,7 +754,7 @@ GitLabDropdown = (function() {
: selectedObject.id; : selectedObject.id;
if (isInput) { if (isInput) {
field = $(this.el); field = $(this.el);
} else if (value) { } else if (value != null) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
} }
...@@ -761,7 +762,7 @@ GitLabDropdown = (function() { ...@@ -761,7 +762,7 @@ GitLabDropdown = (function() {
return; return;
} }
if (el.hasClass(ACTIVE_CLASS)) { if (el.hasClass(ACTIVE_CLASS) && value !== 0) {
isMarking = false; isMarking = false;
el.removeClass(ACTIVE_CLASS); el.removeClass(ACTIVE_CLASS);
if (field && field.length) { if (field && field.length) {
...@@ -867,7 +868,7 @@ GitLabDropdown = (function() { ...@@ -867,7 +868,7 @@ GitLabDropdown = (function() {
if (href && href !== '#') { if (href && href !== '#') {
gl.utils.visitUrl(href); gl.utils.visitUrl(href);
} else { } else {
$el.first().trigger('click'); $el.trigger('click');
} }
} }
}; };
......
import Jed from 'jed'; import Jed from 'jed';
import sprintf from './sprintf';
/**
This is required to require all the translation folders in the current directory
this saves us having to do this manually & keep up to date with new languages
**/
function requireAll(requireContext) { return requireContext.keys().map(requireContext); }
const allLocales = requireAll(require.context('./', true, /^(?!.*(?:index.js$)).*\.js$/));
const locales = allLocales.reduce((d, obj) => {
const data = d;
const localeKey = Object.keys(obj)[0];
data[localeKey] = obj[localeKey];
return data;
}, {});
const langAttribute = document.querySelector('html').getAttribute('lang'); const langAttribute = document.querySelector('html').getAttribute('lang');
const lang = (langAttribute || 'en').replace(/-/g, '_'); const lang = (langAttribute || 'en').replace(/-/g, '_');
const locale = new Jed(locales[lang]); const locale = new Jed(window.translations || {});
/** /**
Translates `text` Translates `text`
@param text The text to be translated @param text The text to be translated
@returns {String} The translated text @returns {String} The translated text
**/ **/
...@@ -66,4 +50,5 @@ export { lang }; ...@@ -66,4 +50,5 @@ export { lang };
export { gettext as __ }; export { gettext as __ };
export { ngettext as n__ }; export { ngettext as n__ };
export { pgettext as s__ }; export { pgettext as s__ };
export { sprintf };
export default locale; export default locale;
import _ from 'underscore';
/**
Very limited implementation of sprintf supporting only named parameters.
@param input (translated) text with parameters (e.g. '%{num_users} users use us')
@param parameters object mapping parameter names to values (e.g. { num_users: 5 })
@param escapeParameters whether parameter values should be escaped (see http://underscorejs.org/#escape)
@returns {String} the text with parameters replaces (e.g. '5 users use us')
@see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf
@see https://gitlab.com/gitlab-org/gitlab-ce/issues/37992
**/
export default (input, parameters, escapeParameters = true) => {
let output = input;
if (parameters) {
Object.keys(parameters).forEach((parameterName) => {
const parameterValue = parameters[parameterName];
const escapedParameterValue = escapeParameters ? _.escape(parameterValue) : parameterValue;
output = output.replace(new RegExp(`%{${parameterName}}`, 'g'), escapedParameterValue);
});
}
return output;
};
...@@ -127,6 +127,21 @@ import IssuablesHelper from './helpers/issuables_helper'; ...@@ -127,6 +127,21 @@ import IssuablesHelper from './helpers/issuables_helper';
$el.text(gl.text.addDelimiter(count)); $el.text(gl.text.addDelimiter(count));
}; };
MergeRequest.prototype.hideCloseButton = function() {
const el = document.querySelector('.merge-request .issuable-actions');
const closeDropdownItem = el.querySelector('li.close-item');
if (closeDropdownItem) {
closeDropdownItem.classList.add('hidden');
// Selects the next dropdown item
el.querySelector('li.report-item').click();
} else {
// No dropdown just hide the Close button
el.querySelector('.btn-close').classList.add('hidden');
}
// Dropdown for mobile screen
el.querySelector('li.js-close-item').classList.add('hidden');
};
return MergeRequest; return MergeRequest;
})(); })();
}).call(window); }).call(window);
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
showEmptyState: true, showEmptyState: true,
updateAspectRatio: false, updateAspectRatio: false,
updatedAspectRatios: 0, updatedAspectRatios: 0,
hoverData: {},
resizeThrottled: {}, resizeThrottled: {},
}; };
}, },
...@@ -64,6 +65,10 @@ ...@@ -64,6 +65,10 @@
this.updatedAspectRatios = 0; this.updatedAspectRatios = 0;
} }
}, },
hoverChanged(data) {
this.hoverData = data;
},
}, },
created() { created() {
...@@ -72,10 +77,12 @@ ...@@ -72,10 +77,12 @@
deploymentEndpoint: this.deploymentEndpoint, deploymentEndpoint: this.deploymentEndpoint,
}); });
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$on('hoverChanged', this.hoverChanged);
}, },
beforeDestroy() { beforeDestroy() {
eventHub.$off('toggleAspectRatio', this.toggleAspectRatio); eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
eventHub.$off('hoverChanged', this.hoverChanged);
window.removeEventListener('resize', this.resizeThrottled, false); window.removeEventListener('resize', this.resizeThrottled, false);
}, },
...@@ -102,6 +109,7 @@ ...@@ -102,6 +109,7 @@
v-for="(graphData, index) in groupData.metrics" v-for="(graphData, index) in groupData.metrics"
:key="index" :key="index"
:graph-data="graphData" :graph-data="graphData"
:hover-data="hoverData"
:update-aspect-ratio="updateAspectRatio" :update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData" :deployment-data="store.deploymentData"
/> />
......
...@@ -73,34 +73,22 @@ ...@@ -73,34 +73,22 @@
<template> <template>
<div class="prometheus-state"> <div class="prometheus-state">
<div class="row"> <div class="state-svg svg-content">
<div class="col-md-4 col-md-offset-4 state-svg svg-content"> <img :src="currentState.svgUrl"/>
<img :src="currentState.svgUrl"/>
</div>
</div> </div>
<div class="row"> <h4 class="state-title">
<div class="col-md-6 col-md-offset-3"> {{currentState.title}}
<h4 class="text-center state-title"> </h4>
{{currentState.title}} <p class="state-description">
</h4> {{currentState.description}}
</div> <a v-if="showButtonDescription" :href="settingsPath">
</div> Prometheus server
<div class="row"> </a>
<div class="col-md-6 col-md-offset-3"> </p>
<div class="description-text text-center state-description"> <div class="state-button">
{{currentState.description}} <a class="btn btn-success" :href="buttonPath">
<a v-if="showButtonDescription" :href="settingsPath"> {{currentState.buttonText}}
Prometheus server </a>
</a>
</div>
</div>
</div>
<div class="row state-button-section">
<div class="col-md-4 col-md-offset-4 text-center state-button">
<a class="btn btn-success" :href="buttonPath">
{{currentState.buttonText}}
</a>
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -7,12 +7,10 @@ ...@@ -7,12 +7,10 @@
import MonitoringMixin from '../mixins/monitoring_mixins'; import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import measurements from '../utils/measurements'; import measurements from '../utils/measurements';
import { timeScaleFormat } from '../utils/date_time_formatters'; import { timeScaleFormat, bisectDate } from '../utils/date_time_formatters';
import createTimeSeries from '../utils/multiple_time_series'; import createTimeSeries from '../utils/multiple_time_series';
import bp from '../../breakpoints'; import bp from '../../breakpoints';
const bisectDate = d3.bisector(d => d.time).left;
export default { export default {
props: { props: {
graphData: { graphData: {
...@@ -27,6 +25,11 @@ ...@@ -27,6 +25,11 @@
type: Array, type: Array,
required: true, required: true,
}, },
hoverData: {
type: Object,
required: false,
default: () => ({}),
},
}, },
mixins: [MonitoringMixin], mixins: [MonitoringMixin],
...@@ -52,6 +55,7 @@ ...@@ -52,6 +55,7 @@
currentXCoordinate: 0, currentXCoordinate: 0,
currentFlagPosition: 0, currentFlagPosition: 0,
showFlag: false, showFlag: false,
showFlagContent: false,
showDeployInfo: true, showDeployInfo: true,
timeSeries: [], timeSeries: [],
}; };
...@@ -122,22 +126,14 @@ ...@@ -122,22 +126,14 @@
const d1 = firstTimeSeries.values[overlayIndex]; const d1 = firstTimeSeries.values[overlayIndex];
if (d0 === undefined || d1 === undefined) return; if (d0 === undefined || d1 === undefined) return;
const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay; const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
this.currentData = evalTime ? d1 : d0; const hoveredDataIndex = evalTime ? overlayIndex : (overlayIndex - 1);
this.currentDataIndex = evalTime ? overlayIndex : (overlayIndex - 1); const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time;
this.currentXCoordinate = Math.floor(firstTimeSeries.timeSeriesScaleX(this.currentData.time));
const currentDeployXPos = this.mouseOverDeployInfo(point.x); const currentDeployXPos = this.mouseOverDeployInfo(point.x);
if (this.currentXCoordinate > (this.graphWidth - 200)) { eventHub.$emit('hoverChanged', {
this.currentFlagPosition = this.currentXCoordinate - 103; hoveredDate,
} else { currentDeployXPos,
this.currentFlagPosition = this.currentXCoordinate; });
}
if (currentDeployXPos) {
this.showFlag = false;
} else {
this.showFlag = true;
}
}, },
renderAxesPaths() { renderAxesPaths() {
...@@ -194,6 +190,10 @@ ...@@ -194,6 +190,10 @@
eventHub.$emit('toggleAspectRatio'); eventHub.$emit('toggleAspectRatio');
} }
}, },
hoverData() {
this.positionFlag();
},
}, },
mounted() { mounted() {
...@@ -203,7 +203,10 @@ ...@@ -203,7 +203,10 @@
</script> </script>
<template> <template>
<div class="prometheus-graph"> <div
class="prometheus-graph"
@mouseover="showFlagContent = true"
@mouseleave="showFlagContent = false">
<h5 class="text-center graph-title"> <h5 class="text-center graph-title">
{{graphData.title}} {{graphData.title}}
</h5> </h5>
...@@ -247,6 +250,7 @@ ...@@ -247,6 +250,7 @@
<graph-deployment <graph-deployment
:show-deploy-info="showDeployInfo" :show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData" :deployment-data="reducedDeploymentData"
:graph-width="graphWidth"
:graph-height="graphHeight" :graph-height="graphHeight"
:graph-height-offset="graphHeightOffset" :graph-height-offset="graphHeightOffset"
/> />
...@@ -257,6 +261,7 @@ ...@@ -257,6 +261,7 @@
:current-flag-position="currentFlagPosition" :current-flag-position="currentFlagPosition"
:graph-height="graphHeight" :graph-height="graphHeight"
:graph-height-offset="graphHeightOffset" :graph-height-offset="graphHeightOffset"
:show-flag-content="showFlagContent"
/> />
<rect <rect
class="prometheus-graph-overlay" class="prometheus-graph-overlay"
......
...@@ -19,6 +19,10 @@ ...@@ -19,6 +19,10 @@
type: Number, type: Number,
required: true, required: true,
}, },
graphWidth: {
type: Number,
required: true,
},
}, },
computed: { computed: {
...@@ -47,6 +51,14 @@ ...@@ -47,6 +51,14 @@
transformDeploymentGroup(deployment) { transformDeploymentGroup(deployment) {
return `translate(${Math.floor(deployment.xPos) + 1}, 20)`; return `translate(${Math.floor(deployment.xPos) + 1}, 20)`;
}, },
positionFlag(deployment) {
let xPosition = 3;
if (deployment.xPos > (this.graphWidth - 200)) {
xPosition = -97;
}
return xPosition;
},
}, },
}; };
</script> </script>
...@@ -77,7 +89,7 @@ ...@@ -77,7 +89,7 @@
<svg <svg
v-if="deployment.showDeploymentFlag" v-if="deployment.showDeploymentFlag"
class="js-deploy-info-box" class="js-deploy-info-box"
x="3" :x="positionFlag(deployment)"
y="0" y="0"
width="92" width="92"
height="60"> height="60">
......
...@@ -23,6 +23,10 @@ ...@@ -23,6 +23,10 @@
type: Number, type: Number,
required: true, required: true,
}, },
showFlagContent: {
type: Boolean,
required: true,
},
}, },
data() { data() {
...@@ -57,6 +61,7 @@ ...@@ -57,6 +61,7 @@
transform="translate(-5, 20)"> transform="translate(-5, 20)">
</line> </line>
<svg <svg
v-if="showFlagContent"
class="rect-text-metric" class="rect-text-metric"
:x="currentFlagPosition" :x="currentFlagPosition"
y="0"> y="0">
......
import { bisectDate } from '../utils/date_time_formatters';
const mixins = { const mixins = {
methods: { methods: {
mouseOverDeployInfo(mouseXPos) { mouseOverDeployInfo(mouseXPos) {
...@@ -18,6 +20,7 @@ const mixins = { ...@@ -18,6 +20,7 @@ const mixins = {
return dataFound; return dataFound;
}, },
formatDeployments() { formatDeployments() {
this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => { this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at); const time = new Date(deployment.created_at);
...@@ -40,6 +43,25 @@ const mixins = { ...@@ -40,6 +43,25 @@ const mixins = {
return deploymentDataArray; return deploymentDataArray;
}, []); }, []);
}, },
positionFlag() {
const timeSeries = this.timeSeries[0];
const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1);
this.currentData = timeSeries.values[hoveredDataIndex];
this.currentDataIndex = hoveredDataIndex;
this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
if (this.currentXCoordinate > (this.graphWidth - 200)) {
this.currentFlagPosition = this.currentXCoordinate - 103;
} else {
this.currentFlagPosition = this.currentXCoordinate;
}
if (this.hoverData.currentDeployXPos) {
this.showFlag = false;
} else {
this.showFlag = true;
}
},
}, },
}; };
......
...@@ -3,8 +3,5 @@ import Dashboard from './components/dashboard.vue'; ...@@ -3,8 +3,5 @@ import Dashboard from './components/dashboard.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#prometheus-graphs', el: '#prometheus-graphs',
components: { render: createElement => createElement(Dashboard),
Dashboard,
},
render: createElement => createElement('dashboard'),
})); }));
...@@ -13,7 +13,7 @@ function normalizeMetrics(metrics) { ...@@ -13,7 +13,7 @@ function normalizeMetrics(metrics) {
...result, ...result,
values: result.values.map(([timestamp, value]) => ({ values: result.values.map(([timestamp, value]) => ({
time: new Date(timestamp * 1000), time: new Date(timestamp * 1000),
value, value: Number(value),
})), })),
})), })),
})), })),
......
...@@ -2,6 +2,7 @@ import d3 from 'd3'; ...@@ -2,6 +2,7 @@ import d3 from 'd3';
export const dateFormat = d3.time.format('%b %-d, %Y'); export const dateFormat = d3.time.format('%b %-d, %Y');
export const timeFormat = d3.time.format('%-I:%M%p'); export const timeFormat = d3.time.format('%-I:%M%p');
export const bisectDate = d3.bisector(d => d.time).left;
export const timeScaleFormat = d3.time.format.multi([ export const timeScaleFormat = d3.time.format.multi([
['.%L', d => d.getMilliseconds()], ['.%L', d => d.getMilliseconds()],
......
<template>
<div class="cell">
<code-cell
type="input"
:raw-code="rawInputCode"
:count="cell.execution_count"
:code-css-class="codeCssClass" />
<output-cell
v-if="hasOutput"
:count="cell.execution_count"
:output="output"
:code-css-class="codeCssClass" />
</div>
</template>
<script> <script>
import CodeCell from './code/index.vue'; import CodeCell from './code/index.vue';
import OutputCell from './output/index.vue'; import OutputCell from './output/index.vue';
...@@ -51,6 +36,21 @@ export default { ...@@ -51,6 +36,21 @@ export default {
}; };
</script> </script>
<template>
<div class="cell">
<code-cell
type="input"
:raw-code="rawInputCode"
:count="cell.execution_count"
:code-css-class="codeCssClass" />
<output-cell
v-if="hasOutput"
:count="cell.execution_count"
:output="output"
:code-css-class="codeCssClass" />
</div>
</template>
<style scoped> <style scoped>
.cell { .cell {
flex-direction: column; flex-direction: column;
......
<template>
<div :class="type">
<prompt
:type="promptType"
:count="count" />
<pre
class="language-python"
:class="codeCssClass"
ref="code"
v-text="code">
</pre>
</div>
</template>
<script> <script>
import Prism from '../../lib/highlight'; import Prism from '../../lib/highlight';
import Prompt from '../prompt.vue'; import Prompt from '../prompt.vue';
...@@ -55,3 +41,17 @@ ...@@ -55,3 +41,17 @@
}, },
}; };
</script> </script>
<template>
<div :class="type">
<prompt
:type="promptType"
:count="count" />
<pre
class="language-python"
:class="codeCssClass"
ref="code"
v-text="code">
</pre>
</div>
</template>
<template>
<div class="cell text-cell">
<prompt />
<div class="markdown" v-html="markdown"></div>
</div>
</template>
<script> <script>
/* global katex */ /* global katex */
import marked from 'marked'; import marked from 'marked';
...@@ -95,6 +88,13 @@ ...@@ -95,6 +88,13 @@
}; };
</script> </script>
<template>
<div class="cell text-cell">
<prompt />
<div class="markdown" v-html="markdown"></div>
</div>
</template>
<style> <style>
.markdown .katex { .markdown .katex {
display: block; display: block;
......
<template>
<div class="output">
<prompt />
<div v-html="rawCode"></div>
</div>
</template>
<script> <script>
import Prompt from '../prompt.vue'; import Prompt from '../prompt.vue';
...@@ -20,3 +13,10 @@ export default { ...@@ -20,3 +13,10 @@ export default {
}, },
}; };
</script> </script>
<template>
<div class="output">
<prompt />
<div v-html="rawCode"></div>
</div>
</template>
<template>
<div class="output">
<prompt />
<img
:src="'data:' + outputType + ';base64,' + rawCode" />
</div>
</template>
<script> <script>
import Prompt from '../prompt.vue'; import Prompt from '../prompt.vue';
...@@ -25,3 +17,11 @@ export default { ...@@ -25,3 +17,11 @@ export default {
}, },
}; };
</script> </script>
<template>
<div class="output">
<prompt />
<img
:src="'data:' + outputType + ';base64,' + rawCode" />
</div>
</template>
<template>
<component :is="componentName"
type="output"
:outputType="outputType"
:count="count"
:raw-code="rawCode"
:code-css-class="codeCssClass" />
</template>
<script> <script>
import CodeCell from '../code/index.vue'; import CodeCell from '../code/index.vue';
import Html from './html.vue'; import Html from './html.vue';
...@@ -81,3 +72,12 @@ export default { ...@@ -81,3 +72,12 @@ export default {
}, },
}; };
</script> </script>
<template>
<component :is="componentName"
type="output"
:outputType="outputType"
:count="count"
:raw-code="rawCode"
:code-css-class="codeCssClass" />
</template>
<template>
<div class="prompt">
<span v-if="type && count">
{{ type }} [{{ count }}]:
</span>
</div>
</template>
<script> <script>
export default { export default {
props: { props: {
...@@ -21,6 +13,14 @@ ...@@ -21,6 +13,14 @@
}; };
</script> </script>
<template>
<div class="prompt">
<span v-if="type && count">
{{ type }} [{{ count }}]:
</span>
</div>
</template>
<style scoped> <style scoped>
.prompt { .prompt {
padding: 0 10px; padding: 0 10px;
......
<template>
<div v-if="hasNotebook">
<component
v-for="(cell, index) in cells"
:is="cellType(cell.cell_type)"
:cell="cell"
:key="index"
:code-css-class="codeCssClass" />
</div>
</template>
<script> <script>
import { import {
MarkdownCell, MarkdownCell,
...@@ -59,6 +48,17 @@ ...@@ -59,6 +48,17 @@
}; };
</script> </script>
<template>
<div v-if="hasNotebook">
<component
v-for="(cell, index) in cells"
:is="cellType(cell.cell_type)"
:cell="cell"
:key="index"
:code-css-class="codeCssClass" />
</div>
</template>
<style> <style>
.cell, .cell,
.input, .input,
......
...@@ -272,6 +272,7 @@ ...@@ -272,6 +272,7 @@
v-model="note" v-model="note"
ref="textarea" ref="textarea"
slot="textarea" slot="textarea"
:disabled="isSubmitting"
placeholder="Write a comment or drag your files here..." placeholder="Write a comment or drag your files here..."
@keydown.up="editCurrentUserLastNote()" @keydown.up="editCurrentUserLastNote()"
@keydown.meta.enter="handleSave()"> @keydown.meta.enter="handleSave()">
......
<template>
<div class="pdf-viewer" v-if="hasPDF">
<page v-for="(page, index) in pages"
:key="index"
:v-if="!loading"
:page="page"
:number="index + 1" />
</div>
</template>
<script> <script>
import pdfjsLib from 'vendor/pdf'; import pdfjsLib from 'vendor/pdf';
import workerSrc from 'vendor/pdf.worker.min'; import workerSrc from 'vendor/pdf.worker.min';
...@@ -64,6 +54,16 @@ ...@@ -64,6 +54,16 @@
}; };
</script> </script>
<template>
<div class="pdf-viewer" v-if="hasPDF">
<page v-for="(page, index) in pages"
:key="index"
:v-if="!loading"
:page="page"
:number="index + 1" />
</div>
</template>
<style> <style>
.pdf-viewer { .pdf-viewer {
background: url('./assets/img/bg.gif'); background: url('./assets/img/bg.gif');
......
<template>
<canvas
class="pdf-page"
ref="canvas"
:data-page="number" />
</template>
<script> <script>
export default { export default {
props: { props: {
...@@ -48,6 +41,13 @@ ...@@ -48,6 +41,13 @@
}; };
</script> </script>
<template>
<canvas
class="pdf-page"
ref="canvas"
:data-page="number" />
</template>
<style> <style>
.pdf-page { .pdf-page {
margin: 8px auto 0 auto; margin: 8px auto 0 auto;
......
export default () => { export default () => {
$('.fork-thumbnail a').on('click', function forkThumbnailClicked() { $('.js-fork-thumbnail').on('click', function forkThumbnailClicked() {
if ($(this).hasClass('disabled')) return false; if ($(this).hasClass('disabled')) return false;
$('.fork-namespaces').hide(); return $('.js-fork-content').toggle();
return $('.save-project-loader').show();
}); });
}; };
import _ from 'underscore';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
import ProtectedBranchDropdown from './protected_branch_dropdown'; import ProtectedBranchDropdown from './protected_branch_dropdown';
import AccessorUtilities from '../lib/utils/accessor';
const PB_LOCAL_STORAGE_KEY = 'protected-branches-defaults';
export default class ProtectedBranchCreate { export default class ProtectedBranchCreate {
constructor() { constructor() {
this.$form = $('.js-new-protected-branch'); this.$form = $('.js-new-protected-branch');
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.currentProjectUserDefaults = {};
this.buildDropdowns(); this.buildDropdowns();
} }
buildDropdowns() { buildDropdowns() {
const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge'); const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge');
const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push'); const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push');
const $protectedBranchDropdown = this.$form.find('.js-protected-branch-select');
// Cache callback // Cache callback
this.onSelectCallback = this.onSelect.bind(this); this.onSelectCallback = this.onSelect.bind(this);
...@@ -28,15 +35,13 @@ export default class ProtectedBranchCreate { ...@@ -28,15 +35,13 @@ export default class ProtectedBranchCreate {
onSelect: this.onSelectCallback, onSelect: this.onSelectCallback,
}); });
// Select default
$allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0);
$allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0);
// Protected branch dropdown // Protected branch dropdown
this.protectedBranchDropdown = new ProtectedBranchDropdown({ this.protectedBranchDropdown = new ProtectedBranchDropdown({
$dropdown: this.$form.find('.js-protected-branch-select'), $dropdown: $protectedBranchDropdown,
onSelect: this.onSelectCallback, onSelect: this.onSelectCallback,
}); });
this.loadPreviousSelection($allowedToMergeDropdown.data('glDropdown'), $allowedToPushDropdown.data('glDropdown'));
} }
// This will run after clicked callback // This will run after clicked callback
...@@ -45,7 +50,41 @@ export default class ProtectedBranchCreate { ...@@ -45,7 +50,41 @@ export default class ProtectedBranchCreate {
const $branchInput = this.$form.find('input[name="protected_branch[name]"]'); const $branchInput = this.$form.find('input[name="protected_branch[name]"]');
const $allowedToMergeInput = this.$form.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]'); const $allowedToMergeInput = this.$form.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
const $allowedToPushInput = this.$form.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]'); const $allowedToPushInput = this.$form.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
const completedForm = !(
$branchInput.val() &&
$allowedToMergeInput.length &&
$allowedToPushInput.length
);
this.savePreviousSelection($allowedToMergeInput.val(), $allowedToPushInput.val());
this.$form.find('input[type="submit"]').attr('disabled', completedForm);
}
loadPreviousSelection(mergeDropdown, pushDropdown) {
let mergeIndex = 0;
let pushIndex = 0;
if (this.isLocalStorageAvailable) {
const savedDefaults = JSON.parse(window.localStorage.getItem(PB_LOCAL_STORAGE_KEY));
if (savedDefaults != null) {
mergeIndex = _.findLastIndex(mergeDropdown.fullData.roles, {
id: parseInt(savedDefaults.mergeSelection, 0),
});
pushIndex = _.findLastIndex(pushDropdown.fullData.roles, {
id: parseInt(savedDefaults.pushSelection, 0),
});
}
}
mergeDropdown.selectRowAtIndex(mergeIndex);
pushDropdown.selectRowAtIndex(pushIndex);
}
this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length)); savePreviousSelection(mergeSelection, pushSelection) {
if (this.isLocalStorageAvailable) {
const branchDefaults = {
mergeSelection,
pushSelection,
};
window.localStorage.setItem(PB_LOCAL_STORAGE_KEY, JSON.stringify(branchDefaults));
}
} }
} }
<script>
/* globals Flash */
import { mapGetters, mapActions } from 'vuex';
import '../../flash';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import store from '../stores';
import collapsibleContainer from './collapsible_container.vue';
import { errorMessages, errorMessagesTypes } from '../constants';
export default {
name: 'registryListApp',
props: {
endpoint: {
type: String,
required: true,
},
},
store,
components: {
collapsibleContainer,
loadingIcon,
},
computed: {
...mapGetters([
'isLoading',
'repos',
]),
},
methods: {
...mapActions([
'setMainEndpoint',
'fetchRepos',
]),
},
created() {
this.setMainEndpoint(this.endpoint);
},
mounted() {
this.fetchRepos()
.catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS]));
},
};
</script>
<template>
<div>
<loading-icon
v-if="isLoading"
size="3"
/>
<collapsible-container
v-else-if="!isLoading && repos.length"
v-for="(item, index) in repos"
:key="index"
:repo="item"
/>
<p v-else-if="!isLoading && !repos.length">
{{__("No container images stored for this project. Add one by following the instructions above.")}}
</p>
</div>
</template>
<script>
/* globals Flash */
import { mapActions } from 'vuex';
import '../../flash';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import tableRegistry from './table_registry.vue';
import { errorMessages, errorMessagesTypes } from '../constants';
export default {
name: 'collapsibeContainerRegisty',
props: {
repo: {
type: Object,
required: true,
},
},
components: {
clipboardButton,
loadingIcon,
tableRegistry,
},
directives: {
tooltip,
},
data() {
return {
isOpen: false,
};
},
computed: {
clipboardText() {
return `docker pull ${this.repo.location}`;
},
},
methods: {
...mapActions([
'fetchRepos',
'fetchList',
'deleteRepo',
]),
toggleRepo() {
this.isOpen = !this.isOpen;
if (this.isOpen) {
this.fetchList({ repo: this.repo })
.catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
}
},
handleDeleteRepository() {
this.deleteRepo(this.repo)
.then(() => this.fetchRepos())
.catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
},
showError(message) {
Flash((errorMessages[message]));
},
},
};
</script>
<template>
<div class="container-image">
<div
class="container-image-head">
<button
type="button"
@click="toggleRepo"
class="js-toggle-repo btn-link">
<i
class="fa"
:class="{
'fa-chevron-right': !isOpen,
'fa-chevron-up': isOpen,
}"
aria-hidden="true">
</i>
{{repo.name}}
</button>
<clipboard-button
v-if="repo.location"
:text="clipboardText"
:title="repo.location"
/>
<div class="controls hidden-xs pull-right">
<button
v-if="repo.canDelete"
type="button"
class="js-remove-repo btn btn-danger"
:title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')"
v-tooltip
@click="handleDeleteRepository">
<i
class="fa fa-trash"
aria-hidden="true">
</i>
</button>
</div>
</div>
<loading-icon
v-if="repo.isLoading"
class="append-bottom-20"
size="2"
/>
<div
v-else-if="!repo.isLoading && isOpen"
class="container-image-tags">
<table-registry
v-if="repo.list.length"
:repo="repo"
/>
<div
v-else
class="nothing-here-block">
{{s__("ContainerRegistry|No tags in Container Registry for this container image.")}}
</div>
</div>
</div>
</template>
<script>
/* globals Flash */
import { mapActions } from 'vuex';
import { n__ } from '../../locale';
import '../../flash';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { errorMessages, errorMessagesTypes } from '../constants';
export default {
props: {
repo: {
type: Object,
required: true,
},
},
components: {
clipboardButton,
tablePagination,
},
mixins: [
timeagoMixin,
],
directives: {
tooltip,
},
computed: {
shouldRenderPagination() {
return this.repo.pagination.total > this.repo.pagination.perPage;
},
},
methods: {
...mapActions([
'fetchList',
'deleteRegistry',
]),
layers(item) {
return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
},
handleDeleteRegistry(registry) {
this.deleteRegistry(registry)
.then(() => this.fetchList({ repo: this.repo }))
.catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
},
onPageChange(pageNumber) {
this.fetchList({ repo: this.repo, page: pageNumber })
.catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY));
},
clipboardText(text) {
return `docker pull ${text}`;
},
showError(message) {
Flash((errorMessages[message]));
},
},
};
</script>
<template>
<div>
<table class="table tags">
<thead>
<tr>
<th>{{s__('ContainerRegistry|Tag')}}</th>
<th>{{s__('ContainerRegistry|Tag ID')}}</th>
<th>{{s__("ContainerRegistry|Size")}}</th>
<th>{{s__("ContainerRegistry|Created")}}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, i) in repo.list"
:key="i">
<td>
{{item.tag}}
<clipboard-button
v-if="item.location"
:title="item.location"
:text="clipboardText(item.location)"
/>
</td>
<td>
<span
v-tooltip
:title="item.revision"
data-placement="bottom">
{{item.shortRevision}}
</span>
</td>
<td>
{{item.size}}
<template v-if="item.size && item.layers">
&middot;
</template>
{{layers(item)}}
</td>
<td>
{{timeFormated(item.createdAt)}}
</td>
<td class="content">
<button
v-if="item.canDelete"
type="button"
class="js-delete-registry btn btn-danger hidden-xs pull-right"
:title="s__('ContainerRegistry|Remove tag')"
:aria-label="s__('ContainerRegistry|Remove tag')"
data-container="body"
v-tooltip
@click="handleDeleteRegistry(item)">
<i
class="fa fa-trash"
aria-hidden="true">
</i>
</button>
</td>
</tr>
</tbody>
</table>
<table-pagination
v-if="shouldRenderPagination"
:change="onPageChange"
:page-info="repo.pagination"
/>
</div>
</template>
import { __ } from '../locale';
export const errorMessagesTypes = {
FETCH_REGISTRY: 'FETCH_REGISTRY',
FETCH_REPOS: 'FETCH_REPOS',
DELETE_REPO: 'DELETE_REPO',
DELETE_REGISTRY: 'DELETE_REGISTRY',
};
export const errorMessages = {
[errorMessagesTypes.FETCH_REGISTRY]: __('Something went wrong while fetching the registry list.'),
[errorMessagesTypes.FETCH_REPOS]: __('Something went wrong while fetching the projects.'),
[errorMessagesTypes.DELETE_REPO]: __('Something went wrong on our end.'),
[errorMessagesTypes.DELETE_REGISTRY]: __('Something went wrong on our end.'),
};
import Vue from 'vue';
import registryApp from './components/app.vue';
import Translate from '../vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#js-vue-registry-images',
components: {
registryApp,
},
data() {
const dataset = document.querySelector(this.$options.el).dataset;
return {
endpoint: dataset.endpoint,
};
},
render(createElement) {
return createElement('registry-app', {
props: {
endpoint: this.endpoint,
},
});
},
}));
import Vue from 'vue';
import VueResource from 'vue-resource';
import * as types from './mutation_types';
Vue.use(VueResource);
export const fetchRepos = ({ commit, state }) => {
commit(types.TOGGLE_MAIN_LOADING);
return Vue.http.get(state.endpoint)
.then(res => res.json())
.then((response) => {
commit(types.TOGGLE_MAIN_LOADING);
commit(types.SET_REPOS_LIST, response);
});
};
export const fetchList = ({ commit }, { repo, page }) => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
return Vue.http.get(repo.tagsPath, { params: { page } })
.then((response) => {
const headers = response.headers;
return response.json().then((resp) => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
commit(types.SET_REGISTRY_LIST, { repo, resp, headers });
});
});
};
export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath)
.then(res => res.json());
export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath)
.then(res => res.json());
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
export const isLoading = state => state.isLoading;
export const repos = state => state.repos;
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
isLoading: false,
endpoint: '', // initial endpoint to fetch the repos list
/**
* Each object in `repos` has the following strucure:
* {
* name: String,
* isLoading: Boolean,
* tagsPath: String // endpoint to request the list
* destroyPath: String // endpoit to delete the repo
* list: Array // List of the registry images
* }
*
* Each registry image inside `list` has the following structure:
* {
* tag: String,
* revision: String
* shortRevision: String
* size: Number
* layers: Number
* createdAt: String
* destroyPath: String // endpoit to delete each image
* }
*/
repos: [],
},
actions,
getters,
mutations,
});
export const SET_MAIN_ENDPOINT = 'SET_MAIN_ENDPOINT';
export const SET_REPOS_LIST = 'SET_REPOS_LIST';
export const TOGGLE_MAIN_LOADING = 'TOGGLE_MAIN_LOADING';
export const SET_REGISTRY_LIST = 'SET_REGISTRY_LIST';
export const TOGGLE_REGISTRY_LIST_LOADING = 'TOGGLE_REGISTRY_LIST_LOADING';
import * as types from './mutation_types';
import { parseIntPagination, normalizeHeaders } from '../../lib/utils/common_utils';
export default {
[types.SET_MAIN_ENDPOINT](state, endpoint) {
Object.assign(state, { endpoint });
},
[types.SET_REPOS_LIST](state, list) {
Object.assign(state, {
repos: list.map(el => ({
canDelete: !!el.destroy_path,
destroyPath: el.destroy_path,
id: el.id,
isLoading: false,
list: [],
location: el.location,
name: el.path,
tagsPath: el.tags_path,
})),
});
},
[types.TOGGLE_MAIN_LOADING](state) {
Object.assign(state, { isLoading: !state.isLoading });
},
[types.SET_REGISTRY_LIST](state, { repo, resp, headers }) {
const listToUpdate = state.repos.find(el => el.id === repo.id);
const normalizedHeaders = normalizeHeaders(headers);
const pagination = parseIntPagination(normalizedHeaders);
listToUpdate.pagination = pagination;
listToUpdate.list = resp.map(element => ({
tag: element.name,
revision: element.revision,
shortRevision: element.short_revision,
size: element.size,
layers: element.layers,
location: element.location,
createdAt: element.created_at,
destroyPath: element.destroy_path,
canDelete: !!element.destroy_path,
}));
},
[types.TOGGLE_REGISTRY_LIST_LOADING](state, list) {
const listToUpdate = state.repos.find(el => el.id === list.id);
listToUpdate.isLoading = !listToUpdate.isLoading;
},
};
...@@ -74,8 +74,8 @@ export default { ...@@ -74,8 +74,8 @@ export default {
<thead v-if="!isMini"> <thead v-if="!isMini">
<tr> <tr>
<th class="name">Name</th> <th class="name">Name</th>
<th class="hidden-sm hidden-xs last-commit">Last Commit</th> <th class="hidden-sm hidden-xs last-commit">Last commit</th>
<th class="hidden-xs last-update text-right">Last Update</th> <th class="hidden-xs last-update text-right">Last update</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
......
...@@ -7,7 +7,7 @@ export default { ...@@ -7,7 +7,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon status="loading" showDisabledButton /> <status-icon status="loading" :show-disabled-button="true" />
<div class="media-body space-children"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
Checking ability to merge automatically Checking ability to merge automatically
......
...@@ -16,9 +16,9 @@ export default { ...@@ -16,9 +16,9 @@ export default {
<div class="media-body"> <div class="media-body">
<mr-widget-author-and-time <mr-widget-author-and-time
actionText="Closed by" actionText="Closed by"
:author="mr.closedBy" :author="mr.closedEvent.author"
:dateTitle="mr.updatedAt" :dateTitle="mr.closedEvent.updatedAt"
:dateReadable="mr.closedAt" :dateReadable="mr.closedEvent.formattedUpdatedAt"
/> />
<section class="mr-info-list"> <section class="mr-info-list">
<p> <p>
......
...@@ -12,7 +12,11 @@ export default { ...@@ -12,7 +12,11 @@ export default {
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon <status-icon
status="failed" status="failed"
<<<<<<< HEAD
showDisabledButton /> showDisabledButton />
=======
:show-disabled-button="true" />
>>>>>>> ce/master
<div class="media-body space-children"> <div class="media-body space-children">
<span <span
v-if="mr.shouldBeRebased" v-if="mr.shouldBeRebased"
......
...@@ -51,7 +51,7 @@ export default { ...@@ -51,7 +51,7 @@ export default {
</span> </span>
</template> </template>
<template v-else> <template v-else>
<status-icon status="failed" showDisabledButton /> <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
<span <span
......
...@@ -69,9 +69,9 @@ export default { ...@@ -69,9 +69,9 @@ export default {
<div class="space-children"> <div class="space-children">
<mr-widget-author-and-time <mr-widget-author-and-time
actionText="Merged by" actionText="Merged by"
:author="mr.mergedBy" :author="mr.mergedEvent.author"
:dateTitle="mr.updatedAt" :date-title="mr.mergedEvent.updatedAt"
:dateReadable="mr.mergedAt" /> :date-readable="mr.mergedEvent.formattedUpdatedAt" />
<a <a
v-if="mr.canRevertInCurrentMR" v-if="mr.canRevertInCurrentMR"
v-tooltip v-tooltip
......
...@@ -24,7 +24,7 @@ export default { ...@@ -24,7 +24,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon status="failed" showDisabledButton /> <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children"> <div class="media-body space-children">
<span class="bold js-branch-text"> <span class="bold js-branch-text">
<span class="capitalize"> <span class="capitalize">
......
...@@ -7,7 +7,7 @@ export default { ...@@ -7,7 +7,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon status="success" showDisabledButton /> <status-icon status="success" :show-disabled-button="true" />
<div class="media-body space-children"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
Ready to be merged automatically. Ready to be merged automatically.
......
...@@ -7,7 +7,7 @@ export default { ...@@ -7,7 +7,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon status="failed" showDisabledButton /> <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
Pipeline blocked. The pipeline for this merge request requires a manual action to proceed Pipeline blocked. The pipeline for this merge request requires a manual action to proceed
......
...@@ -7,7 +7,7 @@ export default { ...@@ -7,7 +7,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon status="failed" showDisabledButton /> <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
......
...@@ -38,24 +38,40 @@ export default { ...@@ -38,24 +38,40 @@ export default {
return this.useCommitMessageWithDescription ? withoutDesc : withDesc; return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
}, },
mergeButtonClass() { status() {
const defaultClass = 'btn btn-sm btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr; const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
if (hasCI && !ciStatus) { if (hasCI && !ciStatus) {
return failedClass; return 'failed';
} else if (!pipeline) { } else if (!pipeline) {
return defaultClass; return 'success';
} else if (isPipelineActive) { } else if (isPipelineActive) {
return inActionClass; return 'pending';
} else if (isPipelineFailed) { } else if (isPipelineFailed) {
return 'failed';
}
return 'success';
},
mergeButtonClass() {
const defaultClass = 'btn btn-sm btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
if (this.status === 'failed') {
return failedClass; return failedClass;
} else if (this.status === 'pending') {
return inActionClass;
} }
return defaultClass; return defaultClass;
}, },
iconClass() {
if (this.status === 'failed' || !this.commitMessage.length || !this.isMergeAllowed() || this.mr.preventMerge) {
return 'failed';
}
return 'success';
},
mergeButtonText() { mergeButtonText() {
if (this.isMergingImmediately) { if (this.isMergingImmediately) {
return 'Merge in progress'; return 'Merge in progress';
...@@ -160,6 +176,7 @@ export default { ...@@ -160,6 +176,7 @@ export default {
eventHub.$emit('FetchActionsContent'); eventHub.$emit('FetchActionsContent');
if (window.mergeRequest) { if (window.mergeRequest) {
window.mergeRequest.updateStatusText('status-box-open', 'status-box-merged', 'Merged'); window.mergeRequest.updateStatusText('status-box-open', 'status-box-merged', 'Merged');
window.mergeRequest.hideCloseButton();
window.mergeRequest.decreaseCounter(); window.mergeRequest.decreaseCounter();
} }
stopPolling(); stopPolling();
...@@ -212,7 +229,7 @@ export default { ...@@ -212,7 +229,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon status="success" /> <status-icon :status="iconClass" />
<div class="media-body"> <div class="media-body">
<div class="mr-widget-body-controls media space-children"> <div class="mr-widget-body-controls media space-children">
<span class="btn-group append-bottom-5"> <span class="btn-group append-bottom-5">
......
...@@ -7,7 +7,7 @@ export default { ...@@ -7,7 +7,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon status="failed" showDisabledButton /> <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
The source branch HEAD has recently changed. Please reload the page and review the changes before merging The source branch HEAD has recently changed. Please reload the page and review the changes before merging
......
...@@ -10,7 +10,7 @@ export default { ...@@ -10,7 +10,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon status="failed" showDisabledButton /> <status-icon status="failed" :show-disabled-button="true" />
<div class="media-body space-children"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
There are unresolved discussions. Please resolve these discussions There are unresolved discussions. Please resolve these discussions
......
...@@ -38,7 +38,7 @@ export default { ...@@ -38,7 +38,7 @@ export default {
}, },
template: ` template: `
<div class="mr-widget-body media"> <div class="mr-widget-body media">
<status-icon status="failed" :showDisabledButton="Boolean(mr.removeWIPPath)" /> <status-icon status="failed" :show-disabled-button="Boolean(mr.removeWIPPath)" />
<div class="media-body space-children"> <div class="media-body space-children">
<span class="bold"> <span class="bold">
This is a Work in Progress This is a Work in Progress
......
...@@ -39,10 +39,8 @@ export default class MergeRequestStore { ...@@ -39,10 +39,8 @@ export default class MergeRequestStore {
} }
this.updatedAt = data.updated_at; this.updatedAt = data.updated_at;
this.mergedAt = MergeRequestStore.getEventDate(data.merge_event); this.mergedEvent = MergeRequestStore.getEventObject(data.merge_event);
this.closedAt = MergeRequestStore.getEventDate(data.closed_event); this.closedEvent = MergeRequestStore.getEventObject(data.closed_event);
this.mergedBy = MergeRequestStore.getAuthorObject(data.merge_event);
this.closedBy = MergeRequestStore.getAuthorObject(data.closed_event);
this.setToMWPSBy = MergeRequestStore.getAuthorObject({ author: data.merge_user || {} }); this.setToMWPSBy = MergeRequestStore.getAuthorObject({ author: data.merge_user || {} });
this.mergeUserId = data.merge_user_id; this.mergeUserId = data.merge_user_id;
this.currentUserId = gon.current_user_id; this.currentUserId = gon.current_user_id;
...@@ -122,6 +120,14 @@ export default class MergeRequestStore { ...@@ -122,6 +120,14 @@ export default class MergeRequestStore {
} }
} }
static getEventObject(event) {
return {
author: MergeRequestStore.getAuthorObject(event),
updatedAt: gl.utils.formatDate(MergeRequestStore.getEventUpdatedAtDate(event)),
formattedUpdatedAt: MergeRequestStore.getEventDate(event),
};
}
static getAuthorObject(event) { static getAuthorObject(event) {
if (!event) { if (!event) {
return {}; return {};
...@@ -135,6 +141,14 @@ export default class MergeRequestStore { ...@@ -135,6 +141,14 @@ export default class MergeRequestStore {
}; };
} }
static getEventUpdatedAtDate(event) {
if (!event) {
return '';
}
return event.updated_at;
}
static getEventDate(event) { static getEventDate(event) {
const timeagoInstance = new Timeago(); const timeagoInstance = new Timeago();
...@@ -142,6 +156,6 @@ export default class MergeRequestStore { ...@@ -142,6 +156,6 @@ export default class MergeRequestStore {
return ''; return '';
} }
return timeagoInstance.format(event.updated_at); return timeagoInstance.format(MergeRequestStore.getEventUpdatedAtDate(event));
} }
} }
<script>
/**
* Falls back to the code used in `copy_to_clipboard.js`
*/
export default {
name: 'clipboardButton',
props: {
text: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
},
};
</script>
<template>
<button
type="button"
class="btn btn-transparent btn-clipboard"
:data-title="title"
:data-clipboard-text="text">
<i
aria-hidden="true"
class="fa fa-clipboard">
</i>
</button>
</template>
...@@ -2,6 +2,7 @@ import { ...@@ -2,6 +2,7 @@ import {
__, __,
n__, n__,
s__, s__,
sprintf,
} from '../locale'; } from '../locale';
export default (Vue) => { export default (Vue) => {
...@@ -37,6 +38,7 @@ export default (Vue) => { ...@@ -37,6 +38,7 @@ export default (Vue) => {
@returns {String} Translated context based text @returns {String} Translated context based text
**/ **/
s__, s__,
sprintf,
}, },
}); });
}; };
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
&.s60 { @include avatar-size(60px, 12px); } &.s60 { @include avatar-size(60px, 12px); }
&.s70 { @include avatar-size(70px, 14px); } &.s70 { @include avatar-size(70px, 14px); }
&.s90 { @include avatar-size(90px, 15px); } &.s90 { @include avatar-size(90px, 15px); }
&.s100 { @include avatar-size(100px, 15px); }
&.s110 { @include avatar-size(110px, 15px); } &.s110 { @include avatar-size(110px, 15px); }
&.s140 { @include avatar-size(140px, 15px); } &.s140 { @include avatar-size(140px, 15px); }
&.s160 { @include avatar-size(160px, 20px); } &.s160 { @include avatar-size(160px, 20px); }
...@@ -82,6 +83,7 @@ ...@@ -82,6 +83,7 @@
&.s60 { font-size: 32px; line-height: 58px; } &.s60 { font-size: 32px; line-height: 58px; }
&.s70 { font-size: 34px; line-height: 70px; } &.s70 { font-size: 34px; line-height: 70px; }
&.s90 { font-size: 36px; line-height: 88px; } &.s90 { font-size: 36px; line-height: 88px; }
&.s100 { font-size: 36px; line-height: 98px; }
&.s110 { font-size: 40px; line-height: 108px; font-weight: $gl-font-weight-normal; } &.s110 { font-size: 40px; line-height: 108px; font-weight: $gl-font-weight-normal; }
&.s140 { font-size: 72px; line-height: 138px; } &.s140 { font-size: 72px; line-height: 138px; }
&.s160 { font-size: 96px; line-height: 158px; } &.s160 { font-size: 96px; line-height: 158px; }
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
.prepend-top-15 { margin-top: 15px; } .prepend-top-15 { margin-top: 15px; }
.prepend-top-default { margin-top: $gl-padding !important; } .prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top: 20px; } .prepend-top-20 { margin-top: 20px; }
.prepend-left-4 { margin-left: 4px; }
.prepend-left-5 { margin-left: 5px; } .prepend-left-5 { margin-left: 5px; }
.prepend-left-10 { margin-left: 10px; } .prepend-left-10 { margin-left: 10px; }
.prepend-left-default { margin-left: $gl-padding; } .prepend-left-default { margin-left: $gl-padding; }
...@@ -134,11 +135,6 @@ span.update-author { ...@@ -134,11 +135,6 @@ span.update-author {
} }
} }
.user-mention {
color: $user-mention-color;
font-weight: $gl-font-weight-bold;
}
.field_with_errors { .field_with_errors {
display: inline; display: inline;
} }
......
...@@ -745,6 +745,10 @@ ...@@ -745,6 +745,10 @@
#{$selector}.dropdown-menu-nav { #{$selector}.dropdown-menu-nav {
margin-bottom: 24px; margin-bottom: 24px;
&.dropdown-open-top {
margin-bottom: $dropdown-vertical-offset;
}
li { li {
display: block; display: block;
padding: 0 1px; padding: 0 1px;
...@@ -873,6 +877,13 @@ ...@@ -873,6 +877,13 @@
min-width: 100%; min-width: 100%;
} }
} }
header.navbar-gitlab-new .header-content .dropdown {
.dropdown-menu {
left: 0;
min-width: 100%;
}
}
} }
@include new-style-dropdown('.breadcrumbs-list .dropdown '); @include new-style-dropdown('.breadcrumbs-list .dropdown ');
......
...@@ -6,3 +6,14 @@ ...@@ -6,3 +6,14 @@
.gfm-commit_range { .gfm-commit_range {
@extend .commit-sha; @extend .commit-sha;
} }
.gfm-project_member {
padding: 0 2px;
border-radius: #{$border-radius-default / 2};
background-color: $user-mention-bg;
&:hover {
background-color: $user-mention-bg-hover;
text-decoration: none;
}
}
...@@ -235,6 +235,10 @@ ul.content-list { ...@@ -235,6 +235,10 @@ ul.content-list {
.label-default { .label-default {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
} }
.avatar-cell {
align-self: flex-start;
}
} }
.panel > .content-list > li { .panel > .content-list > li {
......
...@@ -295,7 +295,7 @@ header.navbar-gitlab-new { ...@@ -295,7 +295,7 @@ header.navbar-gitlab-new {
.header-user .dropdown-menu-nav, .header-user .dropdown-menu-nav,
.header-new .dropdown-menu-nav { .header-new .dropdown-menu-nav {
margin-top: 4px; margin-top: $dropdown-vertical-offset;
} }
.breadcrumbs { .breadcrumbs {
......
...@@ -48,21 +48,30 @@ ...@@ -48,21 +48,30 @@
} }
&:hover { &:hover {
background-color: $white-normal; border-color: $gray-darkest;
border-color: $border-white-normal;
color: $gl-text-color; color: $gl-text-color;
} }
} }
} }
<<<<<<< HEAD
.select2-drop { .select2-drop {
background-color: $white-light; background-color: $white-light;
border: 1px solid $dropdown-border-color; border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base; border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color; box-shadow: 0 2px 4px $dropdown-shadow-color;
=======
.select2-drop,
.select2-drop.select2-drop-above {
box-shadow: 0 2px 4px $dropdown-shadow-color;
border-radius: $border-radius-base;
border: 1px solid $dropdown-border-color;
>>>>>>> ce/master
min-width: 175px; min-width: 175px;
color: $gl-text-color;
} }
<<<<<<< HEAD
.select2-results .select2-result-label, .select2-results .select2-result-label,
.select2-more-results { .select2-more-results {
padding: 10px 15px; padding: 10px 15px;
...@@ -74,6 +83,11 @@ ...@@ -74,6 +83,11 @@
.select2-highlighted { .select2-highlighted {
background: $dropdown-hover-color !important; background: $dropdown-hover-color !important;
=======
.select2-drop.select2-drop-above.select2-drop-active {
border-top: 1px solid $dropdown-border-color;
margin-top: -6px;
>>>>>>> ce/master
} }
.select2-results li.select2-result-with-children > .select2-result-label { .select2-results li.select2-result-with-children > .select2-result-label {
...@@ -88,13 +102,11 @@ ...@@ -88,13 +102,11 @@
} }
} }
.select2-dropdown-open { .select2-dropdown-open,
.select2-dropdown-open.select2-drop-above {
.select2-choice { .select2-choice {
border-color: $border-white-normal; border-color: $gray-darkest;
outline: 0; outline: 0;
background-image: none;
background-color: $white-dark;
box-shadow: $gl-btn-active-gradient;
} }
} }
...@@ -132,28 +144,23 @@ ...@@ -132,28 +144,23 @@
} }
} }
} }
<<<<<<< HEAD
&.select2-container-active .select2-choices, &.select2-container-active .select2-choices,
&.select2-dropdown-open .select2-choices { &.select2-dropdown-open .select2-choices {
border-color: $dropdown-input-focus-border; border-color: $dropdown-input-focus-border;
box-shadow: 0 0 4px $search-input-focus-shadow-color; box-shadow: 0 0 4px $search-input-focus-shadow-color;
} }
=======
>>>>>>> ce/master
} }
.select2-drop-active { .select2-drop-active {
margin-top: 6px; margin-top: $dropdown-vertical-offset;
font-size: 14px; font-size: 14px;
&.select2-drop-above {
margin-bottom: 8px;
}
.select2-results { .select2-results {
max-height: 350px; max-height: 350px;
.select2-highlighted {
background: $gl-primary;
}
} }
} }
...@@ -187,19 +194,35 @@ ...@@ -187,19 +194,35 @@
background-size: 16px 16px !important; background-size: 16px 16px !important;
} }
.select2-results .select2-no-results,
.select2-results .select2-searching,
.select2-results .select2-ajax-error,
.select2-results .select2-selection-limit {
background: $gray-light;
display: list-item;
padding: 10px 15px;
}
.select2-results { .select2-results {
margin: 0; margin: 0;
padding: 10px 0; padding: #{$gl-padding / 2} 0;
.select2-no-results,
.select2-searching,
.select2-ajax-error,
.select2-selection-limit {
background: transparent;
padding: #{$gl-padding / 2} $gl-padding;
}
.select2-result-label,
.select2-more-results {
padding: #{$gl-padding / 2} $gl-padding;
}
.select2-highlighted {
background: transparent;
color: $gl-text-color;
.select2-result-label {
background: $dropdown-item-hover-bg;
}
}
.select2-result {
padding: 0 1px;
}
} }
.ajax-users-select { .ajax-users-select {
...@@ -277,6 +300,7 @@ ...@@ -277,6 +300,7 @@
min-width: 250px !important; min-width: 250px !important;
} }
<<<<<<< HEAD
// TODO: change global style // TODO: change global style
.ajax-project-dropdown, .ajax-project-dropdown,
.ajax-users-dropdown, .ajax-users-dropdown,
...@@ -331,5 +355,12 @@ body[data-page="projects:blob:edit"] #select2-drop { ...@@ -331,5 +355,12 @@ body[data-page="projects:blob:edit"] #select2-drop {
} }
} }
} }
=======
.select2-result-selectable,
.select2-result-unselectable {
.select2-match {
font-weight: $gl-font-weight-bold;
text-decoration: none;
>>>>>>> ce/master
} }
} }
...@@ -268,7 +268,8 @@ $well-pre-bg: #eee; ...@@ -268,7 +268,8 @@ $well-pre-bg: #eee;
$well-pre-color: #555; $well-pre-color: #555;
$loading-color: #555; $loading-color: #555;
$update-author-color: #999; $update-author-color: #999;
$user-mention-color: #2fa0bb; $user-mention-bg: rgba($blue-500, 0.044);
$user-mention-bg-hover: rgba($blue-500, 0.15);
$time-color: #999; $time-color: #999;
$project-member-show-color: #aaa; $project-member-show-color: #aaa;
$gl-promo-color: #aaa; $gl-promo-color: #aaa;
...@@ -334,6 +335,7 @@ $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-San ...@@ -334,6 +335,7 @@ $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-San
* Dropdowns * Dropdowns
*/ */
$dropdown-width: 300px; $dropdown-width: 300px;
$dropdown-vertical-offset: 4px;
$dropdown-link-color: #555; $dropdown-link-color: #555;
$dropdown-link-hover-bg: $row-hover; $dropdown-link-hover-bg: $row-hover;
$dropdown-empty-row-bg: rgba(#000, .04); $dropdown-empty-row-bg: rgba(#000, .04);
......
...@@ -9,6 +9,14 @@ ...@@ -9,6 +9,14 @@
.container-image-head { .container-image-head {
padding: 0 16px; padding: 0 16px;
line-height: 4em; line-height: 4em;
.btn-link {
padding: 0;
&:focus {
outline: none;
}
}
} }
.table.tags { .table.tags {
......
...@@ -77,6 +77,18 @@ ...@@ -77,6 +77,18 @@
word-wrap: break-word; word-wrap: break-word;
} }
} }
&.left-side-selected {
td.line_content.parallel.right-side {
@include user-select(none);
}
}
&.right-side-selected {
td.line_content.parallel.left-side {
@include user-select(none);
}
}
} }
tr.line_holder.parallel { tr.line_holder.parallel {
......
...@@ -366,10 +366,13 @@ ...@@ -366,10 +366,13 @@
} }
.prometheus-state { .prometheus-state {
margin-top: 10px; max-width: 430px;
margin: 10px auto;
text-align: center;
.state-button-section { .state-svg {
margin-top: 10px; max-width: 80vw;
margin: 0 auto;
} }
} }
......
...@@ -362,7 +362,7 @@ ...@@ -362,7 +362,7 @@
.dropdown-menu { .dropdown-menu {
top: initial; top: initial;
bottom: 40px; bottom: 100%;
width: 298px; width: 298px;
} }
......
...@@ -389,11 +389,11 @@ table.u2f-registrations { ...@@ -389,11 +389,11 @@ table.u2f-registrations {
} }
} }
.gpg-email-badge { .email-badge {
display: inline; display: inline;
margin-right: $gl-padding / 2; margin-right: $gl-padding / 2;
.gpg-email-badge-email { .email-badge-email {
display: inline; display: inline;
margin-right: $gl-padding / 4; margin-right: $gl-padding / 4;
} }
......
...@@ -507,73 +507,56 @@ a.deploy-project-label { ...@@ -507,73 +507,56 @@ a.deploy-project-label {
} }
} }
.fork-namespaces { .fork-thumbnail {
.row { height: 200px;
-webkit-flex-wrap: wrap; width: calc((100% / 2) - #{$gl-padding * 2});
display: -webkit-flex;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
.fork-thumbnail { @media (min-width: $screen-md-min) {
border-radius: $border-radius-base; width: calc((100% / 4) - #{$gl-padding * 2});
background-color: $white-light; }
border: 1px solid $border-white-light;
height: 202px;
margin: $gl-padding;
text-align: center;
width: 169px;
&:hover:not(.disabled), @media (min-width: $screen-lg-min) {
&.forked { width: calc((100% / 5) - #{$gl-padding * 2});
background-color: $row-hover; }
border-color: $row-hover-border;
}
.no-avatar { &:hover:not(.disabled),
width: 100px; &.forked {
height: 100px; background-color: $row-hover;
background-color: $gray-light; border-color: $row-hover-border;
border: 1px solid $white-normal; }
margin: 0 auto;
border-radius: 50%;
i {
font-size: 100px;
color: $white-normal;
}
}
a { .avatar-container,
display: block; .identicon {
width: 100%; float: none;
height: 100%; margin-left: auto;
padding-top: $gl-padding; margin-right: auto;
color: $gl-text-color; }
&.disabled {
opacity: .3;
cursor: not-allowed;
&:hover {
text-decoration: none;
}
}
.caption {
min-height: 30px;
padding: $gl-padding 0;
}
}
img { a {
border-radius: 50%; display: block;
max-width: 100px; width: 100%;
} height: 100%;
padding-top: $gl-padding;
text-decoration: none;
&.disabled {
opacity: .3;
cursor: not-allowed;
} }
} }
} }
.fork-thumbnail-container {
display: flex;
flex-wrap: wrap;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
> h5 {
width: 100%;
}
}
.project-template, .project-template,
.project-import { .project-import {
.form-group { .form-group {
......
...@@ -12,3 +12,7 @@ ...@@ -12,3 +12,7 @@
margin-left: 10px; margin-left: 10px;
} }
} }
.registry-placeholder {
min-height: 60px;
}
...@@ -155,7 +155,7 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -155,7 +155,7 @@ class Admin::UsersController < Admin::ApplicationController
def remove_email def remove_email
email = user.emails.find(params[:email_id]) email = user.emails.find(params[:email_id])
success = Emails::DestroyService.new(current_user, user: user, email: email.email).execute success = Emails::DestroyService.new(current_user, user: user).execute(email)
respond_to do |format| respond_to do |format|
if success if success
......
...@@ -85,12 +85,21 @@ class ApplicationController < ActionController::Base ...@@ -85,12 +85,21 @@ class ApplicationController < ActionController::Base
super super
payload[:remote_ip] = request.remote_ip payload[:remote_ip] = request.remote_ip
if current_user.present? logged_user = auth_user
payload[:user_id] = current_user.id
payload[:username] = current_user.username if logged_user.present?
payload[:user_id] = logged_user.try(:id)
payload[:username] = logged_user.try(:username)
end end
end end
# Controllers such as GitHttpController may use alternative methods
# (e.g. tokens) to authenticate the user, whereas Devise sets current_user
def auth_user
return current_user if current_user.present?
return try(:authenticated_user)
end
# This filter handles both private tokens and personal access tokens # This filter handles both private tokens and personal access tokens
def authenticate_user_from_private_token! def authenticate_user_from_private_token!
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
......
...@@ -15,9 +15,9 @@ module NotesActions ...@@ -15,9 +15,9 @@ module NotesActions
notes = notes_finder.execute notes = notes_finder.execute
.inc_relations_for_view .inc_relations_for_view
.reject { |n| n.cross_reference_not_visible_for?(current_user) }
notes = prepare_notes_for_rendering(notes) notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
notes_json[:notes] = notes_json[:notes] =
if noteable.discussions_rendered_on_frontend? if noteable.discussions_rendered_on_frontend?
......
...@@ -12,13 +12,14 @@ class ConfirmationsController < Devise::ConfirmationsController ...@@ -12,13 +12,14 @@ class ConfirmationsController < Devise::ConfirmationsController
users_almost_there_path users_almost_there_path
end end
def after_confirmation_path_for(resource_name, resource) def after_confirmation_path_for(_resource_name, resource)
if signed_in?(resource_name) # incoming resource can either be a :user or an :email
if signed_in?(:user)
after_sign_in(resource) after_sign_in(resource)
else else
Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}") Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}")
flash[:notice] += " Please sign in." flash[:notice] += " Please sign in."
new_session_path(resource_name) new_session_path(:user)
end end
end end
......
...@@ -7,9 +7,8 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -7,9 +7,8 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def index def index
@sort = params[:sort] @sort = params[:sort]
@todos = @todos.page(params[:page]) @todos = @todos.page(params[:page])
if @todos.out_of_range? && @todos.total_pages != 0
redirect_to url_for(params.merge(page: @todos.total_pages, only_path: true)) return if redirect_out_of_range(@todos)
end
end end
def destroy def destroy
...@@ -60,7 +59,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -60,7 +59,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end end
def find_todos def find_todos
@todos ||= TodosFinder.new(current_user, params).execute @todos ||= TodosFinder.new(current_user, todo_params).execute
end end
def todos_counts def todos_counts
...@@ -69,4 +68,27 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -69,4 +68,27 @@ class Dashboard::TodosController < Dashboard::ApplicationController
done_count: number_with_delimiter(current_user.todos_done_count) done_count: number_with_delimiter(current_user.todos_done_count)
} }
end end
def todo_params
params.permit(:action_id, :author_id, :project_id, :type, :sort, :state)
end
def redirect_out_of_range(todos)
total_pages =
if todo_params.except(:sort, :page).empty?
(current_user.todos_pending_count / todos.limit_value).ceil
else
todos.total_pages
end
return false if total_pages.zero?
out_of_range = todos.current_page > total_pages
if out_of_range
redirect_to url_for(params.merge(page: total_pages, only_path: true))
end
out_of_range
end
end end
class Profiles::EmailsController < Profiles::ApplicationController class Profiles::EmailsController < Profiles::ApplicationController
before_action :find_email, only: [:destroy, :resend_confirmation_instructions]
def index def index
@primary = current_user.email @primary_email = current_user.email
@emails = current_user.emails.order_id_desc @emails = current_user.emails.order_id_desc
end end
def create def create
@email = Emails::CreateService.new(current_user, email_params.merge(user: current_user)).execute @email = Emails::CreateService.new(current_user, email_params.merge(user: current_user)).execute
unless @email.errors.blank?
if @email.errors.blank?
NotificationService.new.new_email(@email)
else
flash[:alert] = @email.errors.full_messages.first flash[:alert] = @email.errors.full_messages.first
end end
...@@ -17,9 +16,7 @@ class Profiles::EmailsController < Profiles::ApplicationController ...@@ -17,9 +16,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
end end
def destroy def destroy
@email = current_user.emails.find(params[:id]) Emails::DestroyService.new(current_user, user: current_user).execute(@email)
Emails::DestroyService.new(current_user, user: current_user, email: @email.email).execute
respond_to do |format| respond_to do |format|
format.html { redirect_to profile_emails_url, status: 302 } format.html { redirect_to profile_emails_url, status: 302 }
...@@ -27,9 +24,23 @@ class Profiles::EmailsController < Profiles::ApplicationController ...@@ -27,9 +24,23 @@ class Profiles::EmailsController < Profiles::ApplicationController
end end
end end
def resend_confirmation_instructions
if Emails::ConfirmService.new(current_user, user: current_user).execute(@email)
flash[:notice] = "Confirmation email sent to #{@email.email}"
else
flash[:alert] = "There was a problem sending the confirmation email"
end
redirect_to profile_emails_url
end
private private
def email_params def email_params
params.require(:email).permit(:email) params.require(:email).permit(:email)
end end
def find_email
@email = current_user.emails.find(params[:id])
end
end end
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
def index def index
set_index_vars set_index_vars
@personal_access_token = finder.build
end end
def create def create
...@@ -40,7 +41,6 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController ...@@ -40,7 +41,6 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
def set_index_vars def set_index_vars
@scopes = Gitlab::Auth.available_scopes @scopes = Gitlab::Auth.available_scopes
@personal_access_token = finder.build
@inactive_personal_access_tokens = finder(state: 'inactive').execute @inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at) @active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
end end
......
...@@ -9,6 +9,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController ...@@ -9,6 +9,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
alias_method :user, :actor alias_method :user, :actor
alias_method :authenticated_user, :actor
# Git clients will not know what authenticity token to send along # Git clients will not know what authenticity token to send along
skip_before_action :verify_authenticity_token skip_before_action :verify_authenticity_token
......
...@@ -6,17 +6,26 @@ module Projects ...@@ -6,17 +6,26 @@ module Projects
def index def index
@images = project.container_repositories @images = project.container_repositories
respond_to do |format|
format.html
format.json do
render json: ContainerRepositoriesSerializer
.new(project: project, current_user: current_user)
.represent(@images)
end
end
end end
def destroy def destroy
if image.destroy if image.destroy
redirect_to project_container_registry_index_path(@project), respond_to do |format|
status: 302, format.json { head :no_content }
notice: 'Image repository has been removed successfully!' end
else else
redirect_to project_container_registry_index_path(@project), respond_to do |format|
status: 302, format.json { head :bad_request }
alert: 'Failed to remove image repository!' end
end end
end end
......
...@@ -3,20 +3,35 @@ module Projects ...@@ -3,20 +3,35 @@ module Projects
class TagsController < ::Projects::Registry::ApplicationController class TagsController < ::Projects::Registry::ApplicationController
before_action :authorize_update_container_image!, only: [:destroy] before_action :authorize_update_container_image!, only: [:destroy]
def index
respond_to do |format|
format.json do
render json: ContainerTagsSerializer
.new(project: @project, current_user: @current_user)
.with_pagination(request, response)
.represent(tags)
end
end
end
def destroy def destroy
if tag.delete if tag.delete
redirect_to project_container_registry_index_path(@project), respond_to do |format|
status: 302, format.json { head :no_content }
notice: 'Registry tag has been removed successfully!' end
else else
redirect_to project_container_registry_index_path(@project), respond_to do |format|
status: 302, format.json { head :bad_request }
alert: 'Failed to remove registry tag!' end
end end
end end
private private
def tags
Kaminari::PaginatableArray.new(image.tags, limit: 15)
end
def image def image
@image ||= project.container_repositories @image ||= project.container_repositories
.find(params[:repository_id]) .find(params[:repository_id])
......
...@@ -18,16 +18,12 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -18,16 +18,12 @@ class Projects::WikisController < Projects::ApplicationController
response.headers['Content-Security-Policy'] = "default-src 'none'" response.headers['Content-Security-Policy'] = "default-src 'none'"
response.headers['X-Content-Security-Policy'] = "default-src 'none'" response.headers['X-Content-Security-Policy'] = "default-src 'none'"
if file.on_disk? send_data(
send_file file.on_disk_path, disposition: 'inline' file.raw_data,
else type: file.mime_type,
send_data( disposition: 'inline',
file.raw_data, filename: file.name
type: file.mime_type, )
disposition: 'inline',
filename: file.name
)
end
else else
return render('empty') unless can?(current_user, :create_wiki, @project) return render('empty') unless can?(current_user, :create_wiki, @project)
@page = WikiPage.new(@project_wiki) @page = WikiPage.new(@project_wiki)
......
...@@ -33,19 +33,21 @@ module DiffHelper ...@@ -33,19 +33,21 @@ module DiffHelper
end end
def diff_match_line(old_pos, new_pos, text: '', view: :inline, bottom: false) def diff_match_line(old_pos, new_pos, text: '', view: :inline, bottom: false)
content = content_tag :td, text, class: "line_content match #{view == :inline ? '' : view}" content_line_class = %w[line_content match]
cls = ['diff-line-num', 'unfold', 'js-unfold'] content_line_class << 'parallel' if view == :parallel
cls << 'js-unfold-bottom' if bottom
line_num_class = %w[diff-line-num unfold js-unfold]
line_num_class << 'js-unfold-bottom' if bottom
html = '' html = ''
if old_pos if old_pos
html << content_tag(:td, '...', class: cls + ['old_line'], data: { linenumber: old_pos }) html << content_tag(:td, '...', class: [*line_num_class, 'old_line'], data: { linenumber: old_pos })
html << content unless view == :inline html << content_tag(:td, text, class: [*content_line_class, 'left-side']) if view == :parallel
end end
if new_pos if new_pos
html << content_tag(:td, '...', class: cls + ['new_line'], data: { linenumber: new_pos }) html << content_tag(:td, '...', class: [*line_num_class, 'new_line'], data: { linenumber: new_pos })
html << content html << content_tag(:td, text, class: [*content_line_class, ('right-side' if view == :parallel)])
end end
html.html_safe html.html_safe
......
module EventsHelper module EventsHelper
ICON_NAMES_BY_EVENT_TYPE = { ICON_NAMES_BY_EVENT_TYPE = {
'pushed to' => 'icon_commit', 'pushed to' => 'commit',
'pushed new' => 'icon_commit', 'pushed new' => 'commit',
'created' => 'icon_status_open', 'created' => 'status_open',
'opened' => 'icon_status_open', 'opened' => 'status_open',
'closed' => 'icon_status_closed', 'closed' => 'status_closed',
'accepted' => 'icon_code_fork', 'accepted' => 'fork',
'commented on' => 'icon_comment_o', 'commented on' => 'comment',
'deleted' => 'icon_trash_o' 'deleted' => 'remove',
'imported' => 'import',
'joined' => 'users'
}.freeze }.freeze
def link_to_author(event, self_added: false) def link_to_author(event, self_added: false)
...@@ -197,7 +199,7 @@ module EventsHelper ...@@ -197,7 +199,7 @@ module EventsHelper
def icon_for_event(note) def icon_for_event(note)
icon_name = ICON_NAMES_BY_EVENT_TYPE[note] icon_name = ICON_NAMES_BY_EVENT_TYPE[note]
custom_icon(icon_name) if icon_name sprite_icon(icon_name) if icon_name
end end
def icon_for_profile_event(event) def icon_for_profile_event(event)
......
...@@ -7,12 +7,6 @@ module Emails ...@@ -7,12 +7,6 @@ module Emails
mail(to: @user.notification_email, subject: subject("Account was created for you")) mail(to: @user.notification_email, subject: subject("Account was created for you"))
end end
def new_email_email(email_id)
@email = Email.find(email_id)
@current_user = @user = @email.user
mail(to: @user.notification_email, subject: subject("Email was added to your account"))
end
def new_ssh_key_email(key_id) def new_ssh_key_email(key_id)
@key = Key.find_by(id: key_id) @key = Key.find_by(id: key_id)
......
...@@ -234,6 +234,10 @@ module Ci ...@@ -234,6 +234,10 @@ module Ci
variables variables
end end
def features
{ trace_sections: true }
end
def merge_request def merge_request
return @merge_request if defined?(@merge_request) return @merge_request if defined?(@merge_request)
......
module RepositoryMirroring module RepositoryMirroring
<<<<<<< HEAD
def storage_path def storage_path
@project.repository_storage_path @project.repository_storage_path
end end
...@@ -13,11 +14,30 @@ module RepositoryMirroring ...@@ -13,11 +14,30 @@ module RepositoryMirroring
def set_remote_as_mirror(name) def set_remote_as_mirror(name)
config = raw_repository.rugged.config config = raw_repository.rugged.config
=======
IMPORT_HEAD_REFS = '+refs/heads/*:refs/heads/*'.freeze
IMPORT_TAG_REFS = '+refs/tags/*:refs/tags/*'.freeze
>>>>>>> ce/master
def set_remote_as_mirror(name)
# This is used to define repository as equivalent as "git clone --mirror" # This is used to define repository as equivalent as "git clone --mirror"
config["remote.#{name}.fetch"] = 'refs/*:refs/*' raw_repository.rugged.config["remote.#{name}.fetch"] = 'refs/*:refs/*'
config["remote.#{name}.mirror"] = true raw_repository.rugged.config["remote.#{name}.mirror"] = true
config["remote.#{name}.prune"] = true raw_repository.rugged.config["remote.#{name}.prune"] = true
end
def set_import_remote_as_mirror(remote_name)
# Add first fetch with Rugged so it does not create its own.
raw_repository.rugged.config["remote.#{remote_name}.fetch"] = IMPORT_HEAD_REFS
add_remote_fetch_config(remote_name, IMPORT_TAG_REFS)
raw_repository.rugged.config["remote.#{remote_name}.mirror"] = true
raw_repository.rugged.config["remote.#{remote_name}.prune"] = true
end
def add_remote_fetch_config(remote_name, refspec)
run_git(%W[config --add remote.#{remote_name}.fetch #{refspec}])
end end
def fetch_mirror(remote, url) def fetch_mirror(remote, url)
......
...@@ -7,6 +7,13 @@ class Email < ActiveRecord::Base ...@@ -7,6 +7,13 @@ class Email < ActiveRecord::Base
validates :email, presence: true, uniqueness: true, email: true validates :email, presence: true, uniqueness: true, email: true
validate :unique_email, if: ->(email) { email.email_changed? } validate :unique_email, if: ->(email) { email.email_changed? }
scope :confirmed, -> { where.not(confirmed_at: nil) }
after_commit :update_invalid_gpg_signatures, if: -> { previous_changes.key?('confirmed_at') }
devise :confirmable
self.reconfirmable = false # currently email can't be changed, no need to reconfirm
def email=(value) def email=(value)
write_attribute(:email, value.downcase.strip) write_attribute(:email, value.downcase.strip)
end end
...@@ -14,4 +21,9 @@ class Email < ActiveRecord::Base ...@@ -14,4 +21,9 @@ class Email < ActiveRecord::Base
def unique_email def unique_email
self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email) self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email)
end end
# once email is confirmed, update the gpg signatures
def update_invalid_gpg_signatures
user.update_invalid_gpg_signatures if confirmed?
end
end end
...@@ -37,6 +37,7 @@ class Key < ActiveRecord::Base ...@@ -37,6 +37,7 @@ class Key < ActiveRecord::Base
value&.delete!("\n\r") value&.delete!("\n\r")
value.strip! unless value.blank? value.strip! unless value.blank?
write_attribute(:key, value) write_attribute(:key, value)
@public_key = nil
end end
def publishable_key def publishable_key
......
...@@ -439,6 +439,8 @@ class MergeRequest < ActiveRecord::Base ...@@ -439,6 +439,8 @@ class MergeRequest < ActiveRecord::Base
end end
def create_merge_request_diff def create_merge_request_diff
fetch_ref
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435 # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435
Gitlab::GitalyClient.allow_n_plus_1_calls do Gitlab::GitalyClient.allow_n_plus_1_calls do
merge_request_diffs.create merge_request_diffs.create
...@@ -486,6 +488,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -486,6 +488,7 @@ class MergeRequest < ActiveRecord::Base
return unless open? return unless open?
old_diff_refs = self.diff_refs old_diff_refs = self.diff_refs
create_merge_request_diff create_merge_request_diff
MergeRequests::MergeRequestDiffCacheService.new.execute(self) MergeRequests::MergeRequestDiffCacheService.new.execute(self)
new_diff_refs = self.diff_refs new_diff_refs = self.diff_refs
...@@ -585,14 +588,20 @@ class MergeRequest < ActiveRecord::Base ...@@ -585,14 +588,20 @@ class MergeRequest < ActiveRecord::Base
commits_for_notes_limit = 100 commits_for_notes_limit = 100
commit_ids = commit_shas.take(commits_for_notes_limit) commit_ids = commit_shas.take(commits_for_notes_limit)
Note.where( commit_notes = Note
"(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" + .except(:order)
"((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))", .where(project_id: [source_project_id, target_project_id])
mr_id: id, .where(noteable_type: 'Commit', commit_id: commit_ids)
commit_ids: commit_ids,
target_project_id: target_project_id, # We're using a UNION ALL here since this results in better performance
source_project_id: source_project_id # compared to using OR statements. We're using UNION ALL since the queries
) # used won't produce any duplicates (e.g. a note for a commit can't also be
# a note for an MR).
union = Gitlab::SQL::Union
.new([notes, commit_notes], remove_duplicates: false)
.to_sql
Note.from("(#{union}) #{Note.table_name}")
end end
alias_method :discussion_notes, :related_notes alias_method :discussion_notes, :related_notes
...@@ -767,10 +776,9 @@ class MergeRequest < ActiveRecord::Base ...@@ -767,10 +776,9 @@ class MergeRequest < ActiveRecord::Base
end end
def has_ci? def has_ci?
has_ci_integration = source_project.try(:ci_service) return false if has_no_commits?
uses_gitlab_ci = all_pipelines.any?
(has_ci_integration || uses_gitlab_ci) && commits.any? !!(head_pipeline_id || all_pipelines.any? || source_project&.ci_service)
end end
def branch_missing? def branch_missing?
......
...@@ -55,7 +55,6 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -55,7 +55,6 @@ class MergeRequestDiff < ActiveRecord::Base
end end
def ensure_commit_shas def ensure_commit_shas
merge_request.fetch_ref
self.start_commit_sha ||= merge_request.target_branch_sha self.start_commit_sha ||= merge_request.target_branch_sha
self.head_commit_sha ||= merge_request.source_branch_sha self.head_commit_sha ||= merge_request.source_branch_sha
self.base_commit_sha ||= find_base_sha self.base_commit_sha ||= find_base_sha
......
...@@ -17,6 +17,8 @@ class PersonalAccessToken < ActiveRecord::Base ...@@ -17,6 +17,8 @@ class PersonalAccessToken < ActiveRecord::Base
validates :scopes, presence: true validates :scopes, presence: true
validate :validate_scopes validate :validate_scopes
after_initialize :set_default_scopes, if: :persisted?
def revoke! def revoke!
update!(revoked: true) update!(revoked: true)
end end
...@@ -32,4 +34,8 @@ class PersonalAccessToken < ActiveRecord::Base ...@@ -32,4 +34,8 @@ class PersonalAccessToken < ActiveRecord::Base
errors.add :scopes, "can only contain available scopes" errors.add :scopes, "can only contain available scopes"
end end
end end
def set_default_scopes
self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty?
end
end end
...@@ -67,6 +67,7 @@ class Project < ActiveRecord::Base ...@@ -67,6 +67,7 @@ class Project < ActiveRecord::Base
# Storage specific hooks # Storage specific hooks
after_initialize :use_hashed_storage after_initialize :use_hashed_storage
after_create :check_repository_absence!
after_create :ensure_storage_path_exists after_create :ensure_storage_path_exists
after_save :ensure_storage_path_exists, if: :namespace_id_changed? after_save :ensure_storage_path_exists, if: :namespace_id_changed?
...@@ -232,7 +233,7 @@ class Project < ActiveRecord::Base ...@@ -232,7 +233,7 @@ class Project < ActiveRecord::Base
validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?] validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 } validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create validate :check_limit, on: :create
validate :check_repository_path_availability, on: [:create, :update], if: ->(project) { !project.persisted? || project.renamed? } validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
validate :avatar_type, validate :avatar_type,
if: ->(project) { project.avatar.present? && project.avatar_changed? } if: ->(project) { project.avatar.present? && project.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i } validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
...@@ -1025,12 +1026,16 @@ class Project < ActiveRecord::Base ...@@ -1025,12 +1026,16 @@ class Project < ActiveRecord::Base
expires_full_path_cache # we need to clear cache to validate renames correctly expires_full_path_cache # we need to clear cache to validate renames correctly
if gitlab_shell.exists?(repository_storage_path, "#{disk_path}.git") # Check if repository with same path already exists on disk we can
# skip this for the hashed storage because the path does not change
if legacy_storage? && repository_with_same_path_already_exists?
errors.add(:base, 'There is already a repository with that name on disk') errors.add(:base, 'There is already a repository with that name on disk')
return false return false
end end
true true
rescue GRPC::Internal # if the path is too long
false
end end
def create_repository(force: false) def create_repository(force: false)
...@@ -1566,6 +1571,34 @@ class Project < ActiveRecord::Base ...@@ -1566,6 +1571,34 @@ class Project < ActiveRecord::Base
persisted? && path_changed? persisted? && path_changed?
end end
def merge_method
if self.merge_requests_ff_only_enabled
:ff
elsif self.merge_requests_rebase_enabled
:rebase_merge
else
:merge
end
end
def merge_method=(method)
case method.to_s
when "ff"
self.merge_requests_ff_only_enabled = true
self.merge_requests_rebase_enabled = true
when "rebase_merge"
self.merge_requests_ff_only_enabled = false
self.merge_requests_rebase_enabled = true
when "merge"
self.merge_requests_ff_only_enabled = false
self.merge_requests_rebase_enabled = false
end
end
def ff_merge_must_be_possible?
self.merge_requests_ff_only_enabled || self.merge_requests_rebase_enabled
end
def migrate_to_hashed_storage! def migrate_to_hashed_storage!
return if hashed_storage? return if hashed_storage?
...@@ -1641,6 +1674,19 @@ class Project < ActiveRecord::Base ...@@ -1641,6 +1674,19 @@ class Project < ActiveRecord::Base
Gitlab::ReferenceCounter.new(gl_repository(is_wiki: true)).value Gitlab::ReferenceCounter.new(gl_repository(is_wiki: true)).value
end end
def check_repository_absence!
return if skip_disk_validation
if repository_storage_path.blank? || repository_with_same_path_already_exists?
errors.add(:base, 'There is already a repository with that name on disk')
throw :abort
end
end
def repository_with_same_path_already_exists?
gitlab_shell.exists?(repository_storage_path, "#{disk_path}.git")
end
# set last_activity_at to the same as created_at # set last_activity_at to the same as created_at
def set_last_activity_at def set_last_activity_at
update_column(:last_activity_at, self.created_at) update_column(:last_activity_at, self.created_at)
......
...@@ -63,12 +63,15 @@ class ProjectWiki ...@@ -63,12 +63,15 @@ class ProjectWiki
[Gitlab.config.gitlab.relative_url_root, '/', @project.full_path, '/wikis'].join('') [Gitlab.config.gitlab.relative_url_root, '/', @project.full_path, '/wikis'].join('')
end end
# Returns the Gollum::Wiki object. # Returns the Gitlab::Git::Wiki object.
def wiki def wiki
@wiki ||= begin @wiki ||= begin
Gollum::Wiki.new(path_to_repo) gl_repository = Gitlab::GlRepository.gl_repository(project, true)
rescue Rugged::OSError raw_repository = Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', gl_repository)
create_repo!
create_repo!(raw_repository) unless raw_repository.exists?
Gitlab::Git::Wiki.new(raw_repository)
end end
end end
...@@ -95,20 +98,14 @@ class ProjectWiki ...@@ -95,20 +98,14 @@ class ProjectWiki
# Returns an initialized WikiPage instance or nil # Returns an initialized WikiPage instance or nil
def find_page(title, version = nil) def find_page(title, version = nil)
page_title, page_dir = page_title_and_dir(title) page_title, page_dir = page_title_and_dir(title)
if page = wiki.page(page_title, version, page_dir)
if page = wiki.page(title: page_title, version: version, dir: page_dir)
WikiPage.new(self, page, true) WikiPage.new(self, page, true)
else
nil
end end
end end
def find_file(name, version = nil, try_on_disk = true) def find_file(name, version = nil)
version = wiki.ref if version.nil? # Gollum::Wiki#file ? wiki.file(name, version)
if wiki_file = wiki.file(name, version, try_on_disk)
wiki_file
else
nil
end
end end
def create_page(title, content, format = :markdown, message = nil) def create_page(title, content, format = :markdown, message = nil)
...@@ -119,7 +116,7 @@ class ProjectWiki ...@@ -119,7 +116,7 @@ class ProjectWiki
update_elastic_index update_elastic_index
update_project_activity update_project_activity
rescue Gollum::DuplicatePageError => e rescue Gitlab::Git::Wiki::DuplicatePageError => e
@error_message = "Duplicate page: #{e.message}" @error_message = "Duplicate page: #{e.message}"
return false return false
end end
...@@ -127,7 +124,7 @@ class ProjectWiki ...@@ -127,7 +124,7 @@ class ProjectWiki
def update_page(page, content:, title: nil, format: :markdown, message: nil) def update_page(page, content:, title: nil, format: :markdown, message: nil)
commit = commit_details(:updated, message, page.title) commit = commit_details(:updated, message, page.title)
wiki.update_page(page, title || page.name, format.to_sym, content, commit) wiki.update_page(page.path, title || page.name, format.to_sym, content, commit)
update_elastic_index update_elastic_index
...@@ -135,7 +132,7 @@ class ProjectWiki ...@@ -135,7 +132,7 @@ class ProjectWiki
end end
def delete_page(page, message = nil) def delete_page(page, message = nil)
wiki.delete_page(page, commit_details(:deleted, message, page.title)) wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
update_elastic_index update_elastic_index
...@@ -160,20 +157,8 @@ class ProjectWiki ...@@ -160,20 +157,8 @@ class ProjectWiki
wiki.class.default_ref wiki.class.default_ref
end end
def create_repo!
if init_repo(disk_path)
wiki = Gollum::Wiki.new(path_to_repo)
else
raise CouldNotCreateWikiError
end
repository.after_create
wiki
end
def ensure_repository def ensure_repository
create_repo! unless repository_exists? raise CouldNotCreateWikiError unless wiki.repository_exists?
end end
def hook_attrs def hook_attrs
...@@ -188,24 +173,24 @@ class ProjectWiki ...@@ -188,24 +173,24 @@ class ProjectWiki
private private
def init_repo(disk_path) def create_repo!(raw_repository)
gitlab_shell.add_repository(project.repository_storage, disk_path) gitlab_shell.add_repository(project.repository_storage, disk_path)
raise CouldNotCreateWikiError unless raw_repository.exists?
repository.after_create
end end
def commit_details(action, message = nil, title = nil) def commit_details(action, message = nil, title = nil)
commit_message = message || default_message(action, title) commit_message = message || default_message(action, title)
{ email: @user.email, name: @user.name, message: commit_message } Gitlab::Git::Wiki::CommitDetails.new(@user.name, @user.email, commit_message)
end end
def default_message(action, title) def default_message(action, title)
"#{@user.username} #{action} page: #{title}" "#{@user.username} #{action} page: #{title}"
end end
def path_to_repo
@path_to_repo ||= File.join(project.repository_storage_path, "#{disk_path}.git")
end
def update_project_activity def update_project_activity
@project.touch(:last_activity_at, :last_repository_updated_at) @project.touch(:last_activity_at, :last_repository_updated_at)
end end
......
...@@ -876,6 +876,25 @@ class Repository ...@@ -876,6 +876,25 @@ class Repository
end end
end end
def ff_merge(user, source, target_branch, merge_request: nil)
our_commit = rugged.branches[target_branch].target
their_commit =
if source.is_a?(Gitlab::Git::Commit)
source.raw_commit
else
rugged.lookup(source)
end
raise 'Invalid merge target' if our_commit.nil?
raise 'Invalid merge source' if their_commit.nil?
with_branch(user, target_branch) do |start_commit|
merge_request&.update(in_progress_merge_commit_sha: their_commit.oid)
their_commit.oid
end
end
def revert( def revert(
user, commit, branch_name, message, user, commit, branch_name, message,
start_branch_name: nil, start_project: project) start_branch_name: nil, start_project: project)
...@@ -1044,7 +1063,7 @@ class Repository ...@@ -1044,7 +1063,7 @@ class Repository
end end
def create_ref(ref, ref_path) def create_ref(ref, ref_path)
fetch_ref(path_to_repo, ref, ref_path) raw_repository.write_ref(ref_path, ref)
end end
def ls_files(ref) def ls_files(ref)
......
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.
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