Commit f39ba1bb authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into pipeline-emails

* upstream/master: (372 commits)
  Enable Lint/StringConversionInInterpolation cop and autocorrect offenses
  resolve duplicated changelog entry
  credit myself 😄
  change determine conditions
  override subject method in devise mailer
  follow the styleguide: Don't use parentheses around a literal
  wrap subject with method subject
  move spec back into shared example `an email sent from GitLab`
  stub config settings in spec
  remove empty line at block body end
  remove extra entry
  create new test in `spec/mailers/notify_spec.rb`
  move changelog to 8.13
  add configurable email subject suffix
  Fixes sidebar navigation.
  Convert "SSH Keys" Spinach features to RSpec
  Enable import/export back for non-admins
  Update gitlab-shell to 3.6.3
  Updated artwork of empty group state.
  Better empty state for Groups view.
  ...
parents db6b2b18 a1aea326
*.erb
lib/gitlab/sanitizers/svg/whitelist.rb
lib/gitlab/diff/position_tracer.rb
image: "ruby:2.3.1"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3-git-2.7-phantomjs-2.1"
cache:
key: "ruby-231"
paths:
- vendor/apt
- vendor/ruby
variables:
......@@ -12,7 +11,7 @@ variables:
RSPEC_RETRY_RETRY_COUNT: "3"
RAILS_ENV: "test"
SIMPLECOV: "true"
USE_DB: "true"
SETUP_DB: "true"
USE_BUNDLE_INSTALL: "true"
GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1"
......@@ -23,7 +22,7 @@ before_script:
- bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
- retry gem install knapsack
- '[ "$USE_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate'
- '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate'
stages:
- prepare
......@@ -35,7 +34,7 @@ stages:
.knapsack-state: &knapsack-state
services: []
variables:
USE_DB: "false"
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
cache:
key: "knapsack"
......@@ -141,14 +140,13 @@ spinach 9 10: *spinach-knapsack
# Execute all testing suites against Ruby 2.1
.ruby-21: &ruby-21
image: "ruby:2.1"
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.1-git-2.7-phantomjs-2.1"
<<: *use-db
only:
- master
cache:
key: "ruby21"
paths:
- vendor/apt
- vendor/ruby
.rspec-knapsack-ruby21: &rspec-knapsack-ruby21
......@@ -196,7 +194,7 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21
.ruby-static-analysis: &ruby-static-analysis
variables:
SIMPLECOV: "false"
USE_DB: "false"
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true"
.exec: &exec
......@@ -209,9 +207,6 @@ rubocop: *exec
rake haml_lint: *exec
rake scss_lint: *exec
rake brakeman: *exec
rake flog:
<<: *exec
allow_failure: yes
rake flay:
<<: *exec
allow_failure: yes
......@@ -224,6 +219,23 @@ rake db:migrate:reset:
script:
- rake db:migrate:reset
rake db:seed_fu:
stage: test
<<: *use-db
variables:
SIZE: "1"
SETUP_DB: "false"
RAILS_ENV: "development"
script:
- git clone https://gitlab.com/gitlab-org/gitlab-test.git
/home/git/repositories/gitlab-org/gitlab-test.git
- bundle exec rake db:setup db:seed_fu
artifacts:
when: on_failure
expire_in: 1d
paths:
- log/development.log
teaspoon:
stage: test
<<: *use-db
......@@ -272,7 +284,7 @@ coverage:
stage: post-test
services: []
variables:
USE_DB: "false"
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true"
script:
- bundle exec scripts/merge-simplecov
......@@ -288,7 +300,7 @@ coverage:
notify:slack:
stage: post-test
variables:
USE_DB: "false"
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
script:
- ./scripts/notify_slack.sh "#builds" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>"
......
......@@ -639,6 +639,10 @@ Lint/RescueException:
Lint/ShadowedException:
Enabled: false
# Checks for Object#to_s usage in string interpolation.
Lint/StringConversionInInterpolation:
Enabled: true
# Do not use prefix `_` for a variable that is used.
Lint/UnderscorePrefixedVariableName:
Enabled: true
......@@ -767,33 +771,26 @@ Rails/ScopeArgs:
RSpec/AnyInstance:
Enabled: false
# Check for expectations where `be(...)` can replace `eql(...)`.
RSpec/BeEql:
Enabled: false
# Check that the first argument to the top level describe is a constant.
# Check that the first argument to the top level describe is the tested class or
# module.
RSpec/DescribeClass:
Enabled: false
# Checks that tests use `described_class`.
RSpec/DescribedClass:
Enabled: false
# Checks that the second argument to `describe` specifies a method.
# Use `described_class` for tested class / module.
RSpec/DescribeMethod:
Enabled: false
# Checks if an example group does not include any tests.
RSpec/EmptyExampleGroup:
# Checks that the second argument to top level describe is the tested method
# name.
RSpec/DescribedClass:
Enabled: false
CustomIncludeMethods: []
# Checks for long examples.
# Checks for long example.
RSpec/ExampleLength:
Enabled: false
Max: 5
# Checks that example descriptions do not start with "should".
# Do not use should when describing your tests.
RSpec/ExampleWording:
Enabled: false
CustomTransform:
......@@ -802,10 +799,6 @@ RSpec/ExampleWording:
not: does not
IgnoredWords: []
# Checks for `expect(...)` calls containing literal values.
RSpec/ExpectActual:
Enabled: false
# Checks the file and folder naming of the spec file.
RSpec/FilePath:
Enabled: false
......@@ -817,65 +810,19 @@ RSpec/FilePath:
RSpec/Focus:
Enabled: true
# Checks the arguments passed to `before`, `around`, and `after`.
RSpec/HookArgument:
Enabled: false
EnforcedStyle: implicit
# Check that a consistent implict expectation style is used.
# TODO (rspeicher): Available in rubocop-rspec 1.8.0
# RSpec/ImplicitExpect:
# Enabled: true
# EnforcedStyle: is_expected
# Checks for the usage of instance variables.
RSpec/InstanceVariable:
Enabled: false
# Checks for `subject` definitions that come after `let` definitions.
RSpec/LeadingSubject:
Enabled: false
# Checks unreferenced `let!` calls being used for test setup.
RSpec/LetSetup:
Enabled: false
# Check that chains of messages are not being stubbed.
RSpec/MessageChain:
Enabled: false
# Checks for consistent message expectation style.
RSpec/MessageExpectation:
Enabled: false
EnforcedStyle: allow
# Checks for multiple top level describes.
# Checks for multiple top-level describes.
RSpec/MultipleDescribes:
Enabled: false
# Checks if examples contain too many `expect` calls.
RSpec/MultipleExpectations:
Enabled: false
Max: 1
# Checks for explicitly referenced test subjects.
RSpec/NamedSubject:
Enabled: false
# Checks for nested example groups.
RSpec/NestedGroups:
Enabled: false
MaxNesting: 2
# Checks for consistent method usage for negating expectations.
# Enforces the usage of the same method on all negative message expectations.
RSpec/NotToNot:
EnforcedStyle: not_to
Enabled: true
# Checks for stubbed test subjects.
RSpec/SubjectStub:
Enabled: false
# Prefer using verifying doubles over normal doubles.
RSpec/VerifiedDoubles:
Enabled: false
......@@ -27,11 +27,6 @@ Lint/Loop:
Lint/ShadowingOuterLocalVariable:
Enabled: false
# Offense count: 6
# Cop supports --auto-correct.
Lint/StringConversionInInterpolation:
Enabled: false
# Offense count: 49
# Cop supports --auto-correct.
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
......
This diff is collapsed.
......@@ -6,10 +6,8 @@ gem 'rails-deprecated_sanitizer', '~> 1.0.3'
# Responders respond_to and respond_with
gem 'responders', '~> 2.0'
# Specify a sprockets version due to increased performance
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/6069
gem 'sprockets', '~> 3.6.0'
gem 'sprockets-es6'
gem 'sprockets', '~> 3.7.0'
gem 'sprockets-es6', '~> 0.9.2'
# Default values for AR models
gem 'default_value_for', '~> 3.0.0'
......@@ -19,7 +17,7 @@ gem 'mysql2', '~> 0.3.16', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
# Authentication libraries
gem 'devise', '~> 4.0'
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
gem 'omniauth', '~> 1.3.1'
gem 'omniauth-auth0', '~> 1.4.1'
......@@ -53,7 +51,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
gem 'gitlab_git', '~> 10.6.6'
gem 'gitlab_git', '~> 10.6.7'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
......@@ -102,7 +100,7 @@ gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
gem 'task_list', '~> 1.0.2', require: 'task_list/railtie'
gem 'deckar01-task_list', '1.0.5', require: 'task_list/railtie'
gem 'github-markup', '~> 1.4'
gem 'redcarpet', '~> 3.3.3'
gem 'RedCloth', '~> 4.3.2'
......@@ -122,8 +120,8 @@ gem 'diffy', '~> 3.0.3'
# Application server
group :unicorn do
gem 'unicorn', '~> 4.9.0'
gem 'unicorn-worker-killer', '~> 0.4.2'
gem 'unicorn', '~> 5.1.0'
gem 'unicorn-worker-killer', '~> 0.4.4'
end
# State machine
......@@ -212,7 +210,7 @@ gem 'oj', '~> 2.17.4'
gem 'chronic', '~> 0.10.2'
gem 'chronic_duration', '~> 0.10.6'
gem 'sass-rails', '~> 5.0.0'
gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2'
gem 'turbolinks', '~> 2.5.0'
......@@ -298,11 +296,10 @@ group :development, :test do
gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.42.0', require: false
gem 'rubocop-rspec', '~> 1.7.0', require: false
gem 'rubocop-rspec', '~> 1.5.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false
gem 'haml_lint', '~> 0.18.2', require: false
gem 'simplecov', '0.12.0', require: false
gem 'flog', '~> 4.3.2', require: false
gem 'flay', '~> 2.6.1', require: false
gem 'bundler-audit', '~> 0.5.0', require: false
......@@ -322,10 +319,6 @@ group :test do
gem 'timecop', '~> 0.8.0'
end
group :production do
gem 'gitlab_meta', '7.0'
end
gem 'newrelic_rpm', '~> 3.16'
gem 'octokit', '~> 4.3.0'
......@@ -334,7 +327,7 @@ gem 'mail_room', '~> 0.8'
gem 'email_reply_parser', '~> 0.5.8'
gem 'ruby-prof', '~> 0.15.9'
gem 'ruby-prof', '~> 0.16.2'
## CI
gem 'activerecord-session_store', '~> 1.0.0'
......
......@@ -157,11 +157,15 @@ GEM
database_cleaner (1.5.3)
debug_inspector (0.0.2)
debugger-ruby_core_source (1.3.8)
deckar01-task_list (1.0.5)
activesupport (~> 4.0)
html-pipeline
rack (~> 1.0)
default_value_for (3.0.2)
activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
devise (4.1.1)
devise (4.2.0)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0, < 5.1)
......@@ -209,9 +213,6 @@ GEM
flay (2.6.1)
ruby_parser (~> 3.0)
sexp_processor (~> 4.0)
flog (4.3.2)
ruby_parser (~> 3.1, > 3.1.0)
sexp_processor (~> 4.4)
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
......@@ -279,12 +280,11 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
gitlab_git (10.6.6)
gitlab_git (10.6.7)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
rugged (~> 0.24.0)
gitlab_meta (7.0)
gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
omniauth (~> 1.0)
......@@ -554,7 +554,7 @@ GEM
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.1.0)
raindrops (0.15.0)
raindrops (0.17.0)
rake (10.5.0)
rb-fsevent (0.9.6)
rb-inotify (0.9.5)
......@@ -588,7 +588,7 @@ GEM
request_store (1.3.1)
rerun (0.11.0)
listen (~> 3.0)
responders (2.1.1)
responders (2.3.0)
railties (>= 4.2.0, < 5.1)
rinku (2.0.0)
rotp (2.1.2)
......@@ -626,11 +626,11 @@ GEM
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-rspec (1.7.0)
rubocop (>= 0.42.0)
rubocop-rspec (1.5.0)
rubocop (>= 0.40.0)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-prof (0.15.9)
ruby-prof (0.16.2)
ruby-progressbar (1.8.1)
ruby-saml (1.3.0)
nokogiri (>= 1.5.10)
......@@ -645,7 +645,7 @@ GEM
sanitize (2.1.0)
nokogiri (>= 1.4.4)
sass (3.4.22)
sass-rails (5.0.5)
sass-rails (5.0.6)
railties (>= 4.0.0, < 6)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
......@@ -706,10 +706,10 @@ GEM
spring (>= 0.9.1)
spring-commands-teaspoon (0.0.2)
spring (>= 0.9.1)
sprockets (3.6.3)
sprockets (3.7.0)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-es6 (0.9.0)
sprockets-es6 (0.9.2)
babel-source (>= 5.8.11)
babel-transpiler
sprockets (>= 3.0.0)
......@@ -729,8 +729,6 @@ GEM
ffi
sysexits (1.2.0)
systemu (2.6.5)
task_list (1.0.2)
html-pipeline
teaspoon (1.1.5)
railties (>= 3.2.5, < 6)
teaspoon-jasmine (2.2.0)
......@@ -760,9 +758,8 @@ GEM
unf_ext
unf_ext (0.0.7.2)
unicode-display_width (1.1.1)
unicorn (4.9.0)
unicorn (5.1.0)
kgio (~> 2.6)
rack
raindrops (~> 0.7)
unicorn-worker-killer (0.4.4)
get_process_mem (~> 0)
......@@ -836,8 +833,9 @@ DEPENDENCIES
creole (~> 0.5.0)
d3_rails (~> 3.5.0)
database_cleaner (~> 1.5.0)
deckar01-task_list (= 1.0.5)
default_value_for (~> 3.0.0)
devise (~> 4.0)
devise (~> 4.2)
devise-two-factor (~> 3.0.0)
diffy (~> 3.0.3)
doorkeeper (~> 4.2.0)
......@@ -847,7 +845,6 @@ DEPENDENCIES
factory_girl_rails (~> 4.6.0)
ffaker (~> 2.0.0)
flay (~> 2.6.1)
flog (~> 4.3.2)
fog-aws (~> 0.9)
fog-azure (~> 0.0)
fog-core (~> 1.40)
......@@ -863,8 +860,7 @@ DEPENDENCIES
github-linguist (~> 4.7.0)
github-markup (~> 1.4)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_git (~> 10.6.6)
gitlab_meta (= 7.0)
gitlab_git (~> 10.6.7)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
......@@ -943,11 +939,11 @@ DEPENDENCIES
rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5)
rubocop (~> 0.42.0)
rubocop-rspec (~> 1.7.0)
rubocop-rspec (~> 1.5.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.15.9)
ruby-prof (~> 0.16.2)
sanitize (~> 2.0)
sass-rails (~> 5.0.0)
sass-rails (~> 5.0.6)
scss_lint (~> 0.47.0)
sdoc (~> 0.3.20)
seed-fu (~> 2.3.5)
......@@ -966,11 +962,10 @@ DEPENDENCIES
spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.1.0)
spring-commands-teaspoon (~> 0.0.2)
sprockets (~> 3.6.0)
sprockets-es6
sprockets (~> 3.7.0)
sprockets-es6 (~> 0.9.2)
state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6)
task_list (~> 1.0.2)
teaspoon (~> 1.1.0)
teaspoon-jasmine (~> 2.2.0)
test_after_commit (~> 0.4.2)
......@@ -981,8 +976,8 @@ DEPENDENCIES
uglifier (~> 2.7.2)
underscore-rails (~> 1.8.0)
unf (~> 0.1.4)
unicorn (~> 4.9.0)
unicorn-worker-killer (~> 0.4.2)
unicorn (~> 5.1.0)
unicorn-worker-killer (~> 0.4.4)
version_sorter (~> 2.1.0)
virtus (~> 1.0.1)
vmstat (~> 2.2)
......@@ -991,4 +986,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
1.13.0
1.13.1
......@@ -7,7 +7,7 @@
## Canonical source
The cannonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
## Open source software to collaborate on code
......
8.12.0-pre
8.13.0-pre
......@@ -16,9 +16,6 @@
.replace(':id', group_id);
return $.ajax({
url: url,
data: {
private_token: gon.api_token
},
dataType: "json"
}).done(function(group) {
return callback(group);
......@@ -31,7 +28,6 @@
return $.ajax({
url: url,
data: {
private_token: gon.api_token,
search: query,
per_page: 20
},
......@@ -46,7 +42,6 @@
return $.ajax({
url: url,
data: {
private_token: gon.api_token,
search: query,
per_page: 20
},
......@@ -61,7 +56,6 @@
return $.ajax({
url: url,
data: {
private_token: gon.api_token,
search: query,
order_by: order,
per_page: 20
......@@ -74,7 +68,6 @@
newLabel: function(project_id, data, callback) {
var url = Api.buildUrl(Api.labelsPath)
.replace(':id', project_id);
data.private_token = gon.api_token;
return $.ajax({
url: url,
type: "POST",
......@@ -93,7 +86,6 @@
return $.ajax({
url: url,
data: {
private_token: gon.api_token,
search: query,
per_page: 20
},
......
......@@ -247,7 +247,7 @@
$this.toggleClass('active');
var notesHolders = $this.closest('.diff-file').find('.notes_holder');
if ($this.hasClass('active')) {
notesHolders.show();
notesHolders.show().find('.hide').show();
} else {
notesHolders.hide();
}
......
......@@ -357,7 +357,7 @@
$('ul.emoji-menu-search, h5.emoji-search').remove();
if (term) {
// Generate a search result block
h5 = $('<h5>').text('Search results');
h5 = $('<h5 class="emoji-search" />').text('Search results');
found_emojis = _this.searchEmojis(term).show();
ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis);
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
......
......@@ -22,6 +22,7 @@
// submitted textarea
})(this));
this.initModePanesAndLinks();
this.initSoftWrap();
new BlobLicenseSelectors({
editor: this.editor
});
......@@ -50,6 +51,7 @@
this.$editModePanes.hide();
currentPane.fadeIn(200);
if (paneId === "#preview") {
this.$toggleButton.hide();
return $.post(currentLink.data("preview-url"), {
content: this.editor.getValue()
}, function(response) {
......@@ -57,10 +59,23 @@
return currentPane.syntaxHighlight();
});
} else {
this.$toggleButton.show();
return this.editor.focus();
}
};
EditBlob.prototype.initSoftWrap = function() {
this.isSoftWrapped = false;
this.$toggleButton = $('.soft-wrap-toggle');
this.$toggleButton.on('click', this.toggleSoftWrap.bind(this));
};
EditBlob.prototype.toggleSoftWrap = function(e) {
this.isSoftWrapped = !this.isSoftWrapped;
this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
};
return EditBlob;
})();
......
......@@ -23,8 +23,9 @@
selectable: true,
filterable: true,
filterByText: true,
fieldName: $dropdown.attr('name'),
filterInput: 'input[type="text"]',
toggleLabel: true,
fieldName: $dropdown.data('field-name'),
filterInput: 'input[type="search"]',
renderRow: function(ref) {
var link;
if (ref.header != null) {
......
((global) => {
const COOKIE_NAME = 'cycle_analytics_help_dismissed';
const store = gl.cycleAnalyticsStore = {
isLoading: true,
hasError: false,
isHelpDismissed: $.cookie(COOKIE_NAME),
analytics: {}
};
gl.CycleAnalytics = class CycleAnalytics {
constructor() {
const that = this;
this.isHelpDismissed = $.cookie(COOKIE_NAME);
this.vue = new Vue({
el: '#cycle-analytics',
name: 'CycleAnalytics',
created: this.fetchData(),
data: this.decorateData({ isLoading: true }),
data: store,
methods: {
dismissLanding() {
that.dismissLanding();
......@@ -21,6 +26,7 @@
}
fetchData(options) {
store.isLoading = true;
options = options || { startDate: 30 };
$.ajax({
......@@ -30,22 +36,20 @@
contentType: 'application/json',
data: { start_date: options.startDate }
}).done((data) => {
this.vue.$data = this.decorateData(data);
this.decorateData(data);
this.initDropdown();
})
.error((data) => {
this.handleError(data);
})
.always(() => {
this.vue.isLoading = false;
store.isLoading = false;
})
}
decorateData(data) {
data.summary = data.summary || [];
data.stats = data.stats || [];
data.isHelpDismissed = this.isHelpDismissed;
data.isLoading = data.isLoading || false;
data.summary.forEach((item) => {
item.value = item.value || '-';
......@@ -53,23 +57,21 @@
data.stats.forEach((item) => {
item.value = item.value || '- - -';
})
});
return data;
store.analytics = data;
}
handleError(data) {
this.vue.$data = {
hasError: true,
isHelpDismissed: this.isHelpDismissed
};
store.hasError = true;
new Flash('There was an error while fetching cycle analytics data.', 'alert');
}
dismissLanding() {
this.vue.isHelpDismissed = true;
$.cookie(COOKIE_NAME, true);
store.isHelpDismissed = true;
$.cookie(COOKIE_NAME, true, {
path: gon.relative_url_root || '/'
});
}
initDropdown() {
......@@ -82,7 +84,6 @@
const value = $target.data('value');
$label.text($target.text().trim());
this.vue.isLoading = true;
this.fetchData({ startDate: value });
})
}
......
((w) => {
w.ResolveBtn = Vue.extend({
mixins: [
ButtonMixins
],
props: {
noteId: Number,
discussionId: String,
resolved: Boolean,
namespacePath: String,
projectPath: String,
canResolve: Boolean,
resolvedBy: String
......@@ -69,10 +65,10 @@
if (this.isResolved) {
promise = ResolveService
.unresolve(this.namespace, this.noteId);
.unresolve(this.projectPath, this.noteId);
} else {
promise = ResolveService
.resolve(this.namespace, this.noteId);
.resolve(this.projectPath, this.noteId);
}
promise.then((response) => {
......
((w) => {
w.ResolveDiscussionBtn = Vue.extend({
mixins: [
ButtonMixins
],
props: {
discussionId: String,
mergeRequestId: Number,
namespacePath: String,
projectPath: String,
canResolve: Boolean,
},
......@@ -50,7 +46,7 @@
},
methods: {
resolve: function () {
ResolveService.toggleResolveForDiscussion(this.namespace, this.mergeRequestId, this.discussionId);
ResolveService.toggleResolveForDiscussion(this.projectPath, this.mergeRequestId, this.discussionId);
}
},
created: function () {
......
((w) => {
w.ButtonMixins = {
computed: {
namespace: function () {
return `${this.namespacePath}/${this.projectPath}`;
}
}
};
})(window);
......@@ -9,32 +9,32 @@
Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken();
}
prepareRequest(namespace) {
prepareRequest(root) {
this.setCSRF();
Vue.http.options.root = `/${namespace}`;
Vue.http.options.root = root;
}
resolve(namespace, noteId) {
this.prepareRequest(namespace);
resolve(projectPath, noteId) {
this.prepareRequest(projectPath);
return this.noteResource.save({ noteId }, {});
}
unresolve(namespace, noteId) {
this.prepareRequest(namespace);
unresolve(projectPath, noteId) {
this.prepareRequest(projectPath);
return this.noteResource.delete({ noteId }, {});
}
toggleResolveForDiscussion(namespace, mergeRequestId, discussionId) {
toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId],
isResolved = discussion.isResolved();
let promise;
if (isResolved) {
promise = this.unResolveAll(namespace, mergeRequestId, discussionId);
promise = this.unResolveAll(projectPath, mergeRequestId, discussionId);
} else {
promise = this.resolveAll(namespace, mergeRequestId, discussionId);
promise = this.resolveAll(projectPath, mergeRequestId, discussionId);
}
promise.then((response) => {
......@@ -57,10 +57,10 @@
})
}
resolveAll(namespace, mergeRequestId, discussionId) {
resolveAll(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
this.prepareRequest(namespace);
this.prepareRequest(projectPath);
discussion.loading = true;
......@@ -70,10 +70,10 @@
}, {});
}
unResolveAll(namespace, mergeRequestId, discussionId) {
unResolveAll(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
this.prepareRequest(namespace);
this.prepareRequest(projectPath);
discussion.loading = true;
......
......@@ -608,27 +608,28 @@
}
}
field = [];
fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName(selectedObject) : this.options.fieldName;
value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
if (isInput) {
field = $(this.el);
} else if(value) {
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']");
}
if (field.length && el.hasClass(ACTIVE_CLASS)) {
if (el.hasClass(ACTIVE_CLASS)) {
el.removeClass(ACTIVE_CLASS);
if (field && field.length) {
if (isInput) {
field.val('');
} else {
field.remove();
}
}
} else if (el.hasClass(INDETERMINATE_CLASS)) {
el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS);
if (field.length && value == null) {
if (field && field.length && value == null) {
field.remove();
}
if (!field.length && fieldName) {
if ((!field || !field.length) && fieldName) {
this.addInput(fieldName, value, selectedObject);
}
} else {
......@@ -638,15 +639,15 @@
this.dropdown.parent().find("input[name='" + fieldName + "']").remove();
}
}
if (field.length && value == null) {
if (field && field.length && value == null) {
field.remove();
}
// Toggle active class for the tick mark
el.addClass(ACTIVE_CLASS);
if (value != null) {
if (!field.length && fieldName) {
if ((!field || !field.length) && fieldName) {
this.addInput(fieldName, value, selectedObject);
} else if (field.length) {
} else if (field && field.length) {
field.val(value).trigger('change');
}
}
......
......@@ -15,24 +15,31 @@
return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
},
initSearch: function() {
this.timer = null;
return $('#issuable_search').off('keyup').on('keyup', function() {
clearTimeout(this.timer);
return this.timer = setTimeout(function() {
var $form, $input, $search;
$search = $('#issuable_search');
$form = $('.js-filter-form');
$input = $("input[name='" + ($search.attr('name')) + "']", $form);
if ($input.length === 0) {
$form.append("<input type='hidden' name='" + ($search.attr('name')) + "' value='" + (_.escape($search.val())) + "'/>");
// `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
const debouncedExecSearch = _.debounce(Issuable.executeSearch, 500, false);
$('#issuable_search').off('keyup').on('keyup', debouncedExecSearch);
// ensures existing filters are preserved when manually submitted
$('#issue_search_form').on('submit', (e) => {
e.preventDefault();
debouncedExecSearch(e);
});
},
executeSearch: function(e) {
const $search = $('#issuable_search');
const $searchName = $search.attr('name');
const $searchValue = $search.val();
const $filtersForm = $('.js-filter-form');
const $input = $(`input[name='${$searchName}']`, $filtersForm);
if (!$input.length) {
$filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`);
} else {
$input.val($search.val());
}
if ($search.val() !== '') {
return Issuable.filterResults($form);
$input.val($searchValue);
}
}, 500);
});
Issuable.filterResults($filtersForm);
},
initLabelFilterRemove: function() {
return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
......
......@@ -4,7 +4,7 @@
var _this;
_this = this;
$('.js-label-select').each(function(i, dropdown) {
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip;
var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
labelUrl = $dropdown.data('labels');
......@@ -24,6 +24,11 @@
$sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
initialSelected = $selectbox
.find('input[name="' + $dropdown.data('field-name') + '"]')
.map(function () {
return this.value;
}).get();
if (issueUpdateURL != null) {
issueURLSplit = issueUpdateURL.split('/');
}
......@@ -43,6 +48,10 @@
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() {
return this.value;
}).get();
if (_.isEqual(initialSelected, selected)) return;
initialSelected = selected;
data = {};
data[abilityName] = {};
data[abilityName].label_ids = selected;
......@@ -280,12 +289,12 @@
if (page === 'projects:boards:show') {
if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
} else if (label.title) {
} else if ($el.hasClass('is-active')) {
gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title);
} else {
var filters = gl.issueBoards.BoardsStore.state.filters['label_name'];
filters = filters.filter(function (label) {
return label !== $el.text().trim();
filters = filters.filter(function (filteredLabel) {
return filteredLabel !== label.title;
});
gl.issueBoards.BoardsStore.state.filters['label_name'] = filters;
}
......
......@@ -19,7 +19,7 @@
while (i < sURLVariables.length) {
sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] === sParam) {
values.push(sParameterName[1]);
values.push(sParameterName[1].replace(/\+/g, ' '));
}
i++;
}
......
......@@ -432,14 +432,12 @@
var $form = $(xhr.target);
if ($form.attr('data-resolve-all') != null) {
var namespacePath = $form.attr('data-namespace-path'),
projectPath = $form.attr('data-project-path')
discussionId = $form.attr('data-discussion-id'),
mergeRequestId = $form.attr('data-noteable-iid'),
namespace = namespacePath + '/' + projectPath;
var projectPath = $form.data('project-path')
discussionId = $form.data('discussion-id'),
mergeRequestId = $form.data('noteable-iid');
if (ResolveService != null) {
ResolveService.toggleResolveForDiscussion(namespace, mergeRequestId, discussionId);
ResolveService.toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId);
}
}
......@@ -854,7 +852,6 @@
.closest('form')
.attr('data-discussion-id', discussionId)
.attr('data-resolve-all', 'true')
.attr('data-namespace-path', $this.attr('data-namespace-path'))
.attr('data-project-path', $this.attr('data-project-path'));
};
......
......@@ -7,7 +7,6 @@
function ProjectFindFile(element1, options) {
this.element = element1;
this.options = options;
this.goToBlob = bind(this.goToBlob, this);
this.goToTree = bind(this.goToTree, this);
this.selectRowDown = bind(this.selectRowDown, this);
this.selectRowUp = bind(this.selectRowUp, this);
......@@ -36,16 +35,6 @@
}
};
})(this));
return this.element.find(".tree-content-holder .tree-table").on("click", function(event) {
var path;
if (event.target.nodeName !== "A") {
path = this.element.find(".tree-item-file-name a", this).attr("href");
if (path) {
return location.href = path;
}
}
});
// init event
};
ProjectFindFile.prototype.findFile = function() {
......@@ -121,11 +110,12 @@
// make tbody row html
ProjectFindFile.prototype.makeHtml = function(filePath, matches, blobItemUrl) {
var $tr;
$tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>");
$tr = $("<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>");
if (matches) {
$tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl));
} else {
$tr.find("a").attr("href", blobItemUrl).text(filePath);
$tr.find("a").attr("href", blobItemUrl);
$tr.find(".str-truncated").text(filePath);
}
return $tr;
};
......@@ -164,14 +154,6 @@
return location.href = this.options.treeUrl;
};
ProjectFindFile.prototype.goToBlob = function() {
var path;
path = this.element.find(".tree-item.selected .tree-item-file-name a").attr("href");
if (path) {
return location.href = path;
}
};
return ProjectFindFile;
})();
......
......@@ -40,7 +40,6 @@
dataType: 'json',
data: {
_method: 'PATCH',
id: this.$wrap.data('banchId'),
protected_branch: {
merge_access_levels_attributes: [{
id: this.$allowedToMergeDropdown.data('access-level-id'),
......
......@@ -389,4 +389,41 @@
})();
$(function() {
var $projectOptionsDataEl = $('.js-search-project-options');
var $groupOptionsDataEl = $('.js-search-group-options');
var $dashboardOptionsDataEl = $('.js-search-dashboard-options');
if ($projectOptionsDataEl.length) {
gl.projectOptions = gl.projectOptions || {};
var projectPath = $projectOptionsDataEl.data('project-path');
gl.projectOptions[projectPath] = {
name: $projectOptionsDataEl.data('name'),
issuesPath: $projectOptionsDataEl.data('issues-path'),
mrPath: $projectOptionsDataEl.data('mr-path')
};
}
if ($groupOptionsDataEl.length) {
gl.groupOptions = gl.groupOptions || {};
var groupPath = $groupOptionsDataEl.data('group-path');
gl.groupOptions[groupPath] = {
name: $groupOptionsDataEl.data('name'),
issuesPath: $groupOptionsDataEl.data('issues-path'),
mrPath: $groupOptionsDataEl.data('mr-path')
};
}
if ($dashboardOptionsDataEl.length) {
gl.dashboardOptions = {
issuesPath: $dashboardOptionsDataEl.data('issues-path'),
mrPath: $dashboardOptionsDataEl.data('mr-path')
};
}
});
}).call(this);
......@@ -10,12 +10,13 @@
ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>';
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. Click to expand it.</div>';
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
function SingleFileDiff(file) {
this.file = file;
this.toggleDiff = bind(this.toggleDiff, this);
this.content = $('.diff-content', this.file);
this.$toggleIcon = $('.diff-toggle-caret', this.file);
this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path');
this.isOpen = !this.diffForPath;
if (this.diffForPath) {
......@@ -23,18 +24,22 @@
this.loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide();
this.content = null;
this.collapsedContent.after(this.loadingContent);
this.$toggleIcon.addClass('fa-caret-right');
} else {
this.collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide();
this.content.after(this.collapsedContent);
this.$toggleIcon.addClass('fa-caret-down');
}
this.collapsedContent.on('click', this.toggleDiff);
$('.file-title > a', this.file).on('click', this.toggleDiff);
$('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff);
}
SingleFileDiff.prototype.toggleDiff = function(e) {
var $target = $(e.target);
if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) {
this.content.hide();
this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down');
this.collapsedContent.show();
if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents();
......@@ -42,10 +47,12 @@
} else if (this.content) {
this.collapsedContent.hide();
this.content.show();
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents();
}
} else {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
return this.getContentHTML();
}
};
......
......@@ -19,10 +19,8 @@
&.diff-collapsed {
padding: 5px;
.click-to-expand {
cursor: pointer;
&:hover {
background-color: $row-hover;
}
}
}
......@@ -129,8 +127,6 @@
position: relative;
.avatar-holder {
margin-bottom: 16px;
.avatar, .identicon {
margin: 0 auto;
float: none;
......@@ -143,13 +139,7 @@
.cover-title {
color: $gl-header-color;
margin: 0;
font-size: 24px;
font-weight: normal;
margin-bottom: 10px;
color: #4c4e54;
font-size: 23px;
line-height: 1.1;
h1 {
color: $gl-gray-dark;
......@@ -213,6 +203,9 @@
}
}
}
&.user-cover-block {
padding: 24px 0 0;
}
.group-info {
......
......@@ -336,3 +336,9 @@
box-shadow: inset 0 0 0 white;
}
}
@media (max-width: $screen-xs-max) {
.btn-wide-on-xs {
width: 100%;
}
}
......@@ -26,6 +26,15 @@
padding: 10px $gl-padding;
word-wrap: break-word;
border-radius: 3px 3px 0 0;
cursor: pointer;
&:hover {
background-color: $dark-background-color;
}
.diff-toggle-caret {
padding-right: 6px;
}
&.file-title-clear {
padding-left: 0;
......
......@@ -3,7 +3,8 @@
margin: 0;
margin-bottom: $gl-padding;
font-size: 14px;
z-index: 100;
position: relative;
z-index: 1;
.flash-notice {
@extend .alert;
......@@ -34,6 +35,12 @@
}
}
.content-wrapper {
.flash-notice .container-fluid {
background-color: transparent;
}
}
@media (max-width: $screen-md-min) {
ul.notes {
.flash-container.timeline-content {
......@@ -41,4 +48,3 @@
}
}
}
......@@ -112,11 +112,15 @@ header {
.header-logo {
position: absolute;
left: 50%;
margin-left: -18px;
top: 7px;
transition-duration: .3s;
z-index: 999;
#logo {
position: relative;
left: -50%;
}
svg, img {
height: 36px;
}
......@@ -126,8 +130,12 @@ header {
}
@media (max-width: $screen-xs-max) {
right: 25px;
right: 20px;
left: auto;
#logo {
left: auto;
}
}
}
......
......@@ -164,7 +164,7 @@ ul.content-list {
}
.no-comments {
opacity: 0.5;
opacity: .5;
}
}
......
......@@ -99,8 +99,7 @@
.top-area {
@include clearfix;
border-bottom: 1px solid #eee;
border-bottom: 1px solid $btn-gray-hover;
.nav-text {
padding-top: 16px;
......
......@@ -142,6 +142,7 @@
transition-duration: .3s;
position: absolute;
top: 0;
cursor: pointer;
&:hover,
&:focus {
......
......@@ -204,7 +204,7 @@ body {
}
h1, h2, h3, h4, h5, h6 {
color: $gl-header-color;
color: $gl-title-color;
font-weight: 600;
}
......
......@@ -102,7 +102,7 @@ $gl-grayish-blue: #7f8fa4;
$gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
$gl-gray-light: $gl-placeholder-color;
$gl-header-color: $gl-title-color;
$gl-header-color: #4c4e54;
/*
* Lists
......@@ -269,6 +269,12 @@ $calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1);
$calendar-unselectable-bg: $gray-light;
/*
* Cycle Analytics
*/
$cycle-analytics-box-padding: 30px;
$cycle-analytics-box-text-color: #8c8c8c;
/*
* Personal Access Tokens
*/
......
......@@ -197,6 +197,7 @@ lex
a {
color: inherit;
word-wrap: break-word;
}
}
......
......@@ -107,6 +107,14 @@
.block {
width: 100%;
&.coverage {
padding: 0 16px 11px;
}
.btn-group-justified {
margin-top: 5px;
}
}
.js-build-variable {
......@@ -210,6 +218,9 @@
.build-detail-row {
margin-bottom: 5px;
&:last-of-type {
margin-bottom: 0;
}
}
.build-light-text {
......
#cycle-analytics {
margin: 24px auto 0;
width: 800px;
max-width: 800px;
position: relative;
.panel {
......@@ -9,11 +9,19 @@
padding: 24px 0;
border-bottom: none;
position: relative;
@media (max-width: $screen-sm-min) {
padding: 6px 0 24px;
}
}
.column {
text-align: center;
@media (max-width: $screen-sm-min) {
padding: 15px 0;
}
.header {
font-size: 30px;
line-height: 38px;
......@@ -28,11 +36,14 @@
&:last-child {
text-align: right;
@media (max-width: $screen-sm-min) {
text-align: center;
}
}
}
.dropdown {
position: relative;
top: 13px;
}
}
......@@ -40,7 +51,7 @@
.bordered-box {
border: 1px solid $border-color;
@include border-radius($border-radius-default);
position: relative;
}
.content-list {
......@@ -60,9 +71,15 @@
line-height: 19px;
font-size: 15px;
font-weight: 600;
color: $gl-title-color;
}
&.text {
color: $layout-link-gray;
&.value-col {
color: $gl-title-color;
}
&:text {
color: #8c8c8c;
}
}
}
......@@ -71,7 +88,9 @@
text-align: right;
span {
line-height: 42px;
position: relative;
vertical-align: middle;
top: 3px;
}
}
}
......@@ -82,21 +101,25 @@
.dismiss-icon {
position: absolute;
right: $gl-padding;
right: $cycle-analytics-box-padding;
cursor: pointer;
color: #b2b2b2;
}
.svg-container {
text-align: center;
svg {
margin: 0 20px;
float: left;
width: 136px;
height: 136px;
}
}
.inner-content {
width: 480px;
float: left;
@media (max-width: $screen-sm-min) {
padding: 0 28px;
text-align: center;
}
h4 {
color: $gl-text-color;
......@@ -104,7 +127,7 @@
}
p {
color: #8c8c8c;
color: $cycle-analytics-box-text-color;
margin-bottom: $gl-padding;
}
}
......
......@@ -59,6 +59,7 @@
}
.encoding-selector,
.soft-wrap-toggle,
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector {
......@@ -67,6 +68,24 @@
font-family: $regular_font;
}
.soft-wrap-toggle {
margin: 0 $btn-side-margin;
.soft-wrap {
display: block;
}
.no-wrap {
display: none;
}
&.soft-wrap-active {
.soft-wrap {
display: none;
}
.no-wrap {
display: block;
}
}
}
.gitignore-selector, .license-selector, .gitlab-ci-yml-selector {
.dropdown {
line-height: 21px;
......
......@@ -55,3 +55,50 @@
}
}
}
.groups-header {
@media (min-width: $screen-sm-min) {
.nav-links {
width: 35%;
}
.nav-controls {
width: 65%;
}
}
}
.groups-empty-state {
padding: 50px 100px;
overflow: hidden;
@media (max-width: $screen-md-min) {
padding: 50px 0;
}
svg {
float: right;
@media (max-width: $screen-md-min) {
float: none;
display: block;
width: 250px;
position: relative;
left: 50%;
margin-left: -125px;
}
}
.text-content {
float: left;
width: 460px;
margin-top: 120px;
@media (max-width: $screen-md-min) {
float: none;
margin-top: 60px;
width: auto;
text-align: center;
}
}
}
......@@ -2,7 +2,10 @@
max-width: 90%;
}
li.milestone {
.milestones {
.milestone {
padding: 10px 16px;
h4 {
font-weight: bold;
}
......@@ -10,6 +13,7 @@ li.milestone {
.progress {
height: 6px;
}
}
}
.milestone-content {
......@@ -29,6 +33,7 @@ li.milestone {
// Issue title
span a {
color: $gl-text-color;
word-wrap: break-word;
}
}
}
......@@ -64,3 +69,14 @@ li.milestone {
border-bottom: 1px solid $border-color;
padding: 20px 0;
}
@media (max-width: $screen-sm-min) {
.milestone-actions {
@include clearfix();
padding-top: $gl-vert-padding;
.btn:first-child {
margin-left: 0;
}
}
}
......@@ -177,6 +177,10 @@
border-bottom: 2px solid $border-color;
}
}
a {
display: block;
}
}
}
......
......@@ -93,8 +93,9 @@
.profile-user-bio {
// Limits the width of the user bio for readability.
max-width: 750px;
margin: auto;
max-width: 600px;
margin: 15px auto 0;
padding: 0 16px;
}
.user-avatar-button {
......@@ -212,6 +213,28 @@
}
.user-profile {
.cover-controls a {
margin-left: 5px;
}
.profile-header {
margin: 0 auto;
.avatar-holder {
width: 90px;
display: inline-block;
}
.user-info {
display: inline-block;
text-align: left;
vertical-align: middle;
margin-left: 15px;
.handle {
color: $gl-gray-light;
}
.member-date {
margin-bottom: 4px;
}
}
}
@media (max-width: $screen-xs-max) {
.cover-block {
padding-top: 20px;
......@@ -219,14 +242,24 @@
.cover-controls {
position: static;
padding: 0 16px;
margin-bottom: 20px;
display: -webkit-flex;
display: flex;
.btn {
display: inline-block;
width: 46%;
-webkit-flex-grow: 1;
flex-grow: 1;
&:first-child {
margin-left: 0;
}
}
}
}
}
.user-profile-nav {
margin-top: 15px;
}
table.u2f-registrations {
......
......@@ -743,6 +743,62 @@ pre.light-well {
.dropdown-menu {
width: 300px;
}
&.from .compare-dropdown-toggle {
width: 237px;
}
&.to .compare-dropdown-toggle {
width: 254px;
}
.dropdown-toggle-text {
display: block;
height: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
}
.compare-ellipsis {
display: inline;
}
@media (max-width: $screen-xs-max) {
.compare-form-group {
.input-group {
width: 100%;
& > .compare-dropdown-toggle {
width: 100%;
}
}
.dropdown-menu {
width: 100%;
}
}
.compare-switch-container {
text-align: center;
padding: 0 0 $gl-padding;
.commits-compare-switch {
float: none;
}
}
.compare-ellipsis {
display: block;
text-align: center;
padding: 0 0 $gl-padding;
}
.commits-compare-btn {
width: 100%;
}
}
.clearable-input {
......
......@@ -103,7 +103,7 @@
// Custom dropdown positioning
.dropdown-menu {
top: 30px;
top: 37px;
left: -5px;
padding: 0;
......
......@@ -12,11 +12,18 @@
.snippet-file-content {
border-radius: 3px;
margin-bottom: $gl-padding;
.btn-clipboard {
@extend .btn;
}
}
.project-snippets .awards {
border-bottom: 1px solid $table-border-color;
padding-bottom: $gl-padding;
}
.snippet-title {
font-size: 24px;
font-weight: 600;
......@@ -29,3 +36,7 @@
float: right;
}
}
.snippet-scope-menu .btn-new {
margin-top: 15px;
}
......@@ -27,7 +27,12 @@
}
.last-commit {
@include str-truncated(60%);
@include str-truncated(506px);
@media (min-width: $screen-sm-max) and (max-width: $screen-md-max) {
@include str-truncated(450px);
}
}
.commit-history-link-spacer {
......@@ -55,6 +60,15 @@
}
.tree-item {
.link-container {
padding: 0;
a {
padding: 10px $gl-padding;
display: block;
}
}
.tree-item-file-name {
max-width: 320px;
vertical-align: middle;
......
......@@ -10,7 +10,7 @@ class Admin::GroupsController < Admin::ApplicationController
def show
@members = @group.members.order("access_level DESC").page(params[:members_page])
@requesters = @group.requesters
@requesters = AccessRequestsFinder.new(@group).execute(current_user)
@projects = @group.projects.page(params[:projects_page])
end
......
......@@ -22,7 +22,7 @@ class Admin::ProjectsController < Admin::ApplicationController
end
@project_members = @project.members.page(params[:project_members_page])
@requesters = @project.requesters
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
end
def transfer
......
......@@ -14,6 +14,7 @@ module Ci
@config_processor = Ci::GitlabCiYamlProcessor.new(@content)
@stages = @config_processor.stages
@builds = @config_processor.builds
@jobs = @config_processor.jobs
end
rescue
@error = 'Undefined error'
......
......@@ -13,18 +13,10 @@ module IssuableCollections
issues_finder.execute
end
def all_issues_collection
IssuesFinder.new(current_user, filter_params_all).execute
end
def merge_requests_collection
merge_requests_finder.execute
end
def all_merge_requests_collection
MergeRequestsFinder.new(current_user, filter_params_all).execute
end
def issues_finder
@issues_finder ||= issuable_finder_for(IssuesFinder)
end
......@@ -62,10 +54,6 @@ module IssuableCollections
@filter_params
end
def filter_params_all
@filter_params_all ||= filter_params.merge(state: 'all', sort: nil)
end
def set_default_scope
params[:scope] = 'all' if params[:scope].blank?
end
......
......@@ -10,8 +10,6 @@ module IssuesAction
.preload(:author, :project)
.page(params[:page])
@all_issues = all_issues_collection.non_archived
respond_to do |format|
format.html
format.atom { render layout: false }
......
module MembershipActions
extend ActiveSupport::Concern
include MembersHelper
def request_access
membershipable.request_access(current_user)
......@@ -10,11 +9,7 @@ module MembershipActions
end
def approve_access_request
@member = membershipable.requesters.find(params[:id])
return render_403 unless can?(current_user, action_member_permission(:update, @member), @member)
@member.accept_request
Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
redirect_to polymorphic_url([membershipable, :members])
end
......
......@@ -9,7 +9,5 @@ module MergeRequestsAction
.non_archived
.preload(:author, :target_project)
.page(params[:page])
@all_merge_requests = all_merge_requests_collection.non_archived
end
end
......@@ -7,7 +7,7 @@ module SpammableActions
def mark_as_spam
if SpamService.new(spammable).mark_as_spam!
redirect_to spammable, notice: "#{spammable.class.to_s} was submitted to Akismet successfully."
redirect_to spammable, notice: "#{spammable.class} was submitted to Akismet successfully."
else
redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
end
......
......@@ -10,7 +10,9 @@ module ToggleAwardEmoji
if awardable.user_can_award?(current_user, name)
awardable.toggle_award_emoji(name, current_user)
TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
todoable = to_todoable(awardable)
TodoService.new.new_award_emoji(todoable, current_user) if todoable
render json: { ok: true }
else
......@@ -24,8 +26,10 @@ module ToggleAwardEmoji
case awardable
when Note
awardable.noteable
else
when MergeRequest, Issue
awardable
when Snippet
nil
end
end
......
......@@ -15,7 +15,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
@members = @members.order('access_level DESC').page(params[:page]).per(50)
@requesters = @group.requesters if can?(current_user, :admin_group, @group)
@requesters = AccessRequestsFinder.new(@group).execute(current_user)
@group_member = @group.group_members.new
end
......
class Import::GitlabProjectsController < Import::BaseController
before_action :verify_gitlab_project_import_enabled
before_action :authenticate_admin!
def new
@namespace_id = project_params[:namespace_id]
......@@ -48,8 +47,4 @@ class Import::GitlabProjectsController < Import::BaseController
:path, :namespace_id, :file
)
end
def authenticate_admin!
render_404 unless current_user.is_admin?
end
end
......@@ -11,10 +11,8 @@ class JwtController < ApplicationController
service = SERVICES[params[:service]]
return head :not_found unless service
@authentication_result ||= Gitlab::Auth::Result.new
result = service.new(@authentication_result.project, @authentication_result.actor, auth_params).
execute(authentication_abilities: @authentication_result.authentication_abilities)
execute(authentication_abilities: @authentication_result.authentication_abilities || [])
render json: result, status: result[:http_status]
end
......@@ -22,10 +20,12 @@ class JwtController < ApplicationController
private
def authenticate_project_or_user
@authentication_result = Gitlab::Auth::Result.new
authenticate_with_http_basic do |login, password|
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
render_403 unless @authentication_result.success? &&
render_unauthorized unless @authentication_result.success? &&
(@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
end
rescue Gitlab::Auth::MissingPersonalTokenError
......@@ -33,10 +33,21 @@ class JwtController < ApplicationController
end
def render_missing_personal_token
render plain: "HTTP Basic: Access denied\n" \
render json: {
errors: [
{ code: 'UNAUTHORIZED',
message: "HTTP Basic: Access denied\n" \
"You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}",
status: 401
"You can generate one at #{profile_personal_access_tokens_url}" }
] }, status: 401
end
def render_unauthorized
render json: {
errors: [
{ code: 'UNAUTHORIZED',
message: 'HTTP Basic: Access denied' }
] }, status: 401
end
def auth_params
......
......@@ -73,7 +73,8 @@ class ProfilesController < Profiles::ApplicationController
:skype,
:twitter,
:username,
:website_url
:website_url,
:organization
)
end
end
......@@ -33,7 +33,7 @@ module Projects
def issue
@issue ||=
IssuesFinder.new(current_user, project_id: project.id, state: 'all')
IssuesFinder.new(current_user, project_id: project.id)
.execute
.where(iid: params[:id])
.first!
......
......@@ -10,10 +10,11 @@ class Projects::CommitController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :authorize_download_code!, except: [:cancel_builds, :retry_builds]
before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds]
before_action :authorize_read_pipeline!, only: [:pipelines]
before_action :authorize_read_commit_status!, only: [:builds]
before_action :commit
before_action :define_commit_vars, only: [:show, :diff_for_path, :builds]
before_action :define_status_vars, only: [:show, :builds]
before_action :define_commit_vars, only: [:show, :diff_for_path, :builds, :pipelines]
before_action :define_status_vars, only: [:show, :builds, :pipelines]
before_action :define_note_vars, only: [:show, :diff_for_path]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
......@@ -31,6 +32,9 @@ class Projects::CommitController < Projects::ApplicationController
render_diff_for_path(@commit.diffs(diff_options))
end
def pipelines
end
def builds
end
......@@ -96,10 +100,6 @@ class Projects::CommitController < Projects::ApplicationController
@noteable = @commit ||= @project.commit(params[:id])
end
def pipelines
@pipelines ||= project.pipelines.where(sha: commit.sha)
end
def ci_builds
@ci_builds ||= Ci::Build.where(pipeline: pipelines)
end
......@@ -134,8 +134,9 @@ class Projects::CommitController < Projects::ApplicationController
end
def define_status_vars
@statuses = CommitStatus.where(pipeline: pipelines).relevant
@builds = Ci::Build.where(pipeline: pipelines).relevant
@ci_pipelines = project.pipelines.where(sha: commit.sha)
@statuses = CommitStatus.where(pipeline: @ci_pipelines).relevant
@builds = Ci::Build.where(pipeline: @ci_pipelines).relevant
end
def assign_change_commit_vars(mr_source_branch)
......
......@@ -32,11 +32,11 @@ class Projects::GitHttpClientController < Projects::ApplicationController
return # Allow access
end
elsif allow_kerberos_spnego_auth? && spnego_provided?
user = find_kerberos_user
kerberos_user = find_kerberos_user
if user
if kerberos_user
@authentication_result = Gitlab::Auth::Result.new(
user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities)
kerberos_user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities)
send_final_spnego_response
return # Allow access
......
......@@ -28,8 +28,6 @@ class Projects::IssuesController < Projects::ApplicationController
@labels = @project.labels.where(title: params[:label_name])
@all_issues = all_issues_collection
respond_to do |format|
format.html
format.atom { render layout: false }
......@@ -56,7 +54,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def show
raw_notes = @issue.notes_with_associations.fresh
raw_notes = @issue.notes.inc_relations_for_view.fresh
@notes = Banzai::NoteRenderer.
render(raw_notes, @project, current_user, @path, @project_wiki, @ref)
......
......@@ -18,6 +18,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :define_commit_vars, only: [:diffs]
before_action :define_diff_comment_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines]
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
# Allow read any merge_request
before_action :authorize_read_merge_request!
......@@ -37,8 +38,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@labels = @project.labels.where(title: params[:label_name])
@all_merge_requests = all_merge_requests_collection
respond_to do |format|
format.html
format.json do
......@@ -277,7 +276,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def remove_wip
MergeRequests::UpdateService.new(project, current_user, title: @merge_request.wipless_title).execute(@merge_request)
MergeRequests::UpdateService.new(project, current_user, wip_event: 'unwip').execute(@merge_request)
redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request),
notice: "The merge request can now be merged."
......@@ -310,8 +309,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
return
end
TodoService.new.merge_merge_request(merge_request, current_user)
@merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds].present?
......@@ -420,10 +417,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def validates_merge_request
# If source project was removed and merge request for some reason
# wasn't close (Ex. mr from fork to origin)
return invalid_mr if !@merge_request.source_project && @merge_request.open?
# Show git not found page
# if there is no saved commits between source & target branch
if @merge_request.commits.blank?
......@@ -498,7 +491,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def invalid_mr
# Render special view for MR with removed source or target branch
# Render special view for MR with removed target branch
render 'invalid'
end
......@@ -540,4 +533,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@diff_notes_disabled = !@merge_request_diff.latest?
@diffs = @merge_request_diff.diffs(diff_options)
end
def close_merge_request_without_source_project
if !@merge_request.source_project && @merge_request.open?
@merge_request.close
end
end
end
......@@ -29,7 +29,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_members = @group_members.order('access_level DESC')
end
@requesters = @project.requesters if can?(current_user, :admin_project, @project)
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
@project_member = @project.project_members.new
@project_group_links = @project.project_group_links
......
class Projects::SnippetsController < Projects::ApplicationController
include ToggleAwardEmoji
before_action :module_enabled
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji]
# Allow read any snippet
before_action :authorize_read_project_snippet!, except: [:new, :create, :index]
......@@ -80,6 +82,7 @@ class Projects::SnippetsController < Projects::ApplicationController
def snippet
@snippet ||= @project.snippets.find(params[:id])
end
alias_method :awardable, :snippet
def authorize_read_project_snippet!
return render_404 unless can?(current_user, :read_project_snippet, @snippet)
......
......@@ -137,10 +137,10 @@ class ProjectsController < Projects::ApplicationController
noteable =
case params[:type]
when 'Issue'
IssuesFinder.new(current_user, project_id: @project.id, state: 'all').
IssuesFinder.new(current_user, project_id: @project.id).
execute.find_by(iid: params[:type_id])
when 'MergeRequest'
MergeRequestsFinder.new(current_user, project_id: @project.id, state: 'all').
MergeRequestsFinder.new(current_user, project_id: @project.id).
execute.find_by(iid: params[:type_id])
when 'Commit'
@project.commit(params[:type_id])
......@@ -324,7 +324,12 @@ class ProjectsController < Projects::ApplicationController
end
def repo_exists?
project.repository_exists? && !project.empty_repo?
project.repository_exists? && !project.empty_repo? && project.repo
rescue Gitlab::Git::Repository::NoRepository
project.repository.expire_exists_cache
false
end
def project_view_files?
......
......@@ -6,8 +6,6 @@ class SearchController < ApplicationController
layout 'search'
def show
return if params[:search].nil? || params[:search].blank?
if params[:project_id].present?
@project = Project.find_by(id: params[:project_id])
@project = nil unless can?(current_user, :download_code, @project)
......@@ -18,6 +16,8 @@ class SearchController < ApplicationController
@group = nil unless can?(current_user, :read_group, @group)
end
return if params[:search].nil? || params[:search].blank?
@search_term = params[:search]
@scope = params[:scope]
......
class SnippetsController < ApplicationController
include ToggleAwardEmoji
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet
......@@ -85,6 +87,7 @@ class SnippetsController < ApplicationController
PersonalSnippet.find(params[:id])
end
end
alias_method :awardable, :snippet
def authorize_read_snippet!
authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
......
......@@ -65,7 +65,7 @@ class UsersController < ApplicationController
format.html { render 'show' }
format.json do
render json: {
html: view_to_html_string("snippets/_snippets", collection: @snippets)
html: view_to_html_string("snippets/_snippets", collection: @snippets, remote: true)
}
end
end
......
class AccessRequestsFinder
attr_accessor :source
# Arguments:
# source - a Group or Project
def initialize(source)
@source = source
end
def execute(*args)
execute!(*args)
rescue Gitlab::Access::AccessDeniedError
[]
end
def execute!(current_user)
raise Gitlab::Access::AccessDeniedError unless can_see_access_requests?(current_user)
source.requesters
end
private
def can_see_access_requests?(current_user)
source && Ability.allowed?(current_user, :"admin_#{source.class.to_s.underscore}", source)
end
end
......@@ -183,17 +183,12 @@ class IssuableFinder
end
def by_state(items)
case params[:state]
when 'closed'
items.closed
when 'merged'
items.respond_to?(:merged) ? items.merged : items.closed
when 'all'
items
when 'opened'
items.opened
params[:state] ||= 'all'
if items.respond_to?(params[:state])
items.public_send(params[:state])
else
raise 'You must specify default state'
items
end
end
......
......@@ -280,23 +280,6 @@ module ApplicationHelper
end
end
def state_filters_text_for(state, records)
titles = {
opened: "Open"
}
state_title = titles[state] || state.to_s.humanize
count = records.public_send(state).size
html = content_tag :span, state_title
if count.present?
html += " "
html += content_tag :span, number_with_delimiter(count), class: 'badge'
end
html.html_safe
end
def truncate_first_line(message, length = 50)
truncate(message.each_line.first.chomp, length: length) if message
end
......
module AwardEmojiHelper
def toggle_award_url(awardable)
if @project
url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
else
url_for([:toggle_award_emoji, awardable])
end
end
end
......@@ -56,7 +56,7 @@ module CiStatusHelper
def render_commit_status(commit, tooltip_placement: 'auto left')
project = commit.project
path = builds_namespace_project_commit_path(project.namespace, project, commit)
path = pipelines_namespace_project_commit_path(project.namespace, project, commit)
render_status_with_link('commit', commit.status, path, tooltip_placement: tooltip_placement)
end
......
......@@ -70,6 +70,10 @@ module GitlabRoutingHelper
namespace_project_runner_path(@project.namespace, @project, runner, *args)
end
def environment_path(environment, *args)
namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args)
end
def issue_path(entity, *args)
namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args)
end
......@@ -102,6 +106,14 @@ module GitlabRoutingHelper
end
end
def toggle_award_emoji_personal_snippet_path(*args)
toggle_award_emoji_snippet_path(*args)
end
def toggle_award_emoji_namespace_project_project_snippet_path(*args)
toggle_award_emoji_namespace_project_snippet_path(*args)
end
## Members
def project_members_url(project, *args)
namespace_project_project_members_url(project.namespace, project)
......
......@@ -94,6 +94,24 @@ module IssuablesHelper
label_names.join(', ')
end
def issuables_state_counter_text(issuable_type, state)
titles = {
opened: "Open"
}
state_title = titles[state] || state.to_s.humanize
count =
Rails.cache.fetch(issuables_state_counter_cache_key(issuable_type, state), expires_in: 2.minutes) do
issuables_count_for_state(issuable_type, state)
end
html = content_tag(:span, state_title)
html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge')
html.html_safe
end
private
def sidebar_gutter_collapsed?
......@@ -111,4 +129,22 @@ module IssuablesHelper
issuable.open? ? :opened : :closed
end
end
def issuables_count_for_state(issuable_type, state)
issuables_finder = public_send("#{issuable_type}_finder")
issuables_finder.params[:state] = state
issuables_finder.execute.page(1).total_count
end
IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page]
private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY
def issuables_state_counter_cache_key(issuable_type, state)
opts = params.with_indifferent_access
opts[:state] = state
opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-'))
end
end
module LfsHelper
include Gitlab::Routing.url_helpers
def require_lfs_enabled!
return if Gitlab.config.lfs.enabled
render(
json: {
message: 'Git LFS is not enabled on this GitLab server, contact your admin.',
documentation_url: "#{Gitlab.config.gitlab.url}/help",
documentation_url: help_url,
},
status: 501
)
......@@ -46,7 +48,7 @@ module LfsHelper
render(
json: {
message: 'Access forbidden. Check your access level.',
documentation_url: "#{Gitlab.config.gitlab.url}/help",
documentation_url: help_url,
},
content_type: "application/vnd.git-lfs+json",
status: 403
......@@ -57,7 +59,7 @@ module LfsHelper
render(
json: {
message: 'Not found.',
documentation_url: "#{Gitlab.config.gitlab.url}/help",
documentation_url: help_url,
},
content_type: "application/vnd.git-lfs+json",
status: 404
......
......@@ -35,6 +35,30 @@ module MilestonesHelper
milestone.issues.with_label(label.title).send(state).size
end
# Returns count of milestones for different states
# Uses explicit hash keys as the 'opened' state URL params differs from the db value
# and we need to add the total
def milestone_counts(milestones)
counts = milestones.reorder(nil).group(:state).count
{
opened: counts['active'] || 0,
closed: counts['closed'] || 0,
all: counts.values.sum || 0
}
end
# Show 'active' class if provided GET param matches check
# `or_blank` allows the function to return 'active' when given an empty param
# Could be refactored to be simpler but that may make it harder to read
def milestone_class_for_state(param, check, match_blank_param = false)
if match_blank_param
'active' if param.blank? || param == check
else
'active' if param == check
end
end
def milestone_progress_bar(milestone)
options = {
class: 'progress-bar progress-bar-success',
......
......@@ -139,7 +139,7 @@ module ProjectsHelper
end
options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field))
content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control", data: { field: field }).html_safe
content_tag(:select, options, name: "project[project_feature_attributes][#{field}]", id: "project_project_feature_attributes_#{field}", class: "pull-right form-control", data: { field: field }).html_safe
end
private
......
......@@ -3,4 +3,12 @@ class DeviseMailer < Devise::Mailer
default reply_to: Gitlab.config.gitlab.email_reply_to
layout 'devise_mailer'
protected
def subject_for(key)
subject = super
subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present?
subject
end
end
......@@ -45,7 +45,7 @@ module Emails
@token = token
mail(to: member.invite_email,
subject: "Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}")
subject: subject("Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}"))
end
def member_invite_accepted_email(member_source_type, member_id)
......
......@@ -93,6 +93,7 @@ class Notify < BaseMailer
subject = ""
subject << "#{@project.name} | " if @project
subject << extra.join(' | ') if extra.present?
subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present?
subject
end
......@@ -110,7 +111,7 @@ class Notify < BaseMailer
headers['X-GitLab-Reply-Key'] = reply_key
if !@labels_url && @sent_notification && @sent_notification.unsubscribable?
headers['List-Unsubscribe'] = unsubscribe_sent_notification_url(@sent_notification, force: true)
headers['List-Unsubscribe'] = "<#{unsubscribe_sent_notification_url(@sent_notification, force: true)}>"
@sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
end
......
......@@ -4,4 +4,12 @@ class Board < ActiveRecord::Base
has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all
validates :project, presence: true
def backlog_list
lists.merge(List.backlog).take
end
def done_list
lists.merge(List.done).take
end
end
......@@ -91,7 +91,7 @@ module Ci
sha: build.sha,
ref: build.ref,
tag: build.tag,
options: build.options[:environment],
options: build.options.to_h[:environment],
variables: build.variables)
service.execute(build)
end
......@@ -378,7 +378,7 @@ module Ci
end
def artifacts?
!artifacts_expired? && self[:artifacts_file].present?
!artifacts_expired? && artifacts_file.exists?
end
def artifacts_metadata?
......@@ -498,8 +498,11 @@ module Ci
end
def hide_secrets(trace)
trace = Ci::MaskSecret.mask(trace, project.runners_token) if project
trace = Ci::MaskSecret.mask(trace, token)
return unless trace
trace = trace.dup
Ci::MaskSecret.mask!(trace, project.runners_token) if project
Ci::MaskSecret.mask!(trace, token)
trace
end
end
......
......@@ -80,7 +80,7 @@ class CommitRange
end
def inspect
%(#<#{self.class}:#{object_id} #{to_s}>)
%(#<#{self.class}:#{object_id} #{self}>)
end
def to_s
......
......@@ -8,9 +8,6 @@ module AccessRequestable
extend ActiveSupport::Concern
def request_access(user)
members.create(
access_level: Gitlab::Access::DEVELOPER,
user: user,
requested_at: Time.now.utc)
Members::RequestAccessService.new(self, user).execute
end
end
......@@ -71,6 +71,12 @@ module Awardable
end
end
def user_authored?(current_user)
author = self.respond_to?(:author) ? self.author : self.user
author == current_user
end
def awarded_emoji?(emoji_name, current_user)
award_emoji.where(name: emoji_name, user: current_user).exists?
end
......
......@@ -200,10 +200,6 @@ module Issuable
end
end
def user_authored?(user)
user == author
end
def subscribed_without_subscriptions?(user)
participants(user).include?(user)
end
......
......@@ -10,15 +10,33 @@ class CycleAnalytics
end
def commits
repository = @project.repository.raw_repository
if @project.default_branch
repository.log(ref: @project.default_branch, after: @from).count
end
ref = @project.default_branch.presence
count_commits_for(ref)
end
def deploys
@project.deployments.where("created_at > ?", @from).count
end
private
# Don't use the `Gitlab::Git::Repository#log` method, because it enforces
# a limit. Since we need a commit count, we _can't_ enforce a limit, so
# the easiest way forward is to replicate the relevant portions of the
# `log` function here.
def count_commits_for(ref)
return unless ref
repository = @project.repository.raw_repository
sha = @project.repository.commit(ref).sha
cmd = %W(git --git-dir=#{repository.path} log)
cmd << '--format=%H'
cmd << "--after=#{@from.iso8601}"
cmd << sha
raw_output = IO.popen(cmd) { |io| io.read }
raw_output.lines.count
end
end
end
......@@ -8,7 +8,8 @@ class GlobalMilestone
milestones = milestones.group_by(&:title)
milestones.map do |title, milestones|
new(title, milestones)
milestones_relation = Milestone.where(id: milestones.map(&:id))
new(title, milestones_relation)
end
end
......@@ -31,7 +32,7 @@ class GlobalMilestone
end
def projects
@projects ||= Project.for_milestones(milestones.map(&:id))
@projects ||= Project.for_milestones(milestones.select(:id))
end
def state
......@@ -53,19 +54,19 @@ class GlobalMilestone
end
def issues
@issues ||= Issue.of_milestones(milestones.map(&:id)).includes(:project)
@issues ||= Issue.of_milestones(milestones.select(:id)).includes(:project, :assignee, :labels)
end
def merge_requests
@merge_requests ||= MergeRequest.of_milestones(milestones.map(&:id)).includes(:target_project)
@merge_requests ||= MergeRequest.of_milestones(milestones.select(:id)).includes(:target_project, :assignee, :labels)
end
def participants
@participants ||= milestones.map(&:participants).flatten.compact.uniq
@participants ||= milestones.includes(:participants).map(&:participants).flatten.compact.uniq
end
def labels
@labels ||= GlobalLabel.build_collection(milestones.map(&:labels).flatten)
@labels ||= GlobalLabel.build_collection(milestones.includes(:labels).map(&:labels).flatten)
.sort_by!(&:title)
end
......
......@@ -102,40 +102,44 @@ class Group < Namespace
self[:lfs_enabled]
end
def add_users(user_ids, access_level, current_user: nil, expires_at: nil)
user_ids.each do |user_id|
Member.add_user(
self.group_members,
user_id,
def add_users(users, access_level, current_user: nil, expires_at: nil)
GroupMember.add_users_to_group(
self,
users,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
def add_user(user, access_level, current_user: nil, expires_at: nil)
add_users([user], access_level, current_user: current_user, expires_at: expires_at)
GroupMember.add_user(
self,
user,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
def add_guest(user, current_user = nil)
add_user(user, Gitlab::Access::GUEST, current_user: current_user)
add_user(user, :guest, current_user: current_user)
end
def add_reporter(user, current_user = nil)
add_user(user, Gitlab::Access::REPORTER, current_user: current_user)
add_user(user, :reporter, current_user: current_user)
end
def add_developer(user, current_user = nil)
add_user(user, Gitlab::Access::DEVELOPER, current_user: current_user)
add_user(user, :developer, current_user: current_user)
end
def add_master(user, current_user = nil)
add_user(user, Gitlab::Access::MASTER, current_user: current_user)
add_user(user, :master, current_user: current_user)
end
def add_owner(user, current_user = nil)
add_user(user, Gitlab::Access::OWNER, current_user: current_user)
add_user(user, :owner, current_user: current_user)
end
def has_owner?(user)
......
......@@ -80,49 +80,70 @@ class Member < ActiveRecord::Base
find_by(invite_token: invite_token)
end
# This method is used to find users that have been entered into the "Add members" field.
# These can be the User objects directly, their IDs, their emails, or new emails to be invited.
def user_for_id(user_id)
return user_id if user_id.is_a?(User)
user = User.find_by(id: user_id)
user ||= User.find_by(email: user_id)
user ||= user_id
user
end
def add_user(members, user_id, access_level, current_user: nil, expires_at: nil)
user = user_for_id(user_id)
def add_user(source, user, access_level, current_user: nil, expires_at: nil)
user = retrieve_user(user)
access_level = retrieve_access_level(access_level)
# `user` can be either a User object or an email to be invited
member =
if user.is_a?(User)
member = members.find_or_initialize_by(user_id: user.id)
source.members.find_by(user_id: user.id) ||
source.requesters.find_by(user_id: user.id) ||
source.members.build(user_id: user.id)
else
member = members.build
member.invite_email = user
source.members.build(invite_email: user)
end
if can_update_member?(current_user, member) || project_creator?(member, access_level)
member.created_by ||= current_user
member.access_level = access_level
member.expires_at = expires_at
return member unless can_update_member?(current_user, member)
member.attributes = {
created_by: member.created_by || current_user,
access_level: access_level,
expires_at: expires_at
}
if member.request?
::Members::ApproveAccessRequestService.new(source, current_user, id: member.id).execute
else
member.save
end
member
end
def access_levels
Gitlab::Access.sym_options
end
private
# This method is used to find users that have been entered into the "Add members" field.
# These can be the User objects directly, their IDs, their emails, or new emails to be invited.
def retrieve_user(user)
return user if user.is_a?(User)
User.find_by(id: user) || User.find_by(email: user) || user
end
def retrieve_access_level(access_level)
access_levels.fetch(access_level) { access_level.to_i }
end
def can_update_member?(current_user, member)
# There is no current user for bulk actions, in which case anything is allowed
!current_user ||
current_user.can?(:update_group_member, member) ||
current_user.can?(:update_project_member, member)
!current_user || current_user.can?(:"update_#{member.type.underscore}", member)
end
def project_creator?(member, access_level)
member.new_record? && member.owner? &&
access_level.to_i == ProjectMember::MASTER
def add_users_to_source(source, users, access_level, current_user: nil, expires_at: nil)
users.each do |user|
add_user(
source,
user,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
end
......
......@@ -12,6 +12,22 @@ class GroupMember < Member
Gitlab::Access.options_with_owner
end
def self.access_levels
Gitlab::Access.sym_options_with_owner
end
def self.add_users_to_group(group, users, access_level, current_user: nil, expires_at: nil)
self.transaction do
add_users_to_source(
group,
users,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
def group
source
end
......
......@@ -34,25 +34,14 @@ class ProjectMember < Member
# :master
# )
#
def add_users_to_projects(project_ids, user_ids, access, current_user: nil, expires_at: nil)
access_level = if roles_hash.has_key?(access)
roles_hash[access]
elsif roles_hash.values.include?(access.to_i)
access
else
raise "Non valid access"
end
users = user_ids.map { |user_id| Member.user_for_id(user_id) }
ProjectMember.transaction do
def add_users_to_projects(project_ids, users, access_level, current_user: nil, expires_at: nil)
self.transaction do
project_ids.each do |project_id|
project = Project.find(project_id)
users.each do |user|
Member.add_user(
project.project_members,
user,
add_users_to_source(
project,
users,
access_level,
current_user: current_user,
expires_at: expires_at
......@@ -61,11 +50,6 @@ class ProjectMember < Member
end
end
true
rescue
false
end
def truncate_teams(project_ids)
ProjectMember.transaction do
members = ProjectMember.where(source_id: project_ids)
......@@ -84,13 +68,15 @@ class ProjectMember < Member
truncate_teams [project.id]
end
def roles_hash
Gitlab::Access.sym_options
end
def access_level_roles
Gitlab::Access.options
end
private
def can_update_member?(current_user, member)
super || (member.owner? && member.new_record?)
end
end
def access_field
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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