Commit fe276d0d authored by Alfredo Sumaran's avatar Alfredo Sumaran

Merge branch 'master' into issue_14678

# Conflicts:
#	app/assets/javascripts/todos.js.coffee
parents 45c93b52 7998725e
...@@ -691,7 +691,7 @@ Style/ZeroLengthPredicate: ...@@ -691,7 +691,7 @@ Style/ZeroLengthPredicate:
# branches, and conditions. # branches, and conditions.
Metrics/AbcSize: Metrics/AbcSize:
Enabled: true Enabled: true
Max: 70 Max: 60
# Avoid excessive block nesting. # Avoid excessive block nesting.
Metrics/BlockNesting: Metrics/BlockNesting:
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.7.0 (unreleased) v 8.7.0 (unreleased)
- Enable gzip for assets, makes the page size significantly smaller. !3544 / !3632 (Connor Shea)
- Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea)
- All images in discussions and wikis now link to their source files !3464 (Connor Shea). - All images in discussions and wikis now link to their source files !3464 (Connor Shea).
- Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu) - Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu)
- Improved Markdown rendering performance !3389 (Yorick Peterse) - Improved Markdown rendering performance !3389 (Yorick Peterse)
...@@ -12,6 +14,7 @@ v 8.7.0 (unreleased) ...@@ -12,6 +14,7 @@ v 8.7.0 (unreleased)
- Allow back dating on issues when created through the API - Allow back dating on issues when created through the API
- Fix Error 500 after renaming a project path (Stan Hu) - Fix Error 500 after renaming a project path (Stan Hu)
- Fix avatar stretching by providing a cropping feature - Fix avatar stretching by providing a cropping feature
- API: Expose `subscribed` for issues and merge requests (Robert Schilling)
- Allow SAML to handle external users based on user's information !3530 - Allow SAML to handle external users based on user's information !3530
- Add endpoints to archive or unarchive a project !3372 - Add endpoints to archive or unarchive a project !3372
- Add links to CI setup documentation from project settings and builds pages - Add links to CI setup documentation from project settings and builds pages
...@@ -26,19 +29,24 @@ v 8.7.0 (unreleased) ...@@ -26,19 +29,24 @@ v 8.7.0 (unreleased)
- Fix creation of merge requests for orphaned branches (Stan Hu) - Fix creation of merge requests for orphaned branches (Stan Hu)
- Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla) - Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla)
- Remove "Congratulations!" tweet button on newly-created project. (Connor Shea) - Remove "Congratulations!" tweet button on newly-created project. (Connor Shea)
- Improved UX of the navigation sidebar
- Fix admin/projects when using visibility levels on search (PotHix) - Fix admin/projects when using visibility levels on search (PotHix)
- Build status notifications - Build status notifications
- API: Expose user location (Robert Schilling) - API: Expose user location (Robert Schilling)
- ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591 - ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591
- Update number of Todos in the sidebar when it's marked as "Done". !3600 - Update number of Todos in the sidebar when it's marked as "Done". !3600
v 8.6.5 (unreleased) v 8.6.5
- Only update repository language if it is not set to improve performance - Fix importing from GitHub Enterprise. !3529
- Check permissions when user attempts to import members from another project - Perform the language detection after updating merge requests in `GitPushService`, leading to faster visual feedback for the end-user. !3533
- Check permissions when user attempts to import members from another project. !3535
- Only update repository language if it is not set to improve performance. !3556
- Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu). !3583
- Unblock user when active_directory is disabled and it can be found !3550
- Fix a 2FA authentication spoofing vulnerability.
v 8.6.4 v 8.6.4
- Don't attempt to fetch any tags from a forked repo (Stan Hu) - Don't attempt to fetch any tags from a forked repo (Stan Hu)
- Redesign the Labels page
v 8.6.3 v 8.6.3
- Mentions on confidential issues doesn't create todos for non-members. !3374 - Mentions on confidential issues doesn't create todos for non-members. !3374
...@@ -155,6 +163,9 @@ v 8.6.0 ...@@ -155,6 +163,9 @@ v 8.6.0
- Trigger a todo for mentions on commits page - Trigger a todo for mentions on commits page
- Let project owners and admins soft delete issues and merge requests - Let project owners and admins soft delete issues and merge requests
v 8.5.10
- Fix a 2FA authentication spoofing vulnerability.
v 8.5.9 v 8.5.9
- Don't attempt to fetch any tags from a forked repo (Stan Hu). - Don't attempt to fetch any tags from a forked repo (Stan Hu).
...@@ -299,6 +310,9 @@ v 8.5.0 ...@@ -299,6 +310,9 @@ v 8.5.0
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul) - Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Add Todos - Add Todos
v 8.4.8
- Fix a 2FA authentication spoofing vulnerability.
v 8.4.7 v 8.4.7
- Don't attempt to fetch any tags from a forked repo (Stan Hu). - Don't attempt to fetch any tags from a forked repo (Stan Hu).
...@@ -418,6 +432,9 @@ v 8.4.0 ...@@ -418,6 +432,9 @@ v 8.4.0
- Add IP check against DNSBLs at account sign-up - Add IP check against DNSBLs at account sign-up
- Added cache:key to .gitlab-ci.yml allowing to fine tune the caching - Added cache:key to .gitlab-ci.yml allowing to fine tune the caching
v 8.3.7
- Fix a 2FA authentication spoofing vulnerability.
v 8.3.6 v 8.3.6
- Don't attempt to fetch any tags from a forked repo (Stan Hu). - Don't attempt to fetch any tags from a forked repo (Stan Hu).
......
...@@ -448,7 +448,7 @@ merge request: ...@@ -448,7 +448,7 @@ merge request:
- multi-line method chaining style **Option B**: dot `.` on previous line - multi-line method chaining style **Option B**: dot `.` on previous line
- string literal quoting style **Option A**: single quoted by default - string literal quoting style **Option A**: single quoted by default
1. [Rails](https://github.com/bbatsov/rails-style-guide) 1. [Rails](https://github.com/bbatsov/rails-style-guide)
1. [Testing](https://github.com/thoughtbot/guides/tree/master/style/testing) 1. [Testing](doc/development/testing.md)
1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript) 1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript)
1. [SCSS styleguide][scss-styleguide] 1. [SCSS styleguide][scss-styleguide]
1. [Shell commands](doc/development/shell_commands.md) created by GitLab 1. [Shell commands](doc/development/shell_commands.md) created by GitLab
......
source "https://rubygems.org" source "https://rubygems.org"
gem 'rails', '4.2.5.2' gem 'rails', '4.2.6'
gem 'rails-deprecated_sanitizer', '~> 1.0.3' gem 'rails-deprecated_sanitizer', '~> 1.0.3'
# Responders respond_to and respond_with # Responders respond_to and respond_with
...@@ -8,7 +8,7 @@ gem 'responders', '~> 2.0' ...@@ -8,7 +8,7 @@ gem 'responders', '~> 2.0'
# Specify a sprockets version due to increased performance # Specify a sprockets version due to increased performance
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/6069 # See https://gitlab.com/gitlab-org/gitlab-ce/issues/6069
gem 'sprockets', '~> 3.3.5' gem 'sprockets', '~> 3.6.0'
# Default values for AR models # Default values for AR models
gem "default_value_for", "~> 3.0.0" gem "default_value_for", "~> 3.0.0"
...@@ -149,6 +149,10 @@ gem 'version_sorter', '~> 2.0.0' ...@@ -149,6 +149,10 @@ gem 'version_sorter', '~> 2.0.0'
# Cache # Cache
gem "redis-rails", '~> 4.0.0' gem "redis-rails", '~> 4.0.0'
# Redis
gem 'redis', '~> 3.2'
gem 'connection_pool', '~> 2.0'
# Campfire integration # Campfire integration
gem 'tinder', '~> 1.10.0' gem 'tinder', '~> 1.10.0'
...@@ -229,14 +233,13 @@ group :metrics do ...@@ -229,14 +233,13 @@ group :metrics do
gem 'allocations', '~> 1.0', require: false, platform: :mri gem 'allocations', '~> 1.0', require: false, platform: :mri
gem 'method_source', '~> 0.8', require: false gem 'method_source', '~> 0.8', require: false
gem 'influxdb', '~> 0.2', require: false gem 'influxdb', '~> 0.2', require: false
gem 'connection_pool', '~> 2.0', require: false
end end
group :development do group :development do
gem "foreman" gem "foreman"
gem 'brakeman', '~> 3.2.0', require: false gem 'brakeman', '~> 3.2.0', require: false
gem "annotate", "~> 2.6.0" gem "annotate", "~> 2.7.0"
gem "letter_opener", '~> 1.1.2' gem "letter_opener", '~> 1.1.2'
gem 'quiet_assets', '~> 1.0.2' gem 'quiet_assets', '~> 1.0.2'
gem 'rerun', '~> 0.11.0' gem 'rerun', '~> 0.11.0'
......
...@@ -4,41 +4,41 @@ GEM ...@@ -4,41 +4,41 @@ GEM
CFPropertyList (2.3.2) CFPropertyList (2.3.2)
RedCloth (4.2.9) RedCloth (4.2.9)
ace-rails-ap (2.0.1) ace-rails-ap (2.0.1)
actionmailer (4.2.5.2) actionmailer (4.2.6)
actionpack (= 4.2.5.2) actionpack (= 4.2.6)
actionview (= 4.2.5.2) actionview (= 4.2.6)
activejob (= 4.2.5.2) activejob (= 4.2.6)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
actionpack (4.2.5.2) actionpack (4.2.6)
actionview (= 4.2.5.2) actionview (= 4.2.6)
activesupport (= 4.2.5.2) activesupport (= 4.2.6)
rack (~> 1.6) rack (~> 1.6)
rack-test (~> 0.6.2) rack-test (~> 0.6.2)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (4.2.5.2) actionview (4.2.6)
activesupport (= 4.2.5.2) activesupport (= 4.2.6)
builder (~> 3.1) builder (~> 3.1)
erubis (~> 2.7.0) erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
activejob (4.2.5.2) activejob (4.2.6)
activesupport (= 4.2.5.2) activesupport (= 4.2.6)
globalid (>= 0.3.0) globalid (>= 0.3.0)
activemodel (4.2.5.2) activemodel (4.2.6)
activesupport (= 4.2.5.2) activesupport (= 4.2.6)
builder (~> 3.1) builder (~> 3.1)
activerecord (4.2.5.2) activerecord (4.2.6)
activemodel (= 4.2.5.2) activemodel (= 4.2.6)
activesupport (= 4.2.5.2) activesupport (= 4.2.6)
arel (~> 6.0) arel (~> 6.0)
activerecord-deprecated_finders (1.0.4) activerecord-deprecated_finders (1.0.4)
activerecord-session_store (0.1.2) activerecord-session_store (0.1.2)
actionpack (>= 4.0.0, < 5) actionpack (>= 4.0.0, < 5)
activerecord (>= 4.0.0, < 5) activerecord (>= 4.0.0, < 5)
railties (>= 4.0.0, < 5) railties (>= 4.0.0, < 5)
activesupport (4.2.5.2) activesupport (4.2.6)
i18n (~> 0.7) i18n (~> 0.7)
json (~> 1.7, >= 1.7.7) json (~> 1.7, >= 1.7.7)
minitest (~> 5.1) minitest (~> 5.1)
...@@ -51,8 +51,8 @@ GEM ...@@ -51,8 +51,8 @@ GEM
activerecord (>= 3.0) activerecord (>= 3.0)
akismet (2.0.0) akismet (2.0.0)
allocations (1.0.4) allocations (1.0.4)
annotate (2.6.10) annotate (2.7.0)
activerecord (>= 3.2, <= 4.3) activerecord (>= 3.2, < 6.0)
rake (~> 10.4) rake (~> 10.4)
arel (6.0.3) arel (6.0.3)
asana (0.4.0) asana (0.4.0)
...@@ -145,7 +145,7 @@ GEM ...@@ -145,7 +145,7 @@ GEM
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
creole (0.5.0) creole (0.5.0)
css_parser (1.3.7) css_parser (1.4.1)
addressable addressable
d3_rails (3.5.11) d3_rails (3.5.11)
railties (>= 3.1.0) railties (>= 3.1.0)
...@@ -459,8 +459,8 @@ GEM ...@@ -459,8 +459,8 @@ GEM
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
macaddr (1.7.1) macaddr (1.7.1)
systemu (~> 2.6.2) systemu (~> 2.6.2)
mail (2.6.3) mail (2.6.4)
mime-types (>= 1.16, < 3) mime-types (>= 1.16, < 4)
mail_room (0.6.1) mail_room (0.6.1)
method_source (0.8.2) method_source (0.8.2)
mime-types (1.25.1) mime-types (1.25.1)
...@@ -559,8 +559,8 @@ GEM ...@@ -559,8 +559,8 @@ GEM
premailer (1.8.6) premailer (1.8.6)
css_parser (>= 1.3.6) css_parser (>= 1.3.6)
htmlentities (>= 4.0.0) htmlentities (>= 4.0.0)
premailer-rails (1.9.0) premailer-rails (1.9.2)
actionmailer (>= 3, < 5) actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
pry (0.10.3) pry (0.10.3)
coderay (~> 1.1.0) coderay (~> 1.1.0)
...@@ -589,16 +589,16 @@ GEM ...@@ -589,16 +589,16 @@ GEM
rack rack
rack-test (0.6.3) rack-test (0.6.3)
rack (>= 1.0) rack (>= 1.0)
rails (4.2.5.2) rails (4.2.6)
actionmailer (= 4.2.5.2) actionmailer (= 4.2.6)
actionpack (= 4.2.5.2) actionpack (= 4.2.6)
actionview (= 4.2.5.2) actionview (= 4.2.6)
activejob (= 4.2.5.2) activejob (= 4.2.6)
activemodel (= 4.2.5.2) activemodel (= 4.2.6)
activerecord (= 4.2.5.2) activerecord (= 4.2.6)
activesupport (= 4.2.5.2) activesupport (= 4.2.6)
bundler (>= 1.3.0, < 2.0) bundler (>= 1.3.0, < 2.0)
railties (= 4.2.5.2) railties (= 4.2.6)
sprockets-rails sprockets-rails
rails-deprecated_sanitizer (1.0.3) rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha) activesupport (>= 4.2.0.alpha)
...@@ -608,9 +608,9 @@ GEM ...@@ -608,9 +608,9 @@ GEM
rails-deprecated_sanitizer (>= 1.0.1) rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3) rails-html-sanitizer (1.0.3)
loofah (~> 2.0) loofah (~> 2.0)
railties (4.2.5.2) railties (4.2.6)
actionpack (= 4.2.5.2) actionpack (= 4.2.6)
activesupport (= 4.2.5.2) activesupport (= 4.2.6)
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.1.0) rainbow (2.1.0)
...@@ -776,12 +776,13 @@ GEM ...@@ -776,12 +776,13 @@ GEM
spring (>= 0.9.1) spring (>= 0.9.1)
spring-commands-teaspoon (0.0.2) spring-commands-teaspoon (0.0.2)
spring (>= 0.9.1) spring (>= 0.9.1)
sprockets (3.3.5) sprockets (3.6.0)
concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
sprockets-rails (2.3.3) sprockets-rails (3.0.4)
actionpack (>= 3.0) actionpack (>= 4.0)
activesupport (>= 3.0) activesupport (>= 4.0)
sprockets (>= 2.8, < 4.0) sprockets (>= 3.0.0)
state_machines (0.4.0) state_machines (0.4.0)
state_machines-activemodel (0.3.0) state_machines-activemodel (0.3.0)
activemodel (~> 4.1) activemodel (~> 4.1)
...@@ -887,7 +888,7 @@ DEPENDENCIES ...@@ -887,7 +888,7 @@ DEPENDENCIES
after_commit_queue after_commit_queue
akismet (~> 2.0) akismet (~> 2.0)
allocations (~> 1.0) allocations (~> 1.0)
annotate (~> 2.6.0) annotate (~> 2.7.0)
asana (~> 0.4.0) asana (~> 0.4.0)
asciidoctor (~> 1.5.2) asciidoctor (~> 1.5.2)
attr_encrypted (~> 1.3.4) attr_encrypted (~> 1.3.4)
...@@ -992,13 +993,14 @@ DEPENDENCIES ...@@ -992,13 +993,14 @@ DEPENDENCIES
rack-attack (~> 4.3.1) rack-attack (~> 4.3.1)
rack-cors (~> 0.4.0) rack-cors (~> 0.4.0)
rack-oauth2 (~> 1.2.1) rack-oauth2 (~> 1.2.1)
rails (= 4.2.5.2) rails (= 4.2.6)
rails-deprecated_sanitizer (~> 1.0.3) rails-deprecated_sanitizer (~> 1.0.3)
raphael-rails (~> 2.1.2) raphael-rails (~> 2.1.2)
rblineprof rblineprof
rdoc (~> 3.6) rdoc (~> 3.6)
recaptcha recaptcha
redcarpet (~> 3.3.3) redcarpet (~> 3.3.3)
redis (~> 3.2)
redis-namespace redis-namespace
redis-rails (~> 4.0.0) redis-rails (~> 4.0.0)
request_store (~> 1.3.0) request_store (~> 1.3.0)
...@@ -1032,7 +1034,7 @@ DEPENDENCIES ...@@ -1032,7 +1034,7 @@ DEPENDENCIES
spring-commands-rspec (~> 1.0.4) spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.0.0) spring-commands-spinach (~> 1.0.0)
spring-commands-teaspoon (~> 0.0.2) spring-commands-teaspoon (~> 0.0.2)
sprockets (~> 3.3.5) sprockets (~> 3.6.0)
state_machines-activerecord (~> 0.3.0) state_machines-activerecord (~> 0.3.0)
task_list (~> 1.0.2) task_list (~> 1.0.2)
teaspoon (~> 1.1.0) teaspoon (~> 1.1.0)
......
...@@ -22,8 +22,19 @@ class @AwardsHandler ...@@ -22,8 +22,19 @@ class @AwardsHandler
emoji = $(this) emoji = $(this)
.find(".icon") .find(".icon")
.data "emoji" .data "emoji"
if emoji is "thumbsup" and awards_handler.didUserClickEmoji $(this), "thumbsdown"
awards_handler.addAward "thumbsdown"
else if emoji is "thumbsdown" and awards_handler.didUserClickEmoji $(this), "thumbsup"
awards_handler.addAward "thumbsup"
awards_handler.addAward emoji awards_handler.addAward emoji
didUserClickEmoji: (that, emoji) ->
if $(that).siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title")
$(that).siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title").indexOf('me') > -1
showEmojiMenu: -> showEmojiMenu: ->
if $(".emoji-menu").length if $(".emoji-menu").length
if $(".emoji-menu").is ".is-visible" if $(".emoji-menu").is ".is-visible"
...@@ -105,7 +116,7 @@ class @AwardsHandler ...@@ -105,7 +116,7 @@ class @AwardsHandler
if origTitle if origTitle
authors = origTitle.split(', ') authors = origTitle.split(', ')
authors.push("me") authors.push("me")
award_block.attr("title", authors.join(", ")) award_block.attr("data-original-title", authors.join(", "))
@resetTooltip(award_block) @resetTooltip(award_block)
resetTooltip: (award) -> resetTooltip: (award) ->
...@@ -122,7 +133,7 @@ class @AwardsHandler ...@@ -122,7 +133,7 @@ class @AwardsHandler
nodes = [] nodes = []
nodes.push( nodes.push(
"<button class='btn award-control js-emoji-btn has-tooltip active' title='me'>", "<button class='btn award-control js-emoji-btn has-tooltip active' data-original-title='me'>",
"<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>", "<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>",
"<span class='award-control-text js-counter'>1</span>", "<span class='award-control-text js-counter'>1</span>",
"</button>" "</button>"
......
class @Compare
constructor: (@opts) ->
@source_loading = $ ".js-source-loading"
@target_loading = $ ".js-target-loading"
$('.js-compare-dropdown').each (i, dropdown) =>
$dropdown = $(dropdown)
$dropdown.glDropdown(
selectable: true
fieldName: $dropdown.data 'field-name'
filterable: true
id: (obj, $el) ->
$el.data 'id'
toggleLabel: (obj, $el) ->
$el.text().trim()
clicked: (e, el) =>
if $dropdown.is '.js-target-branch'
@getTargetHtml()
else if $dropdown.is '.js-source-branch'
@getSourceHtml()
else if $dropdown.is '.js-target-project'
@getTargetProject()
)
@initialState()
initialState: ->
@getSourceHtml()
@getTargetHtml()
getTargetProject: ->
$.ajax(
url: @opts.targetProjectUrl
data:
target_project_id: $("input[name='merge_request[target_project_id]']").val()
beforeSend: ->
$('.mr_target_commit').empty()
success: (html) ->
$('.js-target-branch-dropdown .dropdown-content').html html
)
getSourceHtml: ->
@sendAjax(@opts.sourceBranchUrl, @source_loading, '.mr_source_commit',
ref: $("input[name='merge_request[source_branch]']").val()
)
getTargetHtml: ->
@sendAjax(@opts.targetBranchUrl, @target_loading, '.mr_target_commit',
target_project_id: $("input[name='merge_request[target_project_id]']").val()
ref: $("input[name='merge_request[target_branch]']").val()
)
sendAjax: (url, loading, target, data) ->
$target = $(target)
$.ajax(
url: url
data: data
beforeSend: ->
loading.show()
$target.empty()
success: (html) ->
loading.hide()
$target.html html
$('.js-timeago', $target).timeago()
)
...@@ -57,14 +57,30 @@ class GitLabDropdownFilter ...@@ -57,14 +57,30 @@ class GitLabDropdownFilter
filter: (search_text) -> filter: (search_text) ->
data = @options.data() data = @options.data()
results = data
if search_text isnt "" if data?
results = fuzzaldrinPlus.filter(data, search_text, results = data
key: @options.keys
)
@options.callback results if search_text isnt ''
results = fuzzaldrinPlus.filter(data, search_text,
key: @options.keys
)
@options.callback results
else
elements = @options.elements()
if search_text
elements.each ->
$el = $(@)
matches = fuzzaldrinPlus.match($el.text().trim(), search_text)
if matches.length
$el.show()
else
$el.hide()
else
elements.show()
class GitLabDropdownRemote class GitLabDropdownRemote
constructor: (@dataEndpoint, @options) -> constructor: (@dataEndpoint, @options) ->
...@@ -123,7 +139,7 @@ class GitLabDropdown ...@@ -123,7 +139,7 @@ class GitLabDropdown
if _.isString(@filterInput) if _.isString(@filterInput)
@filterInput = @getElement(@filterInput) @filterInput = @getElement(@filterInput)
search_fields = if @options.search then @options.search.fields else []; searchFields = if @options.search then @options.search.fields else [];
if @options.data if @options.data
# If data is an array # If data is an array
...@@ -147,7 +163,14 @@ class GitLabDropdown ...@@ -147,7 +163,14 @@ class GitLabDropdown
filterInputBlur: @filterInputBlur filterInputBlur: @filterInputBlur
remote: @options.filterRemote remote: @options.filterRemote
query: @options.data query: @options.data
keys: @options.search.fields keys: searchFields
elements: =>
selector = '.dropdown-content li:not(.divider)'
if @dropdown.find('.dropdown-toggle-page').length
selector = ".dropdown-page-one #{selector}"
return $(selector)
data: => data: =>
return @fullData return @fullData
callback: (data) => callback: (data) =>
...@@ -376,7 +399,7 @@ class GitLabDropdown ...@@ -376,7 +399,7 @@ class GitLabDropdown
# Toggle the dropdown label # Toggle the dropdown label
if @options.toggleLabel if @options.toggleLabel
$(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject) $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject, el)
if value? if value?
if !field.length and fieldName if !field.length and fieldName
# Create hidden input for form # Create hidden input for form
......
...@@ -73,7 +73,8 @@ class @MergeRequestTabs ...@@ -73,7 +73,8 @@ class @MergeRequestTabs
@expandView() @expandView()
else if action == 'diffs' else if action == 'diffs'
@loadDiff($target.attr('href')) @loadDiff($target.attr('href'))
@shrinkView() if bp? and bp.getBreakpointSize() isnt 'lg'
@shrinkView()
else if action == 'builds' else if action == 'builds'
@loadBuilds($target.attr('href')) @loadBuilds($target.attr('href'))
@expandView() @expandView()
......
...@@ -85,15 +85,21 @@ class @MilestoneSelect ...@@ -85,15 +85,21 @@ class @MilestoneSelect
# display:block overrides the hide-collapse rule # display:block overrides the hide-collapse rule
$value.removeAttr('style') $value.removeAttr('style')
clicked: (selected) -> clicked: (selected) ->
page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index'
isMRIndex = page is page is 'projects:merge_requests:index'
if $dropdown.hasClass 'js-filter-bulk-update' if $dropdown.hasClass 'js-filter-bulk-update'
return return
if $dropdown.hasClass('js-filter-submit') if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
if selected.name? if selected.name?
selectedMilestone = selected.name selectedMilestone = selected.name
else else
selectedMilestone = '' selectedMilestone = ''
Issues.filterResults $dropdown.closest('form') Issues.filterResults $dropdown.closest('form')
else if $dropdown.hasClass('js-filter-submit')
$dropdown.closest('form').submit()
else else
selected = $selectbox selected = $selectbox
.find('input[type="hidden"]') .find('input[type="hidden"]')
......
...@@ -4,6 +4,7 @@ expanded = 'page-sidebar-expanded' ...@@ -4,6 +4,7 @@ expanded = 'page-sidebar-expanded'
toggleSidebar = -> toggleSidebar = ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}") $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded") $('header').toggleClass("header-collapsed header-expanded")
$('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
$.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' }) $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
setTimeout ( -> setTimeout ( ->
......
...@@ -10,10 +10,10 @@ class @Subscription ...@@ -10,10 +10,10 @@ class @Subscription
btn = $(event.currentTarget) btn = $(event.currentTarget)
action = btn.find('span').text() action = btn.find('span').text()
current_status = @subscription_status.attr('data-status') current_status = @subscription_status.attr('data-status')
btn.prop('disabled', true) btn.addClass('disabled')
$.post @url, => $.post @url, =>
btn.prop('disabled', false) btn.removeClass('disabled')
status = if current_status == 'subscribed' then 'unsubscribed' else 'subscribed' status = if current_status == 'subscribed' then 'unsubscribed' else 'subscribed'
@subscription_status.attr('data-status', status) @subscription_status.attr('data-status', status)
action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe' action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe'
......
...@@ -108,5 +108,10 @@ class @Todos ...@@ -108,5 +108,10 @@ class @Todos
uri + separator + key + '=' + value uri + separator + key + '=' + value
goToTodoUrl: -> goToTodoUrl: (e)->
Turbolinks.visit($(this).data('url')) todoLink = $(this).data('url')
if e.metaKey
e.preventDefault()
window.open(todoLink,'_blank')
else
Turbolinks.visit(todoLink)
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
&:focus, &:focus,
&:active { &:active {
outline: none; outline: none;
background-color: $btn-active-gray;
@include box-shadow($gl-btn-active-background); @include box-shadow($gl-btn-active-background);
} }
} }
...@@ -27,7 +28,8 @@ ...@@ -27,7 +28,8 @@
color: $color; color: $color;
} }
&:active { &:active,
&.active {
@include box-shadow ($gl-btn-active-background); @include box-shadow ($gl-btn-active-background);
background-color: $dark; background-color: $dark;
...@@ -61,7 +63,7 @@ ...@@ -61,7 +63,7 @@
} }
@mixin btn-white { @mixin btn-white {
@include btn-color($white-light, $border-white-light, $white-normal, $border-white-normal, $white-dark, $border-white-dark, #313236); @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $btn-white-active);
} }
.btn { .btn {
...@@ -218,3 +220,26 @@ ...@@ -218,3 +220,26 @@
margin-right: 5px; margin-right: 5px;
} }
} }
.btn-text-field {
width: 100%;
text-align: left;
padding: 6px 16px;
border-color: $border-color;
color: $btn-placeholder-gray;
background-color: $background-color;
&:hover,
&:active,
&:focus {
cursor: text;
box-shadow: none;
border-color: $border-color;
color: $btn-placeholder-gray;
background-color: $background-color;
}
}
.btn-file-option {
background: linear-gradient(180deg, $white-light 25%, $gray-light 100%);
}
...@@ -248,7 +248,7 @@ ...@@ -248,7 +248,7 @@
.dropdown-title { .dropdown-title {
position: relative; position: relative;
padding: 0 0 15px; padding: 0 25px 15px;
margin: 0 10px 10px; margin: 0 10px 10px;
font-weight: 600; font-weight: 600;
line-height: 1; line-height: 1;
...@@ -275,7 +275,7 @@ ...@@ -275,7 +275,7 @@
} }
.dropdown-menu-close { .dropdown-menu-close {
right: 7px; right: 5px;
width: 20px; width: 20px;
height: 20px; height: 20px;
top: -1px; top: -1px;
......
...@@ -15,12 +15,13 @@ ...@@ -15,12 +15,13 @@
.file-title { .file-title {
position: relative; position: relative;
background: $background-color; background-color: $background-color;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
margin: 0; margin: 0;
text-align: left; text-align: left;
padding: 10px $gl-padding; padding: 10px $gl-padding;
word-wrap: break-word; word-wrap: break-word;
border-radius: 3px 3px 0 0;
.file-actions { .file-actions {
float: right; float: right;
...@@ -49,7 +50,7 @@ ...@@ -49,7 +50,7 @@
} }
} }
a { a:not(.btn) {
color: $gl-dark-link-color; color: $gl-dark-link-color;
} }
......
...@@ -33,15 +33,10 @@ ...@@ -33,15 +33,10 @@
background: $color; background: $color;
} }
.complex-sidebar .nav-primary {
border-right: 1px solid lighten($color, 3%);
}
.sidebar-wrapper { .sidebar-wrapper {
background: $color-darker; background: $color-darker;
.sidebar-user { .sidebar-user {
border-top: 1px solid lighten($color, 3%);
background: $color-darker; background: $color-darker;
color: $color-light; color: $color-light;
...@@ -67,6 +62,7 @@ ...@@ -67,6 +62,7 @@
.count { .count {
color: $color-light; color: $color-light;
background: $color-dark;
} }
} }
......
...@@ -123,11 +123,11 @@ header { ...@@ -123,11 +123,11 @@ header {
} }
@mixin collapsed-header { @mixin collapsed-header {
margin-left: 40px; margin-left: $sidebar_collapsed_width;
} }
.header-collapsed { .header-collapsed {
margin-left: 40px; margin-left: $sidebar_collapsed_width;
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
@include collapsed-header; @include collapsed-header;
......
...@@ -144,7 +144,7 @@ ...@@ -144,7 +144,7 @@
} }
a { a {
padding: 7px 12px; padding: 7px 15px;
font-size: $gl-font-size; font-size: $gl-font-size;
line-height: 24px; line-height: 24px;
color: $gray; color: $gray;
...@@ -169,12 +169,10 @@ ...@@ -169,12 +169,10 @@
} }
.count { .count {
&:before { float: right;
content: '('; background: #eee;
} padding: 0 8px;
&:after { @include border-radius(6px);
content: ')';
}
} }
&.back-link i { &.back-link i {
...@@ -193,27 +191,6 @@ ...@@ -193,27 +191,6 @@
} }
} }
.expand-nav a {
color: $gl-icon-color;
width: 60px;
position: fixed;
top: 0;
left: 0;
font-size: 20px;
background: #fff;
height: 59px;
text-align: center;
line-height: 59px;
border-bottom: 1px solid #eee;
transition-duration: .3s;
outline: none;
z-index: 100;
&:hover {
text-decoration: none;
}
}
.collapse-nav a { .collapse-nav a {
width: $sidebar_width; width: $sidebar_width;
position: fixed; position: fixed;
...@@ -233,12 +210,55 @@ ...@@ -233,12 +210,55 @@
} }
.page-sidebar-collapsed { .page-sidebar-collapsed {
padding-left: $sidebar_collapsed_width;
.sidebar-wrapper { .sidebar-wrapper {
display: none; width: $sidebar_collapsed_width;
.header-logo {
width: $sidebar_collapsed_width;
a {
padding-left: ($sidebar_collapsed_width - 36) / 2;
.gitlab-text-container {
display: none;
}
}
}
.nav-sidebar {
width: $sidebar_collapsed_width;
li {
width: auto;
a {
span {
display: none;
}
}
}
}
.collapse-nav a {
width: $sidebar_collapsed_width;
}
.sidebar-user {
padding-left: ($sidebar_collapsed_width - 36) / 2;
width: $sidebar_collapsed_width;
.username {
display: none;
}
}
} }
} }
.page-sidebar-expanded { .page-sidebar-expanded {
padding-left: $sidebar_collapsed_width;
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
padding-left: $sidebar_width; padding-left: $sidebar_width;
} }
...@@ -289,48 +309,3 @@ ...@@ -289,48 +309,3 @@
padding-right: $sidebar_collapsed_width; padding-right: $sidebar_collapsed_width;
} }
} }
.complex-sidebar {
display: inline-block;
.nav-primary {
width: 61px;
float: left;
height: 100vh;
.nav-sidebar {
width: 60px;
li a {
width: 60px;
span {
display: none;
}
}
}
}
.nav-secondary {
$nav-secondary-width: 168px;
float: left;
width: $nav-secondary-width;
.nav-sidebar {
width: $nav-secondary-width;
li {
width: $nav-secondary-width;
a {
width: $nav-secondary-width;
i {
display: none;
}
}
}
}
}
}
...@@ -14,10 +14,6 @@ ...@@ -14,10 +14,6 @@
background: $row-hover; background: $row-hover;
} }
&:last-child {
border-bottom: none;
}
.avatar { .avatar {
margin-right: 15px; margin-right: 15px;
} }
......
...@@ -10,10 +10,10 @@ $gutter_inner_width: 258px; ...@@ -10,10 +10,10 @@ $gutter_inner_width: 258px;
/* /*
* UI elements * UI elements
*/ */
$border-color: #efeff1; $border-color: #e5e5e5;
$focus-border-color: #3aabf0; $focus-border-color: #3aabf0;
$table-border-color: #eef0f2; $table-border-color: #eef0f2;
$background-color: #faf9f9; $background-color: #fafafa;
/* /*
* Text * Text
...@@ -81,7 +81,7 @@ $provider-btn-not-active-color: #4688f1; ...@@ -81,7 +81,7 @@ $provider-btn-not-active-color: #4688f1;
$white-light: #fff; $white-light: #fff;
$white-normal: #ededed; $white-normal: #ededed;
$white-dark: #ededed; $white-dark: #ececec;
$gray-light: #faf9f9; $gray-light: #faf9f9;
$gray-normal: #f5f5f5; $gray-normal: #f5f5f5;
...@@ -108,6 +108,8 @@ $red-light: #e52c5a; ...@@ -108,6 +108,8 @@ $red-light: #e52c5a;
$red-normal: #d22852; $red-normal: #d22852;
$red-dark: darken($red-normal, 5%); $red-dark: darken($red-normal, 5%);
$black-transparent: rgba(0, 0, 0, 0.3);
$border-white-light: #f1f2f4; $border-white-light: #f1f2f4;
$border-white-normal: #d6dae2; $border-white-normal: #d6dae2;
$border-white-dark: #c6cacf; $border-white-dark: #c6cacf;
...@@ -150,15 +152,22 @@ $gl-success: $green-normal; ...@@ -150,15 +152,22 @@ $gl-success: $green-normal;
$gl-info: $blue-normal; $gl-info: $blue-normal;
$gl-warning: $orange-normal; $gl-warning: $orange-normal;
$gl-danger: $red-normal; $gl-danger: $red-normal;
$gl-btn-active-background: rgba(0, 0, 0, 0.12); $gl-btn-active-background: rgba(0, 0, 0, 0.16);
$gl-btn-active-gradient: inset 0 0 4px $gl-btn-active-background; $gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background;
/* /*
* Commit Diff Colors * Commit Diff Colors
*/ */
$added: #63c363; $added: #63c363;
$deleted: #f77; $deleted: #f77;
$line-added: #ecfdf0;
$line-added-dark: #c7f0d2;
$line-removed: #fbe9eb;
$line-removed-dark: #fac5cd;
$line-number-old: #f9d7dc;
$line-number-new: #ddfbe6;
$match-line: #fafafa;
$table-border-gray: #f0f0f0;
/* /*
* Fonts * Fonts
*/ */
...@@ -191,6 +200,13 @@ $dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%); ...@@ -191,6 +200,13 @@ $dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%);
$dropdown-toggle-icon-color: #c4c4c4; $dropdown-toggle-icon-color: #c4c4c4;
$dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color;
/*
* Buttons
*/
$btn-active-gray: #ececec;
$btn-placeholder-gray: #c7c7c7;
$btn-white-active: #848484;
/* /*
* Award emoji * Award emoji
*/ */
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
} }
.diff-line-num, .diff-line-num a { .diff-line-num, .diff-line-num a {
color: rgba(0, 0, 0, 0.3); color: $black-transparent;
} }
// Code itself // Code itself
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
} }
.line_content.match { .line_content.match {
color: rgba(0, 0, 0, 0.3); color: $black-transparent;
background: rgba(255, 255, 255, 0.4); background: rgba(255, 255, 255, 0.4);
} }
} }
......
...@@ -6,12 +6,12 @@ ...@@ -6,12 +6,12 @@
} }
.diff-line-num, .diff-line-num a { .diff-line-num, .diff-line-num a {
color: rgba(0, 0, 0, 0.3); color: $black-transparent;
} }
// Code itself // Code itself
pre.code, .diff-line-num { pre.code, .diff-line-num {
border-color: $border-color; border-color: $table-border-gray;
} }
&, pre.code, .line_holder .line_content { &, pre.code, .line_holder .line_content {
...@@ -23,36 +23,36 @@ ...@@ -23,36 +23,36 @@
.line_holder { .line_holder {
.diff-line-num { .diff-line-num {
&.old { &.old {
background: #fdd; background-color: $line-number-old;
border-color: #f1c0c0; border-color: $line-removed-dark;
} }
&.new { &.new {
background: #dbffdb; background-color: $line-number-new;
border-color: #c1e9c1; border-color: $line-added-dark;
} }
} }
.line_content { .line_content {
&.old { &.old {
background: #ffecec; background: $line-removed;
span.idiff { span.idiff {
background-color: #f8cbcb; background-color: $line-removed-dark;
} }
} }
&.new { &.new {
background: #eaffea; background-color: $line-added;
span.idiff { span.idiff {
background-color: #a6f3a6; background-color: $line-added-dark;
} }
} }
&.match { &.match {
color: rgba(0, 0, 0, 0.3); color: $black-transparent;
background: #fafafa; background: $match-line;
} }
} }
} }
......
...@@ -20,6 +20,8 @@ ...@@ -20,6 +20,8 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
margin-top: 10px; margin-top: 10px;
word-break: normal;
white-space: pre-wrap;
} }
.commit-info-row { .commit-info-row {
......
...@@ -47,6 +47,7 @@ li.commit { ...@@ -47,6 +47,7 @@ li.commit {
.commit_short_id { .commit_short_id {
min-width: 65px; min-width: 65px;
color: $gl-dark-link-color;
font-family: $monospace_font; font-family: $monospace_font;
} }
...@@ -88,6 +89,10 @@ li.commit { ...@@ -88,6 +89,10 @@ li.commit {
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
a {
color: $gl-dark-link-color;
}
} }
.commit-row-info { .commit-row-info {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
.diff-file { .diff-file {
border: 1px solid $border-color; border: 1px solid $border-color;
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
border-radius: 3px;
.diff-header { .diff-header {
position: relative; position: relative;
...@@ -10,6 +11,7 @@ ...@@ -10,6 +11,7 @@
padding: 10px 16px; padding: 10px 16px;
color: #555; color: #555;
z-index: 10; z-index: 10;
border-radius: 3px 3px 0 0;
.diff-title { .diff-title {
font-family: $monospace_font; font-family: $monospace_font;
...@@ -31,6 +33,7 @@ ...@@ -31,6 +33,7 @@
overflow-y: hidden; overflow-y: hidden;
background: #fff; background: #fff;
color: #333; color: #333;
border-radius: 0 0 3px 3px;
.unfold { .unfold {
cursor: pointer; cursor: pointer;
...@@ -109,6 +112,10 @@ ...@@ -109,6 +112,10 @@
display: table-cell; display: table-cell;
} }
} }
.text-file.diff-wrap-lines table .line_holder td span {
white-space: pre-wrap;
}
} }
.image { .image {
background: #ddd; background: #ddd;
...@@ -321,6 +328,16 @@ ...@@ -321,6 +328,16 @@
float: right; float: right;
} }
.diffs {
.content-block {
border-bottom: none;
}
}
.files-changed {
border-bottom: none;
}
// Mobile // Mobile
@media (max-width: 480px) { @media (max-width: 480px) {
.diff-title { .diff-title {
......
...@@ -59,6 +59,9 @@ ...@@ -59,6 +59,9 @@
position: relative; position: relative;
overflow-y: auto; overflow-y: auto;
padding: 15px; padding: 15px;
.form-actions {
margin: -$gl-padding+1;
}
} }
body.modal-open { body.modal-open {
......
...@@ -49,6 +49,15 @@ ...@@ -49,6 +49,15 @@
} }
.label-row { .label-row {
.label-name {
display: inline-block;
width: 200px;
@media (max-width: $screen-xs-min) {
display: block;
}
}
.label { .label {
padding: 9px; padding: 9px;
font-size: 14px; font-size: 14px;
...@@ -69,3 +78,52 @@ ...@@ -69,3 +78,52 @@
background-color: $gl-danger; background-color: $gl-danger;
color: $white-light; color: $white-light;
} }
.manage-labels-list {
.prepend-left-10 {
display: inline-block;
width: 40%;
vertical-align: middle;
@media (max-width: $screen-xs-min) {
display: block;
width: 100%;
margin-left: 0;
padding: 10px 0;
}
}
.pull-info-right {
float: right;
@media (max-width: $screen-xs-min) {
float: none;
}
.action-buttons {
border-color: transparent;
padding: 6px;
color: $gl-text-color;
&.subscribe-button {
padding-left: 0;
}
}
i {
color: $gl-text-color;
}
.append-right-20 {
a {
color: $gl-text-color;
}
@media (max-width: $screen-xs-min) {
display: block;
margin-bottom: 10px;
}
}
}
}
...@@ -123,6 +123,8 @@ ...@@ -123,6 +123,8 @@
.mr_source_commit, .mr_source_commit,
.mr_target_commit { .mr_target_commit {
margin-bottom: 0;
.commit { .commit {
margin: 0; margin: 0;
padding: 2px 0; padding: 2px 0;
...@@ -174,10 +176,6 @@ ...@@ -174,10 +176,6 @@
display: none; display: none;
} }
.merge-request-form .select2-container {
width: 250px !important;
}
#modal_merge_info .modal-dialog { #modal_merge_info .modal-dialog {
width: 600px; width: 600px;
...@@ -200,3 +198,76 @@ ...@@ -200,3 +198,76 @@
overflow-x: scroll; overflow-x: scroll;
} }
} }
.panel-new-merge-request {
.panel-heading {
padding: 5px 10px;
font-weight: 600;
line-height: 25px;
}
.panel-body {
padding: 10px 5px;
}
.panel-footer {
padding: 5px 10px;
}
.commit {
.commit-row-title {
margin-bottom: 4px;
}
.avatar {
width: 20px;
height: 20px;
margin-right: 5px;
}
.commit-row-info {
line-height: 20px;
}
}
.btn-clipboard {
margin-right: 5px;
padding: 0;
background: transparent;
}
.ci-status-link {
margin-right: 5px;
}
}
.merge-request-select {
padding-left: 5px;
padding-right: 5px;
margin-bottom: 10px;
&:last-child {
margin-bottom: 0;
}
@media (min-width: $screen-sm-min) {
float: left;
width: 50%;
margin-bottom: 0;
}
.dropdown-menu-toggle {
width: 100%;
}
.dropdown-menu {
left: 5px;
right: 5px;
width: auto;
}
}
.issuable-form-select-holder {
display: inline-block;
width: 250px;
}
/** /**
* Note Form * Note Form
*/ */
.reply-btn { .comment-btn {
@extend .btn-primary; @extend .btn-create;
margin: 10px $gl-padding;
} }
.diff-file .diff-content { .diff-file .diff-content {
tr.line_holder:hover > td .line_note_link { tr.line_holder:hover > td .line_note_link {
opacity: 1.0; opacity: 1.0;
...@@ -113,13 +113,12 @@ ...@@ -113,13 +113,12 @@
.discussion-body, .discussion-body,
.diff-file { .diff-file {
.notes .note { .notes .note {
border-color: #ddd;
padding: 10px 15px; padding: 10px 15px;
} }
.discussion-reply-holder { .discussion-reply-holder {
background: $background-color; background-color: $white-light;
border-top: 1px solid $border-color; padding: 10px 16px;
} }
} }
......
...@@ -58,6 +58,7 @@ ul.notes { ...@@ -58,6 +58,7 @@ ul.notes {
.note { .note {
display: block; display: block;
position: relative; position: relative;
border-bottom: 1px solid $table-border-gray;
&.is-editting { &.is-editting {
.note-header, .note-header,
...@@ -117,9 +118,6 @@ ul.notes { ...@@ -117,9 +118,6 @@ ul.notes {
padding-bottom: 3px; padding-bottom: 3px;
} }
&:last-child {
border-bottom: 1px solid $border-color;
}
} }
} }
...@@ -137,14 +135,14 @@ ul.notes { ...@@ -137,14 +135,14 @@ ul.notes {
font-family: $regular_font; font-family: $regular_font;
td { td {
border: 1px solid #ddd; border: 1px solid $table-border-gray;
border-left: none; border-left: none;
&.notes_line { &.notes_line {
vertical-align: middle; vertical-align: middle;
text-align: center; text-align: center;
padding: 10px 0; padding: 10px 0;
background: #fff; background: $background-color;
color: $text-color; color: $text-color;
} }
&.notes_line2 { &.notes_line2 {
...@@ -175,9 +173,6 @@ ul.notes { ...@@ -175,9 +173,6 @@ ul.notes {
} }
} }
.author_link {
font-weight: 600;
}
} }
.note-headline-light, .note-headline-light,
...@@ -203,14 +198,26 @@ ul.notes { ...@@ -203,14 +198,26 @@ ul.notes {
line-height: 24px; line-height: 24px;
.fa { .fa {
color: $notes-action-color;
position: relative; position: relative;
top: 1px; top: 1px;
font-size: 17px; font-size: 17px;
} }
.fa-trash-o { &.js-note-delete {
top: 0; i {
font-size: 16px; &:hover {
color: $gl-text-red;
}
}
}
&.js-note-edit {
i {
&:hover {
color: $gl-link-color;
}
}
} }
} }
......
...@@ -47,6 +47,16 @@ class ApplicationController < ActionController::Base ...@@ -47,6 +47,16 @@ class ApplicationController < ActionController::Base
email: current_user.email, email: current_user.email,
username: current_user.username, username: current_user.username,
) )
Raven.tags_context(program: sentry_program_context)
end
end
def sentry_program_context
if Sidekiq.server?
'sidekiq'
else
'rails'
end end
end end
......
...@@ -207,20 +207,20 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -207,20 +207,20 @@ class Projects::MergeRequestsController < Projects::ApplicationController
#This is always source #This is always source
@source_project = @merge_request.nil? ? @project : @merge_request.source_project @source_project = @merge_request.nil? ? @project : @merge_request.source_project
@commit = @repository.commit(params[:ref]) if params[:ref].present? @commit = @repository.commit(params[:ref]) if params[:ref].present?
render layout: false
end end
def branch_to def branch_to
@target_project = selected_target_project @target_project = selected_target_project
@commit = @target_project.commit(params[:ref]) if params[:ref].present? @commit = @target_project.commit(params[:ref]) if params[:ref].present?
render layout: false
end end
def update_branches def update_branches
@target_project = selected_target_project @target_project = selected_target_project
@target_branches = @target_project.repository.branch_names @target_branches = @target_project.repository.branch_names
respond_to do |format| render layout: false
format.js
end
end end
def ci_status def ci_status
......
...@@ -27,9 +27,9 @@ module BlobHelper ...@@ -27,9 +27,9 @@ module BlobHelper
link_opts) link_opts)
if !on_top_of_branch?(project, ref) if !on_top_of_branch?(project, ref)
button_tag "Edit", class: "btn btn-default disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' }
elsif can_edit_blob?(blob, project, ref) elsif can_edit_blob?(blob, project, ref)
link_to "Edit", edit_path, class: 'btn' link_to "Edit", edit_path, class: 'btn btn-file-option'
elsif can?(current_user, :fork_project, project) elsif can?(current_user, :fork_project, project)
continue_params = { continue_params = {
to: edit_path, to: edit_path,
...@@ -38,7 +38,7 @@ module BlobHelper ...@@ -38,7 +38,7 @@ module BlobHelper
} }
fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
link_to "Edit", fork_path, class: 'btn', method: :post link_to "Edit", fork_path, class: 'btn btn-file-option', method: :post
end end
end end
......
...@@ -28,7 +28,7 @@ module CommitsHelper ...@@ -28,7 +28,7 @@ module CommitsHelper
def commit_to_html(commit, project, inline = true) def commit_to_html(commit, project, inline = true)
template = inline ? "inline_commit" : "commit" template = inline ? "inline_commit" : "commit"
escape_javascript(render "projects/commits/#{template}", commit: commit, project: project) unless commit.nil? render "projects/commits/#{template}", commit: commit, project: project unless commit.nil?
end end
# Breadcrumb links for a Project and, if applicable, a tree path # Breadcrumb links for a Project and, if applicable, a tree path
...@@ -117,7 +117,7 @@ module CommitsHelper ...@@ -117,7 +117,7 @@ module CommitsHelper
end end
end end
link_to( link_to(
"Browse Files »", "Browse Files",
namespace_project_tree_path(project.namespace, project, commit), namespace_project_tree_path(project.namespace, project, commit),
class: "pull-right" class: "pull-right"
) )
...@@ -197,7 +197,7 @@ module CommitsHelper ...@@ -197,7 +197,7 @@ module CommitsHelper
link_to( link_to(
namespace_project_blob_path(project.namespace, project, namespace_project_blob_path(project.namespace, project,
tree_join(commit_sha, diff.new_path)), tree_join(commit_sha, diff.new_path)),
class: 'btn view-file js-view-file' class: 'btn view-file js-view-file btn-file-option'
) do ) do
raw('View file @') + content_tag(:span, commit_sha[0..6], raw('View file @') + content_tag(:span, commit_sha[0..6],
class: 'commit-short-id') class: 'commit-short-id')
......
module FormHelper
def form_errors(model)
return unless model.errors.any?
pluralized = 'error'.pluralize(model.errors.count)
headline = "The form contains the following #{pluralized}:"
content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do
content_tag(:h4, headline) <<
content_tag(:ul) do
model.errors.full_messages.
map { |msg| content_tag(:li, msg) }.
join.
html_safe
end
end
end
end
...@@ -116,29 +116,6 @@ module GitlabMarkdownHelper ...@@ -116,29 +116,6 @@ module GitlabMarkdownHelper
end end
end end
MARKDOWN_TIPS = [
"End a line with two or more spaces for a line-break, or soft-return",
"Inline code can be denoted by `surrounding it with backticks`",
"Blocks of code can be denoted by three backticks ``` or four leading spaces",
"Emoji can be added by :emoji_name:, for example :thumbsup:",
"Notify other participants using @user_name",
"Notify a specific group using @group_name",
"Notify the entire team using @all",
"Reference an issue using a hash, for example issue #123",
"Reference a merge request using an exclamation point, for example MR !123",
"Italicize words or phrases using *asterisks* or _underscores_",
"Bold words or phrases using **double asterisks** or __double underscores__",
"Strikethrough words or phrases using ~~two tildes~~",
"Make a bulleted list using + pluses, - minuses, or * asterisks",
"Denote blockquotes using > at the beginning of a line",
"Make a horizontal line using three or more hyphens ---, asterisks ***, or underscores ___"
].freeze
# Returns a random markdown tip for use as a textarea placeholder
def random_markdown_tip
MARKDOWN_TIPS.sample
end
private private
# Return +text+, truncated to +max_chars+ characters, excluding any HTML # Return +text+, truncated to +max_chars+ characters, excluding any HTML
......
...@@ -115,17 +115,32 @@ module IssuesHelper ...@@ -115,17 +115,32 @@ module IssuesHelper
icon('eye-slash') if issue.confidential? icon('eye-slash') if issue.confidential?
end end
def emoji_icon(name, unicode = nil, aliases = []) def emoji_icon(name, unicode = nil, aliases = [], sprite: true)
unicode ||= Emoji.emoji_filename(name) rescue "" unicode ||= Emoji.emoji_filename(name) rescue ""
content_tag :div, "", data = {
class: "icon emoji-icon emoji-#{unicode}", aliases: aliases.join(" "),
title: name, emoji: name,
data: { unicode_name: unicode
aliases: aliases.join(' '), }
emoji: name,
unicode_name: unicode if sprite
} # Emoji icons for the emoji menu, these use a spritesheet.
content_tag :div, "",
class: "icon emoji-icon emoji-#{unicode}",
title: name,
data: data
else
# Emoji icons displayed separately, used for the awards already given
# to an issue or merge request.
content_tag :img, "",
class: "icon emoji",
title: name,
height: "20px",
width: "20px",
src: url_to_image("#{unicode}.png"),
data: data
end
end end
def emoji_author_list(notes, current_user) def emoji_author_list(notes, current_user)
......
...@@ -69,10 +69,7 @@ module NotesHelper ...@@ -69,10 +69,7 @@ module NotesHelper
line_type: line_type line_type: line_type
} }
button_tag class: 'btn btn-nr reply-btn js-discussion-reply-button', button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button',
data: data, title: 'Add a reply' do data: data, title: 'Add a reply'
link_text = icon('comment')
link_text << ' Reply'
end
end end
end end
...@@ -3,11 +3,9 @@ ...@@ -3,11 +3,9 @@
%p Please use this form to report users who create spam issues, comments or behave inappropriately. %p Please use this form to report users who create spam issues, comments or behave inappropriately.
%hr %hr
= form_for @abuse_report, html: { class: 'form-horizontal js-quick-submit js-requires-input'} do |f| = form_for @abuse_report, html: { class: 'form-horizontal js-quick-submit js-requires-input'} do |f|
= form_errors(@abuse_report)
= f.hidden_field :user_id = f.hidden_field :user_id
- if @abuse_report.errors.any?
.alert.alert-danger
- @abuse_report.errors.full_messages.each do |msg|
%p= msg
.form-group .form-group
= f.label :user_id, class: 'control-label' = f.label :user_id, class: 'control-label'
.col-sm-10 .col-sm-10
......
= form_for @appearance, url: admin_appearances_path, html: { class: 'form-horizontal'} do |f| = form_for @appearance, url: admin_appearances_path, html: { class: 'form-horizontal'} do |f|
- if @appearance.errors.any? = form_errors(@appearance)
.alert.alert-danger
- @appearance.errors.full_messages.each do |msg|
%p= msg
%fieldset.sign-in %fieldset.sign-in
%legend %legend
......
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| = form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
- if @application_setting.errors.any? = form_errors(@application_setting)
#error_explanation
.alert.alert-danger
- @application_setting.errors.full_messages.each do |msg|
%p= msg
%fieldset %fieldset
%legend Visibility and Access Controls %legend Visibility and Access Controls
......
= form_for [:admin, @application], url: @url, html: {class: 'form-horizontal', role: 'form'} do |f| = form_for [:admin, @application], url: @url, html: {class: 'form-horizontal', role: 'form'} do |f|
- if application.errors.any? = form_errors(application)
.alert.alert-danger
%button{ type: "button", class: "close", "data-dismiss" => "alert"} &times;
- application.errors.full_messages.each do |msg|
%p= msg
= content_tag :div, class: 'form-group' do = content_tag :div, class: 'form-group' do
= f.label :name, class: 'col-sm-2 control-label' = f.label :name, class: 'col-sm-2 control-label'
.col-sm-10 .col-sm-10
......
...@@ -4,10 +4,8 @@ ...@@ -4,10 +4,8 @@
= render_broadcast_message(@broadcast_message.message.presence || "Your message here") = render_broadcast_message(@broadcast_message.message.presence || "Your message here")
= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f| = form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f|
-if @broadcast_message.errors.any? = form_errors(@broadcast_message)
.alert.alert-danger
- @broadcast_message.errors.full_messages.each do |msg|
%p= msg
.form-group .form-group
= f.label :message, class: 'control-label' = f.label :message, class: 'control-label'
.col-sm-10 .col-sm-10
......
...@@ -4,11 +4,7 @@ ...@@ -4,11 +4,7 @@
%div %div
= form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f| = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f|
-if @deploy_key.errors.any? = form_errors(@deploy_key)
.alert.alert-danger
%ul
- @deploy_key.errors.full_messages.each do |msg|
%li= msg
.form-group .form-group
= f.label :title, class: "control-label" = f.label :title, class: "control-label"
......
= form_for [:admin, @group], html: { class: "form-horizontal" } do |f| = form_for [:admin, @group], html: { class: "form-horizontal" } do |f|
- if @group.errors.any? = form_errors(@group)
.alert.alert-danger
%span= @group.errors.full_messages.first
= render 'shared/group_form', f: f = render 'shared/group_form', f: f
.form-group.group-description-holder .form-group.group-description-holder
......
...@@ -10,10 +10,8 @@ ...@@ -10,10 +10,8 @@
= form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f| = form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f|
-if @hook.errors.any? = form_errors(@hook)
.alert.alert-danger
- @hook.errors.full_messages.each do |msg|
%p= msg
.form-group .form-group
= f.label :url, "URL:", class: 'control-label' = f.label :url, "URL:", class: 'control-label'
.col-sm-10 .col-sm-10
......
= form_for [:admin, @user, @identity], html: { class: 'form-horizontal fieldset-form' } do |f| = form_for [:admin, @user, @identity], html: { class: 'form-horizontal fieldset-form' } do |f|
- if @identity.errors.any? = form_errors(@identity)
#error_explanation
.alert.alert-danger
- @identity.errors.full_messages.each do |msg|
%p= msg
.form-group .form-group
= f.label :provider, class: 'control-label' = f.label :provider, class: 'control-label'
......
= form_for [:admin, @label], html: { class: 'form-horizontal label-form js-requires-input' } do |f| = form_for [:admin, @label], html: { class: 'form-horizontal label-form js-requires-input' } do |f|
-if @label.errors.any? = form_errors(@label)
.row
.col-sm-offset-2.col-sm-10
.alert.alert-danger
- @label.errors.full_messages.each do |msg|
%span= msg
%br
.form-group .form-group
= f.label :title, class: 'control-label' = f.label :title, class: 'control-label'
......
.user_new .user_new
= form_for [:admin, @user], html: { class: 'form-horizontal fieldset-form' } do |f| = form_for [:admin, @user], html: { class: 'form-horizontal fieldset-form' } do |f|
-if @user.errors.any? = form_errors(@user)
#error_explanation
.alert.alert-danger
- @user.errors.full_messages.each do |msg|
%p= msg
%fieldset %fieldset
%legend Account %legend Account
......
= form_for application, url: doorkeeper_submit_path(application), html: {role: 'form'} do |f| = form_for application, url: doorkeeper_submit_path(application), html: {role: 'form'} do |f|
- if application.errors.any? = form_errors(application)
.alert.alert-danger
%ul
- application.errors.full_messages.each do |msg|
%li= msg
.form-group .form-group
= f.label :name, class: 'label-light' = f.label :name, class: 'label-light'
......
...@@ -5,9 +5,7 @@ ...@@ -5,9 +5,7 @@
Group settings Group settings
.panel-body .panel-body
= form_for @group, html: { multipart: true, class: "form-horizontal" }, authenticity_token: true do |f| = form_for @group, html: { multipart: true, class: "form-horizontal" }, authenticity_token: true do |f|
- if @group.errors.any? = form_errors(@group)
.alert.alert-danger
%span= @group.errors.full_messages.first
= render 'shared/group_form', f: f = render 'shared/group_form', f: f
.form-group .form-group
......
...@@ -6,10 +6,7 @@ ...@@ -6,10 +6,7 @@
%hr %hr
= form_for @group, html: { class: 'group-form form-horizontal' } do |f| = form_for @group, html: { class: 'group-form form-horizontal' } do |f|
- if @group.errors.any? = form_errors(@group)
.alert.alert-danger
%span= @group.errors.full_messages.first
= render 'shared/group_form', f: f, autofocus: true = render 'shared/group_form', f: f, autofocus: true
.form-group.group-description-holder .form-group.group-description-holder
......
- if nav_menu_collapsed?
= link_to icon('angle-right'), '#', class: 'toggle-nav-collapse', title: "Open/Close"
- else
= link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Open/Close"
.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } .page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
= render "layouts/broadcast" = render "layouts/broadcast"
.expand-nav
= link_to icon('bars'), '#', class: 'toggle-nav-collapse', title: "Open sidebar"
.sidebar-wrapper.nicescroll{ class: nav_sidebar_class } .sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
.header-logo .header-logo
%a#logo %a#logo
...@@ -10,19 +8,15 @@ ...@@ -10,19 +8,15 @@
.gitlab-text-container .gitlab-text-container
%h3 GitLab %h3 GitLab
- primary_sidebar = current_user ? 'dashboard' : 'explore' - if defined?(sidebar) && sidebar
= render "layouts/nav/#{sidebar}"
- if defined?(sidebar) && sidebar && sidebar != primary_sidebar - elsif current_user
.complex-sidebar = render 'layouts/nav/dashboard'
.nav-primary
= render "layouts/nav/#{primary_sidebar}"
.nav-secondary
= render "layouts/nav/#{sidebar}"
- else - else
= render "layouts/nav/#{primary_sidebar}" = render 'layouts/nav/explore'
.collapse-nav .collapse-nav
= link_to icon('angle-left'), '#', class: 'toggle-nav-collapse', title: "Hide sidebar" = render partial: 'layouts/collapse_button'
- if current_user - if current_user
= link_to current_user, class: 'sidebar-user', title: "Profile" do = link_to current_user, class: 'sidebar-user', title: "Profile" do
= image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36'
......
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
Spam Logs Spam Logs
%span.count= number_with_delimiter(SpamLog.count(:all)) %span.count= number_with_delimiter(SpamLog.count(:all))
= nav_link(controller: :application_settings) do = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
= link_to admin_application_settings_path, title: 'Settings' do = link_to admin_application_settings_path, title: 'Settings' do
= icon('cogs fw') = icon('cogs fw')
%span %span
......
...@@ -15,12 +15,12 @@ ...@@ -15,12 +15,12 @@
= icon('dashboard fw') = icon('dashboard fw')
%span %span
Activity Activity
= nav_link(path: ['dashboard/groups#index', 'explore/groups#index']) do = nav_link(controller: :groups) do
= link_to dashboard_groups_path, title: 'Groups' do = link_to dashboard_groups_path, title: 'Groups' do
= icon('group fw') = icon('group fw')
%span %span
Groups Groups
= nav_link(path: 'dashboard#milestones') do = nav_link(controller: :milestones) do
= link_to dashboard_milestones_path, title: 'Milestones' do = link_to dashboard_milestones_path, title: 'Milestones' do
= icon('clock-o fw') = icon('clock-o fw')
%span %span
...@@ -48,6 +48,7 @@ ...@@ -48,6 +48,7 @@
%span %span
Help Help
%li.separate-item
= nav_link(controller: :profile) do = nav_link(controller: :profile) do
= link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
= icon('user fw') = icon('user fw')
......
%ul.nav.nav-sidebar %ul.nav.nav-sidebar
= nav_link do
= link_to root_path, title: 'Go to dashboard', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
Go to dashboard
%li.separate-item
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do = nav_link(path: 'groups#show', html_options: {class: 'home'}) do
= link_to group_path(@group), title: 'Home' do = link_to group_path(@group), title: 'Home' do
= icon('group fw') = icon('group fw')
...@@ -34,7 +42,7 @@ ...@@ -34,7 +42,7 @@
%span %span
Members Members
- if can?(current_user, :admin_group, @group) - if can?(current_user, :admin_group, @group)
= nav_link do = nav_link(html_options: { class: "separate-item" }) do
= link_to edit_group_path(@group), title: 'Settings' do = link_to edit_group_path(@group), title: 'Settings' do
= icon ('cogs fw') = icon ('cogs fw')
%span %span
......
%ul.nav.nav-sidebar %ul.nav.nav-sidebar
= nav_link do
= link_to root_path, title: 'Go to dashboard', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
Go to dashboard
%li.separate-item
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
= link_to profile_path, title: 'Profile Settings' do = link_to profile_path, title: 'Profile Settings' do
= icon('user fw') = icon('user fw')
......
%ul.nav.nav-sidebar %ul.nav.nav-sidebar
- if @project.group
= nav_link do
= link_to group_path(@project.group), title: 'Go to group', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
Go to group
- else
= nav_link do
= link_to root_path, title: 'Go to dashboard', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
Go to dashboard
%li.separate-item
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do = nav_link(path: 'projects#show', html_options: {class: 'home'}) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
= icon('bookmark fw') = icon('bookmark fw')
...@@ -98,7 +113,7 @@ ...@@ -98,7 +113,7 @@
Snippets Snippets
- if project_nav_tab? :settings - if project_nav_tab? :settings
= nav_link(html_options: {class: "#{project_tab_class}"}) do = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do
= link_to edit_project_path(@project), title: 'Settings' do = link_to edit_project_path(@project), title: 'Settings' do
= icon('cogs fw') = icon('cogs fw')
%span %span
......
%div %div
= form_for [:profile, @key], html: { class: 'js-requires-input' } do |f| = form_for [:profile, @key], html: { class: 'js-requires-input' } do |f|
- if @key.errors.any? = form_errors(@key)
.alert.alert-danger
%ul
- @key.errors.full_messages.each do |msg|
%li= msg
.form-group .form-group
= f.label :key, class: 'label-light' = f.label :key, class: 'label-light'
......
...@@ -2,11 +2,7 @@ ...@@ -2,11 +2,7 @@
- header_title page_title, profile_notifications_path - header_title page_title, profile_notifications_path
= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f| = form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f|
-if @user.errors.any? = form_errors(@user)
%div.alert.alert-danger
%ul
- @user.errors.full_messages.each do |msg|
%li= msg
= hidden_field_tag :notification_type, 'global' = hidden_field_tag :notification_type, 'global'
.row .row
......
...@@ -13,11 +13,8 @@ ...@@ -13,11 +13,8 @@
- unless @user.password_automatically_set? - unless @user.password_automatically_set?
or recover your current one or recover your current one
= form_for @user, url: profile_password_path, method: :put, html: {class: "update-password"} do |f| = form_for @user, url: profile_password_path, method: :put, html: {class: "update-password"} do |f|
-if @user.errors.any? = form_errors(@user)
.alert.alert-danger
%ul
- @user.errors.full_messages.each do |msg|
%li= msg
- unless @user.password_automatically_set? - unless @user.password_automatically_set?
.form-group .form-group
= f.label :current_password, class: 'label-light' = f.label :current_password, class: 'label-light'
......
...@@ -7,11 +7,8 @@ ...@@ -7,11 +7,8 @@
Please set a new password before proceeding. Please set a new password before proceeding.
%br %br
After a successful password update you will be redirected to login screen. After a successful password update you will be redirected to login screen.
-if @user.errors.any?
.alert.alert-danger = form_errors(@user)
%ul
- @user.errors.full_messages.each do |msg|
%li= msg
- unless @user.password_automatically_set? - unless @user.password_automatically_set?
.form-group .form-group
......
= form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit-user prepend-top-default" }, authenticity_token: true do |f| = form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit-user prepend-top-default" }, authenticity_token: true do |f|
-if @user.errors.any? = form_errors(@user)
%div.alert.alert-danger
%ul
- @user.errors.full_messages.each do |msg|
%li= msg
.row .row
.col-lg-3.profile-settings-sidebar .col-lg-3.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
......
- if @project.errors.any? = form_errors(@project)
.alert.alert-danger
%button{ type: "button", class: "close", "data-dismiss" => "alert"} &times;
= @project.errors.full_messages.first
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
.md-header .md-header
%ul.nav-links %ul.nav-links
%li.active %li.active
%a.js-md-write-button{ href: "#md-write-holder" } %a.js-md-write-button{ href: "#md-write-holder", tabindex: -1 }
Write Write
%li %li
%a.js-md-preview-button{ href: "#md-preview-holder" } %a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 }
Preview Preview
%li.pull-right %li.pull-right
%button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button' } %button.zen-cotrol.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 }
Go full screen Go full screen
.md-write-holder .md-write-holder
......
...@@ -19,24 +19,17 @@ ...@@ -19,24 +19,17 @@
.pull-right .pull-right
- if ci_commit - if ci_commit
= render_ci_status(ci_commit) = render_ci_status(ci_commit)
&nbsp;
= clipboard_button(clipboard_text: commit.id) = clipboard_button(clipboard_text: commit.id)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id" = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
.notes_count
- if note_count > 0
%span.light
%i.fa.fa-comments
= note_count
- if commit.description? - if commit.description?
.commit-row-description.js-toggle-content .commit-row-description.js-toggle-content
%pre %pre
= preserve(markdown(escape_once(commit.description), pipeline: :single_line)) = preserve(markdown(escape_once(commit.description), pipeline: :single_line))
.commit-row-info .commit-row-info
by
= commit_author_link(commit, avatar: true, size: 24) = commit_author_link(commit, avatar: true, size: 24)
authored
.committed_ago .committed_ago
#{time_ago_with_tooltip(commit.committed_date, skip_js: true)} &nbsp; #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} &nbsp;
= link_to_browse_code(project, commit) = link_to_browse_code(project, commit)
%div %div
= form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal js-requires-input' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal js-requires-input' } do |f|
-if @key.errors.any? = form_errors(@key)
.alert.alert-danger
%ul
- @key.errors.full_messages.each do |msg|
%li= msg
.form-group .form-group
= f.label :title, class: "control-label" = f.label :title, class: "control-label"
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- diff_files = safe_diff_files(diffs, diff_refs) - diff_files = safe_diff_files(diffs, diff_refs)
.content-block.oneline-block .content-block.oneline-block.files-changed
.inline-parallel-buttons .inline-parallel-buttons
.btn-group .btn-group
= inline_diff_btn = inline_diff_btn
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- if diff_file.diff.submodule? - if diff_file.diff.submodule?
%span %span
= icon('archive fw') = icon('archive fw')
%strong %span
= submodule_link(blob, @commit.id, project.repository) = submodule_link(blob, @commit.id, project.repository)
- else - else
= blob_icon blob.mode, blob.name = blob_icon blob.mode, blob.name
...@@ -11,13 +11,13 @@ ...@@ -11,13 +11,13 @@
= link_to "#diff-#{i}" do = link_to "#diff-#{i}" do
- if diff_file.renamed_file - if diff_file.renamed_file
- old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
%strong.filename.old .filename.old
= old_path = old_path
&rarr; &rarr;
%strong.filename.new .filename.new
= new_path = new_path
- else - else
%strong %span
= diff_file.new_path = diff_file.new_path
- if diff_file.deleted_file - if diff_file.deleted_file
deleted deleted
...@@ -28,8 +28,8 @@ ...@@ -28,8 +28,8 @@
.file-actions.hidden-xs .file-actions.hidden-xs
- if blob_text_viewable?(blob) - if blob_text_viewable?(blob)
= link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file" do = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file" do
= icon('comments') = icon('comment')
\ \
- if editable_diff?(diff_file) - if editable_diff?(diff_file)
......
...@@ -9,10 +9,8 @@ ...@@ -9,10 +9,8 @@
%hr.clearfix %hr.clearfix
= form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hooks_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hooks_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f|
-if @hook.errors.any? = form_errors(@hook)
.alert.alert-danger
- @hook.errors.full_messages.each do |msg|
%p= msg
.form-group .form-group
= f.label :url, "URL", class: 'control-label' = f.label :url, "URL", class: 'control-label'
.col-sm-10 .col-sm-10
......
= form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @label], html: { class: 'form-horizontal label-form js-quick-submit js-requires-input' } do |f|
-if @label.errors.any? = form_errors(@label)
.row
.col-sm-offset-2.col-sm-10
.alert.alert-danger
- @label.errors.full_messages.each do |msg|
%span= msg
%br
.form-group .form-group
= f.label :title, class: 'control-label' = f.label :title, class: 'control-label'
......
%li{id: dom_id(label)} %li{id: dom_id(label)}
= render "shared/label_row", label: label = render "shared/label_row", label: label
.pull-right .pull-info-right
%strong.append-right-20 %span.append-right-20
= link_to_label(label, type: :merge_request) do = link_to_label(label, type: :merge_request) do
= pluralize label.open_merge_requests_count, 'open merge request' = pluralize label.open_merge_requests_count, 'merge request'
%strong.append-right-20 %span.append-right-20
= link_to_label(label) do = link_to_label(label) do
= pluralize label.open_issues_count(current_user), 'open issue' = pluralize label.open_issues_count(current_user), 'open issue'
- if current_user - if current_user
.label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}} .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}}
.subscription-status{data: {status: label_subscription_status(label)}} .subscription-status{data: {status: label_subscription_status(label)}}
%button.btn.btn-sm.btn-info.subscribe-button
%a.subscribe-button.btn.action-buttons{data: {toggle: "tooltip"}}
%span= label_subscription_toggle_button_text(label) %span= label_subscription_toggle_button_text(label)
- if can? current_user, :admin_label, @project - if can? current_user, :admin_label, @project
= link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm' = link_to edit_namespace_project_label_path(@project.namespace, @project, label), title: "Edit", class: 'btn action-buttons', data: {toggle: "tooltip"} do
= link_to 'Delete', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"} %i.fa.fa-pencil-square-o
= link_to namespace_project_label_path(@project.namespace, @project, label), title: "Delete", class: 'btn action-buttons remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?", toggle: "tooltip"} do
%i.fa.fa-trash-o
- if current_user - if current_user
:javascript :javascript
......
...@@ -5,33 +5,74 @@ ...@@ -5,33 +5,74 @@
.hide.alert.alert-danger.mr-compare-errors .hide.alert.alert-danger.mr-compare-errors
.merge-request-branches.row .merge-request-branches.row
.col-md-6 .col-md-6
.panel.panel-default .panel.panel-default.panel-new-merge-request
.panel-heading .panel-heading
%strong Source branch Source branch
.panel-body .panel-body.clearfix
= f.select(:source_project_id, [[@merge_request.source_project_path,@merge_request.source_project.id]] , {}, { class: 'source_project select2 span3', disabled: @merge_request.persisted?, required: true }) .merge-request-select.dropdown
&nbsp; = f.hidden_field :source_project_id
= f.select(:source_branch, @merge_request.source_branches, { include_blank: true }, { class: 'source_branch select2 span2', required: true, data: { placeholder: "Select source branch" } }) = dropdown_toggle @merge_request.source_project_path, { toggle: "dropdown", field_name: "#{f.object_name}[source_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-source-project" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-project
= dropdown_title("Select source project")
= dropdown_filter("Search projects")
= dropdown_content do
- is_active = f.object.source_project_id == @merge_request.source_project.id
%ul
%li
%a{ href: "#", class: "#{("is-active" if is_active)}", data: { id: @merge_request.source_project.id } }
= @merge_request.source_project_path
.merge-request-select.dropdown
= f.hidden_field :source_branch
= dropdown_toggle "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-branch
= dropdown_title("Select source branch")
= dropdown_filter("Search branches")
= dropdown_content do
%ul
- @merge_request.source_branches.each do |branch|
%li
%a{ href: "#", class: "#{("is-active" if f.object.source_branch == branch)}", data: { id: branch } }
= branch
.panel-footer .panel-footer
.mr_source_commit = icon('spinner spin', class: 'js-source-loading')
%ul.list-unstyled.mr_source_commit
.col-md-6 .col-md-6
.panel.panel-default .panel.panel-default.panel-new-merge-request
.panel-heading .panel-heading
%strong Target branch Target branch
.panel-body .panel-body.clearfix
- projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project] - projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project]
= f.select(:target_project_id, options_from_collection_for_select(projects, 'id', 'path_with_namespace', f.object.target_project_id), {}, { class: 'target_project select2 span3', disabled: @merge_request.persisted?, required: true }) .merge-request-select.dropdown
&nbsp; = f.hidden_field :target_project_id
= f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', required: true, data: { placeholder: "Select target branch" } }) = dropdown_toggle f.object.target_project.path_with_namespace, { toggle: "dropdown", field_name: "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" }
.dropdown-menu.dropdown-menu-selectable.dropdown-target-project
= dropdown_title("Select target project")
= dropdown_filter("Search projects")
= dropdown_content do
%ul
- projects.each do |project|
%li
%a{ href: "#", class: "#{("is-active" if f.object.target_project_id == project.id)}", data: { id: project.id } }
= project.path_with_namespace
.merge-request-select.dropdown
= f.hidden_field :target_branch
= dropdown_toggle f.object.target_branch, { toggle: "dropdown", field_name: "#{f.object_name}[target_branch]" }, { toggle_class: "js-compare-dropdown js-target-branch" }
.dropdown-menu.dropdown-menu-selectable.dropdown-target-branch.js-target-branch-dropdown
= dropdown_title("Select target branch")
= dropdown_filter("Search branches")
= dropdown_content do
%ul
- @merge_request.target_branches.each do |branch|
%li
%a{ href: "#", class: "#{("is-active" if f.object.target_branch == branch)}", data: { id: branch } }
= branch
.panel-footer .panel-footer
.mr_target_commit = icon('spinner spin', class: "js-target-loading")
%ul.list-unstyled.mr_target_commit
- if @merge_request.errors.any? - if @merge_request.errors.any?
.alert.alert-danger = form_errors(@merge_request)
- @merge_request.errors.full_messages.each do |msg|
%div= msg
- elsif @merge_request.source_branch.present? && @merge_request.target_branch.present? - elsif @merge_request.source_branch.present? && @merge_request.target_branch.present?
.light-well.append-bottom-default .light-well.append-bottom-default
.center .center
...@@ -45,40 +86,11 @@ ...@@ -45,40 +86,11 @@
and and
%span.label-branch #{@merge_request.target_branch} %span.label-branch #{@merge_request.target_branch}
are the same. are the same.
= f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn"
.form-actions
= f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn"
:javascript
var source_branch = $("#merge_request_source_branch")
, target_branch = $("#merge_request_target_branch")
, target_project = $("#merge_request_target_project_id");
$.get("#{branch_from_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {ref: source_branch.val() });
$.get("#{branch_to_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {target_project_id: target_project.val(),ref: target_branch.val() });
target_project.on("change", function() {
$.get("#{update_branches_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {target_project_id: $(this).val() });
});
source_branch.on("change", function() {
$.get("#{branch_from_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {ref: $(this).val() });
$(".mr-compare-errors").fadeOut();
$(".mr-compare-btn").enable();
});
target_branch.on("change", function() {
$.get("#{branch_to_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}", {target_project_id: target_project.val(),ref: $(this).val() });
$(".mr-compare-errors").fadeOut();
$(".mr-compare-btn").enable();
});
:javascript :javascript
$(".merge-request-form").on('submit', function () { new Compare({
if ($("#merge_request_source_branch").val() === "" || $('#merge_request_target_branch').val() === "") { targetProjectUrl: "#{update_branches_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}",
$(".mr-compare-errors").html("You must select source and target branch to proceed"); sourceBranchUrl: "#{branch_from_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}",
$(".mr-compare-errors").fadeIn(); targetBranchUrl: "#{branch_to_namespace_project_merge_requests_path(@source_project.namespace, @source_project)}"
event.preventDefault();
return;
}
}); });
= commit_to_html(@commit, @source_project, false)
:plain
$(".mr_source_commit").html("#{commit_to_html(@commit, @source_project, false)}");
$('.js-timeago').timeago()
= commit_to_html(@commit, @target_project, false)
:plain
$(".mr_target_commit").html("#{commit_to_html(@commit, @target_project, false)}");
$('.js-timeago').timeago()
%ul
- @target_branches.each do |branch|
%li
%a{ href: "#", class: "#{("is-active" if "a" == branch)}", data: { id: branch } }
= branch
:plain
$(".target_branch").html("#{escape_javascript(options_for_select(@target_branches))}");
$('select.target_branch').select2({
width: 'resolve',
dropdownAutoWidth: true
});
$(".mr_target_commit").html("");
= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input'} do |f| = form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-quick-submit js-requires-input'} do |f|
-if @milestone.errors.any? = form_errors(@milestone)
.alert.alert-danger
%ul
- @milestone.errors.full_messages.each do |msg|
%li= msg
.row .row
.col-md-6 .col-md-6
.form-group .form-group
......
...@@ -3,9 +3,6 @@ ...@@ -3,9 +3,6 @@
- if !defined?(line) || line == note.diff_line - if !defined?(line) || line == note.diff_line
%tr.notes_holder %tr.notes_holder
%td.notes_line{ colspan: 2 } %td.notes_line{ colspan: 2 }
%span.discussion-notes-count
%i.fa.fa-comment
= notes.count
%td.notes_content %td.notes_content
%ul.notes{ data: { discussion_id: note.discussion_id } } %ul.notes{ data: { discussion_id: note.discussion_id } }
= render notes = render notes
......
...@@ -4,9 +4,6 @@ ...@@ -4,9 +4,6 @@
%tr.notes_holder %tr.notes_holder
- if note1 - if note1
%td.notes_line.old %td.notes_line.old
%span.btn.disabled
%i.fa.fa-comment
= notes_left.count
%td.notes_content.parallel.old %td.notes_content.parallel.old
%ul.notes{ data: { discussion_id: note1.discussion_id } } %ul.notes{ data: { discussion_id: note1.discussion_id } }
= render notes_left = render notes_left
...@@ -19,9 +16,6 @@ ...@@ -19,9 +16,6 @@
- if note2 - if note2
%td.notes_line.new %td.notes_line.new
%span.btn.disabled
%i.fa.fa-comment
= notes_right.count
%td.notes_content.parallel.new %td.notes_content.parallel.new
%ul.notes{ data: { discussion_id: note2.discussion_id } } %ul.notes{ data: { discussion_id: note2.discussion_id } }
= render notes_right = render notes_right
......
...@@ -17,8 +17,8 @@ ...@@ -17,8 +17,8 @@
%span.note-role %span.note-role
= access = access
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil-square-o') = icon('pencil')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete' do = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o') = icon('trash-o')
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text .note-text
......
...@@ -13,11 +13,7 @@ ...@@ -13,11 +13,7 @@
- if can? current_user, :admin_project, @project - if can? current_user, :admin_project, @project
= form_for [@project.namespace.becomes(Namespace), @project, @protected_branch], html: { class: 'form-horizontal' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @protected_branch], html: { class: 'form-horizontal' } do |f|
-if @protected_branch.errors.any? = form_errors(@protected_branch)
.alert.alert-danger
%ul
- @protected_branch.errors.full_messages.each do |msg|
%li= msg
.form-group .form-group
= f.label :name, "Branch", class: 'control-label' = f.label :name, "Branch", class: 'control-label'
......
...@@ -13,13 +13,7 @@ ...@@ -13,13 +13,7 @@
= nested_form_for @project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f| = nested_form_for @project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f|
- if @project.errors.any? = form_errors(@project)
#error_explanation
%p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:"
.alert.alert-error
%ul
- @project.errors.full_messages.each do |msg|
%li= msg
= f.fields_for :variables do |variable_form| = f.fields_for :variables do |variable_form|
.form-group .form-group
......
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default js-quick-submit' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default js-quick-submit' } do |f|
-if @page.errors.any? = form_errors(@page)
#error_explanation
.alert.alert-danger
- @page.errors.full_messages.each do |msg|
%p= msg
= f.hidden_field :title, value: @page.title = f.hidden_field :title, value: @page.title
.form-group .form-group
......
%span.label-row %span.label-row
= link_to_label(label, tooltip: false) %span.label-name
= link_to_label(label, tooltip: false)
%span.prepend-left-10 %span.prepend-left-10
= markdown(label.description, pipeline: :single_line) = markdown(label.description, pipeline: :single_line)
- if @service.errors.any? = form_errors(@service)
#error_explanation
.alert.alert-danger
%ul
- @service.errors.full_messages.each do |msg|
%li= msg
- if @service.help.present? - if @service.help.present?
.well .well
......
- if issuable.errors.any? = form_errors(issuable)
.row
.col-sm-offset-2.col-sm-10
.alert.alert-danger
- issuable.errors.full_messages.each do |msg|
%span= msg
%br
.form-group .form-group
= f.label :title, class: 'control-label' = f.label :title, class: 'control-label'
.col-sm-10 .col-sm-10
...@@ -53,10 +48,11 @@ ...@@ -53,10 +48,11 @@
.issue-assignee .issue-assignee
= f.label :assignee_id, "Assignee", class: 'control-label' = f.label :assignee_id, "Assignee", class: 'control-label'
.col-sm-10 .col-sm-10
= users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", .issuable-form-select-holder
placeholder: 'Select assignee', class: 'custom-form-control', null_user: true, = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]",
selected: issuable.assignee_id, project: @target_project || @project, placeholder: 'Select assignee', class: 'custom-form-control', null_user: true,
first_user: true, current_user: true, include_blank: true) selected: issuable.assignee_id, project: @target_project || @project,
first_user: true, current_user: true, include_blank: true)
&nbsp; &nbsp;
= link_to 'Assign to me', '#', class: 'btn assign-to-me-link' = link_to 'Assign to me', '#', class: 'btn assign-to-me-link'
.form-group .form-group
...@@ -64,8 +60,9 @@ ...@@ -64,8 +60,9 @@
= f.label :milestone_id, "Milestone", class: 'control-label' = f.label :milestone_id, "Milestone", class: 'control-label'
.col-sm-10 .col-sm-10
- if milestone_options(issuable).present? - if milestone_options(issuable).present?
= f.select(:milestone_id, milestone_options(issuable), .issuable-form-select-holder
{ include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } }) = f.select(:milestone_id, milestone_options(issuable),
{ include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } })
- else - else
.prepend-top-10 .prepend-top-10
%span.light No open milestones available. %span.light No open milestones available.
......
.snippet-form-holder .snippet-form-holder
= form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f| = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f|
- if @snippet.errors.any? = form_errors(@snippet)
.alert.alert-danger
%ul
- @snippet.errors.full_messages.each do |msg|
%li= msg
.form-group .form-group
= f.label :title, class: 'control-label' = f.label :title, class: 'control-label'
......
...@@ -7,4 +7,4 @@ ...@@ -7,4 +7,4 @@
'#{user_calendar_activities_path}' '#{user_calendar_activities_path}'
); );
.calendar-hint Summary of issues, merge requests and push events .calendar-hint Summary of issues, merge requests, and push events
.awards.votes-block .awards.votes-block
- awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes| - awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes|
%button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user), data: {placement: "top"}} %button.btn.award-control.js-emoji-btn.has-tooltip{class: (note_active_class(notes, current_user)), data: {placement: "top", original_title: emoji_author_list(notes, current_user)}}
= emoji_icon(emoji) = emoji_icon(emoji, sprite: false)
%span.award-control-text.js-counter %span.award-control-text.js-counter
= notes.count = notes.count
......
...@@ -4,11 +4,9 @@ require 'rails/all' ...@@ -4,11 +4,9 @@ require 'rails/all'
require 'devise' require 'devise'
I18n.config.enforce_available_locales = false I18n.config.enforce_available_locales = false
Bundler.require(:default, Rails.env) Bundler.require(:default, Rails.env)
require_relative '../lib/gitlab/redis_config' require_relative '../lib/gitlab/redis'
module Gitlab module Gitlab
REDIS_CACHE_NAMESPACE = 'cache:gitlab'
class Application < Rails::Application class Application < Rails::Application
# Settings in config/environments/* take precedence over those specified here. # Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers # Application configuration should go into files in config/initializers
...@@ -69,8 +67,8 @@ module Gitlab ...@@ -69,8 +67,8 @@ module Gitlab
end end
end end
redis_config_hash = Gitlab::RedisConfig.redis_store_options redis_config_hash = Gitlab::Redis.redis_store_options
redis_config_hash[:namespace] = REDIS_CACHE_NAMESPACE redis_config_hash[:namespace] = Gitlab::Redis::CACHE_NAMESPACE
redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever
config.cache_store = :redis_store, redis_config_hash config.cache_store = :redis_store, redis_config_hash
......
...@@ -21,6 +21,9 @@ Rails.application.configure do ...@@ -21,6 +21,9 @@ Rails.application.configure do
# Generate digests for assets URLs # Generate digests for assets URLs
config.assets.digest = true config.assets.digest = true
# Enable compression of compiled assets using gzip.
config.assets.compress = true
# Defaults to nil and saved in location specified by config.assets.prefix # Defaults to nil and saved in location specified by config.assets.prefix
# config.assets.manifest = YOUR_PATH # config.assets.manifest = YOUR_PATH
......
...@@ -8,6 +8,7 @@ Rails.application.configure do ...@@ -8,6 +8,7 @@ Rails.application.configure do
config.cache_classes = false config.cache_classes = false
# Configure static asset server for tests with Cache-Control for performance # Configure static asset server for tests with Cache-Control for performance
config.assets.digest = false
config.serve_static_files = true config.serve_static_files = true
config.static_cache_control = "public, max-age=3600" config.static_cache_control = "public, max-age=3600"
......
...@@ -7,6 +7,7 @@ if Gitlab::Metrics.enabled? ...@@ -7,6 +7,7 @@ if Gitlab::Metrics.enabled?
# ActiveSupport. # ActiveSupport.
require 'gitlab/metrics/subscribers/action_view' require 'gitlab/metrics/subscribers/action_view'
require 'gitlab/metrics/subscribers/active_record' require 'gitlab/metrics/subscribers/active_record'
require 'gitlab/metrics/subscribers/rails_cache'
Gitlab::Application.configure do |config| Gitlab::Application.configure do |config|
config.middleware.use(Gitlab::Metrics::RackMiddleware) config.middleware.use(Gitlab::Metrics::RackMiddleware)
...@@ -74,6 +75,29 @@ if Gitlab::Metrics.enabled? ...@@ -74,6 +75,29 @@ if Gitlab::Metrics.enabled?
config.instrument_methods(const) config.instrument_methods(const)
config.instrument_instance_methods(const) config.instrument_instance_methods(const)
end end
# Instruments all Banzai filters
Dir[Rails.root.join('lib', 'banzai', 'filter', '*.rb')].each do |file|
klass = File.basename(file, File.extname(file)).camelize
const = Banzai::Filter.const_get(klass)
config.instrument_methods(const)
config.instrument_instance_methods(const)
end
config.instrument_methods(Banzai::ReferenceExtractor)
config.instrument_instance_methods(Banzai::ReferenceExtractor)
config.instrument_methods(Banzai::Renderer)
config.instrument_methods(Banzai::Querying)
[Issuable, Mentionable, Participable].each do |klass|
config.instrument_instance_methods(klass)
config.instrument_instance_methods(klass::ClassMethods)
end
config.instrument_methods(Gitlab::ReferenceExtractor)
config.instrument_instance_methods(Gitlab::ReferenceExtractor)
end end
GC::Profiler.enable GC::Profiler.enable
......
...@@ -13,7 +13,7 @@ end ...@@ -13,7 +13,7 @@ end
if Rails.env.test? if Rails.env.test?
Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session" Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
else else
redis_config = Gitlab::RedisConfig.redis_store_options redis_config = Gitlab::Redis.redis_store_options
redis_config[:namespace] = 'session:gitlab' redis_config[:namespace] = 'session:gitlab'
Gitlab::Application.config.session_store( Gitlab::Application.config.session_store(
......
...@@ -2,7 +2,7 @@ SIDEKIQ_REDIS_NAMESPACE = 'resque:gitlab' ...@@ -2,7 +2,7 @@ SIDEKIQ_REDIS_NAMESPACE = 'resque:gitlab'
Sidekiq.configure_server do |config| Sidekiq.configure_server do |config|
config.redis = { config.redis = {
url: Gitlab::RedisConfig.url, url: Gitlab::Redis.url,
namespace: SIDEKIQ_REDIS_NAMESPACE namespace: SIDEKIQ_REDIS_NAMESPACE
} }
...@@ -29,7 +29,7 @@ end ...@@ -29,7 +29,7 @@ end
Sidekiq.configure_client do |config| Sidekiq.configure_client do |config|
config.redis = { config.redis = {
url: Gitlab::RedisConfig.url, url: Gitlab::Redis.url,
namespace: SIDEKIQ_REDIS_NAMESPACE namespace: SIDEKIQ_REDIS_NAMESPACE
} }
end end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<% <%
require "yaml" require "yaml"
require "json" require "json"
require_relative "lib/gitlab/redis_config" require_relative "lib/gitlab/redis"
rails_env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" rails_env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
...@@ -18,7 +18,7 @@ if File.exists?(config_file) ...@@ -18,7 +18,7 @@ if File.exists?(config_file)
config['mailbox'] = "inbox" if config['mailbox'].nil? config['mailbox'] = "inbox" if config['mailbox'].nil?
if config['enabled'] && config['address'] if config['enabled'] && config['address']
redis_url = Gitlab::RedisConfig.new(rails_env).url redis_url = Gitlab::Redis.new(rails_env).url
%> %>
- -
:host: <%= config['host'].to_json %> :host: <%= config['host'].to_json %>
......
...@@ -4,7 +4,7 @@ Gitlab::Seeder.quiet do ...@@ -4,7 +4,7 @@ Gitlab::Seeder.quiet do
milestone_params = { milestone_params = {
title: "v#{i}.0", title: "v#{i}.0",
description: FFaker::Lorem.sentence, description: FFaker::Lorem.sentence,
state: ['opened', 'closed'].sample, state: [:active, :closed].sample,
} }
milestone = Milestones::CreateService.new( milestone = Milestones::CreateService.new(
......
class UserColorScheme < ActiveRecord::Migration class UserColorScheme < ActiveRecord::Migration
include Gitlab::Database
def up def up
add_column :users, :color_scheme_id, :integer, null: false, default: 1 add_column :users, :color_scheme_id, :integer, null: false, default: 1
User.where(dark_scheme: true).update_all(color_scheme_id: 2) execute("UPDATE users SET color_scheme_id = 2 WHERE dark_scheme = #{true_value}")
remove_column :users, :dark_scheme remove_column :users, :dark_scheme
end end
......
...@@ -3,14 +3,16 @@ class AddLastActivityColumnIntoProject < ActiveRecord::Migration ...@@ -3,14 +3,16 @@ class AddLastActivityColumnIntoProject < ActiveRecord::Migration
add_column :projects, :last_activity_at, :datetime add_column :projects, :last_activity_at, :datetime
add_index :projects, :last_activity_at add_index :projects, :last_activity_at
Project.find_each do |project| select_all('SELECT id, updated_at FROM projects').each do |project|
last_activity_date = if project.last_activity project_id = project['id']
project.last_activity.created_at update_date = project['updated_at']
else event = select_one("SELECT created_at FROM events WHERE project_id = #{project_id} ORDER BY created_at DESC LIMIT 1")
project.updated_at
end
project.update_attribute(:last_activity_at, last_activity_date) if event && event['created_at']
update_date = event['created_at']
end
execute("UPDATE projects SET last_activity_at = '#{update_date}' WHERE id = #{project_id}")
end end
end end
......
class AddVisibilityLevelToProjects < ActiveRecord::Migration class AddVisibilityLevelToProjects < ActiveRecord::Migration
include Gitlab::Database
def self.up def self.up
add_column :projects, :visibility_level, :integer, :default => 0, :null => false add_column :projects, :visibility_level, :integer, :default => 0, :null => false
Project.where(public: true).update_all(visibility_level: Gitlab::VisibilityLevel::PUBLIC) execute("UPDATE projects SET visibility_level = #{Gitlab::VisibilityLevel::PUBLIC} WHERE public = #{true_value}")
remove_column :projects, :public remove_column :projects, :public
end end
def self.down def self.down
add_column :projects, :public, :boolean, :default => false, :null => false add_column :projects, :public, :boolean, :default => false, :null => false
Project.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).update_all(public: true) execute("UPDATE projects SET public = #{true_value} WHERE visibility_level = #{Gitlab::VisibilityLevel::PUBLIC}")
remove_column :projects, :visibility_level remove_column :projects, :visibility_level
end end
end end
class MigrateAlreadyImportedProjects < ActiveRecord::Migration class MigrateAlreadyImportedProjects < ActiveRecord::Migration
include Gitlab::Database
def up def up
Project.where(imported: true).update_all(import_status: "finished") execute("UPDATE projects SET import_status = 'finished' WHERE imported = #{true_value}")
Project.where(imported: false).update_all(import_status: "none") execute("UPDATE projects SET import_status = 'none' WHERE imported = #{false_value}")
remove_column :projects, :imported remove_column :projects, :imported
end end
def down def down
add_column :projects, :imported, :boolean, default: false add_column :projects, :imported, :boolean, default: false
Project.where(import_status: 'finished').update_all(imported: true) execute("UPDATE projects SET imported = #{true_value} WHERE import_status = 'finished'")
end end
end end
class AddVisibilityLevelToSnippet < ActiveRecord::Migration class AddVisibilityLevelToSnippet < ActiveRecord::Migration
include Gitlab::Database
def up def up
add_column :snippets, :visibility_level, :integer, :default => 0, :null => false add_column :snippets, :visibility_level, :integer, :default => 0, :null => false
Snippet.where(private: true).update_all(visibility_level: Gitlab::VisibilityLevel::PRIVATE) execute("UPDATE snippets SET visibility_level = #{Gitlab::VisibilityLevel::PRIVATE} WHERE private = #{true_value}")
Snippet.where(private: false).update_all(visibility_level: Gitlab::VisibilityLevel::INTERNAL) execute("UPDATE snippets SET visibility_level = #{Gitlab::VisibilityLevel::INTERNAL} WHERE private = #{false_value}")
add_index :snippets, :visibility_level add_index :snippets, :visibility_level
...@@ -12,10 +14,10 @@ class AddVisibilityLevelToSnippet < ActiveRecord::Migration ...@@ -12,10 +14,10 @@ class AddVisibilityLevelToSnippet < ActiveRecord::Migration
def down def down
add_column :snippets, :private, :boolean, :default => false, :null => false add_column :snippets, :private, :boolean, :default => false, :null => false
Snippet.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).update_all(private: false) execute("UPDATE snippets SET private = #{false_value} WHERE visibility_level = #{Gitlab::VisibilityLevel::INTERNAL}")
Snippet.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).update_all(private: true) execute("UPDATE snippets SET private = #{true_value} WHERE visibility_level = #{Gitlab::VisibilityLevel::PRIVATE}")
remove_column :snippets, :visibility_level remove_column :snippets, :visibility_level
end end
end end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160331133914) do ActiveRecord::Schema.define(version: 20160331223143) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -44,7 +44,6 @@ ActiveRecord::Schema.define(version: 20160331133914) do ...@@ -44,7 +44,6 @@ ActiveRecord::Schema.define(version: 20160331133914) do
t.datetime "updated_at" t.datetime "updated_at"
t.string "home_page_url" t.string "home_page_url"
t.integer "default_branch_protection", default: 2 t.integer "default_branch_protection", default: 2
t.boolean "twitter_sharing_enabled", default: true
t.text "restricted_visibility_levels" t.text "restricted_visibility_levels"
t.boolean "version_check_enabled", default: true t.boolean "version_check_enabled", default: true
t.integer "max_attachment_size", default: 10, null: false t.integer "max_attachment_size", default: 10, null: false
......
...@@ -76,8 +76,9 @@ Example response: ...@@ -76,8 +76,9 @@ Example response:
"title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.", "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
"created_at" : "2016-01-04T15:31:51.081Z", "created_at" : "2016-01-04T15:31:51.081Z",
"iid" : 6, "iid" : 6,
"labels" : [] "labels" : [],
}, "subscribed" : false
}
] ]
``` ```
...@@ -152,7 +153,8 @@ Example response: ...@@ -152,7 +153,8 @@ Example response:
"id" : 41, "id" : 41,
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z", "updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z" "created_at" : "2016-01-04T15:31:46.176Z",
"subscribed" : false
} }
] ]
``` ```
...@@ -213,7 +215,8 @@ Example response: ...@@ -213,7 +215,8 @@ Example response:
"id" : 41, "id" : 41,
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z", "updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z" "created_at" : "2016-01-04T15:31:46.176Z",
"subscribed": false
} }
``` ```
...@@ -267,7 +270,8 @@ Example response: ...@@ -267,7 +270,8 @@ Example response:
}, },
"description" : null, "description" : null,
"updated_at" : "2016-01-07T12:44:33.959Z", "updated_at" : "2016-01-07T12:44:33.959Z",
"milestone" : null "milestone" : null,
"subscribed" : true
} }
``` ```
...@@ -323,7 +327,8 @@ Example response: ...@@ -323,7 +327,8 @@ Example response:
], ],
"id" : 85, "id" : 85,
"assignee" : null, "assignee" : null,
"milestone" : null "milestone" : null,
"subscribed" : true
} }
``` ```
......
...@@ -66,7 +66,8 @@ Parameters: ...@@ -66,7 +66,8 @@ Parameters:
"due_date": null "due_date": null
}, },
"merge_when_build_succeeds": true, "merge_when_build_succeeds": true,
"merge_status": "can_be_merged" "merge_status": "can_be_merged",
"subscribed" : false
} }
] ]
``` ```
...@@ -128,7 +129,8 @@ Parameters: ...@@ -128,7 +129,8 @@ Parameters:
"due_date": null "due_date": null
}, },
"merge_when_build_succeeds": true, "merge_when_build_succeeds": true,
"merge_status": "can_be_merged" "merge_status": "can_be_merged",
"subscribed" : true
} }
``` ```
...@@ -227,6 +229,7 @@ Parameters: ...@@ -227,6 +229,7 @@ Parameters:
}, },
"merge_when_build_succeeds": true, "merge_when_build_succeeds": true,
"merge_status": "can_be_merged", "merge_status": "can_be_merged",
"subscribed" : true,
"changes": [ "changes": [
{ {
"old_path": "VERSION", "old_path": "VERSION",
...@@ -304,7 +307,8 @@ Parameters: ...@@ -304,7 +307,8 @@ Parameters:
"due_date": null "due_date": null
}, },
"merge_when_build_succeeds": true, "merge_when_build_succeeds": true,
"merge_status": "can_be_merged" "merge_status": "can_be_merged",
"subscribed" : true
} }
``` ```
...@@ -373,7 +377,8 @@ Parameters: ...@@ -373,7 +377,8 @@ Parameters:
"due_date": null "due_date": null
}, },
"merge_when_build_succeeds": true, "merge_when_build_succeeds": true,
"merge_status": "can_be_merged" "merge_status": "can_be_merged",
"subscribed" : true
} }
``` ```
...@@ -466,7 +471,8 @@ Parameters: ...@@ -466,7 +471,8 @@ Parameters:
"due_date": null "due_date": null
}, },
"merge_when_build_succeeds": true, "merge_when_build_succeeds": true,
"merge_status": "can_be_merged" "merge_status": "can_be_merged",
"subscribed" : true
} }
``` ```
...@@ -530,7 +536,8 @@ Parameters: ...@@ -530,7 +536,8 @@ Parameters:
"due_date": null "due_date": null
}, },
"merge_when_build_succeeds": true, "merge_when_build_succeeds": true,
"merge_status": "can_be_merged" "merge_status": "can_be_merged",
"subscribed" : true
} }
``` ```
......
# Introduction to build artifacts # Introduction to build artifacts
Artifacts is a list of files and directories which are attached to a build Artifacts is a list of files and directories which are attached to a build
after it completes successfully. after it completes successfully. This feature is enabled by default in all GitLab installations.
_If you are searching for ways to use artifacts, jump to
[Defining artifacts in `.gitlab-ci.yml`](#defining-artifacts-in-gitlab-ciyml)._
Since GitLab 8.2 and [GitLab Runner] 0.7.0, build artifacts that are created by Since GitLab 8.2 and [GitLab Runner] 0.7.0, build artifacts that are created by
GitLab Runner are uploaded to GitLab and are downloadable as a single archive GitLab Runner are uploaded to GitLab and are downloadable as a single archive
...@@ -16,13 +19,9 @@ The artifacts browser will be available only for new artifacts that are sent ...@@ -16,13 +19,9 @@ The artifacts browser will be available only for new artifacts that are sent
to GitLab using GitLab Runner version 1.0 and up. It will not be possible to to GitLab using GitLab Runner version 1.0 and up. It will not be possible to
browse old artifacts already uploaded to GitLab. browse old artifacts already uploaded to GitLab.
## Enabling build artifacts ## Disabling build artifacts
_If you are searching for ways to use artifacts, jump to
[Defining artifacts in `.gitlab-ci.yml`](#defining-artifacts-in-gitlab-ciyml)._
The artifacts feature is enabled by default in all GitLab installations. To disable artifacts site-wide, follow the steps below.
To disable it site-wide, follow the steps below.
--- ---
......
...@@ -57,7 +57,7 @@ before_script: ...@@ -57,7 +57,7 @@ before_script:
# WARNING: Use this only with the Docker executor, if you use it with shell # WARNING: Use this only with the Docker executor, if you use it with shell
# you will overwrite your user's SSH config. # you will overwrite your user's SSH config.
- mkdir -p ~/.ssh - mkdir -p ~/.ssh
- '[[ -f /.dockerinit ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config` - '[[ -f /.dockerinit ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
``` ```
As a final step, add the _public_ key from the one you created earlier to the As a final step, add the _public_ key from the one you created earlier to the
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
- [Architecture](architecture.md) of GitLab - [Architecture](architecture.md) of GitLab
- [CI setup](ci_setup.md) for testing GitLab - [CI setup](ci_setup.md) for testing GitLab
- [Code review guidelines](code_review.md) for reviewing code and having code
reviewed.
- [Gotchas](gotchas.md) to avoid - [Gotchas](gotchas.md) to avoid
- [How to dump production data to staging](db_dump.md) - [How to dump production data to staging](db_dump.md)
- [Instrumentation](instrumentation.md) - [Instrumentation](instrumentation.md)
...@@ -10,4 +12,5 @@ ...@@ -10,4 +12,5 @@
- [Shell commands](shell_commands.md) in the GitLab codebase - [Shell commands](shell_commands.md) in the GitLab codebase
- [Sidekiq debugging](sidekiq_debugging.md) - [Sidekiq debugging](sidekiq_debugging.md)
- [SQL guidelines](sql.md) for SQL guidelines - [SQL guidelines](sql.md) for SQL guidelines
- [Testing standards and style guidelines](testing.md)
- [UI guide](ui_guide.md) for building GitLab with existing css styles and elements - [UI guide](ui_guide.md) for building GitLab with existing css styles and elements
# Code Review Guidelines
This guide contains advice and best practices for performing code review, and
having your code reviewed.
All merge requests for GitLab CE and EE, whether written by a GitLab team member
or a volunteer contributor, must go through a code review process to ensure the
code is effective, understandable, and maintainable.
Any developer can, and is encouraged to, perform code review on merge requests
of colleagues and contributors. However, the final decision to accept a merge
request is up to one of our merge request "endbosses", denoted on the
[team page](https://about.gitlab.com/team).
## Everyone
- Accept that many programming decisions are opinions. Discuss tradeoffs, which
you prefer, and reach a resolution quickly.
- Ask questions; don't make demands. ("What do you think about naming this
`:user_id`?")
- Ask for clarification. ("I didn't understand. Can you clarify?")
- Avoid selective ownership of code. ("mine", "not mine", "yours")
- Avoid using terms that could be seen as referring to personal traits. ("dumb",
"stupid"). Assume everyone is attractive, intelligent, and well-meaning.
- Be explicit. Remember people don't always understand your intentions online.
- Be humble. ("I'm not sure - let's look it up.")
- Don't use hyperbole. ("always", "never", "endlessly", "nothing")
- Be careful about the use of sarcasm. Everything we do is public; what seems
like good-natured ribbing to you and a long-time colleague might come off as
mean and unwelcoming to a person new to the project.
- Consider one-on-one chats or video calls if there are too many "I didn't
understand" or "Alternative solution:" comments. Post a follow-up comment
summarizing one-on-one discussion.
## Having your code reviewed
- The first reviewer of your code is _you_. Before you perform that first push
of your shiny new branch, read through the entire diff. Does it make sense?
Did you include something unrelated to the overall purpose of the changes? Did
you forget to remove any debugging code?
- Be grateful for the reviewer's suggestions. ("Good call. I'll make that
change.")
- Don't take it personally. The review is of the code, not of you.
- Explain why the code exists. ("It's like that because of these reasons. Would
it be more clear if I rename this class/file/method/variable?")
- Extract unrelated changes and refactorings into future merge requests/issues.
- Seek to understand the reviewer's perspective.
- Try to respond to every comment.
- Push commits based on earlier rounds of feedback as isolated commits to the
branch. Do not squash until the branch is ready to merge. Reviewers should be
able to read individual updates based on their earlier feedback.
## Reviewing code
Understand why the change is necessary (fixes a bug, improves the user
experience, refactors the existing code). Then:
- Communicate which ideas you feel strongly about and those you don't.
- Identify ways to simplify the code while still solving the problem.
- Offer alternative implementations, but assume the author already considered
them. ("What do you think about using a custom validator here?")
- Seek to understand the author's perspective.
- If you don't understand a piece of code, _say so_. There's a good chance
someone else would be confused by it as well.
- After a round of line notes, it can be helpful to post a summary note such as
"LGTM :thumbsup:", or "Just a couple things to address."
- Avoid accepting a merge request before the build succeeds ("Merge when build
succeeds" is fine).
## Credits
Largely based on the [thoughtbot code review guide].
[thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review
---
[Return to Development documentation](README.md)
...@@ -2,36 +2,35 @@ ...@@ -2,36 +2,35 @@
GitLab Performance Monitoring allows instrumenting of custom blocks of Ruby GitLab Performance Monitoring allows instrumenting of custom blocks of Ruby
code. This can be used to measure the time spent in a specific part of a larger code. This can be used to measure the time spent in a specific part of a larger
chunk of code. The resulting data is written to a separate series. chunk of code. The resulting data is stored as a field in the transaction that
executed the block.
To start measuring a block of Ruby code you should use To start measuring a block of Ruby code you should use `Gitlab::Metrics.measure`
`Gitlab::Metrics.measure` and give it a name for the series to store the data and give it a name:
in:
```ruby ```ruby
Gitlab::Metrics.measure(:user_logins) do Gitlab::Metrics.measure(:foo) do
... ...
end end
``` ```
The first argument of this method is the series name and should be plural. This 3 values are measured for a block:
name will be prefixed with `rails_` or `sidekiq_` depending on whether the code
was run in the Rails application or one of the Sidekiq workers. In the
above example the final series names would be as follows:
- rails_user_logins 1. The real time elapsed, stored in NAME_real_time.
- sidekiq_user_logins 2. The CPU time elapsed, stored in NAME_cpu_time.
3. The call count, stored in NAME_call_count.
Series names should be plural as this keeps the naming style in line with the Both the real and CPU timings are measured in milliseconds.
other series names.
By default metrics measured using a block contain a single value, "duration", Multiple calls to the same block will result in the final values being the sum
which contains the number of milliseconds it took to execute the block. Custom of all individual values. Take this code for example:
values can be added by passing a Hash as the 2nd argument. Custom tags can be
added by passing a Hash as the 3rd argument. A simple example is as follows:
```ruby ```ruby
Gitlab::Metrics.measure(:example_series, { number: 10 }, { class: self.class.to_s }) do 3.times do
... Gitlab::Metrics.measure(:sleep) do
sleep 1
end
end end
``` ```
Here the final value of `sleep_real_time` will be `3`, _not_ `1`.
# Testing Standards and Style Guidelines
This guide outlines standards and best practices for automated testing of GitLab
CE and EE.
It is meant to be an _extension_ of the [thoughtbot testing
styleguide](https://github.com/thoughtbot/guides/tree/master/style/testing). If
this guide defines a rule that contradicts the thoughtbot guide, this guide
takes precedence. Some guidelines may be repeated verbatim to stress their
importance.
## Factories
GitLab uses [factory_girl] as a test fixture replacement.
- Factory definitions live in `spec/factories/`, named using the pluralization
of their corresponding model (`User` factories are defined in `users.rb`).
- There should be only one top-level factory definition per file.
- FactoryGirl methods are mixed in to all RSpec groups. This means you can (and
should) call `create(...)` instead of `FactoryGirl.create(...)`.
- Make use of [traits] to clean up definitions and usages.
- When defining a factory, don't define attributes that are not required for the
resulting record to pass validation.
- When instantiating from a factory, don't supply attributes that aren't
required by the test.
- Factories don't have to be limited to `ActiveRecord` objects.
[See example](https://gitlab.com/gitlab-org/gitlab-ce/commit/0b8cefd3b2385a21cfed779bd659978c0402766d).
[factory_girl]: https://github.com/thoughtbot/factory_girl
[traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits
## JavaScript
GitLab uses [Teaspoon] to run its [Jasmine] JavaScript specs. They can be run on
the command line via `bundle exec teaspoon`, or via a web browser at
`http://localhost:3000/teaspoon` when the Rails server is running.
- JavaScript tests live in `spec/javascripts/`, matching the folder structure of
`app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.coffee` has a corresponding
`spec/javascripts/behaviors/autosize_spec.js.coffee` file.
- Haml fixtures required for JavaScript tests live in
`spec/javascripts/fixtures`. They should contain the bare minimum amount of
markup necessary for the test.
> **Warning:** Keep in mind that a Rails view may change and
invalidate your test, but everything will still pass because your fixture
doesn't reflect the latest view.
- Keep in mind that in a CI environment, these tests are run in a headless
browser and you will not have access to certain APIs, such as
[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
which will have to be stubbed.
[Teaspoon]: https://github.com/modeset/teaspoon
[Jasmine]: https://github.com/jasmine/jasmine
## RSpec
### General Guidelines
- Use a single, top-level `describe ClassName` block.
- Use `described_class` instead of repeating the class name being described.
- Use `.method` to describe class methods and `#method` to describe instance
methods.
- Use `context` to test branching logic.
- Don't `describe` symbols (see [Gotchas](gotchas.md#dont-describe-symbols)).
- Prefer `not_to` to `to_not`.
- Try to match the ordering of tests to the ordering within the class.
- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines
to separate phases.
[four-phase-test]: https://robots.thoughtbot.com/four-phase-test
### `let` variables
GitLab's RSpec suite has made extensive use of `let` variables to reduce
duplication. However, this sometimes [comes at the cost of clarity][lets-not],
so we need to set some guidelines for their use going forward:
- `let` variables are preferable to instance variables. Local variables are
preferable to `let` variables.
- Use `let` to reduce duplication throughout an entire spec file.
- Don't use `let` to define variables used by a single test; define them as
local variables inside the test's `it` block.
- Don't define a `let` variable inside the top-level `describe` block that's
only used in a more deeply-nested `context` or `describe` block. Keep the
definition as close as possible to where it's used.
- Try to avoid overriding the definition of one `let` variable with another.
- Don't define a `let` variable that's only used by the definition of another.
Use a helper method instead.
[lets-not]: https://robots.thoughtbot.com/lets-not
### Test speed
GitLab has a massive test suite that, without parallelization, can take more
than an hour to run. It's important that we make an effort to write tests that
are accurate and effective _as well as_ fast.
Here are some things to keep in mind regarding test performance:
- `double` and `spy` are faster than `FactoryGirl.build(...)`
- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`.
- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`,
`spy`, or `double` will do. Database persistence is slow!
- Use `create(:empty_project)` instead of `create(:project)` when you don't need
the underlying Git repository. Filesystem operations are slow!
- Don't mark a feature as requiring JavaScript (through `@javascript` in
Spinach or `js: true` in RSpec) unless it's _actually_ required for the test
to be valid. Headless browser testing is slow!
### Features / Integration
- Feature specs live in `spec/features/` and should be named
`ROLE_ACTION_spec.rb`, such as `user_changes_password_spec.rb`.
- Use only one `feature` block per feature spec file.
- Use scenario titles that describe the success and failure paths.
- Avoid scenario titles that add no information, such as "successfully."
- Avoid scenario titles that repeat the feature title.
## Spinach (feature) tests
GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
for its feature/integration tests in September 2012.
As of March 2016, we are [trying to avoid adding new Spinach
tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward,
opting for [RSpec feature](#features-integration) specs.
Adding new Spinach scenarios is acceptable _only if_ the new scenario requires
no more than one new `step` definition. If more than that is required, the
test should be re-implemented using RSpec instead.
---
[Return to Development documentation](README.md)
# UI Guide for building GitLab # UI Guide for building GitLab
## Best practices for creating new pages in GitLab
TODO: write some best practices when develop GitLab features.
## GitLab UI development kit ## GitLab UI development kit
We created a page inside GitLab where you can check commonly used html and css elements. We created a page inside GitLab where you can check commonly used html and css elements.
......
...@@ -19,26 +19,15 @@ See the documentation below for details on how to configure these services. ...@@ -19,26 +19,15 @@ See the documentation below for details on how to configure these services.
GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html
## Project services ## Project services
Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, Integration with services such as Campfire, Flowdock, Gemnasium, HipChat,
Pivotal Tracker, and Slack are available in the form of a [Project Service][]. Pivotal Tracker, and Slack are available in the form of a [Project Service][].
You can find these within GitLab in the Services page under Project Settings if
you are at least a master on the project.
Project Services are a bit like plugins in that they allow a lot of freedom in
adding functionality to GitLab. For example there is also a service that can
send an email every time someone pushes new commits.
Because GitLab is open source we can ship with the code and tests for all
plugins. This allows the community to keep the plugins up to date so that they
always work in newer GitLab versions.
For an overview of what projects services are available without logging in,
please see the [project_services directory][projects-code].
[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html
[Project Service]: ../project_services/project_services.md [Project Service]: ../project_services/project_services.md
[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services
## SSL certificate errors ## SSL certificate errors
......
# Project Services # Project Services
Project services allow you to integrate GitLab with other applications. Below Project services allow you to integrate GitLab with other applications. Below
is list of the currently supported ones. Click on the service links to see is list of the currently supported ones.
You can find these within GitLab in the Services page under Project Settings if
you are at least a master on the project.
Project Services are a bit like plugins in that they allow a lot of freedom in
adding functionality to GitLab. For example there is also a service that can
send an email every time someone pushes new commits.
Because GitLab is open source we can ship with the code and tests for all
plugins. This allows the community to keep the plugins up to date so that they
always work in newer GitLab versions.
For an overview of what projects services are available without logging in,
please see the [project_services directory][projects-code].
[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services
Click on the service links to see
further configuration instructions and details. Contributions are welcome. further configuration instructions and details. Contributions are welcome.
## Services ## Services
......
...@@ -7,6 +7,10 @@ Feature: Groups ...@@ -7,6 +7,10 @@ Feature: Groups
When I visit group "NonExistentGroup" page When I visit group "NonExistentGroup" page
Then page status code should be 404 Then page status code should be 404
Scenario: I should have back to group button
When I visit group "Owned" page
Then I should see back to dashboard button
@javascript @javascript
Scenario: I should see group "Owned" dashboard list Scenario: I should see group "Owned" dashboard list
When I visit group "Owned" page When I visit group "Owned" page
......
...@@ -4,6 +4,7 @@ Feature: Project Forked Merge Requests ...@@ -4,6 +4,7 @@ Feature: Project Forked Merge Requests
And I am a member of project "Shop" And I am a member of project "Shop"
And I have a project forked off of "Shop" called "Forked Shop" And I have a project forked off of "Shop" called "Forked Shop"
@javascript
Scenario: I submit new unassigned merge request to a forked project Scenario: I submit new unassigned merge request to a forked project
Given I visit project "Forked Shop" merge requests page Given I visit project "Forked Shop" merge requests page
And I click link "New Merge Request" And I click link "New Merge Request"
......
...@@ -70,6 +70,7 @@ Feature: Project Merge Requests ...@@ -70,6 +70,7 @@ Feature: Project Merge Requests
When I click link "Reopen" When I click link "Reopen"
Then I should see reopened merge request "Bug NS-04" Then I should see reopened merge request "Bug NS-04"
@javascript
Scenario: I submit new unassigned merge request Scenario: I submit new unassigned merge request
Given I click link "New Merge Request" Given I click link "New Merge Request"
And I submit new merge request "Wiki Feature" And I submit new merge request "Wiki Feature"
......
...@@ -18,6 +18,15 @@ Feature: Project ...@@ -18,6 +18,15 @@ Feature: Project
Then I should see the default project avatar Then I should see the default project avatar
And I should not see the "Remove avatar" button And I should not see the "Remove avatar" button
Scenario: I should have back to group button
And project "Shop" belongs to group
And I visit project "Shop" page
Then I should see back to group button
Scenario: I should have back to group button
And I visit project "Shop" page
Then I should see back to dashboard button
Scenario: I should have readme on page Scenario: I should have readme on page
And I visit project "Shop" page And I visit project "Shop" page
Then I should see project "Shop" README Then I should see project "Shop" README
......
...@@ -5,9 +5,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps ...@@ -5,9 +5,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
include SharedUser include SharedUser
step 'I click on group milestones' do step 'I click on group milestones' do
page.within '.nav-secondary' do click_link 'Milestones'
click_link("Milestones")
end
end end
step 'I should see group milestones index page has no milestones' do step 'I should see group milestones index page has no milestones' do
......
...@@ -4,6 +4,10 @@ class Spinach::Features::Groups < Spinach::FeatureSteps ...@@ -4,6 +4,10 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
include SharedGroup include SharedGroup
include SharedUser include SharedUser
step 'I should see back to dashboard button' do
expect(page).to have_content 'Go to dashboard'
end
step 'I should see group "Owned"' do step 'I should see group "Owned"' do
expect(page).to have_content '@owned' expect(page).to have_content '@owned'
end end
......
...@@ -82,9 +82,7 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps ...@@ -82,9 +82,7 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
# Sub Tabs: Issues # Sub Tabs: Issues
step 'I click the "Milestones" tab' do step 'I click the "Milestones" tab' do
page.within '.nav-secondary' do click_link('Milestones')
click_link('Milestones')
end
end end
step 'I click the "Labels" tab' do step 'I click the "Labels" tab' do
......
...@@ -36,7 +36,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps ...@@ -36,7 +36,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end end
step 'I goto the Merge Requests page' do step 'I goto the Merge Requests page' do
page.within '.nav-secondary' do page.within '.page-sidebar-expanded' do
click_link "Merge Requests" click_link "Merge Requests"
end end
end end
......
...@@ -34,10 +34,14 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps ...@@ -34,10 +34,14 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
end end
step 'I fill out a "Merge Request On Forked Project" merge request' do step 'I fill out a "Merge Request On Forked Project" merge request' do
select @forked_project.path_with_namespace, from: "merge_request_source_project_id" first('.js-source-project').click
select @project.path_with_namespace, from: "merge_request_target_project_id" first('.dropdown-source-project a', text: @forked_project.path_with_namespace)
select "fix", from: "merge_request_source_branch"
select "master", from: "merge_request_target_branch" first('.js-target-project').click
first('.dropdown-target-project a', text: @project.path_with_namespace)
first('.js-source-branch').click
first('.dropdown-source-branch .dropdown-content a', text: 'fix').click
click_button "Compare branches and continue" click_button "Compare branches and continue"
...@@ -115,10 +119,10 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps ...@@ -115,10 +119,10 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
end end
step 'I fill out an invalid "Merge Request On Forked Project" merge request' do step 'I fill out an invalid "Merge Request On Forked Project" merge request' do
expect(find(:select, "merge_request_source_project_id", {}).value).to eq @forked_project.id.to_s expect(find_by_id("merge_request_source_project_id", visible: false).value).to eq @forked_project.id.to_s
expect(find(:select, "merge_request_target_project_id", {}).value).to eq @project.id.to_s expect(find_by_id("merge_request_target_project_id", visible: false).value).to eq @project.id.to_s
expect(find(:select, "merge_request_source_branch", {}).value).to eq "" expect(find_by_id("merge_request_source_branch", visible: false).value).to eq nil
expect(find(:select, "merge_request_target_branch", {}).value).to eq "master" expect(find_by_id("merge_request_target_branch", visible: false).value).to eq "master"
click_button "Compare branches" click_button "Compare branches"
end end
...@@ -127,7 +131,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps ...@@ -127,7 +131,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
end end
step 'the target repository should be the original repository' do step 'the target repository should be the original repository' do
expect(page).to have_select("merge_request_target_project_id", selected: @project.path_with_namespace) expect(find_by_id("merge_request_target_project_id").value).to eq "#{@project.id}"
end end
step 'I click "Assign to" dropdown"' do step 'I click "Assign to" dropdown"' do
......
...@@ -15,7 +15,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps ...@@ -15,7 +15,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I delete all labels' do step 'I delete all labels' do
page.within '.labels' do page.within '.labels' do
page.all('.btn-remove').each do |remove| page.all('.remove-row').each do |remove|
remove.click remove.click
sleep 0.05 sleep 0.05
end end
......
...@@ -93,8 +93,12 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps ...@@ -93,8 +93,12 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end end
step 'I submit new merge request "Wiki Feature"' do step 'I submit new merge request "Wiki Feature"' do
select "fix", from: "merge_request_source_branch" find('.js-source-branch').click
select "feature", from: "merge_request_target_branch" find('.dropdown-source-branch .dropdown-content a', text: 'fix').click
find('.js-target-branch').click
first('.dropdown-target-branch .dropdown-content a', text: 'feature').click
click_button "Compare branches" click_button "Compare branches"
fill_in "merge_request_title", with: "Wiki Feature" fill_in "merge_request_title", with: "Wiki Feature"
click_button "Submit merge request" click_button "Submit merge request"
......
...@@ -114,9 +114,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps ...@@ -114,9 +114,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end end
step 'I should not see "Snippets" button' do step 'I should not see "Snippets" button' do
page.within '.nav-secondary' do expect(page).not_to have_link 'Snippets'
expect(page).not_to have_link 'Snippets'
end
end end
step 'project "Shop" belongs to group' do step 'project "Shop" belongs to group' do
...@@ -125,6 +123,14 @@ class Spinach::Features::Project < Spinach::FeatureSteps ...@@ -125,6 +123,14 @@ class Spinach::Features::Project < Spinach::FeatureSteps
@project.save! @project.save!
end end
step 'I should see back to dashboard button' do
expect(page).to have_content 'Go to dashboard'
end
step 'I should see back to group button' do
expect(page).to have_content 'Go to group'
end
step 'I click notifications drop down button' do step 'I click notifications drop down button' do
click_link 'notifications-button' click_link 'notifications-button'
end end
......
...@@ -213,13 +213,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps ...@@ -213,13 +213,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end end
step 'I see Browse file link' do step 'I see Browse file link' do
expect(page).to have_link 'Browse File »' expect(page).to have_link 'Browse File'
expect(page).not_to have_link 'Browse Files »' expect(page).not_to have_link 'Browse Files'
end end
step 'I see Browse code link' do step 'I see Browse code link' do
expect(page).to have_link 'Browse Files »' expect(page).to have_link 'Browse Files'
expect(page).not_to have_link 'Browse File »'
expect(page).not_to have_link 'Browse Directory »' expect(page).not_to have_link 'Browse Directory »'
end end
......
...@@ -155,7 +155,7 @@ module SharedDiffNote ...@@ -155,7 +155,7 @@ module SharedDiffNote
step 'I should see a discussion reply button' do step 'I should see a discussion reply button' do
page.within(diff_file_selector) do page.within(diff_file_selector) do
expect(page).to have_button('Reply') expect(page).to have_button('Reply...')
end end
end end
......
...@@ -41,7 +41,7 @@ module SharedProjectTab ...@@ -41,7 +41,7 @@ module SharedProjectTab
end end
step 'the active main tab should be Settings' do step 'the active main tab should be Settings' do
page.within '.nav-secondary' do page.within '.nav-sidebar' do
expect(page).to have_content('Go to project') expect(page).to have_content('Go to project')
end end
end end
......
...@@ -170,6 +170,10 @@ module API ...@@ -170,6 +170,10 @@ module API
expose :label_names, as: :labels expose :label_names, as: :labels
expose :milestone, using: Entities::Milestone expose :milestone, using: Entities::Milestone
expose :assignee, :author, using: Entities::UserBasic expose :assignee, :author, using: Entities::UserBasic
expose :subscribed do |issue, options|
issue.subscribed?(options[:current_user])
end
end end
class MergeRequest < ProjectEntity class MergeRequest < ProjectEntity
...@@ -183,6 +187,10 @@ module API ...@@ -183,6 +187,10 @@ module API
expose :milestone, using: Entities::Milestone expose :milestone, using: Entities::Milestone
expose :merge_when_build_succeeds expose :merge_when_build_succeeds
expose :merge_status expose :merge_status
expose :subscribed do |merge_request, options|
merge_request.subscribed?(options[:current_user])
end
end end
class MergeRequestChanges < MergeRequest class MergeRequestChanges < MergeRequest
......
...@@ -55,7 +55,7 @@ module API ...@@ -55,7 +55,7 @@ module API
issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
issues.reorder(issuable_order_by => issuable_sort) issues.reorder(issuable_order_by => issuable_sort)
present paginate(issues), with: Entities::Issue present paginate(issues), with: Entities::Issue, current_user: current_user
end end
end end
...@@ -92,7 +92,7 @@ module API ...@@ -92,7 +92,7 @@ module API
end end
issues.reorder(issuable_order_by => issuable_sort) issues.reorder(issuable_order_by => issuable_sort)
present paginate(issues), with: Entities::Issue present paginate(issues), with: Entities::Issue, current_user: current_user
end end
# Get a single project issue # Get a single project issue
...@@ -105,7 +105,7 @@ module API ...@@ -105,7 +105,7 @@ module API
get ":id/issues/:issue_id" do get ":id/issues/:issue_id" do
@issue = user_project.issues.find(params[:issue_id]) @issue = user_project.issues.find(params[:issue_id])
not_found! unless can?(current_user, :read_issue, @issue) not_found! unless can?(current_user, :read_issue, @issue)
present @issue, with: Entities::Issue present @issue, with: Entities::Issue, current_user: current_user
end end
# Create a new project issue # Create a new project issue
...@@ -149,7 +149,7 @@ module API ...@@ -149,7 +149,7 @@ module API
issue.add_labels_by_names(params[:labels].split(',')) issue.add_labels_by_names(params[:labels].split(','))
end end
present issue, with: Entities::Issue present issue, with: Entities::Issue, current_user: current_user
else else
render_validation_error!(issue) render_validation_error!(issue)
end end
...@@ -189,7 +189,7 @@ module API ...@@ -189,7 +189,7 @@ module API
issue.add_labels_by_names(params[:labels].split(',')) issue.add_labels_by_names(params[:labels].split(','))
end end
present issue, with: Entities::Issue present issue, with: Entities::Issue, current_user: current_user
else else
render_validation_error!(issue) render_validation_error!(issue)
end end
......
...@@ -56,7 +56,7 @@ module API ...@@ -56,7 +56,7 @@ module API
end end
merge_requests = merge_requests.reorder(issuable_order_by => issuable_sort) merge_requests = merge_requests.reorder(issuable_order_by => issuable_sort)
present paginate(merge_requests), with: Entities::MergeRequest present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user
end end
# Create MR # Create MR
...@@ -94,7 +94,7 @@ module API ...@@ -94,7 +94,7 @@ module API
merge_request.add_labels_by_names(params[:labels].split(",")) merge_request.add_labels_by_names(params[:labels].split(","))
end end
present merge_request, with: Entities::MergeRequest present merge_request, with: Entities::MergeRequest, current_user: current_user
else else
handle_merge_request_errors! merge_request.errors handle_merge_request_errors! merge_request.errors
end end
...@@ -130,7 +130,7 @@ module API ...@@ -130,7 +130,7 @@ module API
authorize! :read_merge_request, merge_request authorize! :read_merge_request, merge_request
present merge_request, with: Entities::MergeRequest present merge_request, with: Entities::MergeRequest, current_user: current_user
end end
# Show MR commits # Show MR commits
...@@ -162,7 +162,7 @@ module API ...@@ -162,7 +162,7 @@ module API
merge_request = user_project.merge_requests. merge_request = user_project.merge_requests.
find(params[:merge_request_id]) find(params[:merge_request_id])
authorize! :read_merge_request, merge_request authorize! :read_merge_request, merge_request
present merge_request, with: Entities::MergeRequestChanges present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
end end
# Update MR # Update MR
...@@ -204,7 +204,7 @@ module API ...@@ -204,7 +204,7 @@ module API
merge_request.add_labels_by_names(params[:labels].split(",")) merge_request.add_labels_by_names(params[:labels].split(","))
end end
present merge_request, with: Entities::MergeRequest present merge_request, with: Entities::MergeRequest, current_user: current_user
else else
handle_merge_request_errors! merge_request.errors handle_merge_request_errors! merge_request.errors
end end
...@@ -246,7 +246,7 @@ module API ...@@ -246,7 +246,7 @@ module API
execute(merge_request) execute(merge_request)
end end
present merge_request, with: Entities::MergeRequest present merge_request, with: Entities::MergeRequest, current_user: current_user
end end
# Cancel Merge if Merge When build succeeds is enabled # Cancel Merge if Merge When build succeeds is enabled
...@@ -325,7 +325,7 @@ module API ...@@ -325,7 +325,7 @@ module API
get "#{path}/closes_issues" do get "#{path}/closes_issues" do
merge_request = user_project.merge_requests.find(params[:merge_request_id]) merge_request = user_project.merge_requests.find(params[:merge_request_id])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
present paginate(issues), with: Entities::Issue present paginate(issues), with: Entities::Issue, current_user: current_user
end end
end end
end end
......
...@@ -103,7 +103,7 @@ module API ...@@ -103,7 +103,7 @@ module API
authorize! :read_milestone, user_project authorize! :read_milestone, user_project
@milestone = user_project.milestones.find(params[:milestone_id]) @milestone = user_project.milestones.find(params[:milestone_id])
present paginate(@milestone.issues), with: Entities::Issue present paginate(@milestone.issues), with: Entities::Issue, current_user: current_user
end end
end end
......
...@@ -19,8 +19,10 @@ module Banzai ...@@ -19,8 +19,10 @@ module Banzai
cache_key = full_cache_key(cache_key, context[:pipeline]) cache_key = full_cache_key(cache_key, context[:pipeline])
if cache_key if cache_key
Rails.cache.fetch(cache_key) do Gitlab::Metrics.measure(:banzai_cached_render) do
cacheless_render(text, context) Rails.cache.fetch(cache_key) do
cacheless_render(text, context)
end
end end
else else
cacheless_render(text, context) cacheless_render(text, context)
...@@ -64,13 +66,15 @@ module Banzai ...@@ -64,13 +66,15 @@ module Banzai
private private
def self.cacheless_render(text, context = {}) def self.cacheless_render(text, context = {})
result = render_result(text, context) Gitlab::Metrics.measure(:banzai_cacheless_render) do
result = render_result(text, context)
output = result[:output] output = result[:output]
if output.respond_to?(:to_html) if output.respond_to?(:to_html)
output.to_html output.to_html
else else
output.to_s output.to_s
end
end end
end end
......
...@@ -43,7 +43,9 @@ module Gitlab ...@@ -43,7 +43,9 @@ module Gitlab
# false if the lease is already taken. # false if the lease is already taken.
def try_obtain def try_obtain
# Performing a single SET is atomic # Performing a single SET is atomic
!!redis.set(redis_key, '1', nx: true, ex: @timeout) Gitlab::Redis.with do |redis|
!!redis.set(redis_key, '1', nx: true, ex: @timeout)
end
end end
# No #cancel method. See comments above! # No #cancel method. See comments above!
......
...@@ -74,24 +74,32 @@ module Gitlab ...@@ -74,24 +74,32 @@ module Gitlab
# #
# Example: # Example:
# #
# Gitlab::Metrics.measure(:find_by_username_timings) do # Gitlab::Metrics.measure(:find_by_username_duration) do
# User.find_by_username(some_username) # User.find_by_username(some_username)
# end # end
# #
# series - The name of the series to store the data in. # name - The name of the field to store the execution time in.
# values - A Hash containing extra values to add to the metric.
# tags - A Hash containing extra tags to add to the metric.
# #
# Returns the value yielded by the supplied block. # Returns the value yielded by the supplied block.
def self.measure(series, values = {}, tags = {}) def self.measure(name)
return yield unless Transaction.current trans = current_transaction
return yield unless trans
real_start = Time.now.to_f
cpu_start = System.cpu_time
start = Time.now.to_f
retval = yield retval = yield
duration = (Time.now.to_f - start) * 1000.0
values = values.merge(duration: duration)
Transaction.current.add_metric(series, values, tags) cpu_stop = System.cpu_time
real_stop = Time.now.to_f
real_time = (real_stop - real_start) * 1000.0
cpu_time = cpu_stop - cpu_start
trans.increment("#{name}_real_time", real_time)
trans.increment("#{name}_cpu_time", cpu_time)
trans.increment("#{name}_call_count", 1)
retval retval
end end
...@@ -107,5 +115,11 @@ module Gitlab ...@@ -107,5 +115,11 @@ module Gitlab
new(udp: { host: host, port: port }) new(udp: { host: host, port: port })
end end
end end
private
def self.current_transaction
Transaction.current
end
end end
end end
...@@ -2,6 +2,8 @@ module Gitlab ...@@ -2,6 +2,8 @@ module Gitlab
module Metrics module Metrics
# Class for storing details of a single metric (label, value, etc). # Class for storing details of a single metric (label, value, etc).
class Metric class Metric
JITTER_RANGE = 0.000001..0.001
attr_reader :series, :values, :tags, :created_at attr_reader :series, :values, :tags, :created_at
# series - The name of the series (as a String) to store the metric in. # series - The name of the series (as a String) to store the metric in.
...@@ -16,11 +18,29 @@ module Gitlab ...@@ -16,11 +18,29 @@ module Gitlab
# Returns a Hash in a format that can be directly written to InfluxDB. # Returns a Hash in a format that can be directly written to InfluxDB.
def to_hash def to_hash
# InfluxDB overwrites an existing point if a new point has the same
# series, tag set, and timestamp. In a highly concurrent environment
# this means that using the number of seconds since the Unix epoch is
# inevitably going to collide with another timestamp. For example, two
# Rails requests processed by different processes may end up generating
# metrics using the _exact_ same timestamp (in seconds).
#
# Due to the way InfluxDB is set up there's no solution to this problem,
# all we can do is lower the amount of collisions. We do this by using
# Time#to_f which returns the seconds as a Float providing greater
# accuracy. We then add a small random value that is large enough to
# distinguish most timestamps but small enough to not alter the amount
# of seconds.
#
# See https://gitlab.com/gitlab-com/operations/issues/175 for more
# information.
time = @created_at.to_f + rand(JITTER_RANGE)
{ {
series: @series, series: @series,
tags: @tags, tags: @tags,
values: @values, values: @values,
timestamp: @created_at.to_i * 1_000_000_000 timestamp: (time * 1_000_000_000).to_i
} }
end end
end end
......
module Gitlab
module Metrics
module Subscribers
# Class for tracking the total time spent in Rails cache calls
class RailsCache < ActiveSupport::Subscriber
attach_to :active_support
def cache_read(event)
increment(:cache_read_duration, event.duration)
end
def cache_write(event)
increment(:cache_write_duration, event.duration)
end
def cache_delete(event)
increment(:cache_delete_duration, event.duration)
end
def cache_exist?(event)
increment(:cache_exists_duration, event.duration)
end
def increment(key, duration)
return unless current_transaction
current_transaction.increment(:cache_duration, duration)
current_transaction.increment(key, duration)
end
private
def current_transaction
Transaction.current
end
end
end
end
end
...@@ -30,6 +30,17 @@ module Gitlab ...@@ -30,6 +30,17 @@ module Gitlab
0 0
end end
end end
# THREAD_CPUTIME is not supported on OS X
if Process.const_defined?(:CLOCK_THREAD_CPUTIME_ID)
def self.cpu_time
Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :millisecond)
end
else
def self.cpu_time
Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :millisecond)
end
end
end end
end end
end end
module Gitlab module Gitlab
class RedisConfig class Redis
CACHE_NAMESPACE = 'cache:gitlab'
attr_reader :url attr_reader :url
# To be thread-safe we must be careful when writing the class instance
# variables @url and @pool. Because @pool depends on @url we need two
# mutexes to prevent deadlock.
URL_MUTEX = Mutex.new
POOL_MUTEX = Mutex.new
private_constant :URL_MUTEX, :POOL_MUTEX
def self.url def self.url
new.url @url || URL_MUTEX.synchronize { @url = new.url }
end
def self.with
if @pool.nil?
POOL_MUTEX.synchronize do
@pool = ConnectionPool.new { ::Redis.new(url: url) }
end
end
@pool.with { |redis| yield redis }
end end
def self.redis_store_options def self.redis_store_options
url = new.url url = new.url
redis_config_hash = Redis::Store::Factory.extract_host_options_from_uri(url) redis_config_hash = ::Redis::Store::Factory.extract_host_options_from_uri(url)
# Redis::Store does not handle Unix sockets well, so let's do it for them # Redis::Store does not handle Unix sockets well, so let's do it for them
redis_uri = URI.parse(url) redis_uri = URI.parse(url)
if redis_uri.scheme == 'unix' if redis_uri.scheme == 'unix'
......
...@@ -4,18 +4,19 @@ namespace :cache do ...@@ -4,18 +4,19 @@ namespace :cache do
desc "GitLab | Clear redis cache" desc "GitLab | Clear redis cache"
task :clear => :environment do task :clear => :environment do
redis = Redis.new(url: Gitlab::RedisConfig.url) Gitlab::Redis.with do |redis|
cursor = REDIS_SCAN_START_STOP cursor = REDIS_SCAN_START_STOP
loop do loop do
cursor, keys = redis.scan( cursor, keys = redis.scan(
cursor, cursor,
match: "#{Gitlab::REDIS_CACHE_NAMESPACE}*", match: "#{Gitlab::Redis::CACHE_NAMESPACE}*",
count: CLEAR_BATCH_SIZE count: CLEAR_BATCH_SIZE
) )
redis.del(*keys) if keys.any? redis.del(*keys) if keys.any?
break if cursor == REDIS_SCAN_START_STOP break if cursor == REDIS_SCAN_START_STOP
end
end end
end end
end end
require 'spec_helper'
describe "Dashboard Issues filtering", feature: true, js: true do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:milestone) { create(:milestone, project: project) }
context 'filtering by milestone' do
before do
project.team << [user, :master]
login_as(user)
create(:issue, project: project, author: user, assignee: user)
create(:issue, project: project, author: user, assignee: user, milestone: milestone)
visit_issues
end
it 'should show all issues with no milestone' do
show_milestone_dropdown
click_link 'No Milestone'
expect(page).to have_selector('.issue', count: 1)
end
it 'should show all issues with any milestone' do
show_milestone_dropdown
click_link 'Any Milestone'
expect(page).to have_selector('.issue', count: 2)
end
it 'should show all issues with the selected milestone' do
show_milestone_dropdown
page.within '.dropdown-content' do
click_link milestone.title
end
expect(page).to have_selector('.issue', count: 1)
end
end
def show_milestone_dropdown
click_button 'Milestone'
expect(page).to have_selector('.dropdown-content', visible: true)
end
def visit_issues
visit issues_dashboard_path
end
end
require 'rails_helper'
describe 'Awards Emoji', feature: true do
let!(:project) { create(:project) }
let!(:user) { create(:user) }
before do
project.team << [user, :master]
login_as(user)
end
describe 'Click award emoji from issue#show' do
let!(:issue) do
create(:issue,
author: @user,
assignee: @user,
project: project)
end
before do
visit namespace_project_issue_path(project.namespace, project, issue)
end
it 'should increment the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click
sleep 2
expect(thumbsdown_emoji).to have_text("1")
end
context 'click the thumbsup emoji' do
it 'should increment the thumbsup emoji', js: true do
find('[data-emoji="thumbsup"]').click
sleep 2
expect(thumbsup_emoji).to have_text("1")
end
it 'should decrement the thumbsdown emoji', js: true do
expect(thumbsdown_emoji).to have_text("0")
end
end
context 'click the thumbsdown emoji' do
it 'should increment the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click
sleep 2
expect(thumbsdown_emoji).to have_text("1")
end
it 'should decrement the thumbsup emoji', js: true do
expect(thumbsup_emoji).to have_text("0")
end
end
end
def thumbsup_emoji
page.all('span.js-counter').first
end
def thumbsdown_emoji
page.all('span.js-counter').last
end
end
require 'spec_helper' require 'spec_helper'
feature 'Create New Merge Request', feature: true, js: false do feature 'Create New Merge Request', feature: true, js: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
...@@ -13,9 +13,12 @@ feature 'Create New Merge Request', feature: true, js: false do ...@@ -13,9 +13,12 @@ feature 'Create New Merge Request', feature: true, js: false do
it 'generates a diff for an orphaned branch' do it 'generates a diff for an orphaned branch' do
click_link 'New Merge Request' click_link 'New Merge Request'
select "orphaned-branch", from: "merge_request_source_branch"
select "master", from: "merge_request_target_branch" first('.js-source-branch').click
first('.dropdown-source-branch .dropdown-content a', text: 'orphaned-branch').click
click_button "Compare branches" click_button "Compare branches"
click_link "Changes"
expect(page).to have_content "README.md" expect(page).to have_content "README.md"
expect(page).to have_content "wm.png" expect(page).to have_content "wm.png"
...@@ -23,6 +26,8 @@ feature 'Create New Merge Request', feature: true, js: false do ...@@ -23,6 +26,8 @@ feature 'Create New Merge Request', feature: true, js: false do
fill_in "merge_request_title", with: "Orphaned MR test" fill_in "merge_request_title", with: "Orphaned MR test"
click_button "Submit merge request" click_button "Submit merge request"
click_link "Check out branch"
expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch' expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch'
end end
end end
...@@ -210,7 +210,7 @@ describe 'Comments', feature: true do ...@@ -210,7 +210,7 @@ describe 'Comments', feature: true do
is_expected.to have_content('Another comment on line 10') is_expected.to have_content('Another comment on line 10')
is_expected.to have_css('.notes_holder') is_expected.to have_css('.notes_holder')
is_expected.to have_css('.notes_holder .note', count: 1) is_expected.to have_css('.notes_holder .note', count: 1)
is_expected.to have_button('Reply') is_expected.to have_button('Reply...')
end end
end end
end end
......
require 'rails_helper'
describe FormHelper do
describe 'form_errors' do
it 'returns nil when model has no errors' do
model = double(errors: [])
expect(helper.form_errors(model)).to be_nil
end
it 'renders an alert div' do
model = double(errors: errors_stub('Error 1'))
expect(helper.form_errors(model)).
to include('<div class="alert alert-danger" id="error_explanation">')
end
it 'contains a summary message' do
single_error = double(errors: errors_stub('A'))
multi_errors = double(errors: errors_stub('A', 'B', 'C'))
expect(helper.form_errors(single_error)).
to include('<h4>The form contains the following error:')
expect(helper.form_errors(multi_errors)).
to include('<h4>The form contains the following errors:')
end
it 'renders each message' do
model = double(errors: errors_stub('Error 1', 'Error 2', 'Error 3'))
errors = helper.form_errors(model)
aggregate_failures do
expect(errors).to include('<li>Error 1</li>')
expect(errors).to include('<li>Error 2</li>')
expect(errors).to include('<li>Error 3</li>')
end
end
def errors_stub(*messages)
ActiveModel::Errors.new(double).tap do |errors|
messages.each { |msg| errors.add(:base, msg) }
end
end
end
end
...@@ -150,13 +150,6 @@ describe GitlabMarkdownHelper do ...@@ -150,13 +150,6 @@ describe GitlabMarkdownHelper do
end end
end end
describe 'random_markdown_tip' do
it 'returns a random Markdown tip' do
stub_const("#{described_class}::MARKDOWN_TIPS", ['Random tip'])
expect(random_markdown_tip).to eq 'Random tip'
end
end
describe '#first_line_in_markdown' do describe '#first_line_in_markdown' do
let(:text) { "@#{user.username}, can you look at this?\nHello world\n"} let(:text) { "@#{user.username}, can you look at this?\nHello world\n"}
......
require 'spec_helper'
describe Gitlab::Metrics::Subscribers::RailsCache do
let(:transaction) { Gitlab::Metrics::Transaction.new }
let(:subscriber) { described_class.new }
let(:event) { double(:event, duration: 15.2) }
describe '#cache_read' do
it 'increments the cache_read duration' do
expect(subscriber).to receive(:increment).
with(:cache_read_duration, event.duration)
subscriber.cache_read(event)
end
end
describe '#cache_write' do
it 'increments the cache_write duration' do
expect(subscriber).to receive(:increment).
with(:cache_write_duration, event.duration)
subscriber.cache_write(event)
end
end
describe '#cache_delete' do
it 'increments the cache_delete duration' do
expect(subscriber).to receive(:increment).
with(:cache_delete_duration, event.duration)
subscriber.cache_delete(event)
end
end
describe '#cache_exist?' do
it 'increments the cache_exists duration' do
expect(subscriber).to receive(:increment).
with(:cache_exists_duration, event.duration)
subscriber.cache_exist?(event)
end
end
describe '#increment' do
context 'without a transaction' do
it 'returns' do
expect(transaction).not_to receive(:increment)
subscriber.increment(:foo, 15.2)
end
end
context 'with a transaction' do
before do
allow(subscriber).to receive(:current_transaction).
and_return(transaction)
end
it 'increments the total and specific cache duration' do
expect(transaction).to receive(:increment).
with(:cache_duration, event.duration)
expect(transaction).to receive(:increment).
with(:cache_delete_duration, event.duration)
subscriber.increment(:cache_delete_duration, event.duration)
end
end
end
end
...@@ -26,4 +26,10 @@ describe Gitlab::Metrics::System do ...@@ -26,4 +26,10 @@ describe Gitlab::Metrics::System do
end end
end end
end end
describe '.cpu_time' do
it 'returns a Fixnum' do
expect(described_class.cpu_time).to be_an_instance_of(Fixnum)
end
end
end end
...@@ -74,24 +74,21 @@ describe Gitlab::Metrics do ...@@ -74,24 +74,21 @@ describe Gitlab::Metrics do
let(:transaction) { Gitlab::Metrics::Transaction.new } let(:transaction) { Gitlab::Metrics::Transaction.new }
before do before do
allow(Gitlab::Metrics::Transaction).to receive(:current). allow(Gitlab::Metrics).to receive(:current_transaction).
and_return(transaction) and_return(transaction)
end end
it 'adds a metric to the current transaction' do it 'adds a metric to the current transaction' do
expect(transaction).to receive(:add_metric). expect(transaction).to receive(:increment).
with(:foo, { duration: a_kind_of(Numeric) }, { tag: 'value' }) with('foo_real_time', a_kind_of(Numeric))
Gitlab::Metrics.measure(:foo, {}, tag: 'value') { 10 } expect(transaction).to receive(:increment).
end with('foo_cpu_time', a_kind_of(Numeric))
it 'supports adding of custom values' do
values = { duration: a_kind_of(Numeric), number: 10 }
expect(transaction).to receive(:add_metric). expect(transaction).to receive(:increment).
with(:foo, values, { tag: 'value' }) with('foo_call_count', 1)
Gitlab::Metrics.measure(:foo, { number: 10 }, tag: 'value') { 10 } Gitlab::Metrics.measure(:foo) { 10 }
end end
it 'returns the return value of the block' do it 'returns the return value of the block' do
......
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